diff --git a/changelog.txt b/changelog.txt index 193fe8dfe70f68..85fa8cc791aec4 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,434 @@ == Changelog == += 17.6.0-rc.1 = + + +## Changelog + +### Features + +#### Interactivity API +- Add `wp-data-on-window` and `wp-data-on-document` directives. ([57931](https://github.com/WordPress/gutenberg/pull/57931)) +- Add `wp-each` directive. ([57859](https://github.com/WordPress/gutenberg/pull/57859)) +- Add `wp-run` directive and `useInit` & `useWatch` hooks. ([57805](https://github.com/WordPress/gutenberg/pull/57805)) + +#### Typography +- Add defaultFontSizes option to theme.json. ([56661](https://github.com/WordPress/gutenberg/pull/56661)) +- Font Library: Add wp_get_font_dir() function. ([57730](https://github.com/WordPress/gutenberg/pull/57730)) + +#### Custom Fields +- Block Bindings: Disable editing of bound block attributes in editor UI. ([58085](https://github.com/WordPress/gutenberg/pull/58085)) + +#### Block Editor +- Add effects/box shadow tools to block inspector. ([57654](https://github.com/WordPress/gutenberg/pull/57654)) + + +### Enhancements + +- Add gettext content when translating 'Header'. ([51066](https://github.com/WordPress/gutenberg/pull/51066)) +- Disable lock button if user cannot control lock state. ([57274](https://github.com/WordPress/gutenberg/pull/57274)) +- Editor: Unify the Editor Mode preference. ([57642](https://github.com/WordPress/gutenberg/pull/57642)) +- Element: Start reexporting PureComponent. ([58076](https://github.com/WordPress/gutenberg/pull/58076)) +- Live Preview: Show the current theme name on the theme activation modal. ([57588](https://github.com/WordPress/gutenberg/pull/57588)) +- Remove right negative margin from pinned items. ([57666](https://github.com/WordPress/gutenberg/pull/57666)) +- Unify the preferences modal UI between post and site editor. ([57639](https://github.com/WordPress/gutenberg/pull/57639)) +- Update style revision top toolbar text. ([58057](https://github.com/WordPress/gutenberg/pull/58057)) +- Use ClipboardJS latest version and clean up focus loss workaround. ([57156](https://github.com/WordPress/gutenberg/pull/57156)) + +#### Components +- Add opt-in prop for 40px default size for `BoxControl`, `BorderControl`, and `BorderBoxControl`. ([56185](https://github.com/WordPress/gutenberg/pull/56185)) +- BorderControl: Replace style picker with ToggleGroupControl. ([57562](https://github.com/WordPress/gutenberg/pull/57562)) +- ColorPicker: Store internal HSLA state for better slider UX. ([57555](https://github.com/WordPress/gutenberg/pull/57555)) +- Migrate PaletteEdit and CircularOptionPicker tests from user-event to ariakit/test. ([57809](https://github.com/WordPress/gutenberg/pull/57809)) +- Replace `TabPanel` with `Tabs` in the Editor Preferences Modal. ([57293](https://github.com/WordPress/gutenberg/pull/57293)) +- Theme: Set `color` on wrapper. ([58095](https://github.com/WordPress/gutenberg/pull/58095)) +- Tooltip: No-op when nested inside another Tooltip component. ([57202](https://github.com/WordPress/gutenberg/pull/57202)) +- `BoxControl`: Update design. ([56665](https://github.com/WordPress/gutenberg/pull/56665)) + +#### Interactivity API +- Render the root interactive blocks. ([57729](https://github.com/WordPress/gutenberg/pull/57729)) +- Interactivity Router: Replace `data-wp-navigation-id` with `data-wp-router-region`. ([58191](https://github.com/WordPress/gutenberg/pull/58191)) +- Interactivity: Export `withScope()` and allow to use it with asynchronous operations. ([58013](https://github.com/WordPress/gutenberg/pull/58013)) +- Prevent the use of components in `wp-text`. ([57879](https://github.com/WordPress/gutenberg/pull/57879)) +- Remove wp-data-navigation-link directive. ([57853](https://github.com/WordPress/gutenberg/pull/57853)) +- Server Directive Processing Refactor. ([58066](https://github.com/WordPress/gutenberg/pull/58066)) +- Update `preact`, `@preact/signals` and `deepsignal` dependencies. ([57891](https://github.com/WordPress/gutenberg/pull/57891)) + +#### Block Editor +- Add copy link button to Link UI. ([58170](https://github.com/WordPress/gutenberg/pull/58170)) +- Improve LinkControl preview. ([57775](https://github.com/WordPress/gutenberg/pull/57775)) +- Keep Link UI open upon initial link creation when used in RichText. ([57726](https://github.com/WordPress/gutenberg/pull/57726)) +- List View: Displace list view items when dragging (a bit more WYSIWYG). ([56625](https://github.com/WordPress/gutenberg/pull/56625)) +- Show initial suggestions in rich text Link UI. ([57743](https://github.com/WordPress/gutenberg/pull/57743)) + +#### Typography +- Font Library Modal: Reset the selected font when installing a new font. ([57817](https://github.com/WordPress/gutenberg/pull/57817)) +- Font Library: Disable font library UI using a PHP filter. ([57818](https://github.com/WordPress/gutenberg/pull/57818)) +- Font Library: Filter fonts upload directory. ([57697](https://github.com/WordPress/gutenberg/pull/57697)) +- Font Library: Use data or src file to define font collection data. ([57734](https://github.com/WordPress/gutenberg/pull/57734)) + +#### Block Library +- Add more taxonomy options to the post navigation link. ([48912](https://github.com/WordPress/gutenberg/pull/48912)) +- Add: Footnotes support for other CPT's. ([57353](https://github.com/WordPress/gutenberg/pull/57353)) +- Better navigation link variations for post types / taxonomies. ([56100](https://github.com/WordPress/gutenberg/pull/56100)) +- Remove "blocks" from copy and delete labels. ([57769](https://github.com/WordPress/gutenberg/pull/57769)) + +#### Data Views +- DataViews: Enable grid layout for templates & parts by default. ([58137](https://github.com/WordPress/gutenberg/pull/58137)) +- DataViews: Make dataviews powered page patterns stable. ([58139](https://github.com/WordPress/gutenberg/pull/58139)) +- DataViews: Make the "Manage Pages" stable. ([58166](https://github.com/WordPress/gutenberg/pull/58166)) + +#### Site Editor +- Group templates in sidebar list. ([57711](https://github.com/WordPress/gutenberg/pull/57711)) +- Initial routing refactoring to separate preview from list view. ([57938](https://github.com/WordPress/gutenberg/pull/57938)) +- Iterate on warning text for block removal for query/post template/post content. ([58138](https://github.com/WordPress/gutenberg/pull/58138)) + +#### Block API +- Block Bindings: Update source registration syntax and remove APIs that should be private. ([58205](https://github.com/WordPress/gutenberg/pull/58205)) +- Block Hooks: Do not remove toggle if hooked block is present elsewhere. ([57928](https://github.com/WordPress/gutenberg/pull/57928)) + +#### Synced Patterns +- Add basic pattern overrides end-to-end tests. ([57792](https://github.com/WordPress/gutenberg/pull/57792)) +- Use a patch format and support `linkTarget` of `core/button` for Pattern Overrides. ([58165](https://github.com/WordPress/gutenberg/pull/58165)) + +#### Patterns +- Add image block support for pattern overrides. ([57909](https://github.com/WordPress/gutenberg/pull/57909)) +- Outline editable blocks that are within a content-locked container. ([57901](https://github.com/WordPress/gutenberg/pull/57901)) + +#### Post Editor +- Post Lock: Use the new modal size preset. ([58197](https://github.com/WordPress/gutenberg/pull/58197)) + +#### Font Library +- Update the default collection data URL to the wordpress.org cdn. ([58186](https://github.com/WordPress/gutenberg/pull/58186)) + +#### Commands +- Minor command tweaks. ([58148](https://github.com/WordPress/gutenberg/pull/58148)) + +#### Extensibility +- Update Navigation block to render hooked inner blocks. ([57754](https://github.com/WordPress/gutenberg/pull/57754)) + +#### Global Styles +- Site editor: Add global styles changes to save flow. ([57470](https://github.com/WordPress/gutenberg/pull/57470)) + +#### Design Tools +- Pullquote Block: Add padding and margin support. ([45731](https://github.com/WordPress/gutenberg/pull/45731)) + + +### New APIs + +#### Block API +- Block Bindings API: Add block bindings PHP registration mechanisms and "Post meta" source under the experimental flag. ([57249](https://github.com/WordPress/gutenberg/pull/57249)) +- Block Bindings API: Refactor logic into Block Bindings class and singleton pattern. ([57742](https://github.com/WordPress/gutenberg/pull/57742)) + + +### Bug Fixes + +- (editor)(fix) Append the `edit-post-header-toolbar` class in NavigableToolbar for backward compatibility with plugin GUI injections. ([58154](https://github.com/WordPress/gutenberg/pull/58154)) +- Bring back the chevron. ([57807](https://github.com/WordPress/gutenberg/pull/57807)) +- Fix flaky "create a new pattern" test. ([57747](https://github.com/WordPress/gutenberg/pull/57747)) +- Fix site editor layout regressions. ([58077](https://github.com/WordPress/gutenberg/pull/58077)) +- Preferences: Add a proxy to retrieve the deprecated preferences with a deprecation message. ([58016](https://github.com/WordPress/gutenberg/pull/58016)) +- Remove unused argument from sprintf in pagination.js. ([57823](https://github.com/WordPress/gutenberg/pull/57823)) +- core-js: Only polyfill stable features. ([57674](https://github.com/WordPress/gutenberg/pull/57674)) + +#### Block Library +- Avatar block: Fix broken aligments in the editor. ([58114](https://github.com/WordPress/gutenberg/pull/58114)) +- Embed Block: Fix retry processing when embedding with trailing slash fails. ([58007](https://github.com/WordPress/gutenberg/pull/58007)) +- Lightbox: Fix "Expand on click" control being disabled unintentionally. ([56053](https://github.com/WordPress/gutenberg/pull/56053)) +- Modified Date Block: Don't render change date tool. ([57914](https://github.com/WordPress/gutenberg/pull/57914)) +- Only prioritise Quote transform where relevant. ([57749](https://github.com/WordPress/gutenberg/pull/57749)) +- Query Loop: Fix posts list variation detection. ([58194](https://github.com/WordPress/gutenberg/pull/58194)) + +#### Components +- Button: Always render the Tooltip component even when a tooltip should not be shown. ([56490](https://github.com/WordPress/gutenberg/pull/56490)) +- CustomSelect: Adjust `renderSelectedValue` to fix sizing. ([57865](https://github.com/WordPress/gutenberg/pull/57865)) +- ToggleGroupControl: Improve controlled value detection. ([57770](https://github.com/WordPress/gutenberg/pull/57770)) +- Tooltip: Accept specific tooltip props. ([58125](https://github.com/WordPress/gutenberg/pull/58125)) +- Tooltip: Forward and merge inner tooltip props correctly. ([57878](https://github.com/WordPress/gutenberg/pull/57878)) + +#### Data Views +- DataViews: Default sort order in templates by title. ([58175](https://github.com/WordPress/gutenberg/pull/58175)) +- DataViews: Don't always display horizontal scrollbar. ([58101](https://github.com/WordPress/gutenberg/pull/58101)) +- DataViews: Fix author sorting in templates and template parts. ([58167](https://github.com/WordPress/gutenberg/pull/58167)) + +#### Patterns +- Add black border back when editing synced pattern in the post editor. ([57631](https://github.com/WordPress/gutenberg/pull/57631)) +- Outline editable blocks when in a pattern that has locked children. ([57991](https://github.com/WordPress/gutenberg/pull/57991)) +- Remove text align control for paragraph, heading, and button in contentOnly editing mode. ([57906](https://github.com/WordPress/gutenberg/pull/57906)) + +#### List View +- Image Block: Make block name affect list view. ([57955](https://github.com/WordPress/gutenberg/pull/57955)) +- More Block: Make block name affect list view. ([58160](https://github.com/WordPress/gutenberg/pull/58160)) + +#### Block API +- Block Hooks: Fix toggle. ([57956](https://github.com/WordPress/gutenberg/pull/57956)) +- Fix formats not working in block bindings content. ([58055](https://github.com/WordPress/gutenberg/pull/58055)) + +#### Global Styles +- Correctly decode border color values. ([57876](https://github.com/WordPress/gutenberg/pull/57876)) +- Fix: Theme.json application of custom root selector for styles. ([58050](https://github.com/WordPress/gutenberg/pull/58050)) + +#### Data Layer +- Data: Allow binding registry selector to multiple registries. ([57943](https://github.com/WordPress/gutenberg/pull/57943)) +- Data: Fix memoized createRegistrySelector. ([57888](https://github.com/WordPress/gutenberg/pull/57888)) + +#### Typography +- #56734 When there is no font, the border should not appear. Display further guidance text. ([56825](https://github.com/WordPress/gutenberg/pull/56825)) +- Fluid typography: Do not calculate fluid font size when min and max viewport widths are equal. ([57866](https://github.com/WordPress/gutenberg/pull/57866)) + +#### Block Editor +- Fix regression: Content locking does not stops when an outside block is selected. ([57737](https://github.com/WordPress/gutenberg/pull/57737)) +- LinkControl: Remove unnecessary right padding of input fields. ([57784](https://github.com/WordPress/gutenberg/pull/57784)) + +#### Custom Fields +- Block Bindings: Fix button popover not showing in patterns. ([58219](https://github.com/WordPress/gutenberg/pull/58219)) + +#### Font Library +- Fix typo. ([58193](https://github.com/WordPress/gutenberg/pull/58193)) + +#### Synced Patterns +- Fix losing overrides after detaching patterns. ([58164](https://github.com/WordPress/gutenberg/pull/58164)) + +#### Interactivity API +- Prevent `wp-data-on=""` from creating `onDefault` handlers. ([57925](https://github.com/WordPress/gutenberg/pull/57925)) + +#### CSS & Styling +- Styles revisions: Remove body padding. ([57748](https://github.com/WordPress/gutenberg/pull/57748)) + +#### Templates API +- Fix visual indication of switch to default template in the post editor. ([57718](https://github.com/WordPress/gutenberg/pull/57718)) + + +### Accessibility + +#### Site Editor +- Fix font variants count color contrast ratio and l10n. ([58117](https://github.com/WordPress/gutenberg/pull/58117)) +- Make the site hub View Site link always visible. ([57423](https://github.com/WordPress/gutenberg/pull/57423)) + +#### Block Editor +- Fix parent selector button focus style and metrics. ([57728](https://github.com/WordPress/gutenberg/pull/57728)) +- Restore visual separator between mover buttons when show button label is on. ([57640](https://github.com/WordPress/gutenberg/pull/57640)) + +#### Widgets Editor +- Fix Widgets page Undo and Redo accessibility and keyboard interaction. ([57677](https://github.com/WordPress/gutenberg/pull/57677)) + + +### Performance + +- Add patterns load test. ([57828](https://github.com/WordPress/gutenberg/pull/57828)) +- Block editor: Avoid list re-rendering on select. ([57188](https://github.com/WordPress/gutenberg/pull/57188)) +- Block editor: Don't register shortcuts for preview editors. ([57984](https://github.com/WordPress/gutenberg/pull/57984)) +- Block editor: Fix performance regression after #57950. ([57971](https://github.com/WordPress/gutenberg/pull/57971)) +- Block editor: Use context for useBlockEditingMode. ([57950](https://github.com/WordPress/gutenberg/pull/57950)) +- BlockSwitcher: Defer transform calculations until the Dropdown is open. ([57892](https://github.com/WordPress/gutenberg/pull/57892)) +- Call variation through callback so it's only loaded when needed - in support of trac 59969. ([56952](https://github.com/WordPress/gutenberg/pull/56952)) +- Editor styles: Cache transform. ([57810](https://github.com/WordPress/gutenberg/pull/57810)) +- Footnotes: Combine format store subscription. ([58129](https://github.com/WordPress/gutenberg/pull/58129)) +- Iframe: Calc compat styles once per page load. ([57798](https://github.com/WordPress/gutenberg/pull/57798)) +- Measure typing with the top toolbar enabled. ([57709](https://github.com/WordPress/gutenberg/pull/57709)) +- Meta boxes: Don't initialise if there are none. ([57182](https://github.com/WordPress/gutenberg/pull/57182)) +- Patterns: Avoid fetching on load. ([57999](https://github.com/WordPress/gutenberg/pull/57999)) +- Site editor: Avoid fetching themes on load. ([57985](https://github.com/WordPress/gutenberg/pull/57985)) +- Site editor: Reduce artificial loading delay from 1s to 100ms. ([57953](https://github.com/WordPress/gutenberg/pull/57953)) +- Site editor: Remove store subscription per block. ([57995](https://github.com/WordPress/gutenberg/pull/57995)) +- Template Part & Query: Avoid server requests on mount. ([57987](https://github.com/WordPress/gutenberg/pull/57987)) +- Template part block: Avoid parsing ALL patterns on mount. ([57856](https://github.com/WordPress/gutenberg/pull/57856)) + +#### Block Editor +- Revert "Block editor: Avoid list re-rendering on select". ([58147](https://github.com/WordPress/gutenberg/pull/58147)) + +#### Post Editor +- Editor: Use hooks instead of HoCs for `EditorNotices`. ([57772](https://github.com/WordPress/gutenberg/pull/57772)) + + +### Experiments + +#### Data Views +- Add: Bulk actions to dataviews with the new design. ([57255](https://github.com/WordPress/gutenberg/pull/57255)) +- Data view list layout: Fix thumbnail dimensions. ([57774](https://github.com/WordPress/gutenberg/pull/57774)) +- Data views table layout: Update cell vertical alignment. ([57804](https://github.com/WordPress/gutenberg/pull/57804)) +- DataViews: Add description to pages. ([57793](https://github.com/WordPress/gutenberg/pull/57793)) +- DataViews: Add front page to pages page sidebar. ([57759](https://github.com/WordPress/gutenberg/pull/57759)) +- DataViews: Better management of `layout` param in templates. ([58116](https://github.com/WordPress/gutenberg/pull/58116)) +- DataViews: Make list layout the default for templates with the experiment enabled. ([57933](https://github.com/WordPress/gutenberg/pull/57933)) +- DataViews: Revert list view as default for pages. ([58081](https://github.com/WordPress/gutenberg/pull/58081)) +- DataViews: Revert list view as default for templates. ([58079](https://github.com/WordPress/gutenberg/pull/58079)) +- DataViews: Set primary field styles. ([57846](https://github.com/WordPress/gutenberg/pull/57846)) +- DataViews: Show loading / no result message for the list layout. ([57764](https://github.com/WordPress/gutenberg/pull/57764)) +- DataViews: Update template parts view. ([57952](https://github.com/WordPress/gutenberg/pull/57952)) +- DataViews: Use button for patterns, pages and templates preview field. ([58071](https://github.com/WordPress/gutenberg/pull/58071)) +- DataViews: Use table layout for templates when experiment disabled. ([57960](https://github.com/WordPress/gutenberg/pull/57960)) +- Stabilise view options button icon. ([57964](https://github.com/WordPress/gutenberg/pull/57964)) +- Update Grid layout design. ([57880](https://github.com/WordPress/gutenberg/pull/57880)) +- Update Pages preview field display. ([57919](https://github.com/WordPress/gutenberg/pull/57919)) +- Update Templates table layout. ([57930](https://github.com/WordPress/gutenberg/pull/57930)) +- Update: Show template sources on templates Dataviews sidebar. ([58124](https://github.com/WordPress/gutenberg/pull/58124)) + +#### Synced Patterns +- Add a control to reset pattern overrides. ([57845](https://github.com/WordPress/gutenberg/pull/57845)) +- Allow heading and button in Pattern Overrides. ([57789](https://github.com/WordPress/gutenberg/pull/57789)) + +#### Typography +- Download then upload font face assets when installing from a collection. ([57694](https://github.com/WordPress/gutenberg/pull/57694)) +- Use `slug` instead of `id` for Font Collection. ([57735](https://github.com/WordPress/gutenberg/pull/57735)) + +#### REST API +- Font Library Refactor. ([57688](https://github.com/WordPress/gutenberg/pull/57688)) + +#### Block Editor +- Allow drag and drop to create Rows and Galleries. ([56186](https://github.com/WordPress/gutenberg/pull/56186)) + + +### Documentation + +- Add a video demonstration to the Quick Start Guide. ([57834](https://github.com/WordPress/gutenberg/pull/57834)) +- Button: Improve `disabled`-related prop descriptions. ([57864](https://github.com/WordPress/gutenberg/pull/57864)) +- Components: Move CHANGELOG entries under the correct release. ([57885](https://github.com/WordPress/gutenberg/pull/57885)) +- Docs: Fix typo in "The block wrapper" document. ([58106](https://github.com/WordPress/gutenberg/pull/58106)) +- Docs: Use 'key' in 'editor.BlockEdit' filter code examples. ([58119](https://github.com/WordPress/gutenberg/pull/58119)) +- Document files/directories requiring backmerging to WP Core for major release. ([58064](https://github.com/WordPress/gutenberg/pull/58064)) +- Fix the iframe markup of the embed video in the Quick Start Guide. ([57857](https://github.com/WordPress/gutenberg/pull/57857)) +- Fix: Link to the nodejs release page. ([57816](https://github.com/WordPress/gutenberg/pull/57816)) +- Fix: Typo on BlockListBlock comments. ([57814](https://github.com/WordPress/gutenberg/pull/57814)) +- Fix: Typos on __unstableSetTemporarilyEditingAsBlocks documentation. ([57768](https://github.com/WordPress/gutenberg/pull/57768)) +- Font Library: Add font collection JSON schema. ([57736](https://github.com/WordPress/gutenberg/pull/57736)) +- Prefixes all php filters with wpdocs_. ([53914](https://github.com/WordPress/gutenberg/pull/53914)) +- Remove the unnecessary TOC and fix grammar/formatting in the Patterns doc. ([57825](https://github.com/WordPress/gutenberg/pull/57825)) +- Remove the 👋 emoji from the Block Editor Handbook. ([58023](https://github.com/WordPress/gutenberg/pull/58023)) +- Update versions-in-wordpress.md. ([57916](https://github.com/WordPress/gutenberg/pull/57916)) +- [Type] Developer Documentation - Fix removeAllNotices dispatch on the removeAllNotices doc section of @wordpress/notices. ([57436](https://github.com/WordPress/gutenberg/pull/57436)) + + +### Code Quality + +- Block Renaming - move backported WP 6.5 code to 6.5 compat dir. ([58126](https://github.com/WordPress/gutenberg/pull/58126)) +- Fix comments block. ([57820](https://github.com/WordPress/gutenberg/pull/57820)) +- Fon Library: Remove 'version' property from font collection schema. ([58025](https://github.com/WordPress/gutenberg/pull/58025)) +- Remove unneeded `margin: 0` override for `Notice` component consumer. ([57794](https://github.com/WordPress/gutenberg/pull/57794)) +- Rename __experimentalGetGlobalBlocksByName to getBlocksByName. ([58156](https://github.com/WordPress/gutenberg/pull/58156)) +- Scripts: Remove unused variable in bin/list-experimental-api-matches.sh. ([57771](https://github.com/WordPress/gutenberg/pull/57771)) +- Shadows: Prevent empty style object when removing shadow. ([58155](https://github.com/WordPress/gutenberg/pull/58155)) +- [Fonts API] removing files and files loading no longer needed. ([57972](https://github.com/WordPress/gutenberg/pull/57972)) + +#### Components +- PaletteEdit: Improve unit tests. ([57645](https://github.com/WordPress/gutenberg/pull/57645)) +- Tooltip and Button: Tidy up unit tests. ([57975](https://github.com/WordPress/gutenberg/pull/57975)) +- Tooltip: Add test for classname leakage. ([58182](https://github.com/WordPress/gutenberg/pull/58182)) + +#### Block Editor +- Soft deprecate custom 'pure' HoC in favor of 'React.memo'. ([57173](https://github.com/WordPress/gutenberg/pull/57173)) +- Stabilise RecursionProvider and useHasRecursion APIs. ([58120](https://github.com/WordPress/gutenberg/pull/58120)) +- Tidy up block patterns selectors. ([57913](https://github.com/WordPress/gutenberg/pull/57913)) + +#### Block Library +- Gallery Block: Remove duplicate return statement. ([57746](https://github.com/WordPress/gutenberg/pull/57746)) +- Navigation: Move the renderer class to the main navigation file. ([57979](https://github.com/WordPress/gutenberg/pull/57979)) + +#### Font Library +- Remove WP_Font_Family class that is no longer used. ([58184](https://github.com/WordPress/gutenberg/pull/58184)) + +#### Block Directory +- DownloadableBlocksPanel: Remove withSelect in favor of useSelect. ([58109](https://github.com/WordPress/gutenberg/pull/58109)) + +#### Patterns +- Stabilize the pattern overrides block context. ([58102](https://github.com/WordPress/gutenberg/pull/58102)) + +#### Block API +- Block Bindings: Remove the experimental flag. ([58089](https://github.com/WordPress/gutenberg/pull/58089)) + +#### Post Editor +- Editor: Use hooks instead of HoCs in 'PostScheduleCheck'. ([57833](https://github.com/WordPress/gutenberg/pull/57833)) + +#### Script Modules API +- Update the code and move it to the compat/wordpress-6.5 folder. ([57778](https://github.com/WordPress/gutenberg/pull/57778)) + +#### Data Views +- Remove obsolete check from dataviews modal actions title. ([57753](https://github.com/WordPress/gutenberg/pull/57753)) + + +### Tools + +- (chore) Revert bump to the v17.5.1 (draft) due to bug in the release found by manual testing. ([58027](https://github.com/WordPress/gutenberg/pull/58027)) +- Automate creation of Issue for major release PHP synchronisation. ([57890](https://github.com/WordPress/gutenberg/pull/57890)) +- Fix misplaced ReactRefreshWebpackPlugin. ([57777](https://github.com/WordPress/gutenberg/pull/57777)) + +#### Testing +- Add `setGutenbergExperiments` to `requestUtils`. ([56663](https://github.com/WordPress/gutenberg/pull/56663)) +- Add: End to end test to content locking stop editing as blocks behavior. ([57812](https://github.com/WordPress/gutenberg/pull/57812)) +- Attempt to fix php unit tests (variations api change). ([58090](https://github.com/WordPress/gutenberg/pull/58090)) +- Migrate 'block grouping' end-to-end tests to Playwright. ([57684](https://github.com/WordPress/gutenberg/pull/57684)) +- Migrate 'embedding' end-to-end tests to Playwright. ([57969](https://github.com/WordPress/gutenberg/pull/57969)) +- Migrate 'typewriter' end-to-end tests to Playwright. ([57673](https://github.com/WordPress/gutenberg/pull/57673)) +- Remove unused Navigation block end-to-end test fixtures. ([57848](https://github.com/WordPress/gutenberg/pull/57848)) + +#### Build Tooling +- Update caniuse-lite package. ([58087](https://github.com/WordPress/gutenberg/pull/58087)) +- Update the cherry pick script to work with the new version of gh. ([57917](https://github.com/WordPress/gutenberg/pull/57917)) + + +### Various + +- Interactivity API: Fix data-wp-on-document flaky test. ([58008](https://github.com/WordPress/gutenberg/pull/58008)) +- Interactivity API: Fix flaky test on-window. ([58134](https://github.com/WordPress/gutenberg/pull/58134)) +- Pattern Categories: Fix capitalization. ([58112](https://github.com/WordPress/gutenberg/pull/58112)) +- Remove check-latest-npm validation. ([57797](https://github.com/WordPress/gutenberg/pull/57797)) + +#### Interactivity API +- Create `@wordpress/interactivity-router` module. ([57924](https://github.com/WordPress/gutenberg/pull/57924)) +- Fix flaky test on-window, remove duplicate expect on-document. ([58181](https://github.com/WordPress/gutenberg/pull/58181)) +- Remove `data-wp-slot` and `data-wp-fill`. ([57854](https://github.com/WordPress/gutenberg/pull/57854)) +- Remove unused `state` and rename `props` to `attributes` in `getElement()`. ([57974](https://github.com/WordPress/gutenberg/pull/57974)) + +#### Patterns +- Remove pattern override experiment completely. ([58105](https://github.com/WordPress/gutenberg/pull/58105)) +- Update pattern overrides to use a hard coded support array. ([57912](https://github.com/WordPress/gutenberg/pull/57912)) + +#### Data Views +- Dataviews: Add Bulk actions to page. ([57826](https://github.com/WordPress/gutenberg/pull/57826)) + +#### Post Editor +- Add description to the save panel header when nothing is checked. ([57716](https://github.com/WordPress/gutenberg/pull/57716)) + +#### HTML API +- Backport updates from Core. ([57022](https://github.com/WordPress/gutenberg/pull/57022)) + +#### Block Editor +- Video Block: Add raw transformation from `video` html. ([47159](https://github.com/WordPress/gutenberg/pull/47159)) + + +## First time contributors + +The following PRs were merged by first time contributors: + +- @kt-12: Call variation through callback so it's only loaded when needed - in support of trac 59969. ([56952](https://github.com/WordPress/gutenberg/pull/56952)) +- @leomuniz: [Type] Developer Documentation - Fix removeAllNotices dispatch on the removeAllNotices doc section of @wordpress/notices. ([57436](https://github.com/WordPress/gutenberg/pull/57436)) + + +## Contributors + +The following contributors merged PRs in this release: + +@aaronrobertshaw @afercia @ajlende @andrewserong @annezazu @artemiomorales @arthur791004 @atachibana @bacoords @bph @brookewp @c4rl0sbr4v0 @carolinan @chad1008 @ciampo @creativecoder @DAreRodz @dcalhoun @derekblank @dmsnell @draganescu @ecgan @ellatrix @fluiddot @fullofcaffeine @gaambo @geriux @getdave @glendaviesnz @gonzomir @inc2734 @jameskoster @jeryj @jffng @jorgefilipecosta @jsnajdr @kevin940726 @kt-12 @leomuniz @luisherranz @MaggieCabrera @Mamaduka @matiasbenedetto @mcsf @michalczaplinski @mikachan @mirka @ndiego @noisysocks @ntsekouras @oandregal @ockham @oguzkocer @pbking @ramonjd @richtabor @SantosGuillamot @scruffian @SiobhyB @sirreal @swissspidy @t-hamano @talldan @tellthemachines @tjcafferkey @tyxla @vcanales @youknowriad + + += 17.5.2 = + +## Changelog + +### Bug Fixes + +- (Preferences)(17.5.1)(fix) Remove non-core-migrated preferences from the deprecation proxy for `get` ([58153](https://github.com/WordPress/gutenberg/pull/58153)) +- (editor)(fix) Append the `edit-post-header-toolbar` class in NavigableToolbar for backward compatibility with plugin GUI injections ([58154](https://github.com/WordPress/gutenberg/pull/58154)) + +## Contributors + +The following contributors merged PRs in this release: + +@fullofcaffeine + + + + = 17.5.1 = ## Changelog diff --git a/docs/getting-started/fundamentals/block-in-the-editor.md b/docs/getting-started/fundamentals/block-in-the-editor.md index 56ba72c283bdf7..2089d8aba2f0ec 100644 --- a/docs/getting-started/fundamentals/block-in-the-editor.md +++ b/docs/getting-started/fundamentals/block-in-the-editor.md @@ -3,6 +3,7 @@ The Block Editor is a React Single Page Application (SPA) and every block in the editor is displayed through a React component defined in the `edit` property of the settings object used to [register the block on the client](https://developer.wordpress.org/block-editor/getting-started/fundamentals/registration-of-a-block/#registration-of-the-block-with-javascript-client-side). The `props` object received by the block's `Edit` React component includes: + - [`attributes`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#attributes) - attributes object - [`setAttributes`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#setattributes) - method to update the attributes object - [`isSelected`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#isselected) - boolean that communicates whether the block is currently selected @@ -14,18 +15,21 @@ The WordPress Gutenberg project uses A good workflow when using a component for the Block Editor is: + - Import the component from a WordPress package - Add the corresponding code for the component to your project in JSX format - Most built-in components will be used to set [block attributes](https://developer.wordpress.org/block-editor/getting-started/fundamentals/block-json/#using-attributes-to-store-block-data), so define any necessary attributes in `block.json` and create event handlers to update those attributes with `setAttributes` in your component - If needed, adapt the code to be serialized and stored in the database - - ## Block Controls: Block Toolbar and Settings Sidebar To simplify block customization and ensure a consistent experience for users, there are a number of built-in UI patterns to help generate the editor preview. @@ -95,7 +98,6 @@ _See the [full block example](https://github.com/WordPress/block-development-exa Note that `BlockControls` is only visible when the block is currently selected and in visual editing mode. `BlockControls` are not shown when editing a block in HTML editing mode. - ### Settings Sidebar The Settings Sidebar is used to display less-often-used settings or settings that require more screen space. The Settings Sidebar should be used for **block-level settings only**. diff --git a/docs/getting-started/fundamentals/block-wrapper.md b/docs/getting-started/fundamentals/block-wrapper.md index 1f2404eca9b031..6582d3af3301fe 100644 --- a/docs/getting-started/fundamentals/block-wrapper.md +++ b/docs/getting-started/fundamentals/block-wrapper.md @@ -2,7 +2,7 @@ Each block's markup is wrapped by a container HTML tag that needs to have the proper attributes to fully work in the Block Editor and to reflect the proper block's style settings when rendered in the Block Editor and the front end. As developers, we have full control over the block's markup, and WordPress provides the tools to add the attributes that need to exist on the wrapper to our block's markup. -Ensuring proper attributes to the block wrapper is especially important when using custom styling or features like `supports`. +Ensuring proper attributes to the block wrapper is especially important when using custom styling or features like `supports`.
The use of supports generates a set of properties that need to be manually added to the wrapping element of the block so they're properly stored as part of the block data. @@ -10,8 +10,8 @@ The use of supports generates a set of properties that need to be m A block can have three sets of markup defined, each one of them with a specific target and purpose: -- The one for the **Block Editor**, defined through a `edit` React component passed to [`registerBlockType`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/#registerblocktype) when registering the block in the client. -- The one used to **save the block in the DB**, defined through a `save` function passed to `registerBlockType` when registering the block in the client. +- The one for the **Block Editor**, defined through a `edit` React component passed to [`registerBlockType`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/#registerblocktype) when registering the block in the client. +- The one used to **save the block in the DB**, defined through a `save` function passed to `registerBlockType` when registering the block in the client. - This markup will be returned to the front end on request if no dynamic render has been defined for the block. - The one used to **dynamically render the markup of the block** returned to the front end on request, defined through the `render_callback` on [`register_block_type`](https://developer.wordpress.org/reference/functions/register_block_type/) or the [`render`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/#render) PHP file in `block.json` - If defined, this server-side generated markup will be returned to the front end, ignoring the markup stored in DB. @@ -21,13 +21,14 @@ For the [`edit` React component and the `save` function](https://developer.wordp ## The Edit component's markup -The [`useBlockProps()`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-block-editor/#useblockprops) hook available on the [`@wordpress/block-editor`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-block-editor) allows passing the required attributes for the Block Editor to the `edit` block's outer wrapper. +The [`useBlockProps()`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-block-editor/#useblockprops) hook available on the [`@wordpress/block-editor`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-block-editor) allows passing the required attributes for the Block Editor to the `edit` block's outer wrapper. Among other things, the `useBlockProps()` hook takes care of including in this wrapper: -- An `id` for the block's markup -- Some accesibility and `data-` attributes + +- An `id` for the block's markup +- Some accessibility and `data-` attributes - Classes and inline styles reflecting custom settings, which include by default: - - The `wp-block` class + - The `wp-block` class - A class that contains the name of the block with its namespace For example, for the following piece of code of a block's registration in the client... @@ -43,18 +44,18 @@ _(see the [code above](https://github.com/WordPress/block-development-examples/b ...the markup of the block in the Block Editor could look like this: ```html -

Hello World - Block Editor

@@ -87,16 +88,16 @@ _(see the [code above](https://github.com/WordPress/block-development-examples/b

Hello World – Frontend

``` -Any additional classes and attributes for the `save` function of the block should be passed as an argument of `useBlockProps.save()` (see [example](https://github.com/WordPress/block-development-examples/blob/trunk/plugins/stylesheets-79a4c3/src/save.js)). +Any additional classes and attributes for the `save` function of the block should be passed as an argument of `useBlockProps.save()` (see [example](https://github.com/WordPress/block-development-examples/blob/trunk/plugins/stylesheets-79a4c3/src/save.js)). When you add `supports` for any feature, the proper classes get added to the object returned by the `useBlockProps.save()` hook. ```html

Hello World

``` @@ -105,10 +106,10 @@ _(check the [example](https://github.com/WordPress/block-development-examples/tr ## The server-side render markup -Any markup in the server-side render definition for the block can use the [`get_block_wrapper_attributes()`](https://developer.wordpress.org/reference/functions/get_block_wrapper_attributes/) function to generate the string of attributes required to reflect the block settings (see [example](https://github.com/WordPress/block-development-examples/blob/f68640f42d993f0866d1879f67c73910285ca114/plugins/block-dynamic-rendering-64756b/src/render.php#L11)). +Any markup in the server-side render definition for the block can use the [`get_block_wrapper_attributes()`](https://developer.wordpress.org/reference/functions/get_block_wrapper_attributes/) function to generate the string of attributes required to reflect the block settings (see [example](https://github.com/WordPress/block-development-examples/blob/f68640f42d993f0866d1879f67c73910285ca114/plugins/block-dynamic-rendering-64756b/src/render.php#L11)). ```php

>

-``` \ No newline at end of file +``` diff --git a/docs/getting-started/fundamentals/file-structure-of-a-block.md b/docs/getting-started/fundamentals/file-structure-of-a-block.md index 72531ccfb2b272..b52f5c5efd0f76 100644 --- a/docs/getting-started/fundamentals/file-structure-of-a-block.md +++ b/docs/getting-started/fundamentals/file-structure-of-a-block.md @@ -30,7 +30,8 @@ This file contains the [metadata of the block](https://developer.wordpress.org/b Among other data it provides properties to define the paths of the files involved in the block's behaviour, output and style. If there's a build process involved, this `block.json` along with the generated files are placed into a destination folder (usually the `build` folder) so the paths provided target to the bundled versions of these files. -The most relevant properties that can be defined in a `block.json` to set the files involved in the block's behaviour, output or style are: +The most relevant properties that can be defined in a `block.json` to set the files involved in the block's behaviour, output, or style are: + - The [`editorScript`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/#editor-script) property, usually set with the path of a bundled `index.js` file (output build from `src/index.js`). - The [`style`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/#style) property, usually set with the path of a bundled `style-index.css` file (output build from `src/style.(css|scss|sass)`). - The [`editorStyle`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/#editor-style) property, usually set with the path of a bundled `index.css` (output build from `src/editor.(css|scss|sass)`). diff --git a/docs/getting-started/fundamentals/markup-representation-block.md b/docs/getting-started/fundamentals/markup-representation-block.md index 506d0feb8d3d17..b048160907a259 100644 --- a/docs/getting-started/fundamentals/markup-representation-block.md +++ b/docs/getting-started/fundamentals/markup-representation-block.md @@ -1,8 +1,9 @@ # Markup representation of a block -When stored, in the database (DB) or in templates as HTML files, blocks are represented using a [specific HTML grammar](https://developer.wordpress.org/block-editor/explanations/architecture/key-concepts/#data-and-attributes), which is technically valid HTML based on HTML comments that act as explicit block delimiters +When stored in the database or in templates as HTML files, blocks are represented using a [specific HTML grammar](https://developer.wordpress.org/block-editor/explanations/architecture/key-concepts/#data-and-attributes), which is technically valid HTML based on HTML comments that act as explicit block delimiters These are some of the rules for the markup used to represent a block: + - All core block comments start with a prefix and the block name: `wp:blockname` - For custom blocks, `blockname` is `namespace/blockname` - The comment can be a single line, self-closing, or wrapper for HTML content. @@ -17,21 +18,22 @@ _Example: Markup representation of an `image` core block_ ``` The [markup representation of a block is parsed for the Block Editor](https://developer.wordpress.org/block-editor/explanations/architecture/data-flow/) and the block's output for the front end: + - In the editor, WordPress parses this block markup, captures its data and loads its `edit` version - In the front end, WordPress parses this block markup, captures its data and generates its final HTML markup Whenever a block is saved, the `save` function, defined when the [block is registered in the client](https://developer.wordpress.org/block-editor/getting-started/fundamentals/registration-of-a-block/#registration-of-the-block-with-javascript-client-side), is called to return the markup that will be saved into the database within the block delimiter's comment. If `save` is `null` (common case for blocks with dynamic rendering), only a single line block delimiter's comment is stored, along with any attributes The Post Editor checks that the markup created by the `save` function is identical to the block's markup saved to the database: + - If there are any differences, the Post Editor triggers a [block validation error](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#validation). - Block validation errors usually happen when a block’s `save` function is updated to change the markup produced by the block. - A block developer can mitigate these issues by adding a [**block deprecation**](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-deprecation/) to register the change in the block. -The markup of a **block with dynamic rendering** is expected to change so the markup of these blocks is not saved to the database. What is saved in the DB as representation of the block, for blocks with dynamic rendering, is a single line of HTML consisting on just the block delimiter's comment (including block attributes values). That HTML is not subject to the Post Editor’s validation. +The markup of a **block with dynamic rendering** is expected to change so the markup of these blocks is not saved to the database. What is saved in the database as representation of the block, for blocks with dynamic rendering, is a single line of HTML consisting on just the block delimiter's comment (including block attributes values). That HTML is not subject to the Post Editor’s validation. _Example: Markup representation of a block with dynamic rendering (`save` = `null`) and attributes_ - ```html ``` diff --git a/docs/getting-started/fundamentals/registration-of-a-block.md b/docs/getting-started/fundamentals/registration-of-a-block.md index 7d7b0e5ac31634..65cd85b0fa3e5b 100644 --- a/docs/getting-started/fundamentals/registration-of-a-block.md +++ b/docs/getting-started/fundamentals/registration-of-a-block.md @@ -69,6 +69,7 @@ The content of block.json (or any other .json file) ca
The client-side block settings object passed as a second parameter includes two especially relevant properties: + - `edit`: The React component that gets used in the editor for our block. - `save`: The function that returns the static HTML markup that gets saved to the Database. diff --git a/docs/getting-started/fundamentals/static-dynamic-rendering.md b/docs/getting-started/fundamentals/static-dynamic-rendering.md index 34d5432850c45e..214fb9da8b40fc 100644 --- a/docs/getting-started/fundamentals/static-dynamic-rendering.md +++ b/docs/getting-started/fundamentals/static-dynamic-rendering.md @@ -63,6 +63,7 @@ The markup stored for a block can be modified before it gets rendered on the fro Some examples of core blocks with static rendering are: + - [`separator`](https://github.com/WordPress/gutenberg/blob/trunk/packages/block-library/src/separator) (see its [`save`](https://github.com/WordPress/gutenberg/blob/trunk/packages/block-library/src/separator/save.js) function) - [`spacer`](https://github.com/WordPress/gutenberg/blob/trunk/packages/block-library/src/spacer) (see its [`save`](https://github.com/WordPress/gutenberg/blob/trunk/packages/block-library/src/spacer/save.js) function). - [`button`](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/button) (see its [`save`](https://github.com/WordPress/gutenberg/blob/trunk/packages/block-library/src/button/save.js) function). @@ -82,10 +83,12 @@ There are some common use cases for dynamic blocks: ### How to define dynamic rendering for a block A block can define dynamic rendering in two main ways: + 1. Via the `render_callback` argument that can be passed to the [`register_block_type()` function](https://developer.wordpress.org/block-editor/getting-started/fundamentals/registration-of-a-block/#registration-of-the-block-with-php-server-side). -1. Via a separate PHP file (usually named `render.php`) which path can be defined at the [`render` property of the `block.json`](https://developer.wordpress.org/block-editor/getting-started/fundamentals/block-json/#files-for-the-blocks-behavior-output-or-style). +2. Via a separate PHP file (usually named `render.php`) which path can be defined at the [`render` property of the `block.json`](https://developer.wordpress.org/block-editor/getting-started/fundamentals/block-json/#files-for-the-blocks-behavior-output-or-style). Both of these ways to define the block's dynamic rendering receive the following data: + - `$attributes` - The array of attributes for this block. - `$content` - Rendered block output (markup of the block as stored in the database). - `$block` - The instance of the [WP_Block](https://developer.wordpress.org/reference/classes/wp_block/) class that represents the block being rendered ([metadata of the block](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/)). @@ -95,7 +98,6 @@ Both of these ways to define the block's dynamic rendering receive the following For example, the [`site-title`](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/site-title) core block with the following function registered as [`render_callback`](https://github.com/WordPress/gutenberg/blob/trunk/packages/block-library/src/site-title/index.php)... - ```php function render_block_core_site_title( $attributes ) { $site_title = get_bloginfo( 'name' ); @@ -159,6 +161,7 @@ For dynamic blocks, the `save` callback function can return just `null`, which t Blocks with dynamic rendering can also save an HTML representation of the block as a backup. If you provide a server-side rendering callback, the HTML representing the block in the database will be replaced with the output of your callback, but will be rendered if your block is deactivated (the plugin that registers the block is uninstalled) or your render callback is removed. In some cases, the block saves an HTML representation of the block and uses a dynamic rendering to fine-tune this markup if some conditions are met. Some examples of core blocks using this approach are: + - The [`cover`](https://github.com/WordPress/gutenberg/blob/trunk/packages/block-library/src/cover) block saves a [full HTML representation of the block in the database](https://github.com/WordPress/gutenberg/blob/trunk/packages/block-library/src/cover/save.js). This markup is processed via a [`render_callback`](https://github.com/WordPress/gutenberg/blob/22741661998834e69db74ad863705ee2ce97b446/packages/block-library/src/cover/index.php#L74) when requested to do some PHP magic that dynamically [injects the featured image if the "use featured image" setting is enabled](https://github.com/WordPress/gutenberg/blob/22741661998834e69db74ad863705ee2ce97b446/packages/block-library/src/cover/index.php#L16). - The [`image`](https://github.com/WordPress/gutenberg/blob/trunk/packages/block-library/src/image) block also saves [its HTML representation in the database](https://github.com/WordPress/gutenberg/blob/trunk/packages/block-library/src/image/save.js) and processes it via a [`render_callback`](https://github.com/WordPress/gutenberg/blob/22741661998834e69db74ad863705ee2ce97b446/packages/block-library/src/image/index.php#L363) when requested to [add some attributes to the markup](https://github.com/WordPress/gutenberg/blob/22741661998834e69db74ad863705ee2ce97b446/packages/block-library/src/image/index.php#L18) if some conditions are met. diff --git a/docs/how-to-guides/block-tutorial/extending-the-query-loop-block.md b/docs/how-to-guides/block-tutorial/extending-the-query-loop-block.md index 9be4f3a993d203..d3628d991f872f 100644 --- a/docs/how-to-guides/block-tutorial/extending-the-query-loop-block.md +++ b/docs/how-to-guides/block-tutorial/extending-the-query-loop-block.md @@ -208,13 +208,13 @@ export const withBookQueryControls = ( BlockEdit ) => ( props ) => { // function to handle that. return isMyBooksVariation( props ) ? ( <> - + { /** Our custom component */ } ) : ( - + ); }; diff --git a/docs/how-to-guides/themes/global-settings-and-styles.md b/docs/how-to-guides/themes/global-settings-and-styles.md index 130b6271d13bdf..617c435b6d70c5 100644 --- a/docs/how-to-guides/themes/global-settings-and-styles.md +++ b/docs/how-to-guides/themes/global-settings-and-styles.md @@ -233,6 +233,7 @@ The settings section has the following structure: }, "custom": {}, "dimensions": { + "aspectRatio": false, "minHeight": false, }, "layout": { @@ -773,6 +774,7 @@ Each block declares which style properties it exposes via the [block supports me "text": "value" }, "dimensions": { + "aspectRatio": "value", "minHeight": "value" }, "filter": { diff --git a/docs/how-to-guides/themes/theme-support.md b/docs/how-to-guides/themes/theme-support.md index 88e69938737b7a..4a952c4de657d1 100644 --- a/docs/how-to-guides/themes/theme-support.md +++ b/docs/how-to-guides/themes/theme-support.md @@ -472,7 +472,7 @@ Use this setting to enable the following Global Styles settings: - color: link - spacing: blockGap, margin, padding - typography: lineHeight -- dimensions: minHeight +- dimensions: aspectRatio, minHeight - position: sticky ```php diff --git a/docs/manifest.json b/docs/manifest.json index 67b8fac99f7137..e8c2e0d9d2b0f0 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -1673,6 +1673,12 @@ "markdown_source": "../packages/icons/README.md", "parent": "packages" }, + { + "title": "@wordpress/interactivity-router", + "slug": "packages-interactivity-router", + "markdown_source": "../packages/interactivity-router/README.md", + "parent": "packages" + }, { "title": "@wordpress/interactivity", "slug": "packages-interactivity", diff --git a/docs/reference-guides/block-api/block-supports.md b/docs/reference-guides/block-api/block-supports.md index 4a59c34813448f..f035e026ff2e11 100644 --- a/docs/reference-guides/block-api/block-supports.md +++ b/docs/reference-guides/block-api/block-supports.md @@ -442,6 +442,7 @@ This value signals that a block supports some of the CSS style properties relate ```js supports: { dimensions: { + aspectRatio: true // Enable aspect ratio control. minHeight: true // Enable min height control. } } @@ -449,12 +450,13 @@ supports: { When a block declares support for a specific dimensions property, its attributes definition is extended to include the `style` attribute. -- `style`: attribute of `object` type with no default assigned. This is added when `minHeight` support is declared. It stores the custom values set by the user, e.g.: +- `style`: attribute of `object` type with no default assigned. This is added when `aspectRatio` or `minHeight` support is declared. It stores the custom values set by the user, e.g.: ```js attributes: { style: { dimensions: { + aspectRatio: "16/9", minHeight: "50vh" } } diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index bee68530eeb239..02c6c397d924ca 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -42,7 +42,7 @@ Create and save content to reuse across your site. Update the pattern, and the c - **Name:** core/block - **Category:** reusable - **Supports:** ~~customClassName~~, ~~html~~, ~~inserter~~, ~~renaming~~ -- **Attributes:** ref +- **Attributes:** overrides, ref ## Button @@ -238,7 +238,7 @@ Add an image or video with a text overlay. ([Source](https://github.com/WordPres - **Name:** core/cover - **Category:** media -- **Supports:** align, anchor, color (heading, text, ~~background~~, ~~enableContrastChecker~~), layout (~~allowJustification~~), spacing (blockGap, margin, padding), typography (fontSize, lineHeight), ~~html~~ +- **Supports:** align, anchor, color (heading, text, ~~background~~, ~~enableContrastChecker~~), dimensions (aspectRatio), layout (~~allowJustification~~), spacing (blockGap, margin, padding), typography (fontSize, lineHeight), ~~html~~ - **Attributes:** allowedBlocks, alt, backgroundType, contentPosition, customGradient, customOverlayColor, dimRatio, focalPoint, gradient, hasParallax, id, isDark, isRepeated, isUserOverlayColor, minHeight, minHeightUnit, overlayColor, tagName, templateLock, url, useFeaturedImage ## Details @@ -341,7 +341,7 @@ Gather blocks in a layout container. ([Source](https://github.com/WordPress/gute - **Name:** core/group - **Category:** design -- **Supports:** align (full, wide), anchor, ariaLabel, background (backgroundImage, backgroundSize), color (background, button, gradients, heading, link, text), dimensions (minHeight), layout (allowSizingOnChildren), position (sticky), spacing (blockGap, margin, padding), typography (fontSize, lineHeight), ~~html~~ +- **Supports:** align (full, wide), anchor, ariaLabel, background (backgroundImage, backgroundSize), color (background, button, gradients, heading, link, text), dimensions (aspectRatio, minHeight), layout (allowSizingOnChildren), position (sticky), spacing (blockGap, margin, padding), typography (fontSize, lineHeight), ~~html~~ - **Attributes:** allowedBlocks, tagName, templateLock ## Heading @@ -639,7 +639,7 @@ Displays the next or previous post link that is adjacent to the current post. ([ - **Name:** core/post-navigation-link - **Category:** theme - **Supports:** color (background, link, text), typography (fontSize, lineHeight), ~~html~~, ~~reusable~~ -- **Attributes:** arrow, inSameTerm, label, linkLabel, showTitle, taxonomy, textAlign, type +- **Attributes:** arrow, label, linkLabel, showTitle, taxonomy, textAlign, type ## Post Template diff --git a/docs/reference-guides/data/data-core-block-editor.md b/docs/reference-guides/data/data-core-block-editor.md index 7b0bd386daaf48..b03e905d166bc0 100644 --- a/docs/reference-guides/data/data-core-block-editor.md +++ b/docs/reference-guides/data/data-core-block-editor.md @@ -409,6 +409,19 @@ _Returns_ - `WPBlock[]`: Block objects. +### getBlocksByName + +Returns all blocks that match a blockName. Results include nested blocks. + +_Parameters_ + +- _state_ `Object`: Global application state. +- _blockName_ `?string`: Optional block name, if not specified, returns an empty array. + +_Returns_ + +- `Array`: Array of clientIds of blocks with name equal to blockName. + ### getBlockSelectionEnd Returns the current block selection end. This value may be null, and it may represent either a singular block selection or multi-selection end. A selection is singular if its start and end match. diff --git a/docs/reference-guides/data/data-core-notices.md b/docs/reference-guides/data/data-core-notices.md index e11e6f226169f9..d36098429811dd 100644 --- a/docs/reference-guides/data/data-core-notices.md +++ b/docs/reference-guides/data/data-core-notices.md @@ -277,7 +277,7 @@ export const ExampleComponent = () => { const notices = useSelect( ( select ) => select( noticesStore ).getNotices() ); - const { removeNotices } = useDispatch( noticesStore ); + const { removeAllNotices } = useDispatch( noticesStore ); return ( <>
    diff --git a/docs/reference-guides/filters/block-filters.md b/docs/reference-guides/filters/block-filters.md index 7e164a10e6c93f..35a041052889c0 100644 --- a/docs/reference-guides/filters/block-filters.md +++ b/docs/reference-guides/filters/block-filters.md @@ -189,7 +189,7 @@ const withMyPluginControls = createHigherOrderComponent( ( BlockEdit ) => { return ( props ) => { return ( <> - + My custom control diff --git a/docs/reference-guides/theme-json-reference/theme-json-living.md b/docs/reference-guides/theme-json-reference/theme-json-living.md index ee88f779ace1ce..5565336ffedb7f 100644 --- a/docs/reference-guides/theme-json-reference/theme-json-living.md +++ b/docs/reference-guides/theme-json-reference/theme-json-living.md @@ -34,7 +34,7 @@ Setting that enables the following UI tools: - background: backgroundImage - border: color, radius, style, width - color: link -- dimensions: minHeight +- dimensions: aspectRatio, minHeight - position: sticky - spacing: blockGap, margin, padding - typography: lineHeight @@ -116,6 +116,7 @@ Settings related to dimensions. | Property | Type | Default | Props | | --- | --- | --- |--- | +| aspectRatio | boolean | false | | | minHeight | boolean | false | | --- @@ -237,6 +238,7 @@ Dimensions styles | Property | Type | Props | | --- | --- |--- | +| aspectRatio | string, object | | | minHeight | string, object | | --- diff --git a/lib/block-supports/dimensions.php b/lib/block-supports/dimensions.php index 1ef43133c2cdfb..1980faba278175 100644 --- a/lib/block-supports/dimensions.php +++ b/lib/block-supports/dimensions.php @@ -74,6 +74,83 @@ function gutenberg_apply_dimensions_support( $block_type, $block_attributes ) { return $attributes; } +/** + * Renders server-side dimensions styles to the block wrapper. + * This block support uses the `render_block` hook to ensure that + * it is also applied to non-server-rendered blocks. + * + * @param string $block_content Rendered block content. + * @param array $block Block object. + * @return string Filtered block content. + */ +function gutenberg_render_dimensions_support( $block_content, $block ) { + $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] ); + $block_attributes = ( isset( $block['attrs'] ) && is_array( $block['attrs'] ) ) ? $block['attrs'] : array(); + $has_aspect_ratio_support = block_has_support( $block_type, array( 'dimensions', 'aspectRatio' ), false ); + + if ( + ! $has_aspect_ratio_support || + wp_should_skip_block_supports_serialization( $block_type, 'dimensions', 'aspectRatio' ) + ) { + return $block_content; + } + + $dimensions_block_styles = array(); + $dimensions_block_styles['aspectRatio'] = $block_attributes['style']['dimensions']['aspectRatio'] ?? null; + + // To ensure the aspect ratio does not get overridden by `minHeight` unset any existing rule. + if ( + isset( $dimensions_block_styles['aspectRatio'] ) + ) { + $dimensions_block_styles['minHeight'] = 'unset'; + } elseif ( + isset( $block_attributes['style']['dimensions']['minHeight'] ) || + isset( $block_attributes['minHeight'] ) + ) { + $dimensions_block_styles['aspectRatio'] = 'unset'; + } + + $styles = gutenberg_style_engine_get_styles( array( 'dimensions' => $dimensions_block_styles ) ); + + if ( ! empty( $styles['css'] ) ) { + // Inject dimensions styles to the first element, presuming it's the wrapper, if it exists. + $tags = new WP_HTML_Tag_Processor( $block_content ); + + if ( $tags->next_tag() ) { + $existing_style = $tags->get_attribute( 'style' ); + $updated_style = ''; + + if ( ! empty( $existing_style ) ) { + $updated_style = $existing_style; + if ( ! str_ends_with( $existing_style, ';' ) ) { + $updated_style .= ';'; + } + } + + $updated_style .= $styles['css']; + $tags->set_attribute( 'style', $updated_style ); + + if ( ! empty( $styles['classnames'] ) ) { + foreach ( explode( ' ', $styles['classnames'] ) as $class_name ) { + if ( + str_contains( $class_name, 'aspect-ratio' ) && + ! isset( $block_attributes['style']['dimensions']['aspectRatio'] ) + ) { + continue; + } + $tags->add_class( $class_name ); + } + } + } + + return $tags->get_updated_html(); + } + + return $block_content; +} + +add_filter( 'render_block', 'gutenberg_render_dimensions_support', 10, 2 ); + // Register the block support. WP_Block_Supports::get_instance()->register( 'dimensions', diff --git a/lib/block-supports/pattern.php b/lib/block-supports/pattern.php deleted file mode 100644 index 0c5868c1fea0cb..00000000000000 --- a/lib/block-supports/pattern.php +++ /dev/null @@ -1,46 +0,0 @@ - array( 'content' ), - 'core/heading' => array( 'content' ), - 'core/image' => array( 'url', 'title', 'alt' ), - 'core/button' => array( 'url', 'text' ), - ); - $pattern_support = array_key_exists( $block_type->name, $allowed_blocks ); - - if ( $pattern_support ) { - if ( ! $block_type->uses_context ) { - $block_type->uses_context = array(); - } - - if ( ! in_array( 'pattern/overrides', $block_type->uses_context, true ) ) { - $block_type->uses_context[] = 'pattern/overrides'; - } - } - } - - // Register the block support. - WP_Block_Supports::get_instance()->register( - 'pattern', - array( - 'register_attribute' => 'gutenberg_register_pattern_support', - ) - ); -} diff --git a/lib/block-supports/shadow.php b/lib/block-supports/shadow.php index 4a28c98b79325d..87258930faf10e 100644 --- a/lib/block-supports/shadow.php +++ b/lib/block-supports/shadow.php @@ -53,9 +53,8 @@ function gutenberg_apply_shadow_support( $block_type, $block_attributes ) { $shadow_block_styles = array(); - $preset_shadow = array_key_exists( 'shadow', $block_attributes ) ? "var:preset|shadow|{$block_attributes['shadow']}" : null; - $custom_shadow = isset( $block_attributes['style']['shadow'] ) ? $block_attributes['style']['shadow'] : null; - $shadow_block_styles['shadow'] = $preset_shadow ? $preset_shadow : $custom_shadow; + $custom_shadow = $block_attributes['style']['shadow'] ?? null; + $shadow_block_styles['shadow'] = $custom_shadow; $attributes = array(); $styles = gutenberg_style_engine_get_styles( $shadow_block_styles ); diff --git a/lib/blocks.php b/lib/blocks.php index d5283afeb7f999..e1d4622a0f23da 100644 --- a/lib/blocks.php +++ b/lib/blocks.php @@ -440,27 +440,6 @@ function gutenberg_legacy_wp_block_post_meta( $value, $object_id, $meta_key, $si add_filter( 'default_post_metadata', 'gutenberg_legacy_wp_block_post_meta', 10, 4 ); -/** - * Registers the metadata block attribute for all block types. - * - * @param array $args Array of arguments for registering a block type. - * @return array $args - */ -function gutenberg_register_metadata_attribute( $args ) { - // Setup attributes if needed. - if ( ! isset( $args['attributes'] ) || ! is_array( $args['attributes'] ) ) { - $args['attributes'] = array(); - } - - if ( ! array_key_exists( 'metadata', $args['attributes'] ) ) { - $args['attributes']['metadata'] = array( - 'type' => 'object', - ); - } - - return $args; -} -add_filter( 'register_block_type_args', 'gutenberg_register_metadata_attribute' ); /** * Strips all HTML from the content of footnotes, and sanitizes the ID. diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index f4266a7ef66dd5..af074c22ead450 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -211,6 +211,7 @@ class WP_Theme_JSON_Gutenberg { * @var array */ const PROPERTIES_METADATA = array( + 'aspect-ratio' => array( 'dimensions', 'aspectRatio' ), 'background' => array( 'color', 'gradient' ), 'background-color' => array( 'color', 'background' ), 'border-radius' => array( 'border', 'radius' ), @@ -381,7 +382,8 @@ class WP_Theme_JSON_Gutenberg { ), 'custom' => null, 'dimensions' => array( - 'minHeight' => null, + 'aspectRatio' => null, + 'minHeight' => null, ), 'layout' => array( 'contentSize' => null, @@ -486,7 +488,8 @@ class WP_Theme_JSON_Gutenberg { 'text' => null, ), 'dimensions' => array( - 'minHeight' => null, + 'aspectRatio' => null, + 'minHeight' => null, ), 'filter' => array( 'duotone' => null, @@ -661,6 +664,7 @@ public static function get_element_class_name( $element ) { array( 'color', 'heading' ), array( 'color', 'button' ), array( 'color', 'caption' ), + array( 'dimensions', 'aspectRatio' ), array( 'dimensions', 'minHeight' ), // BEGIN EXPERIMENTAL. // Allow `position.fixed` to be opted-in by default. @@ -2093,6 +2097,15 @@ protected static function compute_style_properties( $styles, $settings = array() $value = gutenberg_get_typography_font_size_value( array( 'size' => $value ) ); } + if ( 'aspect-ratio' === $css_property ) { + // For aspect ratio to work, other dimensions rules must be unset. + // This ensures that a fixed height does not override the aspect ratio. + $declarations[] = array( + 'name' => 'min-height', + 'value' => 'unset', + ); + } + $declarations[] = array( 'name' => $css_property, 'value' => $value, diff --git a/lib/compat/wordpress-6.5/block-bindings/block-bindings.php b/lib/compat/wordpress-6.5/block-bindings/block-bindings.php new file mode 100644 index 00000000000000..f9b33946613934 --- /dev/null +++ b/lib/compat/wordpress-6.5/block-bindings/block-bindings.php @@ -0,0 +1,163 @@ +register_source( $source_name, $source_properties ); + } +} + +/** + * Retrieves the list of registered block sources. + * + * @return array The list of registered block sources. + */ +if ( ! function_exists( 'wp_block_bindings_get_sources' ) ) { + function wp_block_bindings_get_sources() { + return wp_block_bindings()->get_sources(); + } +} + +/** + * Replaces the HTML content of a block based on the provided source value. + * + * @param string $block_content Block Content. + * @param string $block_name The name of the block to process. + * @param string $block_attr The attribute of the block we want to process. + * @param string $source_value The value used to replace the HTML. + * @return string The modified block content. + */ +function gutenberg_block_bindings_replace_html( $block_content, $block_name, $block_attr, $source_value ) { + $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block_name ); + if ( null === $block_type ) { + return; + } + + // Depending on the attribute source, the processing will be different. + switch ( $block_type->attributes[ $block_attr ]['source'] ) { + case 'html': + case 'rich-text': + $block_reader = new WP_HTML_Tag_Processor( $block_content ); + + // TODO: Support for CSS selectors whenever they are ready in the HTML API. + // In the meantime, support comma-separated selectors by exploding them into an array. + $selectors = explode( ',', $block_type->attributes[ $block_attr ]['selector'] ); + // Add a bookmark to the first tag to be able to iterate over the selectors. + $block_reader->next_tag(); + $block_reader->set_bookmark( 'iterate-selectors' ); + + // TODO: This shouldn't be needed when the `set_inner_html` function is ready. + // Store the parent tag and its attributes to be able to restore them later in the button. + // The button block has a wrapper while the paragraph and heading blocks don't. + if ( 'core/button' === $block_name ) { + $button_wrapper = $block_reader->get_tag(); + $button_wrapper_attribute_names = $block_reader->get_attribute_names_with_prefix( '' ); + $button_wrapper_attrs = array(); + foreach ( $button_wrapper_attribute_names as $name ) { + $button_wrapper_attrs[ $name ] = $block_reader->get_attribute( $name ); + } + } + + foreach ( $selectors as $selector ) { + // If the parent tag, or any of its children, matches the selector, replace the HTML. + if ( strcasecmp( $block_reader->get_tag( $selector ), $selector ) === 0 || $block_reader->next_tag( + array( + 'tag_name' => $selector, + ) + ) ) { + $block_reader->release_bookmark( 'iterate-selectors' ); + + // TODO: Use `set_inner_html` method whenever it's ready in the HTML API. + // Until then, it is hardcoded for the paragraph, heading, and button blocks. + // Store the tag and its attributes to be able to restore them later. + $selector_attribute_names = $block_reader->get_attribute_names_with_prefix( '' ); + $selector_attrs = array(); + foreach ( $selector_attribute_names as $name ) { + $selector_attrs[ $name ] = $block_reader->get_attribute( $name ); + } + $selector_markup = "<$selector>" . wp_kses_post( $source_value ) . ""; + $amended_content = new WP_HTML_Tag_Processor( $selector_markup ); + $amended_content->next_tag(); + foreach ( $selector_attrs as $attribute_key => $attribute_value ) { + $amended_content->set_attribute( $attribute_key, $attribute_value ); + } + if ( 'core/paragraph' === $block_name || 'core/heading' === $block_name ) { + return $amended_content->get_updated_html(); + } + if ( 'core/button' === $block_name ) { + $button_markup = "<$button_wrapper>{$amended_content->get_updated_html()}"; + $amended_button = new WP_HTML_Tag_Processor( $button_markup ); + $amended_button->next_tag(); + foreach ( $button_wrapper_attrs as $attribute_key => $attribute_value ) { + $amended_button->set_attribute( $attribute_key, $attribute_value ); + } + return $amended_button->get_updated_html(); + } + } else { + $block_reader->seek( 'iterate-selectors' ); + } + } + $block_reader->release_bookmark( 'iterate-selectors' ); + return $block_content; + + case 'attribute': + $amended_content = new WP_HTML_Tag_Processor( $block_content ); + if ( ! $amended_content->next_tag( + array( + // TODO: build the query from CSS selector. + 'tag_name' => $block_type->attributes[ $block_attr ]['selector'], + ) + ) ) { + return $block_content; + } + $amended_content->set_attribute( $block_type->attributes[ $block_attr ]['attribute'], esc_attr( $source_value ) ); + return $amended_content->get_updated_html(); + break; + + default: + return $block_content; + break; + } + return; +} diff --git a/lib/compat/wordpress-6.5/block-bindings/class-wp-block-bindings.php b/lib/compat/wordpress-6.5/block-bindings/class-wp-block-bindings.php new file mode 100644 index 00000000000000..68b51348010e3a --- /dev/null +++ b/lib/compat/wordpress-6.5/block-bindings/class-wp-block-bindings.php @@ -0,0 +1,63 @@ +sources[ $source_name ] = $source_properties; + } + + /** + * Retrieves the list of registered block sources. + * + * @return array The array of registered sources. + */ + public function get_sources() { + return $this->sources; + } +} diff --git a/lib/compat/wordpress-6.5/block-bindings/sources/pattern.php b/lib/compat/wordpress-6.5/block-bindings/sources/pattern.php new file mode 100644 index 00000000000000..65ddb7278e7035 --- /dev/null +++ b/lib/compat/wordpress-6.5/block-bindings/sources/pattern.php @@ -0,0 +1,37 @@ +attributes, array( 'metadata', 'id' ), false ) ) { + return null; + } + $block_id = $block_instance->attributes['metadata']['id']; + $attribute_override = _wp_array_get( $block_instance->context, array( 'pattern/overrides', $block_id, $attribute_name ), null ); + if ( null === $attribute_override ) { + return null; + } + switch ( $attribute_override[0] ) { + case 0: // remove + /** + * TODO: This currently doesn't remove the attribute, but only set it to an empty string. + * It's a temporary solution until the block binding API supports different operations. + */ + return ''; + case 1: // replace + return $attribute_override[1]; + default: + return null; + } + }; + wp_block_bindings_register_source( + 'pattern_attributes', + array( + 'label' => __( 'Pattern Attributes' ), + 'apply' => $pattern_source_callback, + ) + ); +} diff --git a/lib/experimental/block-bindings/sources/post-meta.php b/lib/compat/wordpress-6.5/block-bindings/sources/post-meta.php similarity index 87% rename from lib/experimental/block-bindings/sources/post-meta.php rename to lib/compat/wordpress-6.5/block-bindings/sources/post-meta.php index 2d53dab6d321ac..e52b4f289ccdd3 100644 --- a/lib/experimental/block-bindings/sources/post-meta.php +++ b/lib/compat/wordpress-6.5/block-bindings/sources/post-meta.php @@ -18,7 +18,9 @@ }; wp_block_bindings_register_source( 'post_meta', - __( 'Post Meta', 'gutenberg' ), - $post_meta_source_callback + array( + 'label' => __( 'Post Meta' ), + 'apply' => $post_meta_source_callback, + ) ); } diff --git a/lib/compat/wordpress-6.5/block-patterns.php b/lib/compat/wordpress-6.5/block-patterns.php index f43acda2a1035c..cce97cb19c6902 100644 --- a/lib/compat/wordpress-6.5/block-patterns.php +++ b/lib/compat/wordpress-6.5/block-patterns.php @@ -49,7 +49,7 @@ function gutenberg_register_taxonomy_patterns() { 'singular_name' => _x( 'Pattern Category', 'taxonomy singular name' ), 'add_new_item' => __( 'Add New Category' ), 'add_or_remove_items' => __( 'Add or remove pattern categories' ), - 'back_to_items' => __( '← Go to pattern categories' ), + 'back_to_items' => __( '← Go to Pattern Categories' ), 'choose_from_most_used' => __( 'Choose from the most used pattern categories' ), 'edit_item' => __( 'Edit Pattern Category' ), 'item_link' => __( 'Pattern Category Link' ), diff --git a/lib/compat/wordpress-6.5/blocks.php b/lib/compat/wordpress-6.5/blocks.php index b6890c14dc1f97..e1b91364fe22e5 100644 --- a/lib/compat/wordpress-6.5/blocks.php +++ b/lib/compat/wordpress-6.5/blocks.php @@ -22,3 +22,105 @@ function gutenberg_register_block_type_args_shim( $args ) { if ( ! method_exists( 'WP_Block_Type', 'get_variations' ) ) { add_filter( 'register_block_type_args', 'gutenberg_register_block_type_args_shim' ); } + + +/** + * Registers the metadata block attribute for all block types. + * + * @param array $args Array of arguments for registering a block type. + * @return array $args + */ +function gutenberg_register_metadata_attribute( $args ) { + // Setup attributes if needed. + if ( ! isset( $args['attributes'] ) || ! is_array( $args['attributes'] ) ) { + $args['attributes'] = array(); + } + + if ( ! array_key_exists( 'metadata', $args['attributes'] ) ) { + $args['attributes']['metadata'] = array( + 'type' => 'object', + ); + } + + return $args; +} +add_filter( 'register_block_type_args', 'gutenberg_register_metadata_attribute' ); + + +if ( ! function_exists( 'gutenberg_process_block_bindings' ) ) { + /** + * Process the block bindings attribute. + * + * @param string $block_content Block Content. + * @param array $block Block attributes. + * @param WP_Block $block_instance The block instance. + */ + function gutenberg_process_block_bindings( $block_content, $block, $block_instance ) { + + // Allowed blocks that support block bindings. + // TODO: Look for a mechanism to opt-in for this. Maybe adding a property to block attributes? + $allowed_blocks = array( + 'core/paragraph' => array( 'content' ), + 'core/heading' => array( 'content' ), + 'core/image' => array( 'url', 'title', 'alt' ), + 'core/button' => array( 'url', 'text', 'linkTarget' ), + ); + + // If the block doesn't have the bindings property or isn't one of the allowed block types, return. + if ( ! isset( $block['attrs']['metadata']['bindings'] ) || ! isset( $allowed_blocks[ $block_instance->name ] ) ) { + return $block_content; + } + + /* + * Assuming the following format for the bindings property of the "metadata" attribute: + * + * "bindings": { + * "title": { + * "source": { + * "name": "post_meta", + * "attributes": { "value": "text_custom_field" } + * } + * }, + * "url": { + * "source": { + * "name": "post_meta", + * "attributes": { "value": "text_custom_field" } + * } + * } + * } + */ + + $block_bindings_sources = wp_block_bindings_get_sources(); + $modified_block_content = $block_content; + foreach ( $block['attrs']['metadata']['bindings'] as $binding_attribute => $binding_source ) { + + // If the attribute is not in the list, process next attribute. + if ( ! in_array( $binding_attribute, $allowed_blocks[ $block_instance->name ], true ) ) { + continue; + } + // If no source is provided, or that source is not registered, process next attribute. + if ( ! isset( $binding_source['source'] ) || ! isset( $binding_source['source']['name'] ) || ! isset( $block_bindings_sources[ $binding_source['source']['name'] ] ) ) { + continue; + } + + $source_callback = $block_bindings_sources[ $binding_source['source']['name'] ]['apply']; + // Get the value based on the source. + if ( ! isset( $binding_source['source']['attributes'] ) ) { + $source_args = array(); + } else { + $source_args = $binding_source['source']['attributes']; + } + $source_value = $source_callback( $source_args, $block_instance, $binding_attribute ); + // If the value is null, process next attribute. + if ( is_null( $source_value ) ) { + continue; + } + + // Process the HTML based on the block and the attribute. + $modified_block_content = gutenberg_block_bindings_replace_html( $modified_block_content, $block_instance->name, $binding_attribute, $source_value ); + } + return $modified_block_content; + } +} + +add_filter( 'render_block', 'gutenberg_process_block_bindings', 20, 3 ); diff --git a/lib/compat/wordpress-6.5/class-wp-navigation-block-renderer.php b/lib/compat/wordpress-6.5/class-wp-navigation-block-renderer.php deleted file mode 100644 index 9c270f59fa220e..00000000000000 --- a/lib/compat/wordpress-6.5/class-wp-navigation-block-renderer.php +++ /dev/null @@ -1,664 +0,0 @@ -. - * - * @var array - */ - private static $nav_blocks_wrapped_in_list_item = array( - 'core/navigation-link', - 'core/home-link', - 'core/site-title', - 'core/site-logo', - 'core/navigation-submenu', - ); - - /** - * Used to determine which blocks need an
  • wrapper. - * - * @var array - */ - private static $needs_list_item_wrapper = array( - 'core/site-title', - 'core/site-logo', - ); - - /** - * Keeps track of all the navigation names that have been seen. - * - * @var array - */ - private static $seen_menu_names = array(); - - /** - * Returns whether or not this is responsive navigation. - * - * @param array $attributes The block attributes. - * @return bool Returns whether or not this is responsive navigation. - */ - private static function is_responsive( $attributes ) { - /** - * This is for backwards compatibility after the `isResponsive` attribute was been removed. - */ - - $has_old_responsive_attribute = ! empty( $attributes['isResponsive'] ) && $attributes['isResponsive']; - return isset( $attributes['overlayMenu'] ) && 'never' !== $attributes['overlayMenu'] || $has_old_responsive_attribute; - } - - /** - * Returns whether or not a navigation has a submenu. - * - * @param WP_Block_List $inner_blocks The list of inner blocks. - * @return bool Returns whether or not a navigation has a submenu. - */ - private static function has_submenus( $inner_blocks ) { - foreach ( $inner_blocks as $inner_block ) { - $inner_block_content = $inner_block->render(); - $p = new WP_HTML_Tag_Processor( $inner_block_content ); - if ( $p->next_tag( - array( - 'name' => 'LI', - 'class_name' => 'has-child', - ) - ) ) { - return true; - } - } - return false; - } - - /** - * Determine whether to load the view script. - * - * @param array $attributes The block attributes. - * @param WP_Block_List $inner_blocks The list of inner blocks. - * @return bool Returns whether or not to load the view script. - */ - private static function should_load_view_script( $attributes, $inner_blocks ) { - $has_submenus = static::has_submenus( $inner_blocks ); - $is_responsive_menu = static::is_responsive( $attributes ); - return ( $has_submenus && ( $attributes['openSubmenusOnClick'] || $attributes['showSubmenuIcon'] ) ) || $is_responsive_menu; - } - - /** - * Returns whether or not a block needs a list item wrapper. - * - * @param WP_Block $block The block. - * @return bool Returns whether or not a block needs a list item wrapper. - */ - private static function does_block_need_a_list_item_wrapper( $block ) { - return in_array( $block->name, static::$needs_list_item_wrapper, true ); - } - - /** - * Returns the markup for a single inner block. - * - * @param WP_Block $inner_block The inner block. - * @return string Returns the markup for a single inner block. - */ - private static function get_markup_for_inner_block( $inner_block ) { - $inner_block_content = $inner_block->render(); - if ( ! empty( $inner_block_content ) ) { - if ( static::does_block_need_a_list_item_wrapper( $inner_block ) ) { - return '
  • ' . $inner_block_content . '
  • '; - } - - return $inner_block_content; - } - } - - /** - * Returns the html for the inner blocks of the navigation block. - * - * @param array $attributes The block attributes. - * @param WP_Block_List $inner_blocks The list of inner blocks. - * @return string Returns the html for the inner blocks of the navigation block. - */ - private static function get_inner_blocks_html( $attributes, $inner_blocks ) { - $has_submenus = static::has_submenus( $inner_blocks ); - $should_load_view_script = static::should_load_view_script( $attributes, $inner_blocks ); - - $style = static::get_styles( $attributes ); - $class = static::get_classes( $attributes ); - $container_attributes = get_block_wrapper_attributes( - array( - 'class' => 'wp-block-navigation__container ' . $class, - 'style' => $style, - ) - ); - - $inner_blocks_html = ''; - $is_list_open = false; - - foreach ( $inner_blocks as $inner_block ) { - $is_list_item = in_array( $inner_block->name, static::$nav_blocks_wrapped_in_list_item, true ); - - if ( $is_list_item && ! $is_list_open ) { - $is_list_open = true; - $inner_blocks_html .= sprintf( - '
      ', - $container_attributes - ); - } - - if ( ! $is_list_item && $is_list_open ) { - $is_list_open = false; - $inner_blocks_html .= '
    '; - } - - $inner_blocks_html .= static::get_markup_for_inner_block( $inner_block ); - } - - if ( $is_list_open ) { - $inner_blocks_html .= '
'; - } - - // Add directives to the submenu if needed. - if ( $has_submenus && $should_load_view_script ) { - $tags = new WP_HTML_Tag_Processor( $inner_blocks_html ); - $inner_blocks_html = gutenberg_block_core_navigation_add_directives_to_submenu( $tags, $attributes ); - } - - return $inner_blocks_html; - } - - /** - * Gets the inner blocks for the navigation block from the navigation post. - * - * @param array $attributes The block attributes. - * @return WP_Block_List Returns the inner blocks for the navigation block. - */ - private static function get_inner_blocks_from_navigation_post( $attributes ) { - $navigation_post = get_post( $attributes['ref'] ); - if ( ! isset( $navigation_post ) ) { - return new WP_Block_List( array(), $attributes ); - } - - // Only published posts are valid. If this is changed then a corresponding change - // must also be implemented in `use-navigation-menu.js`. - if ( 'publish' === $navigation_post->post_status ) { - $parsed_blocks = parse_blocks( $navigation_post->post_content ); - - // 'parse_blocks' includes a null block with '\n\n' as the content when - // it encounters whitespace. This code strips it. - $compacted_blocks = gutenberg_block_core_navigation_filter_out_empty_blocks( $parsed_blocks ); - - // TODO - this uses the full navigation block attributes for the - // context which could be refined. - return new WP_Block_List( $compacted_blocks, $attributes ); - } - } - - /** - * Gets the inner blocks for the navigation block from the fallback. - * - * @param array $attributes The block attributes. - * @return WP_Block_List Returns the inner blocks for the navigation block. - */ - private static function get_inner_blocks_from_fallback( $attributes ) { - $fallback_blocks = gutenberg_block_core_navigation_get_fallback_blocks(); - - // Fallback my have been filtered so do basic test for validity. - if ( empty( $fallback_blocks ) || ! is_array( $fallback_blocks ) ) { - return new WP_Block_List( array(), $attributes ); - } - - return new WP_Block_List( $fallback_blocks, $attributes ); - } - - /** - * Gets the inner blocks for the navigation block. - * - * @param array $attributes The block attributes. - * @param WP_Block $block The parsed block. - * @return WP_Block_List Returns the inner blocks for the navigation block. - */ - private static function get_inner_blocks( $attributes, $block ) { - $inner_blocks = $block->inner_blocks; - - // Ensure that blocks saved with the legacy ref attribute name (navigationMenuId) continue to render. - if ( array_key_exists( 'navigationMenuId', $attributes ) ) { - $attributes['ref'] = $attributes['navigationMenuId']; - } - - // If: - // - the gutenberg plugin is active - // - `__unstableLocation` is defined - // - we have menu items at the defined location - // - we don't have a relationship to a `wp_navigation` Post (via `ref`). - // ...then create inner blocks from the classic menu assigned to that location. - if ( - defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN && - array_key_exists( '__unstableLocation', $attributes ) && - ! array_key_exists( 'ref', $attributes ) && - ! empty( gutenberg_block_core_navigation_get_menu_items_at_location( $attributes['__unstableLocation'] ) ) - ) { - $inner_blocks = gutenberg_block_core_navigation_get_inner_blocks_from_unstable_location( $attributes ); - } - - // Load inner blocks from the navigation post. - if ( array_key_exists( 'ref', $attributes ) ) { - $inner_blocks = static::get_inner_blocks_from_navigation_post( $attributes ); - } - - // If there are no inner blocks then fallback to rendering an appropriate fallback. - if ( empty( $inner_blocks ) ) { - $inner_blocks = static::get_inner_blocks_from_fallback( $attributes ); - } - - /** - * Filter navigation block $inner_blocks. - * Allows modification of a navigation block menu items. - * - * @since 6.1.0 - * - * @param \WP_Block_List $inner_blocks - */ - $inner_blocks = apply_filters( 'block_core_navigation_render_inner_blocks', $inner_blocks ); - - $post_ids = gutenberg_block_core_navigation_get_post_ids( $inner_blocks ); - if ( $post_ids ) { - _prime_post_caches( $post_ids, false, false ); - } - - return $inner_blocks; - } - - /** - * Gets the name of the current navigation, if it has one. - * - * @param array $attributes The block attributes. - * @return string Returns the name of the navigation. - */ - private static function get_navigation_name( $attributes ) { - - $navigation_name = $attributes['ariaLabel'] ?? ''; - - // Load the navigation post. - if ( array_key_exists( 'ref', $attributes ) ) { - $navigation_post = get_post( $attributes['ref'] ); - if ( ! isset( $navigation_post ) ) { - return $navigation_name; - } - - // Only published posts are valid. If this is changed then a corresponding change - // must also be implemented in `use-navigation-menu.js`. - if ( 'publish' === $navigation_post->post_status ) { - $navigation_name = $navigation_post->post_title; - - // This is used to count the number of times a navigation name has been seen, - // so that we can ensure every navigation has a unique id. - if ( isset( static::$seen_menu_names[ $navigation_name ] ) ) { - ++static::$seen_menu_names[ $navigation_name ]; - } else { - static::$seen_menu_names[ $navigation_name ] = 1; - } - } - } - - return $navigation_name; - } - - /** - * Returns the layout class for the navigation block. - * - * @param array $attributes The block attributes. - * @return string Returns the layout class for the navigation block. - */ - private static function get_layout_class( $attributes ) { - $layout_justification = array( - 'left' => 'items-justified-left', - 'right' => 'items-justified-right', - 'center' => 'items-justified-center', - 'space-between' => 'items-justified-space-between', - ); - - $layout_class = ''; - if ( - isset( $attributes['layout']['justifyContent'] ) && - isset( $layout_justification[ $attributes['layout']['justifyContent'] ] ) - ) { - $layout_class .= $layout_justification[ $attributes['layout']['justifyContent'] ]; - } - if ( isset( $attributes['layout']['orientation'] ) && 'vertical' === $attributes['layout']['orientation'] ) { - $layout_class .= ' is-vertical'; - } - - if ( isset( $attributes['layout']['flexWrap'] ) && 'nowrap' === $attributes['layout']['flexWrap'] ) { - $layout_class .= ' no-wrap'; - } - return $layout_class; - } - - /** - * Return classes for the navigation block. - * - * @param array $attributes The block attributes. - * @return string Returns the classes for the navigation block. - */ - private static function get_classes( $attributes ) { - // Restore legacy classnames for submenu positioning. - $layout_class = static::get_layout_class( $attributes ); - $colors = gutenberg_block_core_navigation_build_css_colors( $attributes ); - $font_sizes = gutenberg_block_core_navigation_build_css_font_sizes( $attributes ); - $is_responsive_menu = static::is_responsive( $attributes ); - - // Manually add block support text decoration as CSS class. - $text_decoration = $attributes['style']['typography']['textDecoration'] ?? null; - $text_decoration_class = sprintf( 'has-text-decoration-%s', $text_decoration ); - - // Sets the is-collapsed class when the navigation is set to always use the overlay. - // This saves us from needing to do this check in the view.js file (see the collapseNav function). - $is_collapsed_class = static::is_always_overlay( $attributes ) ? array( 'is-collapsed' ) : array(); - - $classes = array_merge( - $colors['css_classes'], - $font_sizes['css_classes'], - $is_responsive_menu ? array( 'is-responsive' ) : array(), - $layout_class ? array( $layout_class ) : array(), - $text_decoration ? array( $text_decoration_class ) : array(), - $is_collapsed_class - ); - return implode( ' ', $classes ); - } - - private static function is_always_overlay( $attributes ) { - return isset( $attributes['overlayMenu'] ) && 'always' === $attributes['overlayMenu']; - } - - /** - * Get styles for the navigation block. - * - * @param array $attributes The block attributes. - * @return string Returns the styles for the navigation block. - */ - private static function get_styles( $attributes ) { - $colors = gutenberg_block_core_navigation_build_css_colors( $attributes ); - $font_sizes = gutenberg_block_core_navigation_build_css_font_sizes( $attributes ); - $block_styles = isset( $attributes['styles'] ) ? $attributes['styles'] : ''; - return $block_styles . $colors['inline_styles'] . $font_sizes['inline_styles']; - } - - /** - * Get the responsive container markup - * - * @param array $attributes The block attributes. - * @param WP_Block_List $inner_blocks The list of inner blocks. - * @param string $inner_blocks_html The markup for the inner blocks. - * @return string Returns the container markup. - */ - private static function get_responsive_container_markup( $attributes, $inner_blocks, $inner_blocks_html ) { - $should_load_view_script = static::should_load_view_script( $attributes, $inner_blocks ); - $colors = gutenberg_block_core_navigation_build_css_colors( $attributes ); - $modal_unique_id = wp_unique_id( 'modal-' ); - - $responsive_container_classes = array( - 'wp-block-navigation__responsive-container', - implode( ' ', $colors['overlay_css_classes'] ), - ); - $open_button_classes = array( - 'wp-block-navigation__responsive-container-open', - ); - - $should_display_icon_label = isset( $attributes['hasIcon'] ) && true === $attributes['hasIcon']; - $toggle_button_icon = ''; - if ( isset( $attributes['icon'] ) ) { - if ( 'menu' === $attributes['icon'] ) { - $toggle_button_icon = ''; - } - } - $toggle_button_content = $should_display_icon_label ? $toggle_button_icon : __( 'Menu' ); - $toggle_close_button_icon = ''; - $toggle_close_button_content = $should_display_icon_label ? $toggle_close_button_icon : __( 'Close' ); - $toggle_aria_label_open = $should_display_icon_label ? 'aria-label="' . __( 'Open menu' ) . '"' : ''; // Open button label. - $toggle_aria_label_close = $should_display_icon_label ? 'aria-label="' . __( 'Close menu' ) . '"' : ''; // Close button label. - - // Add Interactivity API directives to the markup if needed. - $open_button_directives = ''; - $responsive_container_directives = ''; - $responsive_dialog_directives = ''; - $close_button_directives = ''; - if ( $should_load_view_script ) { - $open_button_directives = ' - data-wp-on--click="actions.openMenuOnClick" - data-wp-on--keydown="actions.handleMenuKeydown" - '; - $responsive_container_directives = ' - data-wp-class--has-modal-open="state.isMenuOpen" - data-wp-class--is-menu-open="state.isMenuOpen" - data-wp-watch="callbacks.initMenu" - data-wp-on--keydown="actions.handleMenuKeydown" - data-wp-on--focusout="actions.handleMenuFocusout" - tabindex="-1" - '; - $responsive_dialog_directives = ' - data-wp-bind--aria-modal="state.ariaModal" - data-wp-bind--aria-label="state.ariaLabel" - data-wp-bind--role="state.roleAttribute" - '; - $close_button_directives = ' - data-wp-on--click="actions.closeMenuOnClick" - '; - $responsive_container_content_directives = ' - data-wp-watch="callbacks.focusFirstElement" - '; - } - - return sprintf( - ' -
-
-
- -
- %2$s -
-
-
-
', - esc_attr( $modal_unique_id ), - $inner_blocks_html, - $toggle_aria_label_open, - $toggle_aria_label_close, - esc_attr( implode( ' ', $responsive_container_classes ) ), - esc_attr( implode( ' ', $open_button_classes ) ), - esc_attr( safecss_filter_attr( $colors['overlay_inline_styles'] ) ), - $toggle_button_content, - $toggle_close_button_content, - $open_button_directives, - $responsive_container_directives, - $responsive_dialog_directives, - $close_button_directives, - $responsive_container_content_directives - ); - } - - /** - * Get the wrapper attributes - * - * @param array $attributes The block attributes. - * @param WP_Block_List $inner_blocks A list of inner blocks. - * @return string Returns the navigation block markup. - */ - private static function get_nav_wrapper_attributes( $attributes, $inner_blocks ) { - $nav_menu_name = static::get_unique_navigation_name( $attributes ); - $should_load_view_script = static::should_load_view_script( $attributes, $inner_blocks ); - $is_responsive_menu = static::is_responsive( $attributes ); - $style = static::get_styles( $attributes ); - $class = static::get_classes( $attributes ); - $wrapper_attributes = get_block_wrapper_attributes( - array( - 'class' => $class, - 'style' => $style, - 'aria-label' => $nav_menu_name, - ) - ); - - if ( $is_responsive_menu ) { - $nav_element_directives = static::get_nav_element_directives( $should_load_view_script, $attributes ); - $wrapper_attributes .= ' ' . $nav_element_directives; - } - - return $wrapper_attributes; - } - - /** - * Get the nav element directives - * - * @param bool $should_load_view_script Whether or not the view script should be loaded. - * @return string the directives for the navigation element. - */ - private static function get_nav_element_directives( $should_load_view_script, $attributes ) { - if ( ! $should_load_view_script ) { - return ''; - } - // When adding to this array be mindful of security concerns. - $nav_element_context = wp_json_encode( - array( - 'overlayOpenedBy' => array(), - 'type' => 'overlay', - 'roleAttribute' => '', - 'ariaLabel' => __( 'Menu' ), - ), - JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP - ); - $nav_element_directives = ' - data-wp-interactive=\'{"namespace":"core/navigation"}\' - data-wp-context=\'' . $nav_element_context . '\' - '; - - // When the navigation overlayMenu attribute is set to "always" - // we don't need to use JavaScript to collapse the menu as we set the class manually. - if ( ! static::is_always_overlay( $attributes ) ) { - $nav_element_directives .= 'data-wp-init="callbacks.initNav"'; - $nav_element_directives .= ' '; // space separator - $nav_element_directives .= 'data-wp-class--is-collapsed="context.isCollapsed"'; - } - - return $nav_element_directives; - } - - /** - * Handle view script loading. - * - * @param array $attributes The block attributes. - * @param WP_Block $block The parsed block. - * @param WP_Block_List $inner_blocks The list of inner blocks. - */ - private static function handle_view_script_loading( $attributes, $block, $inner_blocks ) { - $should_load_view_script = static::should_load_view_script( $attributes, $inner_blocks ); - $is_gutenberg_plugin = defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN; - $view_js_file = 'wp-block-navigation-view'; - $script_handles = $block->block_type->view_script_handles; - - if ( $is_gutenberg_plugin ) { - if ( $should_load_view_script ) { - gutenberg_enqueue_module( '@wordpress/block-library/navigation-block' ); - } - // Remove the view script because we are using the module. - $block->block_type->view_script_handles = array_diff( $script_handles, array( $view_js_file ) ); - } else { - // If the script already exists, there is no point in removing it from viewScript. - if ( ! wp_script_is( $view_js_file ) ) { - - // If the script is not needed, and it is still in the `view_script_handles`, remove it. - if ( ! $should_load_view_script && in_array( $view_js_file, $script_handles, true ) ) { - $block->block_type->view_script_handles = array_diff( $script_handles, array( $view_js_file ) ); - } - // If the script is needed, but it was previously removed, add it again. - if ( $should_load_view_script && ! in_array( $view_js_file, $script_handles, true ) ) { - $block->block_type->view_script_handles = array_merge( $script_handles, array( $view_js_file ) ); - } - } - } - } - - /** - * Returns the markup for the navigation block. - * - * @param array $attributes The block attributes. - * @param WP_Block_List $inner_blocks The list of inner blocks. - * @return string Returns the navigation wrapper markup. - */ - private static function get_wrapper_markup( $attributes, $inner_blocks ) { - $inner_blocks_html = static::get_inner_blocks_html( $attributes, $inner_blocks ); - if ( static::is_responsive( $attributes ) ) { - return static::get_responsive_container_markup( $attributes, $inner_blocks, $inner_blocks_html ); - } - return $inner_blocks_html; - } - - /** - * Returns a unique name for the navigation. - * - * @param array $attributes The block attributes. - * @return string Returns a unique name for the navigation. - */ - private static function get_unique_navigation_name( $attributes ) { - $nav_menu_name = static::get_navigation_name( $attributes ); - - // If the menu name has been used previously then append an ID - // to the name to ensure uniqueness across a given post. - if ( isset( static::$seen_menu_names[ $nav_menu_name ] ) && static::$seen_menu_names[ $nav_menu_name ] > 1 ) { - $count = static::$seen_menu_names[ $nav_menu_name ]; - $nav_menu_name = $nav_menu_name . ' ' . ( $count ); - } - - return $nav_menu_name; - } - - /** - * Renders the navigation block. - * - * @param array $attributes The block attributes. - * @param string $content The saved content. - * @param WP_Block $block The parsed block. - * @return string Returns the navigation block markup. - */ - public static function render( $attributes, $content, $block ) { - /** - * Deprecated: - * The rgbTextColor and rgbBackgroundColor attributes - * have been deprecated in favor of - * customTextColor and customBackgroundColor ones. - * Move the values from old attrs to the new ones. - */ - if ( isset( $attributes['rgbTextColor'] ) && empty( $attributes['textColor'] ) ) { - $attributes['customTextColor'] = $attributes['rgbTextColor']; - } - - if ( isset( $attributes['rgbBackgroundColor'] ) && empty( $attributes['backgroundColor'] ) ) { - $attributes['customBackgroundColor'] = $attributes['rgbBackgroundColor']; - } - - unset( $attributes['rgbTextColor'], $attributes['rgbBackgroundColor'] ); - - $inner_blocks = static::get_inner_blocks( $attributes, $block ); - // Prevent navigation blocks referencing themselves from rendering. - if ( gutenberg_block_core_navigation_block_contains_core_navigation( $inner_blocks ) ) { - return ''; - } - - static::handle_view_script_loading( $attributes, $block, $inner_blocks ); - - return sprintf( - '', - static::get_nav_wrapper_attributes( $attributes, $inner_blocks ), - static::get_wrapper_markup( $attributes, $inner_blocks ) - ); - } -} diff --git a/lib/compat/wordpress-6.5/class-wp-script-modules.php b/lib/compat/wordpress-6.5/class-wp-script-modules.php new file mode 100644 index 00000000000000..f6a2a348f92ef3 --- /dev/null +++ b/lib/compat/wordpress-6.5/class-wp-script-modules.php @@ -0,0 +1,323 @@ + + */ + private $enqueued_before_registered = array(); + + /** + * Registers the script module if no script module with that script module + * identifier has already been registered. + * + * @since 6.5.0 + * + * @param string $id The identifier of the script module. Should be unique. It will be used in the + * final import map. + * @param string $src Optional. Full URL of the script module, or path of the script module relative + * to the WordPress root directory. If it is provided and the script module has + * not been registered yet, it will be registered. + * @param array $deps { + * Optional. List of dependencies. + * + * @type string|array $0... { + * An array of script module identifiers of the dependencies of this script + * module. The dependencies can be strings or arrays. If they are arrays, + * they need an `id` key with the script module identifier, and can contain + * an `import` key with either `static` or `dynamic`. By default, + * dependencies that don't contain an `import` key are considered static. + * + * @type string $id The script module identifier. + * @type string $import Optional. Import type. May be either `static` or + * `dynamic`. Defaults to `static`. + * } + * } + * @param string|false|null $version Optional. String specifying the script module version number. Defaults to false. + * It is added to the URL as a query string for cache busting purposes. If $version + * is set to false, the version number is the currently installed WordPress version. + * If $version is set to null, no version is added. + */ + public function register( string $id, string $src, array $deps = array(), $version = false ) { + if ( ! isset( $this->registered[ $id ] ) ) { + $dependencies = array(); + foreach ( $deps as $dependency ) { + if ( is_array( $dependency ) ) { + if ( ! isset( $dependency['id'] ) ) { + _doing_it_wrong( __METHOD__, __( 'Missing required id key in entry among dependencies array.' ), '6.5.0' ); + continue; + } + $dependencies[] = array( + 'id' => $dependency['id'], + 'import' => isset( $dependency['import'] ) && 'dynamic' === $dependency['import'] ? 'dynamic' : 'static', + ); + } elseif ( is_string( $dependency ) ) { + $dependencies[] = array( + 'id' => $dependency, + 'import' => 'static', + ); + } else { + _doing_it_wrong( __METHOD__, __( 'Entries in dependencies array must be either strings or arrays with an id key.' ), '6.5.0' ); + } + } + + $this->registered[ $id ] = array( + 'src' => $src, + 'version' => $version, + 'enqueue' => isset( $this->enqueued_before_registered[ $id ] ), + 'dependencies' => $dependencies, + ); + } + } + + /** + * Marks the script module to be enqueued in the page. + * + * If a src is provided and the script module has not been registered yet, it + * will be registered. + * + * @since 6.5.0 + * + * @param string $id The identifier of the script module. Should be unique. It will be used in the + * final import map. + * @param string $src Optional. Full URL of the script module, or path of the script module relative + * to the WordPress root directory. If it is provided and the script module has + * not been registered yet, it will be registered. + * @param array $deps { + * Optional. List of dependencies. + * + * @type string|array $0... { + * An array of script module identifiers of the dependencies of this script + * module. The dependencies can be strings or arrays. If they are arrays, + * they need an `id` key with the script module identifier, and can contain + * an `import` key with either `static` or `dynamic`. By default, + * dependencies that don't contain an `import` key are considered static. + * + * @type string $id The script module identifier. + * @type string $import Optional. Import type. May be either `static` or + * `dynamic`. Defaults to `static`. + * } + * } + * @param string|false|null $version Optional. String specifying the script module version number. Defaults to false. + * It is added to the URL as a query string for cache busting purposes. If $version + * is set to false, the version number is the currently installed WordPress version. + * If $version is set to null, no version is added. + */ + public function enqueue( string $id, string $src = '', array $deps = array(), $version = false ) { + if ( isset( $this->registered[ $id ] ) ) { + $this->registered[ $id ]['enqueue'] = true; + } elseif ( $src ) { + $this->register( $id, $src, $deps, $version ); + $this->registered[ $id ]['enqueue'] = true; + } else { + $this->enqueued_before_registered[ $id ] = true; + } + } + + /** + * Unmarks the script module so it will no longer be enqueued in the page. + * + * @since 6.5.0 + * + * @param string $id The identifier of the script module. + */ + public function dequeue( string $id ) { + if ( isset( $this->registered[ $id ] ) ) { + $this->registered[ $id ]['enqueue'] = false; + } + unset( $this->enqueued_before_registered[ $id ] ); + } + + /** + * Adds the hooks to print the import map, enqueued script modules and script + * module preloads. + * + * In classic themes, the script modules used by the blocks are not yet known + * when the `wp_head` actions is fired, so it needs to print everything in the + * footer. + * + * @since 6.5.0 + */ + public function add_hooks() { + $position = wp_is_block_theme() ? 'wp_head' : 'wp_footer'; + add_action( $position, array( $this, 'print_import_map' ) ); + add_action( $position, array( $this, 'print_enqueued_script_modules' ) ); + add_action( $position, array( $this, 'print_script_module_preloads' ) ); + } + + /** + * Prints the enqueued script modules using script tags with type="module" + * attributes. + * + * @since 6.5.0 + */ + public function print_enqueued_script_modules() { + foreach ( $this->get_marked_for_enqueue() as $id => $script_module ) { + wp_print_script_tag( + array( + 'type' => 'module', + 'src' => $this->get_versioned_src( $script_module ), + 'id' => $id . '-js-module', + ) + ); + } + } + + /** + * Prints the the static dependencies of the enqueued script modules using + * link tags with rel="modulepreload" attributes. + * + * If a script module is marked for enqueue, it will not be preloaded. + * + * @since 6.5.0 + */ + public function print_script_module_preloads() { + foreach ( $this->get_dependencies( array_keys( $this->get_marked_for_enqueue() ), array( 'static' ) ) as $id => $script_module ) { + // Don't preload if it's marked for enqueue. + if ( true !== $script_module['enqueue'] ) { + echo sprintf( + '', + esc_url( $this->get_versioned_src( $script_module ) ), + esc_attr( $id . '-js-modulepreload' ) + ); + } + } + } + + /** + * Prints the import map using a script tag with a type="importmap" attribute. + * + * @since 6.5.0 + */ + public function print_import_map() { + $import_map = $this->get_import_map(); + if ( ! empty( $import_map['imports'] ) ) { + wp_print_inline_script_tag( + wp_json_encode( $import_map, JSON_HEX_TAG | JSON_HEX_AMP ), + array( + 'type' => 'importmap', + 'id' => 'wp-importmap', + ) + ); + } + } + + /** + * Returns the import map array. + * + * @since 6.5.0 + * + * @return array Array with an `imports` key mapping to an array of script module identifiers and their respective + * URLs, including the version query. + */ + private function get_import_map(): array { + $imports = array(); + foreach ( $this->get_dependencies( array_keys( $this->get_marked_for_enqueue() ) ) as $id => $script_module ) { + $imports[ $id ] = $this->get_versioned_src( $script_module ); + } + return array( 'imports' => $imports ); + } + + /** + * Retrieves the list of script modules marked for enqueue. + * + * @since 6.5.0 + * + * @return array Script modules marked for enqueue, keyed by script module identifier. + */ + private function get_marked_for_enqueue(): array { + $enqueued = array(); + foreach ( $this->registered as $id => $script_module ) { + if ( true === $script_module['enqueue'] ) { + $enqueued[ $id ] = $script_module; + } + } + return $enqueued; + } + + /** + * Retrieves all the dependencies for the given script module identifiers, + * filtered by import types. + * + * It will consolidate an array containing a set of unique dependencies based + * on the requested import types: 'static', 'dynamic', or both. This method is + * recursive and also retrieves dependencies of the dependencies. + * + * @since 6.5.0 + * + + * @param string[] $ids The identifiers of the script modules for which to gather dependencies. + * @param array $import_types Optional. Import types of dependencies to retrieve: 'static', 'dynamic', or both. + * Default is both. + * @return array List of dependencies, keyed by script module identifier. + */ + private function get_dependencies( array $ids, array $import_types = array( 'static', 'dynamic' ) ) { + return array_reduce( + $ids, + function ( $dependency_script_modules, $id ) use ( $import_types ) { + $dependencies = array(); + foreach ( $this->registered[ $id ]['dependencies'] as $dependency ) { + if ( + in_array( $dependency['import'], $import_types, true ) && + isset( $this->registered[ $dependency['id'] ] ) && + ! isset( $dependency_script_modules[ $dependency['id'] ] ) + ) { + $dependencies[ $dependency['id'] ] = $this->registered[ $dependency['id'] ]; + } + } + return array_merge( $dependency_script_modules, $dependencies, $this->get_dependencies( array_keys( $dependencies ), $import_types ) ); + }, + array() + ); + } + + /** + * Gets the versioned URL for a script module src. + * + * If $version is set to false, the version number is the currently installed + * WordPress version. If $version is set to null, no version is added. + * Otherwise, the string passed in $version is used. + * + * @since 6.5.0 + * + * @param array $script_module The script module. + * @return string The script module src with a version if relevant. + */ + private function get_versioned_src( array $script_module ): string { + $args = array(); + if ( false === $script_module['version'] ) { + $args['ver'] = get_bloginfo( 'version' ); + } elseif ( null !== $script_module['version'] ) { + $args['ver'] = $script_module['version']; + } + if ( $args ) { + return add_query_arg( $args, $script_module['src'] ); + } + return $script_module['src']; + } + } +} diff --git a/lib/compat/wordpress-6.5/interactivity-api/class-wp-interactivity-api-directives-processor.php b/lib/compat/wordpress-6.5/interactivity-api/class-wp-interactivity-api-directives-processor.php new file mode 100644 index 00000000000000..b437bcefa67568 --- /dev/null +++ b/lib/compat/wordpress-6.5/interactivity-api/class-wp-interactivity-api-directives-processor.php @@ -0,0 +1,143 @@ +get_balanced_tag_bookmarks(); + if ( ! $bookmarks ) { + return null; + } + list( $start_name, $end_name ) = $bookmarks; + + $start = $this->bookmarks[ $start_name ]->start + $this->bookmarks[ $start_name ]->length + 1; + $end = $this->bookmarks[ $end_name ]->start; + + $this->seek( $start_name ); + $this->release_bookmark( $start_name ); + $this->release_bookmark( $end_name ); + + return substr( $this->html, $start, $end - $start ); + } + + /** + * Sets the content between two balanced tags. + * + * @access private + * + * @param string $new_content The string to replace the content between the matching tags. + * @return bool Whether the content was successfully replaced. + */ + public function set_content_between_balanced_tags( string $new_content ): bool { + $this->get_updated_html(); + + $bookmarks = $this->get_balanced_tag_bookmarks(); + if ( ! $bookmarks ) { + return false; + } + list( $start_name, $end_name ) = $bookmarks; + + $start = $this->bookmarks[ $start_name ]->start + $this->bookmarks[ $start_name ]->length + 1; + $end = $this->bookmarks[ $end_name ]->start; + + $this->seek( $start_name ); + $this->release_bookmark( $start_name ); + $this->release_bookmark( $end_name ); + + $this->lexical_updates[] = new Gutenberg_HTML_Text_Replacement_6_5( $start, $end - $start, esc_html( $new_content ) ); + return true; + } + + /** + * Returns a pair of bookmarks for the current opening tag and the matching + * closing tag. + * + * @return array|null A pair of bookmarks, or null if there's no matching closing tag. + */ + private function get_balanced_tag_bookmarks() { + static $i = 0; + $start_name = 'start_of_balanced_tag_' . ++$i; + + $this->set_bookmark( $start_name ); + if ( ! $this->next_balanced_closer() ) { + $this->release_bookmark( $start_name ); + return null; + } + + $end_name = 'end_of_balanced_tag_' . ++$i; + $this->set_bookmark( $end_name ); + + return array( $start_name, $end_name ); + } + + /** + * Finds the matching closing tag for an opening tag. + * + * When called while the processor is on an open tag, it traverses the HTML + * until it finds the matching closing tag, respecting any in-between content, + * including nested tags of the same name. Returns false when called on a + * closing or void tag, or if no matching closing tag was found. + * + * @return bool Whether a matching closing tag was found. + */ + private function next_balanced_closer(): bool { + $depth = 0; + $tag_name = $this->get_tag(); + + if ( $this->is_void() ) { + return false; + } + + while ( $this->next_tag( + array( + 'tag_name' => $tag_name, + 'tag_closers' => 'visit', + ) + ) ) { + if ( ! $this->is_tag_closer() ) { + ++$depth; + continue; + } + + if ( 0 === $depth ) { + return true; + } + + --$depth; + } + + return false; + } + + /** + * Checks whether the current tag is void. + * + * @access private + * + * @return bool Whether the current tag is void or not. + */ + public function is_void(): bool { + $tag_name = $this->get_tag(); + return Gutenberg_HTML_Processor_6_5::is_void( null !== $tag_name ? $tag_name : '' ); + } + } +} diff --git a/lib/compat/wordpress-6.5/interactivity-api/class-wp-interactivity-api.php b/lib/compat/wordpress-6.5/interactivity-api/class-wp-interactivity-api.php new file mode 100644 index 00000000000000..ad9e5d7c439533 --- /dev/null +++ b/lib/compat/wordpress-6.5/interactivity-api/class-wp-interactivity-api.php @@ -0,0 +1,678 @@ + 'data_wp_interactive_processor', + 'data-wp-context' => 'data_wp_context_processor', + 'data-wp-bind' => 'data_wp_bind_processor', + 'data-wp-class' => 'data_wp_class_processor', + 'data-wp-style' => 'data_wp_style_processor', + 'data-wp-text' => 'data_wp_text_processor', + ); + + /** + * Holds the initial state of the different Interactivity API stores. + * + * This state is used during the server directive processing. Then, it is + * serialized and sent to the client as part of the interactivity data to be + * recovered during the hydration of the client interactivity stores. + * + * @since 6.5.0 + * @var array + */ + private $state_data = array(); + + /** + * Holds the configuration required by the different Interactivity API stores. + * + * This configuration is serialized and sent to the client as part of the + * interactivity data and can be accessed by the client interactivity stores. + * + * @since 6.5.0 + * @var array + */ + private $config_data = array(); + + /** + * Gets and/or sets the initial state of an Interactivity API store for a + * given namespace. + * + * If state for that store namespace already exists, it merges the new + * provided state with the existing one. + * + * @since 6.5.0 + * + * @param string $store_namespace The unique store namespace identifier. + * @param array $state Optional. The array that will be merged with the existing state for the specified + * store namespace. + * @return array The current state for the specified store namespace. + */ + public function state( string $store_namespace, array $state = null ): array { + if ( ! isset( $this->state_data[ $store_namespace ] ) ) { + $this->state_data[ $store_namespace ] = array(); + } + if ( is_array( $state ) ) { + $this->state_data[ $store_namespace ] = array_replace_recursive( + $this->state_data[ $store_namespace ], + $state + ); + } + return $this->state_data[ $store_namespace ]; + } + + /** + * Gets and/or sets the configuration of the Interactivity API for a given + * store namespace. + * + * If configuration for that store namespace exists, it merges the new + * provided configuration with the existing one. + * + * @since 6.5.0 + * + * @param string $store_namespace The unique store namespace identifier. + * @param array $config Optional. The array that will be merged with the existing configuration for the + * specified store namespace. + * @return array The current configuration for the specified store namespace. + */ + public function config( string $store_namespace, array $config = null ): array { + if ( ! isset( $this->config_data[ $store_namespace ] ) ) { + $this->config_data[ $store_namespace ] = array(); + } + if ( is_array( $config ) ) { + $this->config_data[ $store_namespace ] = array_replace_recursive( + $this->config_data[ $store_namespace ], + $config + ); + } + return $this->config_data[ $store_namespace ]; + } + + /** + * Prints the serialized client-side interactivity data. + * + * Encodes the config and initial state into JSON and prints them inside a + * script tag of type "application/json". Once in the browser, the state will + * be parsed and used to hydrate the client-side interactivity stores and the + * configuration will be available using a `getConfig` utility. + * + * @since 6.5.0 + */ + public function print_client_interactivity_data() { + $store = array(); + $has_state = ! empty( $this->state_data ); + $has_config = ! empty( $this->config_data ); + + if ( $has_state || $has_config ) { + if ( $has_config ) { + $store['config'] = $this->config_data; + } + if ( $has_state ) { + $store['state'] = $this->state_data; + } + wp_print_inline_script_tag( + wp_json_encode( + $store, + JSON_HEX_TAG | JSON_HEX_AMP + ), + array( + 'type' => 'application/json', + 'id' => 'wp-interactivity-data', + ) + ); + } + } + + /** + * Registers the `@wordpress/interactivity` script modules. + * + * @since 6.5.0 + */ + public function register_script_modules() { + wp_register_script_module( + '@wordpress/interactivity', + gutenberg_url( '/build/interactivity/index.min.js' ), + array(), + defined( 'GUTENBERG_VERSION' ) ? GUTENBERG_VERSION : get_bloginfo( 'version' ) + ); + + wp_register_script_module( + '@wordpress/interactivity-router', + gutenberg_url( '/build/interactivity/router.min.js' ), + array( '@wordpress/interactivity' ), + defined( 'GUTENBERG_VERSION' ) ? GUTENBERG_VERSION : get_bloginfo( 'version' ) + ); + } + + /** + * Adds the necessary hooks for the Interactivity API. + * + * @since 6.5.0 + */ + public function add_hooks() { + add_action( 'wp_footer', array( $this, 'print_client_interactivity_data' ) ); + } + + /** + * Processes the interactivity directives contained within the HTML content + * and updates the markup accordingly. + * + * @since 6.5.0 + * + * @param string $html The HTML content to process. + * @return string The processed HTML content. It returns the original content when the HTML contains unbalanced tags. + */ + public function process_directives( string $html ): string { + $p = new WP_Interactivity_API_Directives_Processor( $html ); + $tag_stack = array(); + $namespace_stack = array(); + $context_stack = array(); + $unbalanced = false; + + $directive_processor_prefixes = array_keys( self::$directive_processors ); + $directive_processor_prefixes_reversed = array_reverse( $directive_processor_prefixes ); + + while ( $p->next_tag( array( 'tag_closers' => 'visit' ) ) && false === $unbalanced ) { + $tag_name = $p->get_tag(); + + if ( $p->is_tag_closer() ) { + list( $opening_tag_name, $directives_prefixes ) = end( $tag_stack ); + + if ( 0 === count( $tag_stack ) || $opening_tag_name !== $tag_name ) { + + /* + * If the tag stack is empty or the matching opening tag is not the + * same than the closing tag, it means the HTML is unbalanced and it + * stops processing it. + */ + $unbalanced = true; + continue; + } else { + + /* + * It removes the last tag from the stack. + */ + array_pop( $tag_stack ); + + /* + * If the matching opening tag didn't have any directives, it can skip + * the processing. + */ + if ( 0 === count( $directives_prefixes ) ) { + continue; + } + } + } else { + $directives_prefixes = array(); + + foreach ( $p->get_attribute_names_with_prefix( 'data-wp-' ) as $attribute_name ) { + + /* + * Extracts the directive prefix to see if there is a server directive + * processor registered for that directive. + */ + list( $directive_prefix ) = $this->extract_prefix_and_suffix( $attribute_name ); + if ( array_key_exists( $directive_prefix, self::$directive_processors ) ) { + $directives_prefixes[] = $directive_prefix; + } + } + + /* + * If this is not a void element, it adds it to the tag stack so it can + * process its closing tag and check for unbalanced tags. + */ + if ( ! $p->is_void() ) { + $tag_stack[] = array( $tag_name, $directives_prefixes ); + } + } + + /* + * Sorts the attributes by the order of the `directives_processor` array + * and checks what directives are present in this element. The processing + * order is reversed for tag closers. + */ + $directives_prefixes = array_intersect( + $p->is_tag_closer() + ? $directive_processor_prefixes_reversed + : $directive_processor_prefixes, + $directives_prefixes + ); + + // Executes the directive processors present in this element. + foreach ( $directives_prefixes as $directive_prefix ) { + $func = is_array( self::$directive_processors[ $directive_prefix ] ) + ? self::$directive_processors[ $directive_prefix ] + : array( $this, self::$directive_processors[ $directive_prefix ] ); + call_user_func_array( + $func, + array( $p, &$context_stack, &$namespace_stack ) + ); + } + } + + /* + * It returns the original content if the HTML is unbalanced because + * unbalanced HTML is not safe to process. In that case, the Interactivity + * API runtime will update the HTML on the client side during the hydration. + */ + return $unbalanced || 0 < count( $tag_stack ) ? $html : $p->get_updated_html(); + } + + /** + * Evaluates the reference path passed to a directive based on the current + * store namespace, state and context. + * + * @since 6.5.0 + * + * @param string|true $directive_value The directive attribute value string or `true` when it's a boolean attribute. + * @param string $default_namespace The default namespace to use if none is explicitly defined in the directive + * value. + * @param array|false $context The current context for evaluating the directive or false if there is no + * context. + * @return mixed|null The result of the evaluation. Null if the reference path doesn't exist. + */ + private function evaluate( $directive_value, string $default_namespace, $context = false ) { + list( $ns, $path ) = $this->extract_directive_value( $directive_value, $default_namespace ); + if ( empty( $path ) ) { + return null; + } + + $store = array( + 'state' => isset( $this->state_data[ $ns ] ) ? $this->state_data[ $ns ] : array(), + 'context' => isset( $context[ $ns ] ) ? $context[ $ns ] : array(), + ); + + // Checks if the reference path is preceded by a negator operator (!). + $should_negate_value = '!' === $path[0]; + $path = $should_negate_value ? substr( $path, 1 ) : $path; + + // Extracts the value from the store using the reference path. + $path_segments = explode( '.', $path ); + $current = $store; + foreach ( $path_segments as $path_segment ) { + if ( isset( $current[ $path_segment ] ) ) { + $current = $current[ $path_segment ]; + } else { + return null; + } + } + + // Returns the opposite if it contains a negator operator (!). + return $should_negate_value ? ! $current : $current; + } + + /** + * Extracts the directive attribute name to separate and return the directive + * prefix and an optional suffix. + * + * The suffix is the string after the first double hyphen and the prefix is + * everything that comes before the suffix. + * + * Example: + * + * extract_prefix_and_suffix( 'data-wp-interactive' ) => array( 'data-wp-interactive', null ) + * extract_prefix_and_suffix( 'data-wp-bind--src' ) => array( 'data-wp-bind', 'src' ) + * extract_prefix_and_suffix( 'data-wp-foo--and--bar' ) => array( 'data-wp-foo', 'and--bar' ) + * + * @since 6.5.0 + * + * @param string $directive_name The directive attribute name. + * @return array An array containing the directive prefix and optional suffix. + */ + private function extract_prefix_and_suffix( string $directive_name ): array { + return explode( '--', $directive_name, 2 ); + } + + /** + * Parses and extracts the namespace and reference path from the given + * directive attribute value. + * + * If the value doesn't contain an explicit namespace, it returns the + * default one. If the value contains a JSON object instead of a reference + * path, the function tries to parse it and return the resulting array. If + * the value contains strings that reprenset booleans ("true" and "false"), + * numbers ("1" and "1.2") or "null", the function also transform them to + * regular booleans, numbers and `null`. + * + * Example: + * + * extract_directive_value( 'actions.foo', 'myPlugin' ) => array( 'myPlugin', 'actions.foo' ) + * extract_directive_value( 'otherPlugin::actions.foo', 'myPlugin' ) => array( 'otherPlugin', 'actions.foo' ) + * extract_directive_value( '{ "isOpen": false }', 'myPlugin' ) => array( 'myPlugin', array( 'isOpen' => false ) ) + * extract_directive_value( 'otherPlugin::{ "isOpen": false }', 'myPlugin' ) => array( 'otherPlugin', array( 'isOpen' => false ) ) + * + * @since 6.5.0 + * + * @param string|true $directive_value The directive attribute value. It can be `true` when it's a boolean + * attribute. + * @param string|null $default_namespace Optional. The default namespace if none is explicitly defined. + * @return array An array containing the namespace in the first item and the JSON, the reference path, or null on the + * second item. + */ + private function extract_directive_value( $directive_value, $default_namespace = null ): array { + if ( empty( $directive_value ) || is_bool( $directive_value ) ) { + return array( $default_namespace, null ); + } + + // Replaces the value and namespace if there is a namespace in the value. + if ( 1 === preg_match( '/^([\w\-_\/]+)::./', $directive_value ) ) { + list($default_namespace, $directive_value) = explode( '::', $directive_value, 2 ); + } + + /* + * Tries to decode the value as a JSON object. If it fails and the value + * isn't `null`, it returns the value as it is. Otherwise, it returns the + * decoded JSON or null for the string `null`. + */ + $decoded_json = json_decode( $directive_value, true ); + if ( null !== $decoded_json || 'null' === $directive_value ) { + $directive_value = $decoded_json; + } + + return array( $default_namespace, $directive_value ); + } + + + /** + * Processes the `data-wp-interactive` directive. + * + * It adds the default store namespace defined in the directive value to the + * stack so it's available for the nested interactivity elements. + * + * @since 6.5.0 + * + * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance. + * @param array $context_stack The reference to the context stack. + * @param array $namespace_stack The reference to the store namespace stack. + */ + private function data_wp_interactive_processor( WP_Interactivity_API_Directives_Processor $p, array &$context_stack, array &$namespace_stack ) { + // In closing tags, it removes the last namespace from the stack. + if ( $p->is_tag_closer() ) { + return array_pop( $namespace_stack ); + } + + // Tries to decode the `data-wp-interactive` attribute value. + $attribute_value = $p->get_attribute( 'data-wp-interactive' ); + $decoded_json = is_string( $attribute_value ) && ! empty( $attribute_value ) + ? json_decode( $attribute_value, true ) + : null; + + /* + * Pushes the newly defined namespace or the current one if the + * `data-wp-interactive` definition was invalid or does not contain a + * namespace. It does so because the function pops out the current namespace + * from the stack whenever it finds a `data-wp-interactive`'s closing tag, + * independently of whether the previous `data-wp-interactive` definition + * contained a valid namespace. + */ + $namespace_stack[] = isset( $decoded_json['namespace'] ) + ? $decoded_json['namespace'] + : end( $namespace_stack ); + } + + /** + * Processes the `data-wp-context` directive. + * + * It adds the context defined in the directive value to the stack so it's + * available for the nested interactivity elements. + * + * @since 6.5.0 + * + * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance. + * @param array $context_stack The reference to the context stack. + * @param array $namespace_stack The reference to the store namespace stack. + */ + private function data_wp_context_processor( WP_Interactivity_API_Directives_Processor $p, array &$context_stack, array &$namespace_stack ) { + // In closing tags, it removes the last context from the stack. + if ( $p->is_tag_closer() ) { + return array_pop( $context_stack ); + } + + $attribute_value = $p->get_attribute( 'data-wp-context' ); + $namespace_value = end( $namespace_stack ); + + // Separates the namespace from the context JSON object. + list( $namespace_value, $decoded_json ) = is_string( $attribute_value ) && ! empty( $attribute_value ) + ? $this->extract_directive_value( $attribute_value, $namespace_value ) + : array( $namespace_value, null ); + + /* + * If there is a namespace, it adds a new context to the stack merging the + * previous context with the new one. + */ + if ( is_string( $namespace_value ) ) { + array_push( + $context_stack, + array_replace_recursive( + end( $context_stack ) !== false ? end( $context_stack ) : array(), + array( $namespace_value => is_array( $decoded_json ) ? $decoded_json : array() ) + ) + ); + } else { + /* + * If there is no namespace, it pushes the current context to the stack. + * It needs to do so because the function pops out the current context + * from the stack whenever it finds a `data-wp-context`'s closing tag. + */ + array_push( $context_stack, end( $context_stack ) ); + } + } + + /** + * Processes the `data-wp-bind` directive. + * + * It updates or removes the bound attributes based on the evaluation of its + * associated reference. + * + * @since 6.5.0 + * + * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance. + * @param array $context_stack The reference to the context stack. + * @param array $namespace_stack The reference to the store namespace stack. + */ + private function data_wp_bind_processor( WP_Interactivity_API_Directives_Processor $p, array &$context_stack, array &$namespace_stack ) { + if ( ! $p->is_tag_closer() ) { + $all_bind_directives = $p->get_attribute_names_with_prefix( 'data-wp-bind--' ); + + foreach ( $all_bind_directives as $attribute_name ) { + list( , $bound_attribute ) = $this->extract_prefix_and_suffix( $attribute_name ); + if ( empty( $bound_attribute ) ) { + return; + } + + $attribute_value = $p->get_attribute( $attribute_name ); + $result = $this->evaluate( $attribute_value, end( $namespace_stack ), end( $context_stack ) ); + + if ( null !== $result && ( false !== $result || '-' === $bound_attribute[4] ) ) { + /* + * If the result of the evaluation is a boolean and the attribute is + * `aria-` or `data-, convert it to a string "true" or "false". It + * follows the exact same logic as Preact because it needs to + * replicate what Preact will later do in the client: + * https://github.com/preactjs/preact/blob/ea49f7a0f9d1ff2c98c0bdd66aa0cbc583055246/src/diff/props.js#L131C24-L136 + */ + if ( is_bool( $result ) && '-' === $bound_attribute[4] ) { + $result = $result ? 'true' : 'false'; + } + $p->set_attribute( $bound_attribute, $result ); + } else { + $p->remove_attribute( $bound_attribute ); + } + } + } + } + + + /** + * Processes the `data-wp-class` directive. + * + * It adds or removes CSS classes in the current HTML element based on the + * evaluation of its associated references. + * + * @since 6.5.0 + * + * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance. + * @param array $context_stack The reference to the context stack. + * @param array $namespace_stack The reference to the store namespace stack. + */ + private function data_wp_class_processor( WP_Interactivity_API_Directives_Processor $p, array &$context_stack, array &$namespace_stack ) { + if ( ! $p->is_tag_closer() ) { + $all_class_directives = $p->get_attribute_names_with_prefix( 'data-wp-class--' ); + + foreach ( $all_class_directives as $attribute_name ) { + list( , $class_name ) = $this->extract_prefix_and_suffix( $attribute_name ); + if ( empty( $class_name ) ) { + return; + } + + $attribute_value = $p->get_attribute( $attribute_name ); + $result = $this->evaluate( $attribute_value, end( $namespace_stack ), end( $context_stack ) ); + + if ( $result ) { + $p->add_class( $class_name ); + } else { + $p->remove_class( $class_name ); + } + } + } + } + + /** + * Processes the `data-wp-style` directive. + * + * It updates the style attribute value of the current HTML element based on + * the evaluation of its associated references. + * + * @since 6.5.0 + * + * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance. + * @param array $context_stack The reference to the context stack. + * @param array $namespace_stack The reference to the store namespace stack. + */ + private function data_wp_style_processor( WP_Interactivity_API_Directives_Processor $p, array &$context_stack, array &$namespace_stack ) { + if ( ! $p->is_tag_closer() ) { + $all_style_attributes = $p->get_attribute_names_with_prefix( 'data-wp-style--' ); + + foreach ( $all_style_attributes as $attribute_name ) { + list( , $style_property ) = $this->extract_prefix_and_suffix( $attribute_name ); + if ( empty( $style_property ) ) { + continue; + } + + $directive_attribute_value = $p->get_attribute( $attribute_name ); + $style_property_value = $this->evaluate( $directive_attribute_value, end( $namespace_stack ), end( $context_stack ) ); + $style_attribute_value = $p->get_attribute( 'style' ); + $style_attribute_value = ( $style_attribute_value && ! is_bool( $style_attribute_value ) ) ? $style_attribute_value : ''; + + /* + * Checks first if the style property is not falsy and the style + * attribute value is not empty because if it is, it doesn't need to + * update the attribute value. + */ + if ( $style_property_value || ( ! $style_property_value && $style_attribute_value ) ) { + $style_attribute_value = $this->set_style_property( $style_attribute_value, $style_property, $style_property_value ); + /* + * If the style attribute value is not empty, it sets it. Otherwise, + * it removes it. + */ + if ( ! empty( $style_attribute_value ) ) { + $p->set_attribute( 'style', $style_attribute_value ); + } else { + $p->remove_attribute( 'style' ); + } + } + } + } + } + + /** + * Sets an individual style property in the `style` attribute of an HTML + * element, updating or removing the property when necessary. + * + * If a property is modified, it is added at the end of the list to make sure + * that it overrides the previous ones. + * + * @since 6.5.0 + * + * Example: + * + * set_style_property( 'color:green;', 'color', 'red' ) => 'color:red;' + * set_style_property( 'background:green;', 'color', 'red' ) => 'background:green;color:red;' + * set_style_property( 'color:green;', 'color', null ) => '' + * + * @param string $style_attribute_value The current style attribute value. + * @param string $style_property_name The style property name to set. + * @param string|false|null $style_property_value The value to set for the style property. With false, null or an + * empty string, it removes the style property. + * @return string The new style attribute value after the specified property has been added, updated or removed. + */ + private function set_style_property( string $style_attribute_value, string $style_property_name, $style_property_value ): string { + $style_assignments = explode( ';', $style_attribute_value ); + $result = array(); + $style_property_value = ! empty( $style_property_value ) ? rtrim( trim( $style_property_value ), ';' ) : null; + $new_style_property = $style_property_value ? $style_property_name . ':' . $style_property_value . ';' : ''; + + // Generate an array with all the properties but the modified one. + foreach ( $style_assignments as $style_assignment ) { + if ( empty( trim( $style_assignment ) ) ) { + continue; + } + list( $name, $value ) = explode( ':', $style_assignment ); + if ( trim( $name ) !== $style_property_name ) { + $result[] = trim( $name ) . ':' . trim( $value ) . ';'; + } + } + + // Add the new/modified property at the end of the list. + array_push( $result, $new_style_property ); + + return implode( '', $result ); + } + + /** + * Processes the `data-wp-text` directive. + * + * It updates the inner content of the current HTML element based on the + * evaluation of its associated reference. + * + * @since 6.5.0 + * + * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance. + * @param array $context_stack The reference to the context stack. + * @param array $namespace_stack The reference to the store namespace stack. + */ + private function data_wp_text_processor( WP_Interactivity_API_Directives_Processor $p, array &$context_stack, array &$namespace_stack ) { + if ( ! $p->is_tag_closer() ) { + $attribute_value = $p->get_attribute( 'data-wp-text' ); + $result = $this->evaluate( $attribute_value, end( $namespace_stack ), end( $context_stack ) ); + + /* + * Follows the same logic as Preact in the client and only changes the + * content if the value is a string or a number. Otherwise, it removes the + * content. + */ + if ( is_string( $result ) || is_numeric( $result ) ) { + $p->set_content_between_balanced_tags( esc_html( $result ) ); + } else { + $p->set_content_between_balanced_tags( '' ); + } + } + } + } + +} diff --git a/lib/compat/wordpress-6.5/interactivity-api/interactivity-api.php b/lib/compat/wordpress-6.5/interactivity-api/interactivity-api.php new file mode 100644 index 00000000000000..cd7ca7fb902870 --- /dev/null +++ b/lib/compat/wordpress-6.5/interactivity-api/interactivity-api.php @@ -0,0 +1,144 @@ +get_registered( $block_name ); + + if ( isset( $block_name ) && isset( $block_type->supports['interactivity'] ) && $block_type->supports['interactivity'] ) { + // Annotates the root interactive block for processing. + $root_interactive_block = array( $block_name, md5( serialize( $parsed_block ) ) ); + + /* + * Adds a filter to process the root interactive block once it has + * finished rendering. + */ + $process_interactive_blocks = static function ( $content, $parsed_block ) use ( &$root_interactive_block, &$process_interactive_blocks ) { + // Checks whether the current block is the root interactive block. + list($root_block_name, $root_block_md5) = $root_interactive_block; + if ( $root_block_name === $parsed_block['blockName'] && md5( serialize( $parsed_block ) ) === $root_block_md5 ) { + // The root interactive blocks has finished rendering, process it. + $content = wp_interactivity_process_directives( $content ); + // Removes the filter and reset the root interactive block. + remove_filter( 'render_block', $process_interactive_blocks ); + $root_interactive_block = null; + } + return $content; + }; + + /* + * Uses a priority of 20 to ensure that other filters can add additional + * directives before the processing starts. + */ + add_filter( 'render_block', $process_interactive_blocks, 20, 2 ); + } + } + + return $parsed_block; + } + add_filter( 'render_block_data', 'wp_interactivity_process_directives_of_interactive_blocks', 10, 1 ); +} + +if ( ! function_exists( 'wp_interactivity' ) ) { + /** + * Retrieves the main WP_Interactivity_API instance. + * + * It provides access to the WP_Interactivity_API instance, creating one if it + * doesn't exist yet. It also registers the hooks and necessary script + * modules. + * + * @since 6.5.0 + * + * @return WP_Interactivity_API The main WP_Interactivity_API instance. + */ + function wp_interactivity() { + static $instance = null; + if ( is_null( $instance ) ) { + $instance = new WP_Interactivity_API(); + $instance->add_hooks(); + $instance->register_script_modules(); + } + return $instance; + } +} + +if ( ! function_exists( 'wp_interactivity_process_directives' ) ) { + /** + * Processes the interactivity directives contained within the HTML content + * and updates the markup accordingly. + * + * @since 6.5.0 + * + * @param string $html The HTML content to process. + * @return string The processed HTML content. It returns the original content when the HTML contains unbalanced tags. + */ + function wp_interactivity_process_directives( $html ) { + return wp_interactivity()->process_directives( $html ); + } +} + +if ( ! function_exists( 'wp_interactivity_state' ) ) { + /** + * Gets and/or sets the initial state of an Interactivity API store for a + * given namespace. + * + * If state for that store namespace already exists, it merges the new + * provided state with the existing one. + * + * @since 6.5.0 + * + * @param string $store_namespace The unique store namespace identifier. + * @param array $state Optional. The array that will be merged with the existing state for the specified + * store namespace. + * @return array The current state for the specified store namespace. + */ + function wp_interactivity_state( $store_namespace, $state = null ) { + return wp_interactivity()->state( $store_namespace, $state ); + } +} + +if ( ! function_exists( 'wp_interactivity_config' ) ) { + /** + * Gets and/or sets the configuration of the Interactivity API for a given + * store namespace. + * + * If configuration for that store namespace exists, it merges the new + * provided configuration with the existing one. + * + * @since 6.5.0 + * + * @param string $store_namespace The unique store namespace identifier. + * @param array $config Optional. The array that will be merged with the existing configuration for the + * specified store namespace. + * @return array The current configuration for the specified store namespace. + */ + function wp_interactivity_config( $store_namespace, $initial_state = null ) { + return wp_interactivity()->config( $store_namespace, $initial_state ); + } +} diff --git a/lib/compat/wordpress-6.5/scripts-modules.php b/lib/compat/wordpress-6.5/scripts-modules.php new file mode 100644 index 00000000000000..ba329b255b1965 --- /dev/null +++ b/lib/compat/wordpress-6.5/scripts-modules.php @@ -0,0 +1,119 @@ +add_hooks(); + } + return $instance; + } +} + +if ( ! function_exists( 'wp_register_script_module' ) ) { + /** + * Registers the script module if no script module with that script module + * identifier has already been registered. + * + * @since 6.5.0 + * + * @param string $id The identifier of the script module. Should be unique. It will be used in the + * final import map. + * @param string $src Optional. Full URL of the script module, or path of the script module relative + * to the WordPress root directory. If it is provided and the script module has + * not been registered yet, it will be registered. + * @param array $deps { + * Optional. List of dependencies. + * + * @type string|array $0... { + * An array of script module identifiers of the dependencies of this script + * module. The dependencies can be strings or arrays. If they are arrays, + * they need an `id` key with the script module identifier, and can contain + * an `import` key with either `static` or `dynamic`. By default, + * dependencies that don't contain an `import` key are considered static. + * + * @type string $id The script module identifier. + * @type string $import Optional. Import type. May be either `static` or + * `dynamic`. Defaults to `static`. + * } + * } + * @param string|false|null $version Optional. String specifying the script module version number. Defaults to false. + * It is added to the URL as a query string for cache busting purposes. If $version + * is set to false, the version number is the currently installed WordPress version. + * If $version is set to null, no version is added. + */ + function wp_register_script_module( string $id, string $src, array $deps = array(), $version = false ) { + wp_script_modules()->register( $id, $src, $deps, $version ); + } +} + +if ( ! function_exists( 'wp_enqueue_script_module' ) ) { + /** + * Marks the script module to be enqueued in the page. + * + * If a src is provided and the script module has not been registered yet, it + * will be registered. + * + * @since 6.5.0 + * + * @param string $id The identifier of the script module. Should be unique. It will be used in the + * final import map. + * @param string $src Optional. Full URL of the script module, or path of the script module relative + * to the WordPress root directory. If it is provided and the script module has + * not been registered yet, it will be registered. + * @param array $deps { + * Optional. List of dependencies. + * + * @type string|array $0... { + * An array of script module identifiers of the dependencies of this script + * module. The dependencies can be strings or arrays. If they are arrays, + * they need an `id` key with the script module identifier, and can contain + * an `import` key with either `static` or `dynamic`. By default, + * dependencies that don't contain an `import` key are considered static. + * + * @type string $id The script module identifier. + * @type string $import Optional. Import type. May be either `static` or + * `dynamic`. Defaults to `static`. + * } + * } + * @param string|false|null $version Optional. String specifying the script module version number. Defaults to false. + * It is added to the URL as a query string for cache busting purposes. If $version + * is set to false, the version number is the currently installed WordPress version. + * If $version is set to null, no version is added. + */ + function wp_enqueue_script_module( string $id, string $src = '', array $deps = array(), $version = false ) { + wp_script_modules()->enqueue( $id, $src, $deps, $version ); + } +} + +if ( ! function_exists( 'wp_dequeue_script_module' ) ) { + /** + * Unmarks the script module so it is no longer enqueued in the page. + * + * @since 6.5.0 + * + * @param string $id The identifier of the script module. + */ + function wp_dequeue_script_module( string $id ) { + wp_script_modules()->dequeue( $id ); + } +} diff --git a/lib/experimental/block-bindings/block-bindings.php b/lib/experimental/block-bindings/block-bindings.php deleted file mode 100644 index 83a1d6132f5f43..00000000000000 --- a/lib/experimental/block-bindings/block-bindings.php +++ /dev/null @@ -1,69 +0,0 @@ -register_source( $source_name, $label, $apply ); - } -} - -/** - * Retrieves the list of registered block sources. - * - * @return array The list of registered block sources. - */ -if ( ! function_exists( 'wp_block_bindings_get_sources' ) ) { - function wp_block_bindings_get_sources() { - return wp_block_bindings()->get_sources(); - } -} - -/** - * Replaces the HTML content of a block based on the provided source value. - * - * @param string $block_content Block Content. - * @param string $block_name The name of the block to process. - * @param string $block_attr The attribute of the block we want to process. - * @param string $source_value The value used to replace the HTML. - * @return string The modified block content. - */ -if ( ! function_exists( 'wp_block_bindings_replace_html' ) ) { - function wp_block_bindings_replace_html( $block_content, $block_name, $block_attr, $source_value ) { - return wp_block_bindings()->replace_html( $block_content, $block_name, $block_attr, $source_value ); - } -} diff --git a/lib/experimental/block-bindings/class-wp-block-bindings.php b/lib/experimental/block-bindings/class-wp-block-bindings.php deleted file mode 100644 index 7eb443dd367a6f..00000000000000 --- a/lib/experimental/block-bindings/class-wp-block-bindings.php +++ /dev/null @@ -1,158 +0,0 @@ -sources[ $source_name ] = array( - 'label' => $label, - 'apply' => $apply, - ); - } - - /** - * Depending on the block attributes, replace the proper HTML based on the value returned by the source. - * - * @param string $block_content Block Content. - * @param string $block_name The name of the block to process. - * @param string $block_attr The attribute of the block we want to process. - * @param string $source_value The value used to replace the HTML. - */ - public function replace_html( $block_content, $block_name, $block_attr, $source_value ) { - $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block_name ); - if ( null === $block_type ) { - return; - } - - // Depending on the attribute source, the processing will be different. - switch ( $block_type->attributes[ $block_attr ]['source'] ) { - case 'html': - case 'rich-text': - $block_reader = new WP_HTML_Tag_Processor( $block_content ); - - // TODO: Support for CSS selectors whenever they are ready in the HTML API. - // In the meantime, support comma-separated selectors by exploding them into an array. - $selectors = explode( ',', $block_type->attributes[ $block_attr ]['selector'] ); - // Add a bookmark to the first tag to be able to iterate over the selectors. - $block_reader->next_tag(); - $block_reader->set_bookmark( 'iterate-selectors' ); - - // TODO: This shouldn't be needed when the `set_inner_html` function is ready. - // Store the parent tag and its attributes to be able to restore them later in the button. - // The button block has a wrapper while the paragraph and heading blocks don't. - if ( 'core/button' === $block_name ) { - $button_wrapper = $block_reader->get_tag(); - $button_wrapper_attribute_names = $block_reader->get_attribute_names_with_prefix( '' ); - $button_wrapper_attrs = array(); - foreach ( $button_wrapper_attribute_names as $name ) { - $button_wrapper_attrs[ $name ] = $block_reader->get_attribute( $name ); - } - } - - foreach ( $selectors as $selector ) { - // If the parent tag, or any of its children, matches the selector, replace the HTML. - if ( strcasecmp( $block_reader->get_tag( $selector ), $selector ) === 0 || $block_reader->next_tag( - array( - 'tag_name' => $selector, - ) - ) ) { - $block_reader->release_bookmark( 'iterate-selectors' ); - - // TODO: Use `set_inner_html` method whenever it's ready in the HTML API. - // Until then, it is hardcoded for the paragraph, heading, and button blocks. - // Store the tag and its attributes to be able to restore them later. - $selector_attribute_names = $block_reader->get_attribute_names_with_prefix( '' ); - $selector_attrs = array(); - foreach ( $selector_attribute_names as $name ) { - $selector_attrs[ $name ] = $block_reader->get_attribute( $name ); - } - $selector_markup = "<$selector>" . esc_html( $source_value ) . ""; - $amended_content = new WP_HTML_Tag_Processor( $selector_markup ); - $amended_content->next_tag(); - foreach ( $selector_attrs as $attribute_key => $attribute_value ) { - $amended_content->set_attribute( $attribute_key, $attribute_value ); - } - if ( 'core/paragraph' === $block_name || 'core/heading' === $block_name ) { - return $amended_content->get_updated_html(); - } - if ( 'core/button' === $block_name ) { - $button_markup = "<$button_wrapper>{$amended_content->get_updated_html()}"; - $amended_button = new WP_HTML_Tag_Processor( $button_markup ); - $amended_button->next_tag(); - foreach ( $button_wrapper_attrs as $attribute_key => $attribute_value ) { - $amended_button->set_attribute( $attribute_key, $attribute_value ); - } - return $amended_button->get_updated_html(); - } - } else { - $block_reader->seek( 'iterate-selectors' ); - } - } - $block_reader->release_bookmark( 'iterate-selectors' ); - return $block_content; - - case 'attribute': - $amended_content = new WP_HTML_Tag_Processor( $block_content ); - if ( ! $amended_content->next_tag( - array( - // TODO: build the query from CSS selector. - 'tag_name' => $block_type->attributes[ $block_attr ]['selector'], - ) - ) ) { - return $block_content; - } - $amended_content->set_attribute( $block_type->attributes[ $block_attr ]['attribute'], esc_attr( $source_value ) ); - return $amended_content->get_updated_html(); - break; - - default: - return $block_content; - break; - } - return; - } - - /** - * Retrieves the list of registered block sources. - * - * @return array The array of registered sources. - */ - public function get_sources() { - return $this->sources; - } -} diff --git a/lib/experimental/block-bindings/index.php b/lib/experimental/block-bindings/index.php deleted file mode 100644 index dc9a6c9b96957b..00000000000000 --- a/lib/experimental/block-bindings/index.php +++ /dev/null @@ -1,19 +0,0 @@ -attributes, array( 'metadata', 'id' ), false ) ) { - return null; - } - $block_id = $block_instance->attributes['metadata']['id']; - return _wp_array_get( $block_instance->context, array( 'pattern/overrides', $block_id, $attribute_name ), null ); - }; - wp_block_bindings_register_source( - 'pattern_attributes', - __( 'Pattern Attributes', 'gutenberg' ), - $pattern_source_callback - ); -} diff --git a/lib/experimental/blocks.php b/lib/experimental/blocks.php index 93b65f95fc61ae..fc67f2c9d43770 100644 --- a/lib/experimental/blocks.php +++ b/lib/experimental/blocks.php @@ -77,92 +77,3 @@ function wp_enqueue_block_view_script( $block_name, $args ) { add_filter( 'render_block', $callback, 10, 2 ); } } - - - - -$gutenberg_experiments = get_option( 'gutenberg-experiments' ); -if ( $gutenberg_experiments && ( - array_key_exists( 'gutenberg-block-bindings', $gutenberg_experiments ) || - array_key_exists( 'gutenberg-pattern-partial-syncing', $gutenberg_experiments ) -) ) { - - require_once __DIR__ . '/block-bindings/index.php'; - - if ( ! function_exists( 'gutenberg_process_block_bindings' ) ) { - /** - * Process the block bindings attribute. - * - * @param string $block_content Block Content. - * @param array $block Block attributes. - * @param WP_Block $block_instance The block instance. - */ - function gutenberg_process_block_bindings( $block_content, $block, $block_instance ) { - - // Allowed blocks that support block bindings. - // TODO: Look for a mechanism to opt-in for this. Maybe adding a property to block attributes? - $allowed_blocks = array( - 'core/paragraph' => array( 'content' ), - 'core/heading' => array( 'content' ), - 'core/image' => array( 'url', 'title', 'alt' ), - 'core/button' => array( 'url', 'text' ), - ); - - // If the block doesn't have the bindings property or isn't one of the allowed block types, return. - if ( ! isset( $block['attrs']['metadata']['bindings'] ) || ! isset( $allowed_blocks[ $block_instance->name ] ) ) { - return $block_content; - } - - // Assuming the following format for the bindings property of the "metadata" attribute: - // - // "bindings": { - // "title": { - // "source": { - // "name": "post_meta", - // "attributes": { "value": "text_custom_field" } - // } - // }, - // "url": { - // "source": { - // "name": "post_meta", - // "attributes": { "value": "text_custom_field" } - // } - // } - // } - // - - $block_bindings_sources = wp_block_bindings_get_sources(); - $modified_block_content = $block_content; - foreach ( $block['attrs']['metadata']['bindings'] as $binding_attribute => $binding_source ) { - - // If the attribute is not in the list, process next attribute. - if ( ! in_array( $binding_attribute, $allowed_blocks[ $block_instance->name ], true ) ) { - continue; - } - // If no source is provided, or that source is not registered, process next attribute. - if ( ! isset( $binding_source['source'] ) || ! isset( $binding_source['source']['name'] ) || ! isset( $block_bindings_sources[ $binding_source['source']['name'] ] ) ) { - continue; - } - - $source_callback = $block_bindings_sources[ $binding_source['source']['name'] ]['apply']; - // Get the value based on the source. - if ( ! isset( $binding_source['source']['attributes'] ) ) { - $source_args = array(); - } else { - $source_args = $binding_source['source']['attributes']; - } - $source_value = $source_callback( $source_args, $block_instance, $binding_attribute ); - // If the value is null, process next attribute. - if ( is_null( $source_value ) ) { - continue; - } - - // Process the HTML based on the block and the attribute. - $modified_block_content = wp_block_bindings_replace_html( $modified_block_content, $block_instance->name, $binding_attribute, $source_value ); - } - return $modified_block_content; - } - } - - add_filter( 'render_block', 'gutenberg_process_block_bindings', 20, 3 ); -} diff --git a/lib/experimental/editor-settings.php b/lib/experimental/editor-settings.php index 729376cf030dd9..37774e07b27691 100644 --- a/lib/experimental/editor-settings.php +++ b/lib/experimental/editor-settings.php @@ -25,18 +25,9 @@ function gutenberg_enable_experiments() { if ( $gutenberg_experiments && array_key_exists( 'gutenberg-group-grid-variation', $gutenberg_experiments ) ) { wp_add_inline_script( 'wp-block-editor', 'window.__experimentalEnableGroupGridVariation = true', 'before' ); } - - if ( $gutenberg_experiments && array_key_exists( 'gutenberg-block-bindings', $gutenberg_experiments ) ) { - wp_add_inline_script( 'wp-block-editor', 'window.__experimentalBlockBindings = true', 'before' ); - } - if ( gutenberg_is_experiment_enabled( 'gutenberg-no-tinymce' ) ) { wp_add_inline_script( 'wp-block-library', 'window.__experimentalDisableTinymce = true', 'before' ); } - - if ( $gutenberg_experiments && array_key_exists( 'gutenberg-pattern-partial-syncing', $gutenberg_experiments ) ) { - wp_add_inline_script( 'wp-block-editor', 'window.__experimentalPatternPartialSyncing = true', 'before' ); - } } add_action( 'admin_init', 'gutenberg_enable_experiments' ); diff --git a/lib/experimental/fonts/font-library/class-wp-font-collection.php b/lib/experimental/fonts/font-library/class-wp-font-collection.php index 6189da5fa984b1..1ff96b1343b453 100644 --- a/lib/experimental/fonts/font-library/class-wp-font-collection.php +++ b/lib/experimental/fonts/font-library/class-wp-font-collection.php @@ -21,41 +21,128 @@ class WP_Font_Collection { /** - * Font collection configuration. + * The unique slug for the font collection. + * + * @since 6.5.0 + * + * @var string + */ + private $slug; + + /** + * The name of the font collection. + * + * @since 6.5.0 + * + * @var string + */ + private $name; + + /** + * Description of the font collection. + * + * @since 6.5.0 + * + * @var string + */ + private $description; + + /** + * Source of the font collection. + * + * @since 6.5.0 + * + * @var string + */ + private $src; + + /** + * Array of font families in the collection. * * @since 6.5.0 * * @var array */ - private $config; + private $font_families; + + /** + * Categories associated with the font collection. + * + * @since 6.5.0 + * + * @var array + */ + private $categories; + /** * WP_Font_Collection constructor. * * @since 6.5.0 * - * @param array $config Font collection config options. - * See {@see wp_register_font_collection()} for the supported fields. - * @throws Exception If the required parameters are missing. + * @param array $config Font collection config options. { + * @type string $slug The font collection's unique slug. + * @type string $name The font collection's name. + * @type string $description The font collection's description. + * @type string $src The font collection's source. + * @type array $font_families An array of font families in the font collection. + * @type array $categories The font collection's categories. + * } */ public function __construct( $config ) { - if ( empty( $config ) || ! is_array( $config ) ) { - throw new Exception( 'Font Collection config options is required as a non-empty array.' ); - } + $this->is_config_valid( $config ); + + $this->slug = isset( $config['slug'] ) ? $config['slug'] : ''; + $this->name = isset( $config['name'] ) ? $config['name'] : ''; + $this->description = isset( $config['description'] ) ? $config['description'] : ''; + $this->src = isset( $config['src'] ) ? $config['src'] : ''; + $this->font_families = isset( $config['font_families'] ) ? $config['font_families'] : array(); + $this->categories = isset( $config['categories'] ) ? $config['categories'] : array(); + } - if ( empty( $config['slug'] ) || ! is_string( $config['slug'] ) ) { - throw new Exception( 'Font Collection config slug is required as a non-empty string.' ); + /** + * Checks if the font collection config is valid. + * + * @since 6.5.0 + * + * @param array $config Font collection config options. { + * @type string $slug The font collection's unique slug. + * @type string $name The font collection's name. + * @type string $description The font collection's description. + * @type string $src The font collection's source. + * @type array $font_families An array of font families in the font collection. + * @type array $categories The font collection's categories. + * } + * @return bool True if the font collection config is valid and false otherwise. + */ + public static function is_config_valid( $config ) { + if ( empty( $config ) || ! is_array( $config ) ) { + _doing_it_wrong( __METHOD__, __( 'Font Collection config options are required as a non-empty array.', 'gutenberg' ), '6.5.0' ); + return false; } - if ( empty( $config['name'] ) || ! is_string( $config['name'] ) ) { - throw new Exception( 'Font Collection config name is required as a non-empty string.' ); + $required_keys = array( 'slug', 'name' ); + foreach ( $required_keys as $key ) { + if ( empty( $config[ $key ] ) ) { + _doing_it_wrong( + __METHOD__, + // translators: %s: Font collection config key. + sprintf( __( 'Font Collection config %s is required as a non-empty string.', 'gutenberg' ), $key ), + '6.5.0' + ); + return false; + } } - if ( ( empty( $config['src'] ) || ! is_string( $config['src'] ) ) && ( empty( $config['data'] ) ) ) { - throw new Exception( 'Font Collection config "src" option OR "data" option is required.' ); + if ( + ( empty( $config['src'] ) && empty( $config['font_families'] ) ) || + ( ! empty( $config['src'] ) && ! empty( $config['font_families'] ) ) + ) { + _doing_it_wrong( __METHOD__, __( 'Font Collection config "src" option OR "font_families" option are required.', 'gutenberg' ), '6.5.0' ); + return false; } - $this->config = $config; + return true; } /** @@ -73,56 +160,59 @@ public function __construct( $config ) { */ public function get_config() { return array( - 'slug' => $this->config['slug'], - 'name' => $this->config['name'], - 'description' => $this->config['description'] ?? '', + 'slug' => $this->slug, + 'name' => $this->name, + 'description' => $this->description, ); } /** - * Gets the font collection config and data. + * Gets the font collection content. * - * This function returns an array containing the font collection's unique ID, - * name, and its data as a PHP array. + * Load the font collection data from the src if it is not already loaded. * * @since 6.5.0 * - * @return array { - * An array of font collection config and data. + * @return array|WP_Error { + * An array of font collection contents. * - * @type string $slug The font collection's unique ID. - * @type string $name The font collection's name. - * @type string $description The font collection's description. - * @type array $data The font collection's data as a PHP array. + * @type array $font_families The font collection's font families. + * @type string $categories The font collection's categories. * } + * + * A WP_Error object if there was an error loading the font collection data. */ - public function get_config_and_data() { - $config_and_data = $this->get_config(); - $config_and_data['data'] = $this->load_data(); - return $config_and_data; + public function get_content() { + // If the font families are not loaded, and the src is not empty, load the data from the src. + if ( empty( $this->font_families ) && ! empty( $this->src ) ) { + $data = $this->load_contents_from_src(); + if ( is_wp_error( $data ) ) { + return $data; + } + } + + return array( + 'font_families' => $this->font_families, + 'categories' => $this->categories, + ); } /** - * Loads the font collection data. + * Loads the font collection data from the src. * * @since 6.5.0 * * @return array|WP_Error An array containing the list of font families in font-collection.json format on success, * else an instance of WP_Error on failure. */ - public function load_data() { - - if ( ! empty( $this->config['data'] ) ) { - return $this->config['data']; - } - + private function load_contents_from_src() { // If the src is a URL, fetch the data from the URL. - if ( str_contains( $this->config['src'], 'http' ) && str_contains( $this->config['src'], '://' ) ) { - if ( ! wp_http_validate_url( $this->config['src'] ) ) { + if ( preg_match( '#^https?://#', $this->src ) ) { + if ( ! wp_http_validate_url( $this->src ) ) { return new WP_Error( 'font_collection_read_error', __( 'Invalid URL for Font Collection data.', 'gutenberg' ) ); } - $response = wp_remote_get( $this->config['src'] ); + $response = wp_remote_get( $this->src ); if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) { return new WP_Error( 'font_collection_read_error', __( 'Error fetching the Font Collection data from a URL.', 'gutenberg' ) ); } @@ -133,15 +223,22 @@ public function load_data() { } // If the src is a file path, read the data from the file. } else { - if ( ! file_exists( $this->config['src'] ) ) { + if ( ! file_exists( $this->src ) ) { return new WP_Error( 'font_collection_read_error', __( 'Font Collection data JSON file does not exist.', 'gutenberg' ) ); } - $data = wp_json_file_decode( $this->config['src'], array( 'associative' => true ) ); + $data = wp_json_file_decode( $this->src, array( 'associative' => true ) ); if ( empty( $data ) ) { return new WP_Error( 'font_collection_read_error', __( 'Error reading the Font Collection data JSON file contents.', 'gutenberg' ) ); } } + if ( empty( $data['font_families'] ) ) { + return new WP_Error( 'font_collection_contents_error', __( 'Font Collection data JSON file does not contain font families.', 'gutenberg' ) ); + } + + $this->font_families = $data['font_families']; + $this->categories = $data['categories'] ?? array(); + return $data; } } diff --git a/lib/experimental/fonts/font-library/class-wp-font-family-utils.php b/lib/experimental/fonts/font-library/class-wp-font-family-utils.php index 35e6856e50aad8..b291a8f1ee348d 100644 --- a/lib/experimental/fonts/font-library/class-wp-font-family-utils.php +++ b/lib/experimental/fonts/font-library/class-wp-font-family-utils.php @@ -91,7 +91,7 @@ public static function format_font_family( $font_family ) { function ( $family ) { $trimmed = trim( $family ); if ( ! empty( $trimmed ) && strpos( $trimmed, ' ' ) !== false && strpos( $trimmed, "'" ) === false && strpos( $trimmed, '"' ) === false ) { - return "'" . $trimmed . "'"; + return '"' . $trimmed . '"'; } return $trimmed; }, @@ -107,4 +107,84 @@ function ( $family ) { return $font_family; } + + /** + * Generates a slug from font face properties, e.g. `open sans;normal;400;100%;U+0-10FFFF` + * + * Used for comparison with other font faces in the same family, to prevent duplicates + * that would both match according the CSS font matching spec. Uses only simple case-insensitive + * matching for fontFamily and unicodeRange, so does not handle overlapping font-family lists or + * unicode ranges. + * + * @since 6.5.0 + * + * @link https://drafts.csswg.org/css-fonts/#font-style-matching + * + * @param array $settings { + * Font face settings. + * + * @type string $fontFamily Font family name. + * @type string $fontStyle Optional font style, defaults to 'normal'. + * @type string $fontWeight Optional font weight, defaults to 400. + * @type string $fontStretch Optional font stretch, defaults to '100%'. + * @type string $unicodeRange Optional unicode range, defaults to 'U+0-10FFFF'. + * } + * @return string Font face slug. + */ + public static function get_font_face_slug( $settings ) { + $settings = wp_parse_args( + $settings, + array( + 'fontFamily' => '', + 'fontStyle' => 'normal', + 'fontWeight' => '400', + 'fontStretch' => '100%', + 'unicodeRange' => 'U+0-10FFFF', + ) + ); + + // Convert all values to lowercase for comparison. + // Font family names may use multibyte characters. + $font_family = mb_strtolower( $settings['fontFamily'] ); + $font_style = strtolower( $settings['fontStyle'] ); + $font_weight = strtolower( $settings['fontWeight'] ); + $font_stretch = strtolower( $settings['fontStretch'] ); + $unicode_range = strtoupper( $settings['unicodeRange'] ); + + // Convert weight keywords to numeric strings. + $font_weight = str_replace( 'normal', '400', $font_weight ); + $font_weight = str_replace( 'bold', '700', $font_weight ); + + // Convert stretch keywords to numeric strings. + $font_stretch_map = array( + 'ultra-condensed' => '50%', + 'extra-condensed' => '62.5%', + 'condensed' => '75%', + 'semi-condensed' => '87.5%', + 'normal' => '100%', + 'semi-expanded' => '112.5%', + 'expanded' => '125%', + 'extra-expanded' => '150%', + 'ultra-expanded' => '200%', + ); + $font_stretch = str_replace( array_keys( $font_stretch_map ), array_values( $font_stretch_map ), $font_stretch ); + + $slug_elements = array( $font_family, $font_style, $font_weight, $font_stretch, $unicode_range ); + + $slug_elements = array_map( + function ( $elem ) { + // Remove quotes to normalize font-family names, and ';' to use as a separator. + $elem = trim( str_replace( array( '"', "'", ';' ), '', $elem ) ); + + // Normalize comma separated lists by removing whitespace in between items, + // but keep whitespace within items (e.g. "Open Sans" and "OpenSans" are different fonts). + // CSS spec for whitespace includes: U+000A LINE FEED, U+0009 CHARACTER TABULATION, or U+0020 SPACE, + // which by default are all matched by \s in PHP. + return preg_replace( '/,\s+/', ',', $elem ); + }, + $slug_elements + ); + + return join( ';', $slug_elements ); + } } diff --git a/lib/experimental/fonts/font-library/class-wp-font-family.php b/lib/experimental/fonts/font-library/class-wp-font-family.php deleted file mode 100644 index f64aebc0c8efa7..00000000000000 --- a/lib/experimental/fonts/font-library/class-wp-font-family.php +++ /dev/null @@ -1,621 +0,0 @@ -data = $font_data; - } - - /** - * Gets the font family data. - * - * @since 6.5.0 - * - * @return array An array in fontFamily theme.json format. - */ - public function get_data() { - return $this->data; - } - - /** - * Gets the font family data. - * - * @since 6.5.0 - * - * @return string fontFamily in theme.json format as stringified JSON. - */ - public function get_data_as_json() { - return wp_json_encode( $this->get_data() ); - } - - /** - * Checks whether the font family has font faces defined. - * - * @since 6.5.0 - * - * @return bool True if the font family has font faces defined, false otherwise. - */ - public function has_font_faces() { - return ! empty( $this->data['fontFace'] ) && is_array( $this->data['fontFace'] ); - } - - /** - * Removes font family assets. - * - * @since 6.5.0 - * - * @return bool True if assets were removed, false otherwise. - */ - private function remove_font_family_assets() { - if ( $this->has_font_faces() ) { - foreach ( $this->data['fontFace'] as $font_face ) { - $were_assets_removed = $this->delete_font_face_assets( $font_face ); - if ( false === $were_assets_removed ) { - return false; - } - } - } - return true; - } - - /** - * Removes a font family from the database and deletes its assets. - * - * @since 6.5.0 - * - * @return bool|WP_Error True if the font family was uninstalled, WP_Error otherwise. - */ - public function uninstall() { - $post = $this->get_data_from_post(); - if ( null === $post ) { - return new WP_Error( - 'font_family_not_found', - __( 'The font family could not be found.', 'gutenberg' ) - ); - } - - if ( - ! $this->remove_font_family_assets() || - ! wp_delete_post( $post->ID, true ) - ) { - return new WP_Error( - 'font_family_not_deleted', - __( 'The font family could not be deleted.', 'gutenberg' ) - ); - } - - return true; - } - - /** - * Deletes a specified font asset file from the fonts directory. - * - * @since 6.5.0 - * - * @param string $src The path of the font asset file to delete. - * @return bool Whether the file was deleted. - */ - private static function delete_asset( $src ) { - $filename = basename( $src ); - $file_path = path_join( wp_get_font_dir()['path'], $filename ); - - wp_delete_file( $file_path ); - - return ! file_exists( $file_path ); - } - - /** - * Deletes all font face asset files associated with a given font face. - * - * @since 6.5.0 - * - * @param array $font_face The font face array containing the 'src' attribute - * with the file path(s) to be deleted. - * @return bool True if delete was successful, otherwise false. - */ - private static function delete_font_face_assets( $font_face ) { - $sources = (array) $font_face['src']; - foreach ( $sources as $src ) { - $was_asset_removed = self::delete_asset( $src ); - if ( ! $was_asset_removed ) { - // Bail if any of the assets could not be removed. - return false; - } - } - return true; - } - - /** - * Gets the overrides for the 'wp_handle_upload' function. - * - * @since 6.5.0 - * - * @param string $filename The filename to be used for the uploaded file. - * @return array The overrides for the 'wp_handle_upload' function. - */ - private function get_upload_overrides( $filename ) { - return array( - // Arbitrary string to avoid the is_uploaded_file() check applied - // when using 'wp_handle_upload'. - 'action' => 'wp_handle_font_upload', - // Not testing a form submission. - 'test_form' => false, - // Seems mime type for files that are not images cannot be tested. - // See wp_check_filetype_and_ext(). - 'test_type' => true, - 'mimes' => WP_Font_Library::get_expected_font_mime_types_per_php_version(), - 'unique_filename_callback' => static function () use ( $filename ) { - // Keep the original filename. - return $filename; - }, - ); - } - - /** - * Downloads a font asset from a specified source URL and saves it to - * the font directory. - * - * @since 6.5.0 - * - * @param string $url The source URL of the font asset to be downloaded. - * @param string $filename The filename to save the downloaded font asset as. - * @return string|bool The relative path to the downloaded font asset. - * False if the download failed. - */ - private function download_asset( $url, $filename ) { - // Include file with download_url() if function doesn't exist. - if ( ! function_exists( 'download_url' ) ) { - require_once ABSPATH . 'wp-admin/includes/file.php'; - } - - // Downloads the font asset or returns false. - $temp_file = download_url( $url ); - if ( is_wp_error( $temp_file ) ) { - return false; - } - - $overrides = $this->get_upload_overrides( $filename ); - - $file = array( - 'tmp_name' => $temp_file, - 'name' => $filename, - ); - - $handled_file = wp_handle_upload( $file, $overrides ); - - // Cleans the temp file. - @unlink( $temp_file ); - - if ( ! isset( $handled_file['url'] ) ) { - return false; - } - - // Returns the relative path to the downloaded font asset to be used as - // font face src. - return $handled_file['url']; - } - - /** - * Moves an uploaded font face asset from temp folder to the fonts directory. - * - * This is used when uploading local fonts. - * - * @since 6.5.0 - * - * @param array $font_face Font face to download. - * @param array $file Uploaded file. - * @return array New font face with all assets downloaded and referenced in - * the font face definition. - */ - private function move_font_face_asset( $font_face, $file ) { - $new_font_face = $font_face; - $filename = WP_Font_Family_Utils::get_filename_from_font_face( - $this->data['slug'], - $font_face, - $file['name'] - ); - - // Remove the uploaded font asset reference from the font face definition - // because it is no longer needed. - unset( $new_font_face['uploadedFile'] ); - - // Move the uploaded font asset from the temp folder to the fonts directory. - if ( ! function_exists( 'wp_handle_upload' ) ) { - require_once ABSPATH . 'wp-admin/includes/file.php'; - } - - $overrides = $this->get_upload_overrides( $filename ); - - $handled_file = wp_handle_upload( $file, $overrides ); - - if ( isset( $handled_file['url'] ) ) { - // If the file was successfully moved, update the font face definition - // to reference the new file location. - $new_font_face['src'] = $handled_file['url']; - } - - return $new_font_face; - } - - /** - * Sanitizes the font family data using WP_Theme_JSON. - * - * @since 6.5.0 - * - * @return array A sanitized font family definition. - */ - private function sanitize() { - // Creates the structure of theme.json array with the new fonts. - $fonts_json = array( - 'version' => '2', - 'settings' => array( - 'typography' => array( - 'fontFamilies' => array( - 'custom' => array( - $this->data, - ), - ), - ), - ), - ); - - // Creates a new WP_Theme_JSON object with the new fonts to - // leverage sanitization and validation. - $fonts_json = WP_Theme_JSON_Gutenberg::remove_insecure_properties( $fonts_json ); - $theme_json = new WP_Theme_JSON_Gutenberg( $fonts_json ); - $theme_data = $theme_json->get_data(); - $sanitized_font = ! empty( $theme_data['settings']['typography']['fontFamilies'] ) - ? $theme_data['settings']['typography']['fontFamilies'][0] - : array(); - - $sanitized_font['slug'] = _wp_to_kebab_case( $sanitized_font['slug'] ); - $sanitized_font['fontFamily'] = WP_Font_Family_Utils::format_font_family( $sanitized_font['fontFamily'] ); - $this->data = $sanitized_font; - return $this->data; - } - - /** - * Downloads font face assets. - * - * Downloads the font face asset(s) associated with a font face. It works with - * both single source URLs and arrays of multiple source URLs. - * - * @since 6.5.0 - * - * @param array $font_face The font face array containing the 'src' attribute - * with the source URL(s) of the assets. - * @return array The modified font face array with the new source URL(s) to - * the downloaded assets. - */ - private function download_font_face_assets( $font_face ) { - $new_font_face = $font_face; - $sources = (array) $font_face['downloadFromUrl']; - $new_font_face['src'] = array(); - $index = 0; - - foreach ( $sources as $src ) { - $suffix = $index++ > 0 ? $index : ''; - $filename = WP_Font_Family_Utils::get_filename_from_font_face( - $this->data['slug'], - $font_face, - $src, - $suffix - ); - $new_src = $this->download_asset( $src, $filename ); - if ( $new_src ) { - $new_font_face['src'][] = $new_src; - } - } - - if ( count( $new_font_face['src'] ) === 1 ) { - $new_font_face['src'] = $new_font_face['src'][0]; - } - - // Remove the download url reference from the font face definition - // because it is no longer needed. - unset( $new_font_face['downloadFromUrl'] ); - - return $new_font_face; - } - - - /** - * Downloads font face assets if the font family is a Google font, - * or moves them if it is a local font. - * - * @since 6.5.0 - * - * @param array $files An array of files to be installed. - * @return bool True if the font faces were downloaded or moved successfully, false otherwise. - */ - private function download_or_move_font_faces( $files ) { - if ( ! $this->has_font_faces() ) { - return true; - } - - $new_font_faces = array(); - foreach ( $this->data['fontFace'] as $font_face ) { - // If the fonts are not meant to be downloaded or uploaded - // (for example to install fonts that use a remote url). - $new_font_face = $font_face; - - $font_face_is_repeated = false; - - // If the font face has the same fontStyle and fontWeight as an existing, continue. - foreach ( $new_font_faces as $font_to_compare ) { - if ( $new_font_face['fontStyle'] === $font_to_compare['fontStyle'] && - $new_font_face['fontWeight'] === $font_to_compare['fontWeight'] ) { - $font_face_is_repeated = true; - } - } - - if ( $font_face_is_repeated ) { - continue; - } - - // If the font face requires the use of the filesystem, create the fonts dir if it doesn't exist. - if ( ! empty( $font_face['downloadFromUrl'] ) && ! empty( $font_face['uploadedFile'] ) ) { - wp_mkdir_p( wp_get_font_dir()['path'] ); - } - - // If installing google fonts, download the font face assets. - if ( ! empty( $font_face['downloadFromUrl'] ) ) { - $new_font_face = $this->download_font_face_assets( $new_font_face ); - } - - // If installing local fonts, move the font face assets from - // the temp folder to the wp fonts directory. - if ( ! empty( $font_face['uploadedFile'] ) && ! empty( $files ) ) { - $new_font_face = $this->move_font_face_asset( - $new_font_face, - $files[ $new_font_face['uploadedFile'] ] - ); - } - - /* - * If the font face assets were successfully downloaded, add the font face - * to the new font. Font faces with failed downloads are not added to the - * new font. - */ - if ( ! empty( $new_font_face['src'] ) ) { - $new_font_faces[] = $new_font_face; - } - } - - if ( ! empty( $new_font_faces ) ) { - $this->data['fontFace'] = $new_font_faces; - return true; - } - - return false; - } - - /** - * Gets the post for a font family. - * - * @since 6.5.0 - * - * @return WP_Post|null The post for this font family object or - * null if the post does not exist. - */ - public function get_font_post() { - $args = array( - 'post_type' => 'wp_font_family', - 'post_name' => $this->data['slug'], - 'name' => $this->data['slug'], - 'posts_per_page' => 1, - ); - - $posts_query = new WP_Query( $args ); - - if ( $posts_query->have_posts() ) { - return $posts_query->posts[0]; - } - - return null; - } - - /** - * Gets the data for this object from the database and - * sets it to the data property. - * - * @since 6.5.0 - * - * @return WP_Post|null The post for this font family object or - * null if the post does not exist. - */ - private function get_data_from_post() { - $post = $this->get_font_post(); - if ( $post ) { - $this->data = json_decode( $post->post_content, true ); - return $post; - } - - return null; - } - - /** - * Creates a post for a font family. - * - * @since 6.5.0 - * - * @return int|WP_Error Post ID if the post was created, WP_Error otherwise. - */ - private function create_font_post() { - $post = array( - 'post_title' => $this->data['name'], - 'post_name' => $this->data['slug'], - 'post_type' => 'wp_font_family', - 'post_content' => $this->get_data_as_json(), - 'post_status' => 'publish', - ); - - $post_id = wp_insert_post( $post ); - if ( 0 === $post_id || is_wp_error( $post_id ) ) { - return new WP_Error( - 'font_post_creation_failed', - __( 'Font post creation failed', 'gutenberg' ) - ); - } - - return $post_id; - } - - /** - * Gets the font faces that are in both the existing and incoming font families. - * - * @since 6.5.0 - * - * @param array $existing The existing font faces. - * @param array $incoming The incoming font faces. - * @return array The font faces that are in both the existing and incoming font families. - */ - private function get_intersecting_font_faces( $existing, $incoming ) { - $intersecting = array(); - foreach ( $existing as $existing_face ) { - foreach ( $incoming as $incoming_face ) { - if ( $incoming_face['fontStyle'] === $existing_face['fontStyle'] && - $incoming_face['fontWeight'] === $existing_face['fontWeight'] && - $incoming_face['src'] !== $existing_face['src'] ) { - $intersecting[] = $existing_face; - } - } - } - return $intersecting; - } - - /** - * Updates a post for a font family. - * - * @since 6.5.0 - * - * @param WP_Post $post The post to update. - * @return int|WP_Error Post ID if the update was successful, WP_Error otherwise. - */ - private function update_font_post( $post ) { - $post_font_data = json_decode( $post->post_content, true ); - $new_data = WP_Font_Family_Utils::merge_fonts_data( $post_font_data, $this->data ); - if ( isset( $post_font_data['fontFace'] ) && ! empty( $post_font_data['fontFace'] ) ) { - $intersecting = $this->get_intersecting_font_faces( $post_font_data['fontFace'], $new_data['fontFace'] ); - } - - if ( isset( $intersecting ) && ! empty( $intersecting ) ) { - $serialized_font_faces = array_map( 'serialize', $new_data['fontFace'] ); - $serialized_intersecting = array_map( 'serialize', $intersecting ); - - $diff = array_diff( $serialized_font_faces, $serialized_intersecting ); - - $new_data['fontFace'] = array_values( array_map( 'unserialize', $diff ) ); - - foreach ( $intersecting as $intersect ) { - $this->delete_font_face_assets( $intersect ); - } - } - $this->data = $new_data; - - $post = array( - 'ID' => $post->ID, - 'post_content' => $this->get_data_as_json(), - ); - - $post_id = wp_update_post( $post ); - - if ( 0 === $post_id || is_wp_error( $post_id ) ) { - return new WP_Error( - 'font_post_update_failed', - __( 'Font post update failed', 'gutenberg' ) - ); - } - - return $post_id; - } - - /** - * Creates a post for a font in the Font Library if it doesn't exist, - * or updates it if it does. - * - * @since 6.5.0 - * - * @return int|WP_Error Post id if the post was created or updated successfully, - * WP_Error otherwise. - */ - private function create_or_update_font_post() { - $this->sanitize(); - - $post = $this->get_font_post(); - if ( $post ) { - return $this->update_font_post( $post ); - } - - return $this->create_font_post(); - } - - /** - * Installs the font family into the library. - * - * @since 6.5.0 - * - * @param array $files Optional. An array of files to be installed. Default null. - * @return array|WP_Error An array of font family data on success, WP_Error otherwise. - */ - public function install( $files = null ) { - add_filter( 'upload_mimes', array( 'WP_Font_Library', 'set_allowed_mime_types' ) ); - add_filter( 'upload_dir', 'wp_get_font_dir' ); - $were_assets_written = $this->download_or_move_font_faces( $files ); - remove_filter( 'upload_dir', 'wp_get_font_dir' ); - remove_filter( 'upload_mimes', array( 'WP_Font_Library', 'set_allowed_mime_types' ) ); - - if ( ! $were_assets_written ) { - return new WP_Error( - 'font_face_download_failed', - __( 'The font face assets could not be written.', 'gutenberg' ) - ); - } - - $post_id = $this->create_or_update_font_post(); - - if ( is_wp_error( $post_id ) ) { - return $post_id; - } - - return $this->get_data(); - } -} diff --git a/lib/experimental/fonts/font-library/class-wp-font-library.php b/lib/experimental/fonts/font-library/class-wp-font-library.php index fd36f6ba073c4f..51a84b957ea117 100644 --- a/lib/experimental/fonts/font-library/class-wp-font-library.php +++ b/lib/experimental/fonts/font-library/class-wp-font-library.php @@ -62,11 +62,17 @@ public static function get_expected_font_mime_types_per_php_version( $php_versio * @return WP_Font_Collection|WP_Error A font collection is it was registered successfully and a WP_Error otherwise. */ public static function register_font_collection( $config ) { + if ( ! WP_Font_Collection::is_config_valid( $config ) ) { + $error_message = __( 'Font collection config is invalid.', 'gutenberg' ); + return new WP_Error( 'font_collection_registration_error', $error_message ); + } + $new_collection = new WP_Font_Collection( $config ); - if ( self::is_collection_registered( $config['slug'] ) ) { + + if ( self::is_collection_registered( $new_collection->get_config()['slug'] ) ) { $error_message = sprintf( /* translators: %s: Font collection slug. */ - __( 'Font collection with slug: "%s" is already registered.', 'default' ), + __( 'Font collection with slug: "%s" is already registered.', 'gutenberg' ), $config['slug'] ); _doing_it_wrong( @@ -76,7 +82,7 @@ public static function register_font_collection( $config ) { ); return new WP_Error( 'font_collection_registration_error', $error_message ); } - self::$collections[ $config['slug'] ] = $new_collection; + self::$collections[ $new_collection->get_config()['slug'] ] = $new_collection; return $new_collection; } diff --git a/lib/experimental/fonts/font-library/class-wp-rest-autosave-font-families-controller.php b/lib/experimental/fonts/font-library/class-wp-rest-autosave-font-families-controller.php deleted file mode 100644 index 0e31bd4004b40f..00000000000000 --- a/lib/experimental/fonts/font-library/class-wp-rest-autosave-font-families-controller.php +++ /dev/null @@ -1,25 +0,0 @@ - WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_font_collections' ), - 'permission_callback' => array( $this, 'update_font_library_permissions_check' ), + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), ), ) ); @@ -54,13 +54,29 @@ public function register_routes() { array( array( 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_font_collection' ), - 'permission_callback' => array( $this, 'update_font_library_permissions_check' ), + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), ), ) ); } + /** + * Gets the font collections available. + * + * @since 6.5.0 + * + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function get_items( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + $collections = array(); + foreach ( WP_Font_Library::get_font_collections() as $collection ) { + $collections[] = $collection->get_config(); + } + + return rest_ensure_response( $collections, 200 ); + } + /** * Gets a font collection. * @@ -69,54 +85,42 @@ public function register_routes() { * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ - public function get_font_collection( $request ) { + public function get_item( $request ) { $slug = $request->get_param( 'slug' ); $collection = WP_Font_Library::get_font_collection( $slug ); + // If the collection doesn't exist returns a 404. if ( is_wp_error( $collection ) ) { $collection->add_data( array( 'status' => 404 ) ); return $collection; } - $config_and_data = $collection->get_config_and_data(); - $collection_data = $config_and_data['data']; - // If there was an error getting the collection data, return the error. - if ( is_wp_error( $collection_data ) ) { - $collection_data->add_data( array( 'status' => 500 ) ); - return $collection_data; - } - - return new WP_REST_Response( $config_and_data ); - } + $config = $collection->get_config(); + $contents = $collection->get_content(); - /** - * Gets the font collections available. - * - * @since 6.5.0 - * - * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. - */ - public function get_font_collections() { - $collections = array(); - foreach ( WP_Font_Library::get_font_collections() as $collection ) { - $collections[] = $collection->get_config_and_data(); + // If there was an error getting the collection data, return the error. + if ( is_wp_error( $contents ) ) { + $contents->add_data( array( 'status' => 500 ) ); + return $contents; } - return new WP_REST_Response( $collections, 200 ); + $collection_data = array_merge( $config, $contents ); + return rest_ensure_response( $collection_data ); } /** - * Checks whether the user has permissions to update the Font Library. + * Checks whether the user has permissions to use the Fonts Collections. * * @since 6.5.0 * * @return true|WP_Error True if the request has write access for the item, WP_Error object otherwise. */ - public function update_font_library_permissions_check() { + public function get_items_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + if ( ! current_user_can( 'edit_theme_options' ) ) { return new WP_Error( - 'rest_cannot_update_font_library', - __( 'Sorry, you are not allowed to update the Font Library on this site.', 'gutenberg' ), + 'rest_cannot_read', + __( 'Sorry, you are not allowed to use the Font Library on this site.', 'gutenberg' ), array( 'status' => rest_authorization_required_code(), ) diff --git a/lib/experimental/fonts/font-library/class-wp-rest-font-faces-controller.php b/lib/experimental/fonts/font-library/class-wp-rest-font-faces-controller.php new file mode 100644 index 00000000000000..fac32362325f49 --- /dev/null +++ b/lib/experimental/fonts/font-library/class-wp-rest-font-faces-controller.php @@ -0,0 +1,836 @@ +namespace, + '/' . $this->rest_base, + array( + 'args' => array( + 'font_family_id' => array( + 'description' => __( 'The ID for the parent font family of the font face.', 'gutenberg' ), + 'type' => 'integer', + 'required' => true, + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => $this->get_create_params(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P[\d]+)', + array( + 'args' => array( + 'font_family_id' => array( + 'description' => __( 'The ID for the parent font family of the font face.', 'gutenberg' ), + 'type' => 'integer', + 'required' => true, + ), + 'id' => array( + 'description' => __( 'Unique identifier for the font face.', 'gutenberg' ), + 'type' => 'integer', + 'required' => true, + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'edit' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'type' => 'boolean', + 'default' => false, + 'description' => __( 'Whether to bypass Trash and force deletion.', 'default' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Checks if a given request has access to font faces. + * + * @since 6.5.0 + * + * @param WP_REST_Request $request Full details about the request. + * @return true|WP_Error True if the request has read access, WP_Error object otherwise. + */ + public function get_items_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- required by parent class + $post_type = get_post_type_object( $this->post_type ); + + if ( ! current_user_can( $post_type->cap->read ) ) { + return new WP_Error( + 'rest_cannot_read', + __( 'Sorry, you are not allowed to access font faces.', 'gutenberg' ), + array( 'status' => rest_authorization_required_code() ) + ); + } + + return true; + } + + /** + * Checks if a given request has access to a font face. + * + * @since 6.5.0 + * + * @param WP_REST_Request $request Full details about the request. + * @return true|WP_Error True if the request has read access, WP_Error object otherwise. + */ + public function get_item_permissions_check( $request ) { + return $this->get_items_permissions_check( $request ); + } + + /** + * Validates settings when creating a font face. + * + * @since 6.5.0 + * + * @param string $value Encoded JSON string of font face settings. + * @param WP_REST_Request $request Request object. + * @return false|WP_Error True if the settings are valid, otherwise a WP_Error object. + */ + public function validate_create_font_face_settings( $value, $request ) { + $settings = json_decode( $value, true ); + + // Check settings string is valid JSON. + if ( null === $settings ) { + return new WP_Error( + 'rest_invalid_param', + __( 'font_face_settings parameter must be a valid JSON string.', 'gutenberg' ), + array( 'status' => 400 ) + ); + } + + // Check that the font face settings match the theme.json schema. + $schema = $this->get_item_schema()['properties']['font_face_settings']; + $has_valid_settings = rest_validate_value_from_schema( $settings, $schema, 'font_face_settings' ); + + if ( is_wp_error( $has_valid_settings ) ) { + $has_valid_settings->add_data( array( 'status' => 400 ) ); + return $has_valid_settings; + } + + // Check that none of the required settings are empty values. + $required = $schema['required']; + foreach ( $required as $key ) { + if ( isset( $settings[ $key ] ) && ! $settings[ $key ] ) { + return new WP_Error( + 'rest_invalid_param', + /* translators: %s: Font family setting key. */ + sprintf( __( 'font_face_setting[%s] cannot be empty.', 'gutenberg' ), $key ), + array( 'status' => 400 ) + ); + } + } + + $srcs = is_array( $settings['src'] ) ? $settings['src'] : array( $settings['src'] ); + + // Check that srcs are non-empty strings. + $filtered_src = array_filter( array_filter( $srcs, 'is_string' ) ); + if ( empty( $filtered_src ) ) { + return new WP_Error( + 'rest_invalid_param', + __( 'font_face_settings[src] values must be non-empty strings.', 'gutenberg' ), + array( 'status' => 400 ) + ); + } + + // Check that each file in the request references a src in the settings. + $files = $request->get_file_params(); + foreach ( array_keys( $files ) as $file ) { + if ( ! in_array( $file, $srcs, true ) ) { + return new WP_Error( + 'rest_invalid_param', + // translators: %s: File key (e.g. `file-0`) in the request data. + sprintf( __( 'File %1$s must be used in font_face_settings[src].', 'gutenberg' ), $file ), + array( 'status' => 400 ) + ); + } + } + + return true; + } + + /** + * Sanitizes the font face settings when creating a font face. + * + * @since 6.5.0 + * + * @param string $value Encoded JSON string of font face settings. + * @param WP_REST_Request $request Request object. + * @return array Decoded array of font face settings. + */ + public function sanitize_font_face_settings( $value ) { + // Settings arrive as stringified JSON, since this is a multipart/form-data request. + $settings = json_decode( $value, true ); + + if ( isset( $settings['fontFamily'] ) ) { + $settings['fontFamily'] = WP_Font_Family_Utils::format_font_family( $settings['fontFamily'] ); + } + + return $settings; + } + + /** + * Retrieves a collection of font faces within the parent font family. + * + * @since 6.5.0 + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function get_items( $request ) { + $font_family = $this->get_parent_font_family_post( $request['font_family_id'] ); + if ( is_wp_error( $font_family ) ) { + return $font_family; + } + + return parent::get_items( $request ); + } + + /** + * Retrieves a single font face within the parent font family. + * + * @since 6.5.0 + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function get_item( $request ) { + $post = $this->get_post( $request['id'] ); + if ( is_wp_error( $post ) ) { + return $post; + } + + // Check that the font face has a valid parent font family. + $font_family = $this->get_parent_font_family_post( $request['font_family_id'] ); + if ( is_wp_error( $font_family ) ) { + return $font_family; + } + + if ( (int) $font_family->ID !== (int) $post->post_parent ) { + return new WP_Error( + 'rest_font_face_parent_id_mismatch', + /* translators: %d: A post id. */ + sprintf( __( 'The font face does not belong to the specified font family with id of "%d"', 'gutenberg' ), $font_family->ID ), + array( 'status' => 404 ) + ); + } + + return parent::get_item( $request ); + } + + /** + * Creates a font face for the parent font family. + * + * @since 6.5.0 + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function create_item( $request ) { + $font_family = $this->get_parent_font_family_post( $request['font_family_id'] ); + if ( is_wp_error( $font_family ) ) { + return $font_family; + } + + // Settings have already been decoded by ::sanitize_font_face_settings(). + $settings = $request->get_param( 'font_face_settings' ); + $file_params = $request->get_file_params(); + + // Check that the necessary font face properties are unique. + $query = new WP_Query( + array( + 'post_type' => $this->post_type, + 'posts_per_page' => 1, + 'title' => WP_Font_Family_Utils::get_font_face_slug( $settings ), + 'update_post_meta_cache' => false, + 'update_post_term_cache' => false, + ) + ); + if ( ! empty( $query->get_posts() ) ) { + return new WP_Error( + 'rest_duplicate_font_face', + __( 'A font face matching those settings already exists.', 'gutenberg' ), + array( 'status' => 400 ) + ); + } + + // Move the uploaded font asset from the temp folder to the fonts directory. + if ( ! function_exists( 'wp_handle_upload' ) ) { + require_once ABSPATH . 'wp-admin/includes/file.php'; + } + + $srcs = is_string( $settings['src'] ) ? array( $settings['src'] ) : $settings['src']; + $processed_srcs = array(); + $font_file_meta = array(); + + foreach ( $srcs as $src ) { + // If src not a file reference, use it as is. + if ( ! isset( $file_params[ $src ] ) ) { + $processed_srcs[] = $src; + continue; + } + + $file = $file_params[ $src ]; + $font_file = $this->handle_font_file_upload( $file ); + if ( is_wp_error( $font_file ) ) { + return $font_file; + } + + $processed_srcs[] = $font_file['url']; + $font_file_meta[] = $this->relative_fonts_path( $font_file['file'] ); + } + + // Store the updated settings for prepare_item_for_database to use. + $settings['src'] = count( $processed_srcs ) === 1 ? $processed_srcs[0] : $processed_srcs; + $request->set_param( 'font_face_settings', $settings ); + + // Ensure that $settings data is slashed, so values with quotes are escaped. + // WP_REST_Posts_Controller::create_item uses wp_slash() on the post_content. + $font_face_post = parent::create_item( $request ); + + if ( is_wp_error( $font_face_post ) ) { + return $font_face_post; + } + + $font_face_id = $font_face_post->data['id']; + + foreach ( $font_file_meta as $font_file_path ) { + add_post_meta( $font_face_id, '_wp_font_face_file', $font_file_path ); + } + + return $font_face_post; + } + + /** + * Deletes a single font face. + * + * @since 6.5.0 + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function delete_item( $request ) { + $post = $this->get_post( $request['id'] ); + if ( is_wp_error( $post ) ) { + return $post; + } + + $font_family = $this->get_parent_font_family_post( $request['font_family_id'] ); + if ( is_wp_error( $font_family ) ) { + return $font_family; + } + + if ( (int) $font_family->ID !== (int) $post->post_parent ) { + return new WP_Error( + 'rest_font_face_parent_id_mismatch', + /* translators: %d: A post id. */ + sprintf( __( 'The font face does not belong to the specified font family with id of "%d"', 'gutenberg' ), $font_family->ID ), + array( 'status' => 404 ) + ); + } + + $force = isset( $request['force'] ) ? (bool) $request['force'] : false; + + // We don't support trashing for font faces. + if ( ! $force ) { + return new WP_Error( + 'rest_trash_not_supported', + /* translators: %s: force=true */ + sprintf( __( "Font faces do not support trashing. Set '%s' to delete.", 'gutenberg' ), 'force=true' ), + array( 'status' => 501 ) + ); + } + + return parent::delete_item( $request ); + } + + /** + * Prepares a single font face output for response. + * + * @since 6.5.0 + * + * @param WP_Post $item Post object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response Response object. + */ + public function prepare_item_for_response( $item, $request ) { + $fields = $this->get_fields_for_response( $request ); + $data = array(); + + if ( rest_is_field_included( 'id', $fields ) ) { + $data['id'] = $item->ID; + } + if ( rest_is_field_included( 'theme_json_version', $fields ) ) { + $data['theme_json_version'] = 2; + } + + if ( rest_is_field_included( 'parent', $fields ) ) { + $data['parent'] = $item->post_parent; + } + + if ( rest_is_field_included( 'font_face_settings', $fields ) ) { + $data['font_face_settings'] = $this->get_settings_from_post( $item ); + } + + $context = ! empty( $request['context'] ) ? $request['context'] : 'edit'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + $response = rest_ensure_response( $data ); + + if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) { + $links = $this->prepare_links( $item ); + $response->add_links( $links ); + } + + /** + * Filters the font face data for a REST API response. + * + * @since 6.5.0 + * + * @param WP_REST_Response $response The response object. + * @param WP_Post $post Font face post object. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'rest_prepare_wp_font_face', $response, $item, $request ); + } + + /** + * Retrieves the post's schema, conforming to JSON Schema. + * + * @since 6.5.0 + * + * @return array Item schema data. + */ + public function get_item_schema() { + if ( $this->schema ) { + return $this->add_additional_fields_schema( $this->schema ); + } + + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => $this->post_type, + 'type' => 'object', + // Base properties for every Post. + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the post.', 'default' ), + 'type' => 'integer', + 'context' => array( 'edit', 'embed' ), + 'readonly' => true, + ), + 'theme_json_version' => array( + 'description' => __( 'Version of the theme.json schema used for the typography settings.', 'gutenberg' ), + 'type' => 'integer', + 'default' => 2, + 'minimum' => 2, + 'maximum' => 2, + 'context' => array( 'edit', 'embed' ), + ), + 'parent' => array( + 'description' => __( 'The ID for the parent font family of the font face.', 'gutenberg' ), + 'type' => 'integer', + 'context' => array( 'edit', 'embed' ), + ), + // Font face settings come directly from theme.json schema + // See https://schemas.wp.org/trunk/theme.json + 'font_face_settings' => array( + 'description' => __( 'font-face declaration in theme.json format.', 'gutenberg' ), + 'type' => 'object', + 'context' => array( 'edit', 'embed' ), + 'properties' => array( + 'fontFamily' => array( + 'description' => __( 'CSS font-family value.', 'gutenberg' ), + 'type' => 'string', + 'default' => '', + ), + 'fontStyle' => array( + 'description' => __( 'CSS font-style value.', 'gutenberg' ), + 'type' => 'string', + 'default' => 'normal', + ), + 'fontWeight' => array( + 'description' => __( 'List of available font weights, separated by a space.', 'gutenberg' ), + 'default' => '400', + // Changed from `oneOf` to avoid errors from loose type checking. + // e.g. a fontWeight of "400" validates as both a string and an integer due to is_numeric check. + 'type' => array( 'string', 'integer' ), + ), + 'fontDisplay' => array( + 'description' => __( 'CSS font-display value.', 'gutenberg' ), + 'type' => 'string', + 'default' => 'fallback', + 'enum' => array( + 'auto', + 'block', + 'fallback', + 'swap', + 'optional', + ), + ), + 'src' => array( + 'description' => __( 'Paths or URLs to the font files.', 'gutenberg' ), + // Changed from `oneOf` to `anyOf` due to rest_sanitize_array converting a string into an array. + 'anyOf' => array( + array( + 'type' => 'string', + ), + array( + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + ), + ), + 'default' => array(), + ), + 'fontStretch' => array( + 'description' => __( 'CSS font-stretch value.', 'gutenberg' ), + 'type' => 'string', + ), + 'ascentOverride' => array( + 'description' => __( 'CSS ascent-override value.', 'gutenberg' ), + 'type' => 'string', + ), + 'descentOverride' => array( + 'description' => __( 'CSS descent-override value.', 'gutenberg' ), + 'type' => 'string', + ), + 'fontVariant' => array( + 'description' => __( 'CSS font-variant value.', 'gutenberg' ), + 'type' => 'string', + ), + 'fontFeatureSettings' => array( + 'description' => __( 'CSS font-feature-settings value.', 'gutenberg' ), + 'type' => 'string', + ), + 'fontVariationSettings' => array( + 'description' => __( 'CSS font-variation-settings value.', 'gutenberg' ), + 'type' => 'string', + ), + 'lineGapOverride' => array( + 'description' => __( 'CSS line-gap-override value.', 'gutenberg' ), + 'type' => 'string', + ), + 'sizeAdjust' => array( + 'description' => __( 'CSS size-adjust value.', 'gutenberg' ), + 'type' => 'string', + ), + 'unicodeRange' => array( + 'description' => __( 'CSS unicode-range value.', 'gutenberg' ), + 'type' => 'string', + ), + 'preview' => array( + 'description' => __( 'URL to a preview image of the font face.', 'gutenberg' ), + 'type' => 'string', + ), + ), + 'required' => array( 'fontFamily', 'src' ), + 'additionalProperties' => false, + ), + ), + ); + + $this->schema = $schema; + + return $this->add_additional_fields_schema( $this->schema ); + } + + /** + * Retrieves the query params for the font face collection. + * + * @since 6.5.0 + * + * @return array Collection parameters. + */ + public function get_collection_params() { + $query_params = parent::get_collection_params(); + + $query_params['context']['default'] = 'edit'; + + // Remove unneeded params. + unset( $query_params['after'] ); + unset( $query_params['modified_after'] ); + unset( $query_params['before'] ); + unset( $query_params['modified_before'] ); + unset( $query_params['search'] ); + unset( $query_params['search_columns'] ); + unset( $query_params['slug'] ); + unset( $query_params['status'] ); + + $query_params['orderby']['default'] = 'id'; + $query_params['orderby']['enum'] = array( 'id', 'include' ); + + /** + * Filters collection parameters for the font face controller. + * + * @since 6.5.0 + * + * @param array $query_params JSON Schema-formatted collection parameters. + */ + return apply_filters( 'rest_wp_font_face_collection_params', $query_params ); + } + + /** + * Get the params used when creating a new font face. + * + * @since 6.5.0 + * + * @return array Font face create arguments. + */ + public function get_create_params() { + $properties = $this->get_item_schema()['properties']; + return array( + 'theme_json_version' => $properties['theme_json_version'], + // When creating, font_face_settings is stringified JSON, to work with multipart/form-data used + // when uploading font files. + 'font_face_settings' => array( + 'description' => __( 'font-face declaration in theme.json format, encoded as a string.', 'gutenberg' ), + 'type' => 'string', + 'required' => true, + 'validate_callback' => array( $this, 'validate_create_font_face_settings' ), + 'sanitize_callback' => array( $this, 'sanitize_font_face_settings' ), + ), + ); + } + + /** + * Get the parent font family, if the ID is valid. + * + * @since 6.5.0 + * + * @param int $font_family_id Supplied ID. + * @return WP_Post|WP_Error Post object if ID is valid, WP_Error otherwise. + */ + protected function get_parent_font_family_post( $font_family_id ) { + $error = new WP_Error( + 'rest_post_invalid_parent', + __( 'Invalid post parent ID.', 'default' ), + array( 'status' => 404 ) + ); + + if ( (int) $font_family_id <= 0 ) { + return $error; + } + + $font_family_post = get_post( (int) $font_family_id ); + + if ( empty( $font_family_post ) || empty( $font_family_post->ID ) + || 'wp_font_family' !== $font_family_post->post_type + ) { + return $error; + } + + return $font_family_post; + } + + /** + * Prepares links for the request. + * + * @since 6.5.0 + * + * @param WP_Post $post Post object. + * @return array Links for the given post. + */ + protected function prepare_links( $post ) { + // Entity meta. + $links = array( + 'self' => array( + 'href' => rest_url( $this->namespace . '/font-families/' . $post->post_parent . '/font-faces/' . $post->ID ), + ), + 'collection' => array( + 'href' => rest_url( $this->namespace . '/font-families/' . $post->post_parent . '/font-faces' ), + ), + 'parent' => array( + 'href' => rest_url( $this->namespace . '/font-families/' . $post->post_parent ), + ), + ); + + return $links; + } + + /** + * Prepares a single font face post for creation. + * + * @since 6.5.0 + * + * @param WP_REST_Request $request Request object. + * @return stdClass|WP_Error Post object or WP_Error. + */ + protected function prepare_item_for_database( $request ) { + $prepared_post = new stdClass(); + + // Settings have already been decoded by ::sanitize_font_face_settings(). + $settings = $request->get_param( 'font_face_settings' ); + + // Store this "slug" as the post_title rather than post_name, since it uses the fontFamily setting, + // which may contain multibyte characters. + $title = WP_Font_Family_Utils::get_font_face_slug( $settings ); + + $prepared_post->post_type = $this->post_type; + $prepared_post->post_parent = $request['font_family_id']; + $prepared_post->post_status = 'publish'; + $prepared_post->post_title = $title; + $prepared_post->post_name = sanitize_title( $title ); + $prepared_post->post_content = wp_json_encode( $settings ); + + return $prepared_post; + } + + /** + * Handles the upload of a font file using wp_handle_upload(). + * + * @since 6.5.0 + * + * @param array $file Single file item from $_FILES. + * @return array Array containing uploaded file attributes on success, or error on failure. + */ + protected function handle_font_file_upload( $file ) { + add_filter( 'upload_mimes', array( 'WP_Font_Library', 'set_allowed_mime_types' ) ); + add_filter( 'upload_dir', 'wp_get_font_dir' ); + + $overrides = array( + 'upload_error_handler' => array( $this, 'handle_font_file_upload_error' ), + // Arbitrary string to avoid the is_uploaded_file() check applied + // when using 'wp_handle_upload'. + 'action' => 'wp_handle_font_upload', + // Not testing a form submission. + 'test_form' => false, + // Seems mime type for files that are not images cannot be tested. + // See wp_check_filetype_and_ext(). + 'test_type' => true, + // Only allow uploading font files for this request. + 'mimes' => WP_Font_Library::get_expected_font_mime_types_per_php_version(), + ); + + $uploaded_file = wp_handle_upload( $file, $overrides ); + + remove_filter( 'upload_dir', 'wp_get_font_dir' ); + remove_filter( 'upload_mimes', array( 'WP_Font_Library', 'set_allowed_mime_types' ) ); + + return $uploaded_file; + } + + /** + * Handles file upload error. + * + * @since 6.5.0 + * + * @param array $file File upload data. + * @param string $message Error message from wp_handle_upload(). + * @return WP_Error WP_Error object. + */ + public function handle_font_file_upload_error( $file, $message ) { + $status = 500; + $code = 'rest_font_upload_unknown_error'; + + if ( __( 'Sorry, you are not allowed to upload this file type.', 'default' ) === $message ) { + $status = 400; + $code = 'rest_font_upload_invalid_file_type'; + } + + return new WP_Error( $code, $message, array( 'status' => $status ) ); + } + + /** + * Returns relative path to an uploaded font file. + * + * The path is relative to the current fonts dir. + * + * @since 6.5.0 + * @access private + * + * @param string $path Full path to the file. + * @return string Relative path on success, unchanged path on failure. + */ + protected function relative_fonts_path( $path ) { + $new_path = $path; + + $fonts_dir = wp_get_font_dir(); + if ( str_starts_with( $new_path, $fonts_dir['path'] ) ) { + $new_path = str_replace( $fonts_dir, '', $new_path ); + $new_path = ltrim( $new_path, '/' ); + } + + return $new_path; + } + + /** + * Gets the font face's settings from the post. + * + * @since 6.5.0 + * + * @param WP_Post $post Font face post object. + * @return array Font face settings array. + */ + protected function get_settings_from_post( $post ) { + $settings = json_decode( $post->post_content, true ); + $properties = $this->get_item_schema()['properties']['font_face_settings']['properties']; + + // Provide required, empty settings if needed. + if ( null === $settings ) { + $settings = array( + 'fontFamily' => '', + 'src' => array(), + ); + } + + // Only return the properties defined in the schema. + return array_intersect_key( $settings, $properties ); + } +} diff --git a/lib/experimental/fonts/font-library/class-wp-rest-font-families-controller.php b/lib/experimental/fonts/font-library/class-wp-rest-font-families-controller.php index ede8762c88c6dc..887a8a5250cc32 100644 --- a/lib/experimental/fonts/font-library/class-wp-rest-font-families-controller.php +++ b/lib/experimental/fonts/font-library/class-wp-rest-font-families-controller.php @@ -1,12 +1,10 @@ rest_base = 'font-families'; - $this->namespace = 'wp/v2'; - $this->post_type = 'wp_font_family'; - } + protected $allow_batch = false; /** - * Registers the routes for the objects of the controller. + * Checks if a given request has access to font families. * * @since 6.5.0 + * + * @param WP_REST_Request $request Full details about the request. + * @return true|WP_Error True if the request has read access, WP_Error object otherwise. */ - public function register_routes() { - register_rest_route( - $this->namespace, - '/' . $this->rest_base, - array( - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_items' ), - 'permission_callback' => array( $this, 'update_font_library_permissions_check' ), - ), - ) - ); + public function get_items_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- required by parent class + $post_type = get_post_type_object( $this->post_type ); - register_rest_route( - $this->namespace, - '/' . $this->rest_base, - array( - array( - 'methods' => WP_REST_Server::EDITABLE, - 'callback' => array( $this, 'install_fonts' ), - 'permission_callback' => array( $this, 'update_font_library_permissions_check' ), - 'args' => array( - 'font_family_settings' => array( - 'required' => true, - 'type' => 'string', - 'validate_callback' => array( $this, 'validate_install_font_families' ), - ), - ), - ), - ) - ); + if ( ! current_user_can( $post_type->cap->read ) ) { + return new WP_Error( + 'rest_cannot_read', + __( 'Sorry, you are not allowed to access font families.', 'gutenberg' ), + array( 'status' => rest_authorization_required_code() ) + ); + } - register_rest_route( - $this->namespace, - '/' . $this->rest_base, - array( - array( - 'methods' => WP_REST_Server::DELETABLE, - 'callback' => array( $this, 'uninstall_fonts' ), - 'permission_callback' => array( $this, 'update_font_library_permissions_check' ), - 'args' => $this->uninstall_schema(), - ), - ) - ); + return true; } /** - * Returns validation errors in font families data for installation. + * Checks if a given request has access to a font family. * * @since 6.5.0 * - * @param array[] $font_families Font families to install. - * @param array $files Files to install. - * @return array $error_messages Array of error messages. + * @param WP_REST_Request $request Full details about the request. + * @return true|WP_Error True if the request has read access, WP_Error object otherwise. */ - private function get_validation_errors( $font_family_settings, $files ) { - $error_messages = array(); + public function get_item_permissions_check( $request ) { + return $this->get_items_permissions_check( $request ); + } - if ( ! is_array( $font_family_settings ) ) { - $error_messages[] = __( 'font_family_settings should be a font family definition.', 'gutenberg' ); - return $error_messages; + /** + * Validates settings when creating or updating a font family. + * + * @since 6.5.0 + * + * @param string $value Encoded JSON string of font family settings. + * @param WP_REST_Request $request Request object. + * @return false|WP_Error True if the settings are valid, otherwise a WP_Error object. + */ + public function validate_font_family_settings( $value, $request ) { + $settings = json_decode( $value, true ); + + // Check settings string is valid JSON. + if ( null === $settings ) { + return new WP_Error( + 'rest_invalid_param', + __( 'font_family_settings parameter must be a valid JSON string.', 'gutenberg' ), + array( 'status' => 400 ) + ); } - if ( - ! isset( $font_family_settings['slug'] ) || - ! isset( $font_family_settings['name'] ) || - ! isset( $font_family_settings['fontFamily'] ) - ) { - $error_messages[] = __( 'Font family should have slug, name and fontFamily properties defined.', 'gutenberg' ); + $schema = $this->get_item_schema()['properties']['font_family_settings']; + $required = $schema['required']; - return $error_messages; - } + if ( isset( $request['id'] ) ) { + // Allow sending individual properties if we are updating an existing font family. + unset( $schema['required'] ); - if ( isset( $font_family_settings['fontFace'] ) ) { - if ( ! is_array( $font_family_settings['fontFace'] ) ) { - $error_messages[] = __( 'Font family should have fontFace property defined as an array.', 'gutenberg' ); + // But don't allow updating the slug, since it is used as a unique identifier. + if ( isset( $settings['slug'] ) ) { + return new WP_Error( + 'rest_invalid_param', + __( 'font_family_settings[slug] cannot be updated.', 'gutenberg' ), + array( 'status' => 400 ) + ); } + } - if ( count( $font_family_settings['fontFace'] ) < 1 ) { - $error_messages[] = __( 'Font family should have at least one font face definition.', 'gutenberg' ); - } + // Check that the font face settings match the theme.json schema. + $has_valid_settings = rest_validate_value_from_schema( $settings, $schema, 'font_family_settings' ); - if ( ! empty( $font_family_settings['fontFace'] ) ) { - for ( $face_index = 0; $face_index < count( $font_family_settings['fontFace'] ); $face_index++ ) { - - $font_face = $font_family_settings['fontFace'][ $face_index ]; - if ( ! isset( $font_face['fontWeight'] ) || ! isset( $font_face['fontStyle'] ) ) { - $error_messages[] = sprintf( - // translators: font face index. - __( 'Font family Font face [%1$s] should have fontWeight and fontStyle properties defined.', 'gutenberg' ), - $face_index - ); - } - - if ( isset( $font_face['downloadFromUrl'] ) && isset( $font_face['uploadedFile'] ) ) { - $error_messages[] = sprintf( - // translators: font face index. - __( 'Font family Font face [%1$s] should have only one of the downloadFromUrl or uploadedFile properties defined and not both.', 'gutenberg' ), - $face_index - ); - } - - if ( isset( $font_face['uploadedFile'] ) ) { - if ( ! isset( $files[ $font_face['uploadedFile'] ] ) ) { - $error_messages[] = sprintf( - // translators: font face index. - __( 'Font family Font face [%1$s] file is not defined in the request files.', 'gutenberg' ), - $face_index - ); - } - } - } + if ( is_wp_error( $has_valid_settings ) ) { + $has_valid_settings->add_data( array( 'status' => 400 ) ); + return $has_valid_settings; + } + + // Check that none of the required settings are empty values. + foreach ( $required as $key ) { + if ( isset( $settings[ $key ] ) && ! $settings[ $key ] ) { + return new WP_Error( + 'rest_invalid_param', + /* translators: %s: Font family setting key. */ + sprintf( __( 'font_family_settings[%s] cannot be empty.', 'gutenberg' ), $key ), + array( 'status' => 400 ) + ); } } - return $error_messages; + return true; } /** - * Validate input for the install endpoint. + * Sanitizes the font family settings when creating or updating a font family. * - * @since 6.5.0 + * @since 6.5.0 * - * @param string $param The font families to install. - * @param WP_REST_Request $request The request object. - * @return true|WP_Error True if the parameter is valid, WP_Error otherwise. + * @param string $value Encoded JSON string of font family settings. + * @param WP_REST_Request $request Request object. + * @return array Decoded array font family settings. */ - public function validate_install_font_families( $param, $request ) { - $font_family_settings = json_decode( $param, true ); - $files = $request->get_file_params(); - $error_messages = $this->get_validation_errors( $font_family_settings, $files ); + public function sanitize_font_family_settings( $value ) { + $settings = json_decode( $value, true ); - if ( empty( $error_messages ) ) { - return true; + if ( isset( $settings['fontFamily'] ) ) { + $settings['fontFamily'] = WP_Font_Family_Utils::format_font_family( $settings['fontFamily'] ); } - return new WP_Error( 'rest_invalid_param', implode( ', ', $error_messages ), array( 'status' => 400 ) ); + // Provide default for preview, if not provided. + if ( ! isset( $settings['preview'] ) ) { + $settings['preview'] = ''; + } + + return $settings; } /** - * Gets the schema for the uninstall endpoint. + * Creates a single font family. * * @since 6.5.0 * - * @return array Schema array. + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ - public function uninstall_schema() { - return array( - 'font_families' => array( - 'type' => 'array', - 'description' => __( 'The font families to install.', 'gutenberg' ), - 'required' => true, - 'minItems' => 1, - 'items' => array( - 'required' => true, - 'type' => 'object', - 'properties' => array( - 'slug' => array( - 'type' => 'string', - 'description' => __( 'The font family slug.', 'gutenberg' ), - 'required' => true, - ), - ), - ), - ), + public function create_item( $request ) { + $settings = $request->get_param( 'font_family_settings' ); + + // Check that the font family slug is unique. + $query = new WP_Query( + array( + 'post_type' => $this->post_type, + 'posts_per_page' => 1, + 'name' => $settings['slug'], + 'update_post_meta_cache' => false, + 'update_post_term_cache' => false, + ) ); + if ( ! empty( $query->get_posts() ) ) { + return new WP_Error( + 'rest_duplicate_font_family', + /* translators: %s: Font family slug. */ + sprintf( __( 'A font family with slug "%s" already exists.', 'gutenberg' ), $settings['slug'] ), + array( 'status' => 400 ) + ); + } + + return parent::create_item( $request ); } /** - * Removes font families from the Font Library and all their assets. + * Deletes a single font family. * * @since 6.5.0 * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ - public function uninstall_fonts( $request ) { - $fonts_to_uninstall = $request->get_param( 'font_families' ); + public function delete_item( $request ) { + $force = isset( $request['force'] ) ? (bool) $request['force'] : false; - $errors = array(); - $successes = array(); - - if ( empty( $fonts_to_uninstall ) ) { - $errors[] = new WP_Error( - 'no_fonts_to_install', - __( 'No fonts to uninstall', 'gutenberg' ) - ); - $data = array( - 'successes' => $successes, - 'errors' => $errors, + // We don't support trashing for font families. + if ( ! $force ) { + return new WP_Error( + 'rest_trash_not_supported', + /* translators: %s: force=true */ + sprintf( __( "Font faces do not support trashing. Set '%s' to delete.", 'gutenberg' ), 'force=true' ), + array( 'status' => 501 ) ); - $response = rest_ensure_response( $data ); - $response->set_status( 400 ); - return $response; } - foreach ( $fonts_to_uninstall as $font_data ) { - $font = new WP_Font_Family( $font_data ); - $result = $font->uninstall(); - if ( is_wp_error( $result ) ) { - $errors[] = $result; - } else { - $successes[] = $result; - } - } - $data = array( - 'successes' => $successes, - 'errors' => $errors, - ); - return rest_ensure_response( $data ); + return parent::delete_item( $request ); } /** - * Checks whether the user has permissions to update the Font Library. + * Prepares a single font family output for response. * * @since 6.5.0 * - * @return true|WP_Error True if the request has write access for the item, WP_Error object otherwise. + * @param WP_Post $item Post object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response Response object. */ - public function update_font_library_permissions_check() { - if ( ! current_user_can( 'edit_theme_options' ) ) { - return new WP_Error( - 'rest_cannot_update_font_library', - __( 'Sorry, you are not allowed to update the Font Library on this site.', 'gutenberg' ), - array( - 'status' => rest_authorization_required_code(), - ) - ); + public function prepare_item_for_response( $item, $request ) { + $fields = $this->get_fields_for_response( $request ); + $data = array(); + + if ( rest_is_field_included( 'id', $fields ) ) { + $data['id'] = $item->ID; } - return true; + + if ( rest_is_field_included( 'theme_json_version', $fields ) ) { + $data['theme_json_version'] = 2; + } + + if ( rest_is_field_included( 'font_faces', $fields ) ) { + $data['font_faces'] = $this->get_font_face_ids( $item->ID ); + } + + if ( rest_is_field_included( 'font_family_settings', $fields ) ) { + $data['font_family_settings'] = $this->get_settings_from_post( $item ); + } + + $context = ! empty( $request['context'] ) ? $request['context'] : 'edit'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + $response = rest_ensure_response( $data ); + + if ( rest_is_field_included( '_links', $fields ) ) { + $links = $this->prepare_links( $item ); + $response->add_links( $links ); + } + + /** + * Filters the font family data for a REST API response. + * + * @since 6.5.0 + * + * @param WP_REST_Response $response The response object. + * @param WP_Post $post Font family post object. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'rest_prepare_wp_font_family', $response, $item, $request ); + } + + /** + * Retrieves the post's schema, conforming to JSON Schema. + * + * @since 6.5.0 + * + * @return array Item schema data. + */ + public function get_item_schema() { + if ( $this->schema ) { + return $this->add_additional_fields_schema( $this->schema ); + } + + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => $this->post_type, + 'type' => 'object', + // Base properties for every Post. + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the post.', 'default' ), + 'type' => 'integer', + 'context' => array( 'edit' ), + 'readonly' => true, + ), + 'theme_json_version' => array( + 'description' => __( 'Version of the theme.json schema used for the typography settings.', 'gutenberg' ), + 'type' => 'integer', + 'default' => 2, + 'minimum' => 2, + 'maximum' => 2, + 'context' => array( 'edit' ), + ), + 'font_faces' => array( + 'description' => __( 'The IDs of the child font faces in the font family.', 'gutenberg' ), + 'type' => 'array', + 'context' => array( 'edit' ), + 'items' => array( + 'type' => 'integer', + ), + ), + // Font family settings come directly from theme.json schema + // See https://schemas.wp.org/trunk/theme.json + 'font_family_settings' => array( + 'description' => __( 'font-face declaration in theme.json format.', 'gutenberg' ), + 'type' => 'object', + 'context' => array( 'edit' ), + 'properties' => array( + 'name' => array( + 'description' => 'Name of the font family preset, translatable.', + 'type' => 'string', + ), + 'slug' => array( + 'description' => 'Kebab-case unique identifier for the font family preset.', + 'type' => 'string', + ), + 'fontFamily' => array( + 'description' => 'CSS font-family value.', + 'type' => 'string', + ), + 'preview' => array( + 'description' => 'URL to a preview image of the font family.', + 'type' => 'string', + ), + ), + 'required' => array( 'name', 'slug', 'fontFamily' ), + 'additionalProperties' => false, + ), + ), + ); + + $this->schema = $schema; + + return $this->add_additional_fields_schema( $this->schema ); } /** - * Checks whether the font directory exists or not. + * Retrieves the query params for the font family collection. * * @since 6.5.0 * - * @return bool Whether the font directory exists. + * @return array Collection parameters. */ - private function has_upload_directory() { - $upload_dir = wp_get_font_dir()['path']; - return is_dir( $upload_dir ); + public function get_collection_params() { + $query_params = parent::get_collection_params(); + + $query_params['context']['default'] = 'edit'; + + // Remove unneeded params. + unset( $query_params['after'] ); + unset( $query_params['modified_after'] ); + unset( $query_params['before'] ); + unset( $query_params['modified_before'] ); + unset( $query_params['search'] ); + unset( $query_params['search_columns'] ); + unset( $query_params['status'] ); + + $query_params['orderby']['default'] = 'id'; + $query_params['orderby']['enum'] = array( 'id', 'include' ); + + /** + * Filters collection parameters for the font family controller. + * + * @since 6.5.0 + * + * @param array $query_params JSON Schema-formatted collection parameters. + */ + return apply_filters( 'rest_wp_font_family_collection_params', $query_params ); } /** - * Checks whether the user has write permissions to the temp and fonts directories. + * Retrieves the query params for the font family collection, defaulting to the 'edit' context. * * @since 6.5.0 * - * @return true|WP_Error True if the user has write permissions, WP_Error object otherwise. + * @param array $args Optional. Additional arguments for context parameter. Default empty array. + * @return array Context parameter details. */ - private function has_write_permission() { - // The update endpoints requires write access to the temp and the fonts directories. - $temp_dir = get_temp_dir(); - $upload_dir = wp_get_font_dir()['path']; - if ( ! is_writable( $temp_dir ) || ! wp_is_writable( $upload_dir ) ) { - return false; + public function get_context_param( $args = array() ) { + if ( isset( $args['default'] ) ) { + $args['default'] = 'edit'; } - return true; + return parent::get_context_param( $args ); } /** - * Checks whether the request needs write permissions. + * Get the arguments used when creating or updating a font family. * * @since 6.5.0 * - * @param array[] $font_family_settings Font family definition. - * @return bool Whether the request needs write permissions. + * @return array Font family create/edit arguments. */ - private function needs_write_permission( $font_family_settings ) { - if ( isset( $font_family_settings['fontFace'] ) ) { - foreach ( $font_family_settings['fontFace'] as $face ) { - // If the font is being downloaded from a URL or uploaded, it needs write permissions. - if ( isset( $face['downloadFromUrl'] ) || isset( $face['uploadedFile'] ) ) { - return true; - } - } + public function get_endpoint_args_for_item_schema( $method = WP_REST_Server::CREATABLE ) { + if ( WP_REST_Server::CREATABLE === $method || WP_REST_Server::EDITABLE === $method ) { + $properties = $this->get_item_schema()['properties']; + return array( + 'theme_json_version' => $properties['theme_json_version'], + // When creating or updating, font_family_settings is stringified JSON, to work with multipart/form-data. + // Font families don't currently support file uploads, but may accept preview files in the future. + 'font_family_settings' => array( + 'description' => __( 'font-family declaration in theme.json format, encoded as a string.', 'gutenberg' ), + 'type' => 'string', + 'required' => true, + 'validate_callback' => array( $this, 'validate_font_family_settings' ), + 'sanitize_callback' => array( $this, 'sanitize_font_family_settings' ), + ), + ); } - return false; + + return parent::get_endpoint_args_for_item_schema( $method ); } /** - * Installs new fonts. + * Get the child font face post IDs. * - * Takes a request containing new fonts to install, downloads their assets, and adds them - * to the Font Library. + * @since 6.5.0 + * + * @param int $font_family_id Font family post ID. + * @return int[] Array of child font face post IDs. + * . + */ + protected function get_font_face_ids( $font_family_id ) { + $query = new WP_Query( + array( + 'fields' => 'ids', + 'post_parent' => $font_family_id, + 'post_type' => 'wp_font_face', + 'posts_per_page' => 99, + 'order' => 'ASC', + 'orderby' => 'id', + 'update_post_meta_cache' => false, + 'update_post_term_cache' => false, + ) + ); + + return $query->get_posts(); + } + + /** + * Prepares font family links for the request. * * @since 6.5.0 * - * @param WP_REST_Request $request The request object containing the new fonts to install - * in the request parameters. - * @return WP_REST_Response|WP_Error The updated Font Library post content. + * @param WP_Post $post Post object. + * @return array Links for the given post. */ - public function install_fonts( $request ) { - // Get new fonts to install. - $font_family_settings = $request->get_param( 'font_family_settings' ); - - /* - * As this is receiving form data, the font families are encoded as a string. - * The form data is used because local fonts need to use that format to - * attach the files in the request. - */ - $font_family_settings = json_decode( $font_family_settings, true ); + protected function prepare_links( $post ) { + // Entity meta. + $links = parent::prepare_links( $post ); - $successes = array(); - $errors = array(); - $response_status = 200; + return array( + 'self' => $links['self'], + 'collection' => $links['collection'], + 'font_faces' => $this->prepare_font_face_links( $post->ID ), + ); + } - if ( empty( $font_family_settings ) ) { - $errors[] = new WP_Error( - 'no_fonts_to_install', - __( 'No fonts to install', 'gutenberg' ) + /** + * Prepares child font face links for the request. + * + * @param int $font_family_id Font family post ID. + * @return array Links for the child font face posts. + */ + protected function prepare_font_face_links( $font_family_id ) { + $font_face_ids = $this->get_font_face_ids( $font_family_id ); + $links = array(); + foreach ( $font_face_ids as $font_face_id ) { + $links[] = array( + 'embeddable' => true, + 'href' => rest_url( $this->namespace . '/' . $this->rest_base . '/' . $font_family_id . '/font-faces/' . $font_face_id ), ); - $response_status = 400; } + return $links; + } - if ( $this->needs_write_permission( $font_family_settings ) ) { - $upload_dir = wp_get_font_dir()['path']; - if ( ! $this->has_upload_directory() ) { - if ( ! wp_mkdir_p( $upload_dir ) ) { - $errors[] = new WP_Error( - 'cannot_create_fonts_folder', - sprintf( - /* translators: %s: Directory path. */ - __( 'Error: Unable to create directory %s.', 'gutenberg' ), - esc_html( $upload_dir ) - ) - ); - $response_status = 500; - } + /** + * Prepares a single font family post for create or update. + * + * @since 6.5.0 + * + * @param WP_REST_Request $request Request object. + * @return stdClass|WP_Error Post object or WP_Error. + */ + protected function prepare_item_for_database( $request ) { + $prepared_post = new stdClass(); + // Settings have already been decoded by ::sanitize_font_family_settings(). + $settings = $request->get_param( 'font_family_settings' ); + + // This is an update and we merge with the existing font family. + if ( isset( $request['id'] ) ) { + $existing_post = $this->get_post( $request['id'] ); + if ( is_wp_error( $existing_post ) ) { + return $existing_post; } - if ( $this->has_upload_directory() && ! $this->has_write_permission() ) { - $errors[] = new WP_Error( - 'cannot_write_fonts_folder', - __( 'Error: WordPress does not have permission to write the fonts folder on your server.', 'gutenberg' ) - ); - $response_status = 500; - } + $prepared_post->ID = $existing_post->ID; + $existing_settings = $this->get_settings_from_post( $existing_post ); + $settings = array_merge( $existing_settings, $settings ); } - if ( ! empty( $errors ) ) { - $data = array( - 'successes' => $successes, - 'errors' => $errors, - ); - $response = rest_ensure_response( $data ); - $response->set_status( $response_status ); - return $response; - } + $prepared_post->post_type = $this->post_type; + $prepared_post->post_status = 'publish'; + $prepared_post->post_title = $settings['name']; + $prepared_post->post_name = sanitize_title( $settings['slug'] ); - // Get uploaded files (used when installing local fonts). - $files = $request->get_file_params(); - $font = new WP_Font_Family( $font_family_settings ); - $result = $font->install( $files ); - if ( is_wp_error( $result ) ) { - $errors[] = $result; - } else { - $successes[] = $result; - } + // Remove duplicate information from settings. + unset( $settings['name'] ); + unset( $settings['slug'] ); + + $prepared_post->post_content = wp_json_encode( $settings ); - $data = array( - 'successes' => $successes, - 'errors' => $errors, + return $prepared_post; + } + + /** + * Gets the font family's settings from the post. + * + * @since 6.5.0 + * + * @param WP_Post $post Font family post object. + * @return array Font family settings array. + */ + protected function get_settings_from_post( $post ) { + $settings_json = json_decode( $post->post_content, true ); + + // Default to empty strings if the settings are missing. + return array( + 'name' => isset( $post->post_title ) && $post->post_title ? $post->post_title : '', + 'slug' => isset( $post->post_name ) && $post->post_name ? $post->post_name : '', + 'fontFamily' => isset( $settings_json['fontFamily'] ) && $settings_json['fontFamily'] ? $settings_json['fontFamily'] : '', + 'preview' => isset( $settings_json['preview'] ) && $settings_json['preview'] ? $settings_json['preview'] : '', ); - return rest_ensure_response( $data ); } } diff --git a/lib/experimental/fonts/font-library/font-library.php b/lib/experimental/fonts/font-library/font-library.php index d1ad8e1447ad9c..e9744da5958f4c 100644 --- a/lib/experimental/fonts/font-library/font-library.php +++ b/lib/experimental/fonts/font-library/font-library.php @@ -22,16 +22,73 @@ function gutenberg_init_font_library_routes() { // @core-merge: This code will go into Core's `create_initial_post_types()`. $args = array( + 'labels' => array( + 'name' => __( 'Font Families', 'gutenberg' ), + 'singular_name' => __( 'Font Family', 'gutenberg' ), + ), 'public' => false, - '_builtin' => true, /* internal use only. don't use this when registering your own post type. */ - 'label' => 'Font Family', + '_builtin' => true, /* internal use only. don't use this when registering your own post type. */ + 'hierarchical' => false, + 'capabilities' => array( + 'read' => 'edit_theme_options', + 'read_post' => 'edit_theme_options', + 'read_private_posts' => 'edit_theme_options', + 'create_posts' => 'edit_theme_options', + 'publish_posts' => 'edit_theme_options', + 'edit_post' => 'edit_theme_options', + 'edit_posts' => 'edit_theme_options', + 'edit_others_posts' => 'edit_theme_options', + 'edit_published_posts' => 'edit_theme_options', + 'delete_post' => 'edit_theme_options', + 'delete_posts' => 'edit_theme_options', + 'delete_others_posts' => 'edit_theme_options', + 'delete_published_posts' => 'edit_theme_options', + ), + 'map_meta_cap' => false, + 'query_var' => false, 'show_in_rest' => true, 'rest_base' => 'font-families', 'rest_controller_class' => 'WP_REST_Font_Families_Controller', - 'autosave_rest_controller_class' => 'WP_REST_Autosave_Font_Families_Controller', + // Disable autosave endpoints for font families. + 'autosave_rest_controller_class' => 'stdClass', ); register_post_type( 'wp_font_family', $args ); + register_post_type( + 'wp_font_face', + array( + 'labels' => array( + 'name' => __( 'Font Faces', 'gutenberg' ), + 'singular_name' => __( 'Font Face', 'gutenberg' ), + ), + 'public' => false, + '_builtin' => true, /* internal use only. don't use this when registering your own post type. */ + 'hierarchical' => false, + 'capabilities' => array( + 'read' => 'edit_theme_options', + 'read_post' => 'edit_theme_options', + 'read_private_posts' => 'edit_theme_options', + 'create_posts' => 'edit_theme_options', + 'publish_posts' => 'edit_theme_options', + 'edit_post' => 'edit_theme_options', + 'edit_posts' => 'edit_theme_options', + 'edit_others_posts' => 'edit_theme_options', + 'edit_published_posts' => 'edit_theme_options', + 'delete_post' => 'edit_theme_options', + 'delete_posts' => 'edit_theme_options', + 'delete_others_posts' => 'edit_theme_options', + 'delete_published_posts' => 'edit_theme_options', + ), + 'map_meta_cap' => false, + 'query_var' => false, + 'show_in_rest' => true, + 'rest_base' => 'font-families/(?P[\d]+)/font-faces', + 'rest_controller_class' => 'WP_REST_Font_Faces_Controller', + // Disable autosave endpoints for font faces. + 'autosave_rest_controller_class' => 'stdClass', + ) + ); + // @core-merge: This code will go into Core's `create_initial_rest_routes()`. $font_collections_controller = new WP_REST_Font_Collections_Controller(); $font_collections_controller->register_routes(); @@ -79,7 +136,7 @@ function wp_unregister_font_collection( $collection_id ) { 'slug' => 'default-font-collection', 'name' => 'Google Fonts', 'description' => __( 'Add from Google Fonts. Fonts are copied to and served from your site.', 'gutenberg' ), - 'src' => 'https://s.w.org/images/fonts/16.7/collections/google-fonts-with-preview.json', + 'src' => 'https://s.w.org/images/fonts/17.6/collections/google-fonts-with-preview.json', ); wp_register_font_collection( $default_font_collection ); @@ -132,3 +189,141 @@ function wp_get_font_dir( $defaults = array() ) { return apply_filters( 'font_dir', $defaults ); } } + +// @core-merge: Filters should go in `src/wp-includes/default-filters.php`, +// functions in a general file for font library. +if ( ! function_exists( '_wp_after_delete_font_family' ) ) { + /** + * Deletes child font faces when a font family is deleted. + * + * @access private + * @since 6.5.0 + * + * @param int $post_id Post ID. + * @param WP_Post $post Post object. + * @return void + */ + function _wp_after_delete_font_family( $post_id, $post ) { + if ( 'wp_font_family' !== $post->post_type ) { + return; + } + + $font_faces = get_children( + array( + 'post_parent' => $post_id, + 'post_type' => 'wp_font_face', + ) + ); + + foreach ( $font_faces as $font_face ) { + wp_delete_post( $font_face->ID, true ); + } + } + add_action( 'deleted_post', '_wp_after_delete_font_family', 10, 2 ); +} + +if ( ! function_exists( '_wp_before_delete_font_face' ) ) { + /** + * Deletes associated font files when a font face is deleted. + * + * @access private + * @since 6.5.0 + * + * @param int $post_id Post ID. + * @param WP_Post $post Post object. + * @return void + */ + function _wp_before_delete_font_face( $post_id, $post ) { + if ( 'wp_font_face' !== $post->post_type ) { + return; + } + + $font_files = get_post_meta( $post_id, '_wp_font_face_file', false ); + + foreach ( $font_files as $font_file ) { + wp_delete_file( wp_get_font_dir()['path'] . '/' . $font_file ); + } + } + add_action( 'before_delete_post', '_wp_before_delete_font_face', 10, 2 ); +} + +// @core-merge: Do not merge this back compat function, it is for supporting a legacy font family format only in Gutenberg. +/** + * Convert legacy font family posts to the new format. + * + * @return void + */ +function gutenberg_convert_legacy_font_family_format() { + if ( get_option( 'gutenberg_font_family_format_converted' ) ) { + return; + } + + $font_families = new WP_Query( + array( + 'post_type' => 'wp_font_family', + // Set a maximum, but in reality there will be far less than this. + 'posts_per_page' => 999, + 'update_post_meta_cache' => false, + 'update_post_term_cache' => false, + ) + ); + + foreach ( $font_families->get_posts() as $font_family ) { + $already_converted = get_post_meta( $font_family->ID, '_gutenberg_legacy_font_family', true ); + if ( $already_converted ) { + continue; + } + + // Stash the old font family content in a meta field just in case we need it. + update_post_meta( $font_family->ID, '_gutenberg_legacy_font_family', $font_family->post_content ); + + $font_family_json = json_decode( $font_family->post_content, true ); + if ( ! $font_family_json ) { + continue; + } + + $font_faces = $font_family_json['fontFace'] ?? array(); + unset( $font_family_json['fontFace'] ); + + // Save wp_font_face posts within the family. + foreach ( $font_faces as $font_face ) { + $args = array(); + $args['post_type'] = 'wp_font_face'; + $args['post_title'] = WP_Font_Family_Utils::get_font_face_slug( $font_face ); + $args['post_name'] = sanitize_title( $args['post_title'] ); + $args['post_status'] = 'publish'; + $args['post_parent'] = $font_family->ID; + $args['post_content'] = wp_json_encode( $font_face ); + + $font_face_id = wp_insert_post( wp_slash( $args ) ); + + $file_urls = (array) $font_face['src'] ?? array(); + + foreach ( $file_urls as $file_url ) { + // continue if the file is not local. + if ( false === strpos( $file_url, site_url() ) ) { + continue; + } + + $relative_path = basename( $file_url ); + update_post_meta( $font_face_id, '_wp_font_face_file', $relative_path ); + } + } + + // Update the font family post to remove the font face data. + $args = array(); + $args['ID'] = $font_family->ID; + $args['post_title'] = $font_family_json['name'] ?? ''; + $args['post_name'] = sanitize_title( $font_family_json['slug'] ); + + unset( $font_family_json['name'] ); + unset( $font_family_json['slug'] ); + + $args['post_content'] = wp_json_encode( $font_family_json ); + + wp_update_post( wp_slash( $args ) ); + } + + update_option( 'gutenberg_font_family_format_converted', true ); +} +add_action( 'init', 'gutenberg_convert_legacy_font_family_format' ); diff --git a/lib/experimental/interactivity-api/class-wp-directive-context.php b/lib/experimental/interactivity-api/class-wp-directive-context.php deleted file mode 100644 index 4276eddca20acb..00000000000000 --- a/lib/experimental/interactivity-api/class-wp-directive-context.php +++ /dev/null @@ -1,82 +0,0 @@ - - * - *
- * - *
- * - * - */ -class WP_Directive_Context { - /** - * The stack used to store contexts internally. - * - * @var array An array of contexts. - */ - protected $stack = array( array() ); - - /** - * Constructor. - * - * Accepts a context as an argument to initialize this with. - * - * @param array $context A context. - */ - public function __construct( $context = array() ) { - $this->set_context( $context ); - } - - /** - * Return the current context. - * - * @return array The current context. - */ - public function get_context() { - return end( $this->stack ); - } - - /** - * Set the current context. - * - * @param array $context The context to be set. - * - * @return void - */ - public function set_context( $context ) { - array_push( - $this->stack, - array_replace_recursive( $this->get_context(), $context ) - ); - } - - /** - * Reset the context to its previous state. - * - * @return void - */ - public function rewind_context() { - array_pop( $this->stack ); - } -} diff --git a/lib/experimental/interactivity-api/class-wp-directive-processor.php b/lib/experimental/interactivity-api/class-wp-directive-processor.php deleted file mode 100644 index 723b36026ce2af..00000000000000 --- a/lib/experimental/interactivity-api/class-wp-directive-processor.php +++ /dev/null @@ -1,283 +0,0 @@ -get_tag(); - - if ( self::is_html_void_element( $tag_name ) ) { - return false; - } - - while ( $this->next_tag( - array( - 'tag_name' => $tag_name, - 'tag_closers' => 'visit', - ) - ) ) { - if ( ! $this->is_tag_closer() ) { - ++$depth; - continue; - } - - if ( 0 === $depth ) { - return true; - } - - --$depth; - } - - return false; - } - - /** - * Returns the content between two balanced tags. - * - * When called on an opening tag, return the HTML content found between that - * opening tag and its matching closing tag. - * - * @return string The content between the current opening and its matching - * closing tag. - */ - public function get_inner_html() { - $bookmarks = $this->get_balanced_tag_bookmarks(); - if ( ! $bookmarks ) { - return false; - } - list( $start_name, $end_name ) = $bookmarks; - - $start = $this->bookmarks[ $start_name ]->start + $this->bookmarks[ $start_name ]->length + 1; - $end = $this->bookmarks[ $end_name ]->start; - - $this->seek( $start_name ); // Return to original position. - $this->release_bookmark( $start_name ); - $this->release_bookmark( $end_name ); - - return substr( $this->html, $start, $end - $start ); - } - - /** - * Sets the content between two balanced tags. - * - * When called on an opening tag, set the HTML content found between that - * opening tag and its matching closing tag. - * - * @param string $new_html The string to replace the content between the - * matching tags with. - * @return bool Whether the content was successfully replaced. - */ - public function set_inner_html( $new_html ) { - $this->get_updated_html(); // Apply potential previous updates. - - $bookmarks = $this->get_balanced_tag_bookmarks(); - if ( ! $bookmarks ) { - return false; - } - list( $start_name, $end_name ) = $bookmarks; - - $start = $this->bookmarks[ $start_name ]->start + $this->bookmarks[ $start_name ]->length + 1; - $end = $this->bookmarks[ $end_name ]->start; - - $this->seek( $start_name ); // Return to original position. - $this->release_bookmark( $start_name ); - $this->release_bookmark( $end_name ); - - $this->lexical_updates[] = new Gutenberg_HTML_Text_Replacement_6_5( $start, $end - $start, $new_html ); - return true; - } - - /** - * Returns a pair of bookmarks for the current opening tag and the matching - * closing tag. - * - * @return array|false A pair of bookmarks, or false if there's no matching - * closing tag. - */ - public function get_balanced_tag_bookmarks() { - $i = 0; - while ( array_key_exists( 'start' . $i, $this->bookmarks ) ) { - ++$i; - } - $start_name = 'start' . $i; - - $this->set_bookmark( $start_name ); - if ( ! $this->next_balanced_closer() ) { - $this->release_bookmark( $start_name ); - return false; - } - - $i = 0; - while ( array_key_exists( 'end' . $i, $this->bookmarks ) ) { - ++$i; - } - $end_name = 'end' . $i; - $this->set_bookmark( $end_name ); - - return array( $start_name, $end_name ); - } - - /** - * Checks whether a given HTML element is void (e.g.
). - * - * @see https://html.spec.whatwg.org/#elements-2 - * - * @param string $tag_name The element in question. - * @return bool True if the element is void. - */ - public static function is_html_void_element( $tag_name ) { - switch ( $tag_name ) { - case 'AREA': - case 'BASE': - case 'BR': - case 'COL': - case 'EMBED': - case 'HR': - case 'IMG': - case 'INPUT': - case 'LINK': - case 'META': - case 'SOURCE': - case 'TRACK': - case 'WBR': - return true; - - default: - return false; - } - } - - /** - * Extracts and return the directive type and the the part after the double - * hyphen from an attribute name (if present), in an array format. - * - * Examples: - * - * 'wp-island' => array( 'wp-island', null ) - * 'wp-bind--src' => array( 'wp-bind', 'src' ) - * 'wp-thing--and--thang' => array( 'wp-thing', 'and--thang' ) - * - * @param string $name The attribute name. - * @return array The resulting array. - */ - public static function parse_attribute_name( $name ) { - return explode( '--', $name, 2 ); - } - - /** - * Parse and extract the namespace and path from the given value. - * - * If the value contains a JSON instead of a path, the function parses it - * and returns the resulting array. - * - * @param string $value Passed value. - * @param string $ns Namespace fallback. - * @return array The resulting array - */ - public static function parse_attribute_value( $value, $ns = null ) { - $matches = array(); - $has_ns = preg_match( '/^([\w\-_\/]+)::(.+)$/', $value, $matches ); - - /* - * Overwrite both `$ns` and `$value` variables if `$value` explicitly - * contains a namespace. - */ - if ( $has_ns ) { - list( , $ns, $value ) = $matches; - } - - /* - * Try to decode `$value` as a JSON object. If it works, `$value` is - * replaced with the resulting array. The original string is preserved - * otherwise. - * - * Note that `json_decode` returns `null` both for an invalid JSON or - * the `'null'` string (a valid JSON). In the latter case, `$value` is - * replaced with `null`. - */ - $data = json_decode( $value, true ); - if ( null !== $data || 'null' === trim( $value ) ) { - $value = $data; - } - - return array( $ns, $value ); - } -} diff --git a/lib/experimental/interactivity-api/class-wp-interactivity-initial-state.php b/lib/experimental/interactivity-api/class-wp-interactivity-initial-state.php deleted file mode 100644 index 15e57edfa4a6a2..00000000000000 --- a/lib/experimental/interactivity-api/class-wp-interactivity-initial-state.php +++ /dev/null @@ -1,82 +0,0 @@ -%s', - wp_json_encode( self::$initial_state, JSON_HEX_TAG | JSON_HEX_AMP ) - ); - } -} diff --git a/lib/experimental/interactivity-api/directive-processing.php b/lib/experimental/interactivity-api/directive-processing.php deleted file mode 100644 index 5a97166d6d22bf..00000000000000 --- a/lib/experimental/interactivity-api/directive-processing.php +++ /dev/null @@ -1,214 +0,0 @@ -get_registered( $parsed_block['blockName'] ); - $is_interactive = isset( $block_type->supports['interactivity'] ) && $block_type->supports['interactivity']; - if ( $is_interactive ) { - WP_Directive_Processor::mark_interactive_root_block( $parsed_block ); - } - } - - return $parsed_block; -} -add_filter( 'render_block_data', 'gutenberg_interactivity_mark_root_interactive_blocks', 10, 1 ); - -/** - * Processes the directives in the root blocks. - * - * @param string $block_content The block content. - * @param array $block The full block. - * - * @return string Filtered block content. - */ -function gutenberg_process_directives_in_root_blocks( $block_content, $block ) { - if ( WP_Directive_Processor::is_marked_as_interactive_root_block( $block ) ) { - WP_Directive_Processor::unmark_interactive_root_block(); - $context = new WP_Directive_Context(); - $namespace_stack = array(); - return gutenberg_process_interactive_html( $block_content, $context, $namespace_stack ); - } - - return $block_content; -} -add_filter( 'render_block', 'gutenberg_process_directives_in_root_blocks', 10, 2 ); - -/** - * Processes interactive HTML by applying directives to the HTML tags. - * - * It uses the WP_Directive_Processor class to parse the HTML and apply the - * directives. If a tag contains a 'WP-INNER-BLOCKS' string and there are inner - * blocks to process, the function processes these inner blocks and replaces the - * 'WP-INNER-BLOCKS' tag in the HTML with those blocks. - * - * @param string $html The HTML to process. - * @param mixed $context The context to use when processing. - * @param array $inner_blocks The inner blocks to process. - * @param array $namespace_stack Stack of namespackes passed by reference. - * - * @return string The processed HTML. - */ -function gutenberg_process_interactive_html( $html, $context, &$namespace_stack = array() ) { - static $directives = array( - 'data-wp-interactive' => 'gutenberg_interactivity_process_wp_interactive', - 'data-wp-context' => 'gutenberg_interactivity_process_wp_context', - 'data-wp-bind' => 'gutenberg_interactivity_process_wp_bind', - 'data-wp-class' => 'gutenberg_interactivity_process_wp_class', - 'data-wp-style' => 'gutenberg_interactivity_process_wp_style', - 'data-wp-text' => 'gutenberg_interactivity_process_wp_text', - ); - - $tags = new WP_Directive_Processor( $html ); - $prefix = 'data-wp-'; - $tag_stack = array(); - while ( $tags->next_tag( array( 'tag_closers' => 'visit' ) ) ) { - $tag_name = $tags->get_tag(); - - if ( $tags->is_tag_closer() ) { - if ( 0 === count( $tag_stack ) ) { - continue; - } - list( $latest_opening_tag_name, $attributes ) = end( $tag_stack ); - if ( $latest_opening_tag_name === $tag_name ) { - array_pop( $tag_stack ); - // If the matching opening tag didn't have any directives, we move on. - if ( 0 === count( $attributes ) ) { - continue; - } - } - } else { - $attributes = array(); - foreach ( $tags->get_attribute_names_with_prefix( $prefix ) as $name ) { - /* - * Removes the part after the double hyphen before looking for - * the directive processor inside `$directives`, e.g., "wp-bind" - * from "wp-bind--src" and "wp-context" from "wp-context" etc... - */ - list( $type ) = $tags::parse_attribute_name( $name ); - if ( array_key_exists( $type, $directives ) ) { - $attributes[] = $type; - } - } - - /* - * If this is an open tag, and if it either has directives, or if - * we're inside a tag that does, take note of this tag and its - * directives so we can call its directive processor once we - * encounter the matching closing tag. - */ - if ( - ! $tags::is_html_void_element( $tag_name ) && - ( 0 !== count( $attributes ) || 0 !== count( $tag_stack ) ) - ) { - $tag_stack[] = array( $tag_name, $attributes ); - } - } - - // Extract all directive names. They'll be used later on. - $directive_names = array_keys( $directives ); - $directive_names_rev = array_reverse( $directive_names ); - - /* - * Sort attributes by the order they appear in the `$directives` - * argument, considering it as the priority order in which - * directives should be processed. Note that the order is reversed - * for tag closers. - */ - $sorted_attrs = array_intersect( - $tags->is_tag_closer() - ? $directive_names_rev - : $directive_names, - $attributes - ); - - foreach ( $sorted_attrs as $attribute ) { - call_user_func_array( - $directives[ $attribute ], - array( - $tags, - $context, - end( $namespace_stack ), - &$namespace_stack, - ) - ); - } - } - - return $tags->get_updated_html(); -} - -/** - * Resolves the passed reference from the store and the context under the given - * namespace. - * - * A reference could be either a single path or a namespace followed by a path, - * separated by two colons, i.e, `namespace::path.to.prop`. If the reference - * contains a namespace, that namespace overrides the one passed as argument. - * - * @param string $reference Reference value. - * @param string $ns Inherited namespace. - * @param array $context Context data. - * @return mixed Resolved value. - */ -function gutenberg_interactivity_evaluate_reference( $reference, $ns, array $context = array() ) { - // Extract the namespace from the reference (if present). - list( $ns, $path ) = WP_Directive_Processor::parse_attribute_value( $reference, $ns ); - - $store = array( - 'state' => WP_Interactivity_Initial_State::get_state( $ns ), - 'context' => $context[ $ns ] ?? array(), - ); - - /* - * Checks first if the directive path is preceded by a negator operator (!), - * indicating that the value obtained from the Interactivity Store (or the - * passed context) using the subsequent path should be negated. - */ - $should_negate_value = '!' === $path[0]; - $path = $should_negate_value ? substr( $path, 1 ) : $path; - $path_segments = explode( '.', $path ); - $current = $store; - foreach ( $path_segments as $p ) { - if ( isset( $current[ $p ] ) ) { - $current = $current[ $p ]; - } else { - return null; - } - } - - /* - * Checks if $current is an anonymous function or an arrow function, and if - * so, call it passing the store. Other types of callables are ignored on - * purpose, as arbitrary strings or arrays could be wrongly evaluated as - * "callables". - * - * E.g., "file" is an string and a "callable" (the "file" function exists). - */ - if ( $current instanceof Closure ) { - /* - * TODO: Figure out a way to implement derived state without having to - * pass the store as argument: - * - * $current = call_user_func( $current ); - */ - } - - // Returns the opposite if it has a negator operator (!). - return $should_negate_value ? ! $current : $current; -} diff --git a/lib/experimental/interactivity-api/directives/wp-bind.php b/lib/experimental/interactivity-api/directives/wp-bind.php deleted file mode 100644 index 57d2e5deb23ab4..00000000000000 --- a/lib/experimental/interactivity-api/directives/wp-bind.php +++ /dev/null @@ -1,33 +0,0 @@ -is_tag_closer() ) { - return; - } - - $prefixed_attributes = $tags->get_attribute_names_with_prefix( 'data-wp-bind--' ); - - foreach ( $prefixed_attributes as $attr ) { - list( , $bound_attr ) = WP_Directive_Processor::parse_attribute_name( $attr ); - if ( empty( $bound_attr ) ) { - continue; - } - - $reference = $tags->get_attribute( $attr ); - $value = gutenberg_interactivity_evaluate_reference( $reference, $ns, $context->get_context() ); - $tags->set_attribute( $bound_attr, $value ); - } -} diff --git a/lib/experimental/interactivity-api/directives/wp-class.php b/lib/experimental/interactivity-api/directives/wp-class.php deleted file mode 100644 index ef91835be86fc1..00000000000000 --- a/lib/experimental/interactivity-api/directives/wp-class.php +++ /dev/null @@ -1,37 +0,0 @@ -is_tag_closer() ) { - return; - } - - $prefixed_attributes = $tags->get_attribute_names_with_prefix( 'data-wp-class--' ); - - foreach ( $prefixed_attributes as $attr ) { - list( , $class_name ) = WP_Directive_Processor::parse_attribute_name( $attr ); - if ( empty( $class_name ) ) { - continue; - } - - $reference = $tags->get_attribute( $attr ); - $add_class = gutenberg_interactivity_evaluate_reference( $reference, $ns, $context->get_context() ); - if ( $add_class ) { - $tags->add_class( $class_name ); - } else { - $tags->remove_class( $class_name ); - } - } -} diff --git a/lib/experimental/interactivity-api/directives/wp-context.php b/lib/experimental/interactivity-api/directives/wp-context.php deleted file mode 100644 index b41b47c86c78c3..00000000000000 --- a/lib/experimental/interactivity-api/directives/wp-context.php +++ /dev/null @@ -1,30 +0,0 @@ -is_tag_closer() ) { - $context->rewind_context(); - return; - } - - $attr_value = $tags->get_attribute( 'data-wp-context' ); - - //Separate namespace and value from the context directive attribute. - list( $ns, $data ) = is_string( $attr_value ) && ! empty( $attr_value ) - ? WP_Directive_Processor::parse_attribute_value( $attr_value, $ns ) - : array( $ns, null ); - - // Add parsed data to the context under the corresponding namespace. - $context->set_context( array( $ns => is_array( $data ) ? $data : array() ) ); -} diff --git a/lib/experimental/interactivity-api/directives/wp-interactive.php b/lib/experimental/interactivity-api/directives/wp-interactive.php deleted file mode 100644 index 9f3471a8b4e6a9..00000000000000 --- a/lib/experimental/interactivity-api/directives/wp-interactive.php +++ /dev/null @@ -1,44 +0,0 @@ -is_tag_closer() ) { - array_pop( $ns_stack ); - return; - } - - /* - * Decode the data-wp-interactive attribute. In the case it is not a valid - * JSON string, NULL is stored in `$island_data`. - */ - $island = $tags->get_attribute( 'data-wp-interactive' ); - $island_data = is_string( $island ) && ! empty( $island ) - ? json_decode( $island, true ) - : null; - - /* - * Push the newly defined namespace, or the current one if the island - * definition was invalid or does not contain a namespace. - * - * This is done because the function pops out the current namespace from the - * stack whenever it finds an island's closing tag, independently of whether - * the island definition was correct or it contained a valid namespace. - */ - $ns_stack[] = isset( $island_data ) && $island_data['namespace'] - ? $island_data['namespace'] - : $ns; -} diff --git a/lib/experimental/interactivity-api/directives/wp-style.php b/lib/experimental/interactivity-api/directives/wp-style.php deleted file mode 100644 index 16432e57282606..00000000000000 --- a/lib/experimental/interactivity-api/directives/wp-style.php +++ /dev/null @@ -1,73 +0,0 @@ -is_tag_closer() ) { - return; - } - - $prefixed_attributes = $tags->get_attribute_names_with_prefix( 'data-wp-style--' ); - - foreach ( $prefixed_attributes as $attr ) { - list( , $style_name ) = WP_Directive_Processor::parse_attribute_name( $attr ); - if ( empty( $style_name ) ) { - continue; - } - - $reference = $tags->get_attribute( $attr ); - $style_value = gutenberg_interactivity_evaluate_reference( $reference, $ns, $context->get_context() ); - if ( $style_value ) { - $style_attr = $tags->get_attribute( 'style' ) ?? ''; - $style_attr = gutenberg_interactivity_set_style( $style_attr, $style_name, $style_value ); - $tags->set_attribute( 'style', $style_attr ); - } else { - // TODO: Do we want to unset styles if they're null? - } - } -} - -/** - * Set style. - * - * @param string $style Existing style to amend. - * @param string $name Style property name. - * @param string $value Style property value. - * @return string Amended styles. - */ -function gutenberg_interactivity_set_style( $style, $name, $value ) { - $style_assignments = explode( ';', $style ); - $modified = false; - foreach ( $style_assignments as $style_assignment ) { - list( $style_name ) = explode( ':', $style_assignment ); - if ( trim( $style_name ) === $name ) { - // TODO: Retain surrounding whitespace from $style_value, if any. - $style_assignment = $style_name . ': ' . $value; - $modified = true; - break; - } - } - - if ( ! $modified ) { - $new_style_assignment = $name . ': ' . $value; - // If the last element is empty or whitespace-only, we insert - // the new "key: value" pair before it. - if ( empty( trim( end( $style_assignments ) ) ) ) { - array_splice( $style_assignments, - 1, 0, $new_style_assignment ); - } else { - array_push( $style_assignments, $new_style_assignment ); - } - } - return implode( ';', $style_assignments ); -} diff --git a/lib/experimental/interactivity-api/directives/wp-text.php b/lib/experimental/interactivity-api/directives/wp-text.php deleted file mode 100644 index c4c5bb27a31e10..00000000000000 --- a/lib/experimental/interactivity-api/directives/wp-text.php +++ /dev/null @@ -1,28 +0,0 @@ -is_tag_closer() ) { - return; - } - - $value = $tags->get_attribute( 'data-wp-text' ); - if ( null === $value ) { - return; - } - - $text = gutenberg_interactivity_evaluate_reference( $value, $ns, $context->get_context() ); - $tags->set_inner_html( esc_html( $text ) ); -} diff --git a/lib/experimental/interactivity-api/initial-state.php b/lib/experimental/interactivity-api/initial-state.php deleted file mode 100644 index a38d0da631f3c4..00000000000000 --- a/lib/experimental/interactivity-api/initial-state.php +++ /dev/null @@ -1,29 +0,0 @@ - $dependency['id'], - 'type' => isset( $dependency['type'] ) && 'dynamic' === $dependency['type'] ? 'dynamic' : 'static', - ); - } elseif ( is_string( $dependency ) ) { - $deps[] = array( - 'id' => $dependency, - 'type' => 'static', - ); - } - } - - self::$registered[ $module_identifier ] = array( - 'src' => $src, - 'version' => $version, - 'enqueued' => in_array( $module_identifier, self::$enqueued_modules_before_register, true ), - 'dependencies' => $deps, - ); - } - } - - /** - * Marks the module to be enqueued in the page. - * - * @param string $module_identifier The identifier of the module. - */ - public static function enqueue( $module_identifier ) { - if ( isset( self::$registered[ $module_identifier ] ) ) { - self::$registered[ $module_identifier ]['enqueued'] = true; - } elseif ( ! in_array( $module_identifier, self::$enqueued_modules_before_register, true ) ) { - self::$enqueued_modules_before_register[] = $module_identifier; - } - } - - /** - * Unmarks the module so it is no longer enqueued in the page. - * - * @param string $module_identifier The identifier of the module. - */ - public static function dequeue( $module_identifier ) { - if ( isset( self::$registered[ $module_identifier ] ) ) { - self::$registered[ $module_identifier ]['enqueued'] = false; - } - $key = array_search( $module_identifier, self::$enqueued_modules_before_register, true ); - if ( false !== $key ) { - array_splice( self::$enqueued_modules_before_register, $key, 1 ); - } - } - - /** - * Returns the import map array. - * - * @return array Array with an 'imports' key mapping to an array of module identifiers and their respective source URLs, including the version query. - */ - public static function get_import_map() { - $imports = array(); - foreach ( self::get_dependencies( array_keys( self::get_enqueued() ) ) as $module_identifier => $module ) { - $imports[ $module_identifier ] = $module['src'] . self::get_version_query_string( $module['version'] ); - } - return array( 'imports' => $imports ); - } - - /** - * Prints the import map using a script tag with an type="importmap" attribute. - */ - public static function print_import_map() { - $import_map = self::get_import_map(); - if ( ! empty( $import_map['imports'] ) ) { - echo ''; - } - } - - /** - * Prints all the enqueued modules using ' - ); - } - - /** - * Gets the version of a module. - * - * If SCRIPT_DEBUG is true, the version is the current timestamp. If $version - * is set to false, the version number is the currently installed WordPress - * version. If $version is set to null, no version is added. - * - * @param array $version The version of the module. - * @return string A string presenting the version. - */ - private static function get_version_query_string( $version ) { - if ( defined( 'SCRIPT_DEBUG ' ) && SCRIPT_DEBUG ) { - return '?ver=' . time(); - } elseif ( false === $version ) { - return '?ver=' . get_bloginfo( 'version' ); - } elseif ( null !== $version ) { - return '?ver=' . $version; - } - return ''; - } - - /** - * Retrieves an array of enqueued modules. - * - * @return array Array of modules keyed by module identifier. - */ - private static function get_enqueued() { - $enqueued = array(); - foreach ( self::$registered as $module_identifier => $module ) { - if ( true === $module['enqueued'] ) { - $enqueued[ $module_identifier ] = $module; - } - } - return $enqueued; - } - - /** - * Retrieves all the dependencies for given modules depending on type. - * - * This method is recursive to also retrieve dependencies of the dependencies. - * It will consolidate an array containing unique dependencies based on the - * requested types ('static' or 'dynamic'). - * - * @param array $module_identifiers The identifiers of the modules for which to gather dependencies. - * @param array $types Optional. Types of dependencies to retrieve: 'static', 'dynamic', or both. Default is both. - * @return array Array of modules keyed by module identifier. - */ - private static function get_dependencies( $module_identifiers, $types = array( 'static', 'dynamic' ) ) { - return array_reduce( - $module_identifiers, - function ( $dependency_modules, $module_identifier ) use ( $types ) { - $dependencies = array(); - foreach ( self::$registered[ $module_identifier ]['dependencies'] as $dependency ) { - if ( - in_array( $dependency['type'], $types, true ) && - isset( self::$registered[ $dependency['id'] ] ) && - ! isset( $dependency_modules[ $dependency['id'] ] ) - ) { - $dependencies[ $dependency['id'] ] = self::$registered[ $dependency['id'] ]; - } - } - return array_merge( $dependency_modules, $dependencies, self::get_dependencies( array_keys( $dependencies ), $types ) ); - }, - array() - ); - } -} - -/** - * Registers the module if no module with that module identifier has already - * been registered. - * - * @param string $module_identifier The identifier of the module. Should be unique. It will be used in the final import map. - * @param string $src Full URL of the module, or path of the script relative to the WordPress root directory. - * @param array $dependencies Optional. An array of module identifiers of the dependencies of this module. The dependencies can be strings or arrays. If they are arrays, they need an `id` key with the module identifier, and can contain a `type` key with either `static` or `dynamic`. By default, dependencies that don't contain a type are considered static. - * @param string|false|null $version Optional. String specifying module version number. Defaults to false. It is added to the URL as a query string for cache busting purposes. If SCRIPT_DEBUG is true, the version is the current timestamp. If $version is set to false, the version number is the currently installed WordPress version. If $version is set to null, no version is added. - */ -function gutenberg_register_module( $module_identifier, $src, $dependencies = array(), $version = false ) { - Gutenberg_Modules::register( $module_identifier, $src, $dependencies, $version ); -} - -/** - * Marks the module to be enqueued in the page. - * - * @param string $module_identifier The identifier of the module. - */ -function gutenberg_enqueue_module( $module_identifier ) { - Gutenberg_Modules::enqueue( $module_identifier ); -} - -/** - * Unmarks the module so it is not longer enqueued in the page. - * - * @param string $module_identifier The identifier of the module. - */ -function gutenberg_dequeue_module( $module_identifier ) { - Gutenberg_Modules::dequeue( $module_identifier ); -} - -$modules_position = wp_is_block_theme() ? 'wp_head' : 'wp_footer'; -// Prints the import map in the head tag in block themes. Otherwise in the footer. -add_action( $modules_position, array( 'Gutenberg_Modules', 'print_import_map' ) ); - -// Prints the enqueued modules in the head tag in block themes. Otherwise in the footer. -add_action( $modules_position, array( 'Gutenberg_Modules', 'print_enqueued_modules' ) ); - -// Prints the preloaded modules in the head tag in block themes. Otherwise in the footer. -add_action( $modules_position, array( 'Gutenberg_Modules', 'print_module_preloads' ) ); - -// Prints the script that loads the import map polyfill in the footer. -add_action( 'wp_head', array( 'Gutenberg_Modules', 'print_import_map_polyfill' ), 11 ); - -/** - * Add module fields from block metadata to WP_Block_Type settings. - * - * This filter allows us to register modules from block metadata and attach additional fields to - * WP_Block_Type instances. - * - * @param array $settings Array of determined settings for registering a block type. - * @param array $metadata Metadata provided for registering a block type. - */ -function gutenberg_filter_block_type_metadata_settings_register_modules( $settings, $metadata = null ) { - $module_fields = array( - 'viewModule' => 'view_module_ids', - ); - foreach ( $module_fields as $metadata_field_name => $settings_field_name ) { - if ( ! empty( $settings[ $metadata_field_name ] ) ) { - $metadata[ $metadata_field_name ] = $settings[ $metadata_field_name ]; - } - if ( ! empty( $metadata[ $metadata_field_name ] ) ) { - $modules = $metadata[ $metadata_field_name ]; - $processed_modules = array(); - if ( is_array( $modules ) ) { - for ( $index = 0; $index < count( $modules ); $index++ ) { - $processed_modules[] = gutenberg_register_block_module_id( - $metadata, - $metadata_field_name, - $index - ); - } - } else { - $processed_modules[] = gutenberg_register_block_module_id( - $metadata, - $metadata_field_name - ); - } - $settings[ $settings_field_name ] = $processed_modules; - } - } - - return $settings; -} - -add_filter( 'block_type_metadata_settings', 'gutenberg_filter_block_type_metadata_settings_register_modules', 10, 2 ); - -/** - * Enqueue modules associated with the block. - * - * @param string $block_content The block content. - * @param array $block The full block, including name and attributes. - * @param WP_Block $instance The block instance. - */ -function gutenberg_filter_render_block_enqueue_view_modules( $block_content, $parsed_block, $block_instance ) { - $block_type = $block_instance->block_type; - - if ( ! empty( $block_type->view_module_ids ) ) { - foreach ( $block_type->view_module_ids as $module_id ) { - gutenberg_enqueue_module( $module_id ); - } - } - - return $block_content; -} - -add_filter( 'render_block', 'gutenberg_filter_render_block_enqueue_view_modules', 10, 3 ); - -/** - * Finds a module ID for the selected block metadata field. It detects - * when a path to file was provided and finds a corresponding asset file - * with details necessary to register the module under an automatically - * generated module ID. - * - * This is analogous to the `register_block_script_handle` in WordPress Core. - * - * @param array $metadata Block metadata. - * @param string $field_name Field name to pick from metadata. - * @param int $index Optional. Index of the script to register when multiple items passed. - * Default 0. - * @return string Module ID. - */ -function gutenberg_register_block_module_id( $metadata, $field_name, $index = 0 ) { - if ( empty( $metadata[ $field_name ] ) ) { - return false; - } - - $module_id = $metadata[ $field_name ]; - if ( is_array( $module_id ) ) { - if ( empty( $module_id[ $index ] ) ) { - return false; - } - $module_id = $module_id[ $index ]; - } - - $module_path = remove_block_asset_path_prefix( $module_id ); - if ( $module_id === $module_path ) { - return $module_id; - } - - $path = dirname( $metadata['file'] ); - $module_asset_raw_path = $path . '/' . substr_replace( $module_path, '.asset.php', - strlen( '.js' ) ); - $module_id = gutenberg_generate_block_asset_module_id( $metadata['name'], $field_name, $index ); - $module_asset_path = wp_normalize_path( realpath( $module_asset_raw_path ) ); - - if ( empty( $module_asset_path ) ) { - _doing_it_wrong( - __FUNCTION__, - sprintf( - // This string is from WordPress Core. See `register_block_script_handle`. - // Translators: This is a translation from WordPress Core (default). No need to translate. - __( 'The asset file (%1$s) for the "%2$s" defined in "%3$s" block definition is missing.', 'default' ), - $module_asset_raw_path, - $field_name, - $metadata['name'] - ), - '6.5.0' - ); - return false; - } - - $module_path_norm = wp_normalize_path( realpath( $path . '/' . $module_path ) ); - $module_uri = get_block_asset_url( $module_path_norm ); - $module_asset = require $module_asset_path; - $module_dependencies = isset( $module_asset['dependencies'] ) ? $module_asset['dependencies'] : array(); - - gutenberg_register_module( - $module_id, - $module_uri, - $module_dependencies, - isset( $module_asset['version'] ) ? $module_asset['version'] : false - ); - - return $module_id; -} - -/** - * Generates the module ID for an asset based on the name of the block - * and the field name provided. - * - * This is analogous to the `generate_block_asset_handle` in WordPress Core. - * - * @param string $block_name Name of the block. - * @param string $field_name Name of the metadata field. - * @param int $index Optional. Index of the asset when multiple items passed. - * Default 0. - * @return string Generated module ID for the block's field. - */ -function gutenberg_generate_block_asset_module_id( $block_name, $field_name, $index = 0 ) { - if ( str_starts_with( $block_name, 'core/' ) ) { - $asset_handle = str_replace( 'core/', 'wp-block-', $block_name ); - if ( str_starts_with( $field_name, 'editor' ) ) { - $asset_handle .= '-editor'; - } - if ( str_starts_with( $field_name, 'view' ) ) { - $asset_handle .= '-view'; - } - if ( $index > 0 ) { - $asset_handle .= '-' . ( $index + 1 ); - } - return $asset_handle; - } - - $field_mappings = array( - 'viewModule' => 'view-module', - ); - $asset_handle = str_replace( '/', '-', $block_name ) . - '-' . $field_mappings[ $field_name ]; - if ( $index > 0 ) { - $asset_handle .= '-' . ( $index + 1 ); - } - return $asset_handle; -} - -function gutenberg_register_view_module_ids_rest_field() { - register_rest_field( - 'block-type', - 'view_module_ids', - array( - 'get_callback' => function ( $item ) { - $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $item['name'] ); - if ( isset( $block_type->view_module_ids ) ) { - return $block_type->view_module_ids; - } - return array(); - }, - ) - ); -} - -add_action( 'rest_api_init', 'gutenberg_register_view_module_ids_rest_field' ); diff --git a/lib/experimental/script-modules.php b/lib/experimental/script-modules.php new file mode 100644 index 00000000000000..1f481a8098b20c --- /dev/null +++ b/lib/experimental/script-modules.php @@ -0,0 +1,234 @@ + 'view_module_ids', + ); + foreach ( $module_fields as $metadata_field_name => $settings_field_name ) { + if ( ! empty( $settings[ $metadata_field_name ] ) ) { + $metadata[ $metadata_field_name ] = $settings[ $metadata_field_name ]; + } + if ( ! empty( $metadata[ $metadata_field_name ] ) ) { + $modules = $metadata[ $metadata_field_name ]; + $processed_modules = array(); + if ( is_array( $modules ) ) { + for ( $index = 0; $index < count( $modules ); $index++ ) { + $processed_modules[] = gutenberg_register_block_module_id( + $metadata, + $metadata_field_name, + $index + ); + } + } else { + $processed_modules[] = gutenberg_register_block_module_id( + $metadata, + $metadata_field_name + ); + } + $settings[ $settings_field_name ] = $processed_modules; + } + } + + return $settings; +} + +add_filter( 'block_type_metadata_settings', 'gutenberg_filter_block_type_metadata_settings_register_modules', 10, 2 ); + +/** + * Enqueue modules associated with the block. + * + * @param string $block_content The block content. + * @param array $parsed_block The full block, including name and attributes. + * @param WP_Block $block_instance The block instance. + */ +function gutenberg_filter_render_block_enqueue_view_modules( $block_content, $parsed_block, $block_instance ) { + $block_type = $block_instance->block_type; + + if ( ! empty( $block_type->view_module_ids ) ) { + foreach ( $block_type->view_module_ids as $module_id ) { + wp_enqueue_script_module( $module_id ); + } + } + + return $block_content; +} + +add_filter( 'render_block', 'gutenberg_filter_render_block_enqueue_view_modules', 10, 3 ); + +/** + * Finds a module ID for the selected block metadata field. It detects + * when a path to file was provided and finds a corresponding asset file + * with details necessary to register the module under an automatically + * generated module ID. + * + * This is analogous to the `register_block_script_handle` in WordPress Core. + * + * @param array $metadata Block metadata. + * @param string $field_name Field name to pick from metadata. + * @param int $index Optional. Index of the script to register when multiple items passed. + * Default 0. + * @return string Module ID. + */ +function gutenberg_register_block_module_id( $metadata, $field_name, $index = 0 ) { + if ( empty( $metadata[ $field_name ] ) ) { + return false; + } + + $module_id = $metadata[ $field_name ]; + if ( is_array( $module_id ) ) { + if ( empty( $module_id[ $index ] ) ) { + return false; + } + $module_id = $module_id[ $index ]; + } + + $module_path = remove_block_asset_path_prefix( $module_id ); + if ( $module_id === $module_path ) { + return $module_id; + } + + $path = dirname( $metadata['file'] ); + $module_asset_raw_path = $path . '/' . substr_replace( $module_path, '.asset.php', - strlen( '.js' ) ); + $module_id = gutenberg_generate_block_asset_module_id( $metadata['name'], $field_name, $index ); + $module_asset_path = wp_normalize_path( realpath( $module_asset_raw_path ) ); + + if ( empty( $module_asset_path ) ) { + _doing_it_wrong( + __FUNCTION__, + sprintf( + // This string is from WordPress Core. See `register_block_script_handle`. + // Translators: This is a translation from WordPress Core (default). No need to translate. + __( 'The asset file (%1$s) for the "%2$s" defined in "%3$s" block definition is missing.', 'default' ), + $module_asset_raw_path, + $field_name, + $metadata['name'] + ), + '6.5.0' + ); + return false; + } + + $module_path_norm = wp_normalize_path( realpath( $path . '/' . $module_path ) ); + $module_uri = get_block_asset_url( $module_path_norm ); + $module_asset = require $module_asset_path; + $module_dependencies = isset( $module_asset['dependencies'] ) ? $module_asset['dependencies'] : array(); + + wp_register_script_module( + $module_id, + $module_uri, + $module_dependencies, + isset( $module_asset['version'] ) ? $module_asset['version'] : false + ); + + return $module_id; +} + +/** + * Generates the module ID for an asset based on the name of the block + * and the field name provided. + * + * This is analogous to the `generate_block_asset_handle` in WordPress Core. + * + * @param string $block_name Name of the block. + * @param string $field_name Name of the metadata field. + * @param int $index Optional. Index of the asset when multiple items passed. + * Default 0. + * @return string Generated module ID for the block's field. + */ +function gutenberg_generate_block_asset_module_id( $block_name, $field_name, $index = 0 ) { + if ( str_starts_with( $block_name, 'core/' ) ) { + $asset_handle = str_replace( 'core/', 'wp-block-', $block_name ); + if ( str_starts_with( $field_name, 'editor' ) ) { + $asset_handle .= '-editor'; + } + if ( str_starts_with( $field_name, 'view' ) ) { + $asset_handle .= '-view'; + } + if ( $index > 0 ) { + $asset_handle .= '-' . ( $index + 1 ); + } + return $asset_handle; + } + + $field_mappings = array( + 'viewModule' => 'view-module', + ); + $asset_handle = str_replace( '/', '-', $block_name ) . + '-' . $field_mappings[ $field_name ]; + if ( $index > 0 ) { + $asset_handle .= '-' . ( $index + 1 ); + } + return $asset_handle; +} + +/** + * Registers a REST field for block types to provide view module IDs. + * + * Adds the `view_module_ids` field to block type objects in the REST API, which + * lists the script module IDs for any script modules associated with the + * block's viewModule(s) key. + * + * @since 6.5.0 + */ +function gutenberg_register_view_module_ids_rest_field() { + register_rest_field( + 'block-type', + 'view_module_ids', + array( + 'get_callback' => function ( $item ) { + $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $item['name'] ); + if ( isset( $block_type->view_module_ids ) ) { + return $block_type->view_module_ids; + } + return array(); + }, + ) + ); +} + +add_action( 'rest_api_init', 'gutenberg_register_view_module_ids_rest_field' ); + +/** + * Registers the module if no module with that module identifier has already + * been registered. + * + * @param string $module_identifier The identifier of the module. Should be unique. It will be used in the final import map. + * @param string $src Full URL of the module, or path of the script relative to the WordPress root directory. + * @param array $dependencies Optional. An array of module identifiers of the dependencies of this module. The dependencies can be strings or arrays. If they are arrays, they need an `id` key with the module identifier, and can contain an `import` key with either `static` or `dynamic`. By default, dependencies that don't contain an import are considered static. + * @param string|false|null $version Optional. String specifying module version number. Defaults to false. It is added to the URL as a query string for cache busting purposes. If $version is set to false, the version number is the currently installed WordPress version. If $version is set to null, no version is added. + * @deprecated 17.6.0 gutenberg_register_module is deprecated. Please use wp_register_script_module instead. + */ +function gutenberg_register_module( $module_identifier, $src = '', $dependencies = array(), $version = false ) { + _deprecated_function( __FUNCTION__, 'Gutenberg 17.6.0', 'wp_register_script_module' ); + wp_script_modules()->register( $module_identifier, $src, $dependencies, $version ); +} + +/** + * Marks the module to be enqueued in the page. + * + * @param string $module_identifier The identifier of the module. + * @deprecated 17.6.0 gutenberg_enqueue_module is deprecated. Please use wp_enqueue_script_module instead. + */ +function gutenberg_enqueue_module( $module_identifier ) { + _deprecated_function( __FUNCTION__, 'Gutenberg 17.6.0', 'wp_enqueue_script_module' ); + wp_script_modules()->enqueue( $module_identifier ); +} + +/** + * Unmarks the module so it is not longer enqueued in the page. + * + * @param string $module_identifier The identifier of the module. + * @deprecated 17.6.0 gutenberg_dequeue_module is deprecated. Please use wp_dequeue_script_module instead. + */ +function gutenberg_dequeue_module( $module_identifier ) { + _deprecated_function( __FUNCTION__, 'Gutenberg 17.6.0', 'wp_dequeue_script_module' ); + wp_script_modules()->dequeue( $module_identifier ); +} diff --git a/lib/experiments-page.php b/lib/experiments-page.php index 8af1eb82c6bed0..bccbed2195958b 100644 --- a/lib/experiments-page.php +++ b/lib/experiments-page.php @@ -126,30 +126,6 @@ function gutenberg_initialize_experiments_settings() { ) ); - add_settings_field( - 'gutenberg-custom-fields', - __( 'Block Bindings & Custom Fields', 'gutenberg' ), - 'gutenberg_display_experiment_field', - 'gutenberg-experiments', - 'gutenberg_experiments_section', - array( - 'label' => __( 'Test connecting block attributes to different sources like custom fields', 'gutenberg' ), - 'id' => 'gutenberg-block-bindings', - ) - ); - - add_settings_field( - 'gutenberg-pattern-partial-syncing', - __( 'Pattern overrides', 'gutenberg' ), - 'gutenberg_display_experiment_field', - 'gutenberg-experiments', - 'gutenberg_experiments_section', - array( - 'label' => __( 'Test overrides in synced patterns', 'gutenberg' ), - 'id' => 'gutenberg-pattern-partial-syncing', - ) - ); - register_setting( 'gutenberg-experiments', 'gutenberg-experiments' diff --git a/lib/load.php b/lib/load.php index 37e4d61c36832e..4b2b4d5d8b0db8 100644 --- a/lib/load.php +++ b/lib/load.php @@ -106,8 +106,17 @@ function gutenberg_is_experiment_enabled( $name ) { // WordPress 6.5 compat. require __DIR__ . '/compat/wordpress-6.5/blocks.php'; require __DIR__ . '/compat/wordpress-6.5/block-patterns.php'; -require __DIR__ . '/compat/wordpress-6.5/class-wp-navigation-block-renderer.php'; require __DIR__ . '/compat/wordpress-6.5/kses.php'; +require __DIR__ . '/compat/wordpress-6.5/interactivity-api/class-wp-interactivity-api.php'; +require __DIR__ . '/compat/wordpress-6.5/interactivity-api/class-wp-interactivity-api-directives-processor.php'; +require __DIR__ . '/compat/wordpress-6.5/interactivity-api/interactivity-api.php'; +require __DIR__ . '/compat/wordpress-6.5/class-wp-script-modules.php'; +require __DIR__ . '/compat/wordpress-6.5/scripts-modules.php'; +require __DIR__ . '/compat/wordpress-6.5/block-bindings/class-wp-block-bindings.php'; +require __DIR__ . '/compat/wordpress-6.5/block-bindings/block-bindings.php'; +require __DIR__ . '/compat/wordpress-6.5/block-bindings/sources/post-meta.php'; +require __DIR__ . '/compat/wordpress-6.5/block-bindings/sources/pattern.php'; + // Experimental features. require __DIR__ . '/experimental/block-editor-settings-mobile.php'; @@ -116,26 +125,12 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/experimental/kses.php'; require __DIR__ . '/experimental/l10n.php'; require __DIR__ . '/experimental/synchronization.php'; +require __DIR__ . '/experimental/script-modules.php'; if ( gutenberg_is_experiment_enabled( 'gutenberg-no-tinymce' ) ) { require __DIR__ . '/experimental/disable-tinymce.php'; } -require __DIR__ . '/experimental/interactivity-api/class-wp-interactivity-initial-state.php'; -require __DIR__ . '/experimental/interactivity-api/initial-state.php'; -require __DIR__ . '/experimental/interactivity-api/modules.php'; -require __DIR__ . '/experimental/interactivity-api/class-wp-directive-processor.php'; -require __DIR__ . '/experimental/interactivity-api/class-wp-directive-context.php'; -require __DIR__ . '/experimental/interactivity-api/directive-processing.php'; -require __DIR__ . '/experimental/interactivity-api/directives/wp-bind.php'; -require __DIR__ . '/experimental/interactivity-api/directives/wp-context.php'; -require __DIR__ . '/experimental/interactivity-api/directives/wp-class.php'; -require __DIR__ . '/experimental/interactivity-api/directives/wp-style.php'; -require __DIR__ . '/experimental/interactivity-api/directives/wp-text.php'; -require __DIR__ . '/experimental/interactivity-api/directives/wp-interactive.php'; - -require __DIR__ . '/experimental/modules/class-gutenberg-modules.php'; - // Fonts API / Font Face. remove_action( 'plugins_loaded', '_wp_theme_json_webfonts_handler' ); // Turns off WordPress 6.0's stopgap handler. @@ -143,10 +138,9 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/experimental/fonts/font-library/class-wp-font-collection.php'; require __DIR__ . '/experimental/fonts/font-library/class-wp-font-library.php'; require __DIR__ . '/experimental/fonts/font-library/class-wp-font-family-utils.php'; -require __DIR__ . '/experimental/fonts/font-library/class-wp-font-family.php'; require __DIR__ . '/experimental/fonts/font-library/class-wp-rest-font-families-controller.php'; +require __DIR__ . '/experimental/fonts/font-library/class-wp-rest-font-faces-controller.php'; require __DIR__ . '/experimental/fonts/font-library/class-wp-rest-font-collections-controller.php'; -require __DIR__ . '/experimental/fonts/font-library/class-wp-rest-autosave-font-families-controller.php'; require __DIR__ . '/experimental/fonts/font-library/font-library.php'; // Load the Font Face and Font Face Resolver, if not already loaded by WordPress Core. @@ -213,7 +207,6 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/block-supports/duotone.php'; require __DIR__ . '/block-supports/shadow.php'; require __DIR__ . '/block-supports/background.php'; -require __DIR__ . '/block-supports/pattern.php'; // Data views. require_once __DIR__ . '/experimental/data-views.php'; diff --git a/package-lock.json b/package-lock.json index 2a2fc30b96099b..c6669febe69f8b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gutenberg", - "version": "17.5.1", + "version": "17.6.0-rc.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "gutenberg", - "version": "17.5.1", + "version": "17.6.0-rc.1", "hasInstallScript": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -165,14 +165,14 @@ "babel-plugin-react-native-platform-specific-extensions": "1.1.1", "babel-plugin-transform-remove-console": "6.9.4", "benchmark": "2.1.4", - "browserslist": "4.21.10", - "caniuse-lite": "1.0.30001538", + "browserslist": "4.22.2", + "caniuse-lite": "1.0.30001579", "chalk": "4.1.1", "change-case": "4.1.2", "commander": "9.2.0", "concurrently": "3.5.0", "copy-webpack-plugin": "10.2.0", - "core-js-builder": "3.31.0", + "core-js-builder": "3.35.1", "cross-env": "3.2.4", "css-loader": "6.2.0", "cssnano": "6.0.1", @@ -17939,6 +17939,10 @@ "resolved": "packages/interactivity", "link": true }, + "node_modules/@wordpress/interactivity-router": { + "resolved": "packages/interactivity-router", + "link": true + }, "node_modules/@wordpress/interface": { "resolved": "packages/interface", "link": true @@ -19623,28 +19627,22 @@ "dev": true }, "node_modules/assert": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", - "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.1.tgz", + "integrity": "sha512-zzw1uCAgLbsKwBfFc8CX78DDg+xZeBksSO3vwVIDDN5i94eOrPsSSyiVhmsSABFDM/OcpE2aagCat9dnWQLG1A==", "dev": true, "dependencies": { - "object-assign": "^4.1.1", - "util": "0.10.3" + "object.assign": "^4.1.4", + "util": "^0.10.4" } }, - "node_modules/assert/node_modules/inherits": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", - "integrity": "sha512-8nWq2nLTAwd02jTqJExUYFSD/fKq6VH9Y/oG2accc/kdI0V98Bag8d5a4gi3XHz73rDWa2PvTtvcWYquKqSENA==", - "dev": true - }, "node_modules/assert/node_modules/util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", - "integrity": "sha512-5KiHfsmkqacuKjkRkdV7SsfDJ2EGiPsK92s2MhNSY0craxjTdKTtqKsJaCWp4LW33ZZ0OPUv1WO/TFvNQRiQxQ==", + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", + "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", "dev": true, "dependencies": { - "inherits": "2.0.1" + "inherits": "2.0.3" } }, "node_modules/assign-symbols": { @@ -19686,10 +19684,16 @@ "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" }, "node_modules/async-each": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", - "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.6.tgz", + "integrity": "sha512-c646jH1avxr+aVpndVMeAfYw7wAa6idufrlN3LPA4PmKS0QEGp6PIC9nwz0WQkkvBGAMEki3pFdtxaF39J9vvg==", "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], "optional": true }, "node_modules/async-limiter": { @@ -20801,20 +20805,23 @@ } }, "node_modules/browserify-sign": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.1.tgz", - "integrity": "sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.2.tgz", + "integrity": "sha512-1rudGyeYY42Dk6texmv7c4VcQ0EsvVbLwZkA+AQB7SxvXxmcD93jcHie8bzecJ+ChDlmAm2Qyu0+Ccg5uhZXCg==", "dev": true, "dependencies": { - "bn.js": "^5.1.1", - "browserify-rsa": "^4.0.1", + "bn.js": "^5.2.1", + "browserify-rsa": "^4.1.0", "create-hash": "^1.2.0", "create-hmac": "^1.1.7", - "elliptic": "^6.5.3", + "elliptic": "^6.5.4", "inherits": "^2.0.4", - "parse-asn1": "^5.1.5", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" + "parse-asn1": "^5.1.6", + "readable-stream": "^3.6.2", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 4" } }, "node_modules/browserify-sign/node_modules/inherits": { @@ -20867,9 +20874,9 @@ } }, "node_modules/browserslist": { - "version": "4.21.10", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz", - "integrity": "sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==", + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz", + "integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==", "funding": [ { "type": "opencollective", @@ -20885,10 +20892,10 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001517", - "electron-to-chromium": "^1.4.477", - "node-releases": "^2.0.13", - "update-browserslist-db": "^1.0.11" + "caniuse-lite": "^1.0.30001565", + "electron-to-chromium": "^1.4.601", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" }, "bin": { "browserslist": "cli.js" @@ -21350,9 +21357,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001538", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001538.tgz", - "integrity": "sha512-HWJnhnID+0YMtGlzcp3T9drmBJUVDchPJ08tpUGFLs9CYlwWPH2uLgpHn8fND5pCgXVtnGS3H4QR9XLMHVNkHw==", + "version": "1.0.30001579", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001579.tgz", + "integrity": "sha512-u5AUVkixruKHJjw/pj9wISlcMpgFWzSrczLZbrqBSxukQixmg0SJ5sZTpvaFvxU0HoQKd4yoyAogyrAz9pzJnA==", "funding": [ { "type": "opencollective", @@ -23114,9 +23121,9 @@ } }, "node_modules/core-js": { - "version": "3.31.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.31.0.tgz", - "integrity": "sha512-NIp2TQSGfR6ba5aalZD+ZQ1fSxGhDo/s1w0nx3RYzf2pnJxt7YynxFlFScP6eV7+GZsKO95NSjGxyJsU3DZgeQ==", + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.35.1.tgz", + "integrity": "sha512-IgdsbxNyMskrTFxa9lWHyMwAJU5gXOPP+1yO+K59d50VLVAIDAbs7gIv705KzALModfK3ZrSZTPNpC0PQgIZuw==", "hasInstallScript": true, "funding": { "type": "opencollective", @@ -23124,15 +23131,15 @@ } }, "node_modules/core-js-builder": { - "version": "3.31.0", - "resolved": "https://registry.npmjs.org/core-js-builder/-/core-js-builder-3.31.0.tgz", - "integrity": "sha512-qx8vgRM3U4+IjlMqNQl7Vj53ectTm1FpzJ+nJSQuT865StCXvusxCO+HuASWIKlkoc+96AfnFa2MEdqvGep9nA==", + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/core-js-builder/-/core-js-builder-3.35.1.tgz", + "integrity": "sha512-CfIvg+khWyS7ElbRXhuQH9QVUQxRDFl8AUwP08BxubJmYtRZuqwjwvc/DVnwRXQ6sqg8ghfNRgnUDWzwNhU/Rw==", "dev": true, "dependencies": { - "core-js": "3.31.0", - "core-js-compat": "3.31.0", + "core-js": "3.35.1", + "core-js-compat": "3.35.1", "mkdirp": ">=0.5.5 <1", - "webpack": ">=4.46.0 <5" + "webpack": ">=4.47.0 <5" }, "engines": { "node": ">=8.9.0" @@ -23327,9 +23334,9 @@ } }, "node_modules/core-js-builder/node_modules/webpack": { - "version": "4.46.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.46.0.tgz", - "integrity": "sha512-6jJuJjg8znb/xRItk7bkT0+Q7AHCYjjFnvKIWQPkNIOyRqoCGvkOs0ipeQzrqz4l5FtN5ZI/ukEHroeX/o1/5Q==", + "version": "4.47.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.47.0.tgz", + "integrity": "sha512-td7fYwgLSrky3fI1EuU5cneU4+pbH6GgOfuKNS1tNPcfdGinGELAqsb/BP4nnvZyKSG2i/xFGU7+n2PvZA8HJQ==", "dev": true, "dependencies": { "@webassemblyjs/ast": "1.9.0", @@ -23386,11 +23393,11 @@ } }, "node_modules/core-js-compat": { - "version": "3.31.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.31.0.tgz", - "integrity": "sha512-hM7YCu1cU6Opx7MXNu0NuumM0ezNeAeRKadixyiQELWY3vT3De9S4J5ZBMraWV2vZnrE1Cirl0GtFtDtMUXzPw==", + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.35.1.tgz", + "integrity": "sha512-sftHa5qUJY3rs9Zht1WEnmkvXputCyDBczPnr7QDgL8n3qrF3CMXY4VPSYtOLLiOUJcah2WNXREd48iOl6mQIw==", "dependencies": { - "browserslist": "^4.21.5" + "browserslist": "^4.22.2" }, "funding": { "type": "opencollective", @@ -23983,9 +23990,9 @@ } }, "node_modules/cyclist": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-0.2.2.tgz", - "integrity": "sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.2.tgz", + "integrity": "sha512-0sVXIohTfLqVIW3kb/0n6IiWF3Ifj5nm2XaSrLq2DI6fKIGa2fYAZdk917rUneaeLVpYfFcyXE2ft0fe3remsA==", "dev": true }, "node_modules/damerau-levenshtein": { @@ -24891,9 +24898,9 @@ } }, "node_modules/des.js": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", - "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz", + "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==", "dev": true, "dependencies": { "inherits": "^2.0.1", @@ -25559,9 +25566,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.525", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.525.tgz", - "integrity": "sha512-GIZ620hDK4YmIqAWkscG4W6RwY6gOx1y5J6f4JUQwctiJrqH2oxZYU4mXHi35oV32tr630UcepBzSBGJ/WYcZA==" + "version": "1.4.643", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.643.tgz", + "integrity": "sha512-QHscvvS7gt155PtoRC0dR2ilhL8E9LHhfTQEq1uD5AL0524rBLAwpAREFH06f87/e45B9XkR6Ki5dbhbCsVEIg==" }, "node_modules/elegant-spinner": { "version": "1.0.1", @@ -27691,9 +27698,10 @@ "dev": true }, "node_modules/figgy-pudding": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz", - "integrity": "sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz", + "integrity": "sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==", + "deprecated": "This module is no longer supported.", "dev": true }, "node_modules/figures": { @@ -28116,13 +28124,13 @@ } }, "node_modules/flush-write-stream": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.0.3.tgz", - "integrity": "sha512-calZMC10u0FMUqoiunI2AiGIIUtUIvifNwkHhNupZH4cbNnW1Itkoh/Nf5HFYmDrwWPjrUxpkZT0KhuCq0jmGw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", + "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", "dev": true, "dependencies": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.4" + "inherits": "^2.0.3", + "readable-stream": "^2.3.6" } }, "node_modules/fn.name": { @@ -39413,9 +39421,9 @@ "dev": true }, "node_modules/nan": { - "version": "2.14.2", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", - "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==", + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz", + "integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==", "dev": true, "optional": true }, @@ -39736,9 +39744,9 @@ "dev": true }, "node_modules/node-releases": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", - "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==" + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" }, "node_modules/node-stream-zip": { "version": "1.15.0", @@ -42114,12 +42122,12 @@ "dev": true }, "node_modules/parallel-transform": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.1.0.tgz", - "integrity": "sha1-1BDwZbBdojCB/NEPKIVMKb2jOwY=", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz", + "integrity": "sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==", "dev": true, "dependencies": { - "cyclist": "~0.2.2", + "cyclist": "^1.0.1", "inherits": "^2.0.3", "readable-stream": "^2.1.5" } @@ -48096,9 +48104,9 @@ } }, "node_modules/source-list-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.0.tgz", - "integrity": "sha512-I2UmuJSRr/T8jisiROLU3A3ltr+swpniSmNPI4Ml3ZCX6tVnDsuZzK7F2hl5jTqbZBWCEKlj5HRQiPExXLgE8A==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", "dev": true }, "node_modules/source-map": { @@ -48527,9 +48535,9 @@ } }, "node_modules/stream-each": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.2.tgz", - "integrity": "sha512-mc1dbFhGBxvTM3bIWmAAINbqiuAk9TATcfIQC8P+/+HJefgaiTlMn2dHvkX8qlI12KeYKSQ1Ua9RrIqrn1VPoA==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz", + "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==", "dev": true, "dependencies": { "end-of-stream": "^1.1.0", @@ -50950,9 +50958,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", - "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", "funding": [ { "type": "opencollective", @@ -53772,7 +53780,7 @@ }, "packages/a11y": { "name": "@wordpress/a11y", - "version": "3.49.0", + "version": "3.50.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -53785,7 +53793,7 @@ }, "packages/annotations": { "name": "@wordpress/annotations", - "version": "2.49.0", + "version": "2.50.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -53813,7 +53821,7 @@ }, "packages/api-fetch": { "name": "@wordpress/api-fetch", - "version": "6.46.0", + "version": "6.47.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -53826,7 +53834,7 @@ }, "packages/autop": { "name": "@wordpress/autop", - "version": "3.49.0", + "version": "3.50.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0" @@ -53837,7 +53845,7 @@ }, "packages/babel-plugin-import-jsx-pragma": { "name": "@wordpress/babel-plugin-import-jsx-pragma", - "version": "4.32.0", + "version": "4.33.0", "dev": true, "license": "GPL-2.0-or-later", "engines": { @@ -53849,7 +53857,7 @@ }, "packages/babel-plugin-makepot": { "name": "@wordpress/babel-plugin-makepot", - "version": "5.33.0", + "version": "5.34.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -53866,7 +53874,7 @@ }, "packages/babel-preset-default": { "name": "@wordpress/babel-preset-default", - "version": "7.33.0", + "version": "7.34.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -53889,13 +53897,13 @@ }, "packages/base-styles": { "name": "@wordpress/base-styles", - "version": "4.40.0", + "version": "4.41.0", "dev": true, "license": "GPL-2.0-or-later" }, "packages/blob": { "name": "@wordpress/blob", - "version": "3.49.0", + "version": "3.50.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0" @@ -53906,7 +53914,7 @@ }, "packages/block-directory": { "name": "@wordpress/block-directory", - "version": "4.26.0", + "version": "4.27.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -53941,7 +53949,7 @@ }, "packages/block-editor": { "name": "@wordpress/block-editor", - "version": "12.17.0", + "version": "12.18.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54038,7 +54046,7 @@ }, "packages/block-library": { "name": "@wordpress/block-library", - "version": "8.26.0", + "version": "8.27.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54062,6 +54070,7 @@ "@wordpress/i18n": "file:../i18n", "@wordpress/icons": "file:../icons", "@wordpress/interactivity": "file:../interactivity", + "@wordpress/interactivity-router": "file:../interactivity-router", "@wordpress/keycodes": "file:../keycodes", "@wordpress/notices": "file:../notices", "@wordpress/patterns": "file:../patterns", @@ -54101,7 +54110,7 @@ }, "packages/block-serialization-default-parser": { "name": "@wordpress/block-serialization-default-parser", - "version": "4.49.0", + "version": "4.50.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0" @@ -54112,7 +54121,7 @@ }, "packages/block-serialization-spec-parser": { "name": "@wordpress/block-serialization-spec-parser", - "version": "4.49.0", + "version": "4.50.0", "license": "GPL-2.0-or-later", "dependencies": { "pegjs": "^0.10.0", @@ -54124,7 +54133,7 @@ }, "packages/blocks": { "name": "@wordpress/blocks", - "version": "12.26.0", + "version": "12.27.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54178,7 +54187,7 @@ }, "packages/browserslist-config": { "name": "@wordpress/browserslist-config", - "version": "5.32.0", + "version": "5.33.0", "dev": true, "license": "GPL-2.0-or-later", "engines": { @@ -54187,7 +54196,7 @@ }, "packages/commands": { "name": "@wordpress/commands", - "version": "0.20.0", + "version": "0.21.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54212,7 +54221,7 @@ }, "packages/components": { "name": "@wordpress/components", - "version": "25.15.0", + "version": "25.16.0", "license": "GPL-2.0-or-later", "dependencies": { "@ariakit/react": "^0.3.12", @@ -54317,7 +54326,7 @@ }, "packages/compose": { "name": "@wordpress/compose", - "version": "6.26.0", + "version": "6.27.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54353,7 +54362,7 @@ }, "packages/core-commands": { "name": "@wordpress/core-commands", - "version": "0.18.0", + "version": "0.19.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54378,7 +54387,7 @@ }, "packages/core-data": { "name": "@wordpress/core-data", - "version": "6.26.0", + "version": "6.27.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54422,7 +54431,7 @@ }, "packages/create-block": { "name": "@wordpress/create-block", - "version": "4.33.0", + "version": "4.34.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -54450,13 +54459,13 @@ }, "packages/create-block-tutorial-template": { "name": "@wordpress/create-block-tutorial-template", - "version": "3.3.0", + "version": "3.4.0", "dev": true, "license": "GPL-2.0-or-later" }, "packages/customize-widgets": { "name": "@wordpress/customize-widgets", - "version": "4.26.0", + "version": "4.27.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54493,7 +54502,7 @@ }, "packages/data": { "name": "@wordpress/data", - "version": "9.19.0", + "version": "9.20.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54521,7 +54530,7 @@ }, "packages/data-controls": { "name": "@wordpress/data-controls", - "version": "3.18.0", + "version": "3.19.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54538,7 +54547,7 @@ }, "packages/dataviews": { "name": "@wordpress/dataviews", - "version": "0.3.0", + "version": "0.4.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54563,7 +54572,7 @@ }, "packages/date": { "name": "@wordpress/date", - "version": "4.49.0", + "version": "4.50.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54577,7 +54586,7 @@ }, "packages/dependency-extraction-webpack-plugin": { "name": "@wordpress/dependency-extraction-webpack-plugin", - "version": "5.0.0", + "version": "5.1.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -54592,7 +54601,7 @@ }, "packages/deprecated": { "name": "@wordpress/deprecated", - "version": "3.49.0", + "version": "3.50.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54604,7 +54613,7 @@ }, "packages/docgen": { "name": "@wordpress/docgen", - "version": "1.58.0", + "version": "1.59.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -54622,7 +54631,7 @@ }, "packages/dom": { "name": "@wordpress/dom", - "version": "3.49.0", + "version": "3.50.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54634,7 +54643,7 @@ }, "packages/dom-ready": { "name": "@wordpress/dom-ready", - "version": "3.49.0", + "version": "3.50.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0" @@ -54645,7 +54654,7 @@ }, "packages/e2e-test-utils": { "name": "@wordpress/e2e-test-utils", - "version": "10.20.0", + "version": "10.21.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -54667,7 +54676,7 @@ }, "packages/e2e-test-utils-playwright": { "name": "@wordpress/e2e-test-utils-playwright", - "version": "0.17.0", + "version": "0.18.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -54690,12 +54699,13 @@ }, "packages/e2e-tests": { "name": "@wordpress/e2e-tests", - "version": "7.20.0", + "version": "7.21.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { "@wordpress/e2e-test-utils": "file:../e2e-test-utils", "@wordpress/interactivity": "file:../interactivity", + "@wordpress/interactivity-router": "file:../interactivity-router", "@wordpress/jest-console": "file:../jest-console", "@wordpress/jest-puppeteer-axe": "file:../jest-puppeteer-axe", "@wordpress/scripts": "file:../scripts", @@ -54728,7 +54738,7 @@ }, "packages/edit-post": { "name": "@wordpress/edit-post", - "version": "7.26.0", + "version": "7.27.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54776,7 +54786,7 @@ }, "packages/edit-site": { "name": "@wordpress/edit-site", - "version": "5.26.0", + "version": "5.27.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54841,7 +54851,7 @@ }, "packages/edit-widgets": { "name": "@wordpress/edit-widgets", - "version": "5.26.0", + "version": "5.27.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54884,7 +54894,7 @@ }, "packages/editor": { "name": "@wordpress/editor", - "version": "13.26.0", + "version": "13.27.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54935,7 +54945,7 @@ }, "packages/element": { "name": "@wordpress/element", - "version": "5.26.0", + "version": "5.27.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54953,7 +54963,7 @@ }, "packages/env": { "name": "@wordpress/env", - "version": "9.1.0", + "version": "9.2.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -55087,7 +55097,7 @@ }, "packages/escape-html": { "name": "@wordpress/escape-html", - "version": "2.49.0", + "version": "2.50.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0" @@ -55098,7 +55108,7 @@ }, "packages/eslint-plugin": { "name": "@wordpress/eslint-plugin", - "version": "17.6.0", + "version": "17.7.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -55141,7 +55151,7 @@ }, "packages/format-library": { "name": "@wordpress/format-library", - "version": "4.26.0", + "version": "4.27.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55168,7 +55178,7 @@ }, "packages/hooks": { "name": "@wordpress/hooks", - "version": "3.49.0", + "version": "3.50.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0" @@ -55179,7 +55189,7 @@ }, "packages/html-entities": { "name": "@wordpress/html-entities", - "version": "3.49.0", + "version": "3.50.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0" @@ -55190,7 +55200,7 @@ }, "packages/i18n": { "name": "@wordpress/i18n", - "version": "4.49.0", + "version": "4.50.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55209,7 +55219,7 @@ }, "packages/icons": { "name": "@wordpress/icons", - "version": "9.40.0", + "version": "9.41.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55222,7 +55232,7 @@ }, "packages/interactivity": { "name": "@wordpress/interactivity", - "version": "3.2.0", + "version": "4.0.0", "license": "GPL-2.0-or-later", "dependencies": { "@preact/signals": "^1.2.2", @@ -55233,6 +55243,17 @@ "node": ">=12" } }, + "packages/interactivity-router": { + "name": "@wordpress/interactivity-router", + "version": "1.0.0", + "license": "GPL-2.0-or-later", + "dependencies": { + "@wordpress/interactivity": "file:../interactivity" + }, + "engines": { + "node": ">=12" + } + }, "packages/interactivity/node_modules/@preact/signals": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@preact/signals/-/signals-1.2.2.tgz", @@ -55284,7 +55305,7 @@ }, "packages/interface": { "name": "@wordpress/interface", - "version": "5.26.0", + "version": "5.27.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55312,7 +55333,7 @@ }, "packages/is-shallow-equal": { "name": "@wordpress/is-shallow-equal", - "version": "4.49.0", + "version": "4.50.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0" @@ -55323,7 +55344,7 @@ }, "packages/jest-console": { "name": "@wordpress/jest-console", - "version": "7.20.0", + "version": "7.21.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -55339,7 +55360,7 @@ }, "packages/jest-preset-default": { "name": "@wordpress/jest-preset-default", - "version": "11.20.0", + "version": "11.21.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -55356,7 +55377,7 @@ }, "packages/jest-puppeteer-axe": { "name": "@wordpress/jest-puppeteer-axe", - "version": "6.20.0", + "version": "6.21.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -55378,7 +55399,7 @@ }, "packages/keyboard-shortcuts": { "name": "@wordpress/keyboard-shortcuts", - "version": "4.26.0", + "version": "4.27.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55396,7 +55417,7 @@ }, "packages/keycodes": { "name": "@wordpress/keycodes", - "version": "3.49.0", + "version": "3.50.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55408,7 +55429,7 @@ }, "packages/lazy-import": { "name": "@wordpress/lazy-import", - "version": "1.36.0", + "version": "1.37.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -55422,7 +55443,7 @@ }, "packages/list-reusable-blocks": { "name": "@wordpress/list-reusable-blocks", - "version": "4.26.0", + "version": "4.27.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55444,7 +55465,7 @@ }, "packages/media-utils": { "name": "@wordpress/media-utils", - "version": "4.40.0", + "version": "4.41.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55459,7 +55480,7 @@ }, "packages/notices": { "name": "@wordpress/notices", - "version": "4.17.0", + "version": "4.18.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55475,7 +55496,7 @@ }, "packages/npm-package-json-lint-config": { "name": "@wordpress/npm-package-json-lint-config", - "version": "4.34.0", + "version": "4.35.0", "dev": true, "license": "GPL-2.0-or-later", "engines": { @@ -55487,7 +55508,7 @@ }, "packages/nux": { "name": "@wordpress/nux", - "version": "8.11.0", + "version": "8.12.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55510,7 +55531,7 @@ }, "packages/patterns": { "name": "@wordpress/patterns", - "version": "1.10.0", + "version": "1.11.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55540,7 +55561,7 @@ }, "packages/plugins": { "name": "@wordpress/plugins", - "version": "6.17.0", + "version": "6.18.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55562,7 +55583,7 @@ }, "packages/postcss-plugins-preset": { "name": "@wordpress/postcss-plugins-preset", - "version": "4.33.0", + "version": "4.34.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -55578,7 +55599,7 @@ }, "packages/postcss-themes": { "name": "@wordpress/postcss-themes", - "version": "5.32.0", + "version": "5.33.0", "dev": true, "license": "GPL-2.0-or-later", "engines": { @@ -55590,7 +55611,7 @@ }, "packages/preferences": { "name": "@wordpress/preferences", - "version": "3.26.0", + "version": "3.27.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55615,7 +55636,7 @@ }, "packages/preferences-persistence": { "name": "@wordpress/preferences-persistence", - "version": "1.41.0", + "version": "1.42.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55627,7 +55648,7 @@ }, "packages/prettier-config": { "name": "@wordpress/prettier-config", - "version": "3.6.0", + "version": "3.7.0", "dev": true, "license": "GPL-2.0-or-later", "engines": { @@ -55639,7 +55660,7 @@ }, "packages/primitives": { "name": "@wordpress/primitives", - "version": "3.47.0", + "version": "3.48.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55652,7 +55673,7 @@ }, "packages/priority-queue": { "name": "@wordpress/priority-queue", - "version": "2.49.0", + "version": "2.50.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55664,7 +55685,7 @@ }, "packages/private-apis": { "name": "@wordpress/private-apis", - "version": "0.31.0", + "version": "0.32.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0" @@ -55675,7 +55696,7 @@ }, "packages/project-management-automation": { "name": "@wordpress/project-management-automation", - "version": "1.48.0", + "version": "1.49.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -55688,7 +55709,7 @@ }, "packages/react-i18n": { "name": "@wordpress/react-i18n", - "version": "3.47.0", + "version": "3.48.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55702,7 +55723,7 @@ }, "packages/react-native-aztec": { "name": "@wordpress/react-native-aztec", - "version": "1.111.0", + "version": "1.111.1", "license": "GPL-2.0-or-later", "dependencies": { "@wordpress/element": "file:../element", @@ -55715,7 +55736,7 @@ }, "packages/react-native-bridge": { "name": "@wordpress/react-native-bridge", - "version": "1.111.0", + "version": "1.111.1", "license": "GPL-2.0-or-later", "dependencies": { "@wordpress/react-native-aztec": "file:../react-native-aztec" @@ -55726,7 +55747,7 @@ }, "packages/react-native-editor": { "name": "@wordpress/react-native-editor", - "version": "1.111.0", + "version": "1.111.1", "hasInstallScript": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -55836,7 +55857,7 @@ }, "packages/readable-js-assets-webpack-plugin": { "name": "@wordpress/readable-js-assets-webpack-plugin", - "version": "2.32.0", + "version": "2.33.0", "dev": true, "license": "GPL-2.0-or-later", "engines": { @@ -55848,7 +55869,7 @@ }, "packages/redux-routine": { "name": "@wordpress/redux-routine", - "version": "4.49.0", + "version": "4.50.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55892,7 +55913,7 @@ }, "packages/reusable-blocks": { "name": "@wordpress/reusable-blocks", - "version": "4.26.0", + "version": "4.27.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55918,7 +55939,7 @@ }, "packages/rich-text": { "name": "@wordpress/rich-text", - "version": "6.26.0", + "version": "6.27.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55942,7 +55963,7 @@ }, "packages/router": { "name": "@wordpress/router", - "version": "0.18.0", + "version": "0.19.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55960,7 +55981,7 @@ }, "packages/scripts": { "name": "@wordpress/scripts", - "version": "27.0.0", + "version": "27.1.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -56182,7 +56203,7 @@ }, "packages/server-side-render": { "name": "@wordpress/server-side-render", - "version": "4.26.0", + "version": "4.27.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56207,7 +56228,7 @@ }, "packages/shortcode": { "name": "@wordpress/shortcode", - "version": "3.49.0", + "version": "3.50.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56219,7 +56240,7 @@ }, "packages/style-engine": { "name": "@wordpress/style-engine", - "version": "1.32.0", + "version": "1.33.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56231,7 +56252,7 @@ }, "packages/stylelint-config": { "name": "@wordpress/stylelint-config", - "version": "21.32.0", + "version": "21.33.0", "dev": true, "license": "MIT", "dependencies": { @@ -56247,7 +56268,7 @@ }, "packages/sync": { "name": "@wordpress/sync", - "version": "0.11.0", + "version": "0.12.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56267,7 +56288,7 @@ }, "packages/token-list": { "name": "@wordpress/token-list", - "version": "2.49.0", + "version": "2.50.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0" @@ -56278,7 +56299,7 @@ }, "packages/undo-manager": { "name": "@wordpress/undo-manager", - "version": "0.9.0", + "version": "0.10.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56290,7 +56311,7 @@ }, "packages/url": { "name": "@wordpress/url", - "version": "3.50.0", + "version": "3.51.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56302,7 +56323,7 @@ }, "packages/viewport": { "name": "@wordpress/viewport", - "version": "5.26.0", + "version": "5.27.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56319,7 +56340,7 @@ }, "packages/warning": { "name": "@wordpress/warning", - "version": "2.49.0", + "version": "2.50.0", "license": "GPL-2.0-or-later", "engines": { "node": ">=12" @@ -56327,7 +56348,7 @@ }, "packages/widgets": { "name": "@wordpress/widgets", - "version": "3.26.0", + "version": "3.27.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56351,7 +56372,7 @@ }, "packages/wordcount": { "name": "@wordpress/wordcount", - "version": "3.49.0", + "version": "3.50.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0" @@ -69263,6 +69284,7 @@ "@wordpress/i18n": "file:../i18n", "@wordpress/icons": "file:../icons", "@wordpress/interactivity": "file:../interactivity", + "@wordpress/interactivity-router": "file:../interactivity-router", "@wordpress/keycodes": "file:../keycodes", "@wordpress/notices": "file:../notices", "@wordpress/patterns": "file:../patterns", @@ -69707,6 +69729,7 @@ "requires": { "@wordpress/e2e-test-utils": "file:../e2e-test-utils", "@wordpress/interactivity": "file:../interactivity", + "@wordpress/interactivity-router": "file:../interactivity-router", "@wordpress/jest-console": "file:../jest-console", "@wordpress/jest-puppeteer-axe": "file:../jest-puppeteer-axe", "@wordpress/scripts": "file:../scripts", @@ -70120,6 +70143,12 @@ } } }, + "@wordpress/interactivity-router": { + "version": "file:packages/interactivity-router", + "requires": { + "@wordpress/interactivity": "file:../interactivity" + } + }, "@wordpress/interface": { "version": "file:packages/interface", "requires": { @@ -71933,28 +71962,22 @@ } }, "assert": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", - "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.1.tgz", + "integrity": "sha512-zzw1uCAgLbsKwBfFc8CX78DDg+xZeBksSO3vwVIDDN5i94eOrPsSSyiVhmsSABFDM/OcpE2aagCat9dnWQLG1A==", "dev": true, "requires": { - "object-assign": "^4.1.1", - "util": "0.10.3" + "object.assign": "^4.1.4", + "util": "^0.10.4" }, "dependencies": { - "inherits": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", - "integrity": "sha512-8nWq2nLTAwd02jTqJExUYFSD/fKq6VH9Y/oG2accc/kdI0V98Bag8d5a4gi3XHz73rDWa2PvTtvcWYquKqSENA==", - "dev": true - }, "util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", - "integrity": "sha512-5KiHfsmkqacuKjkRkdV7SsfDJ2EGiPsK92s2MhNSY0craxjTdKTtqKsJaCWp4LW33ZZ0OPUv1WO/TFvNQRiQxQ==", + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", + "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", "dev": true, "requires": { - "inherits": "2.0.1" + "inherits": "2.0.3" } } } @@ -71989,9 +72012,9 @@ "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" }, "async-each": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", - "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.6.tgz", + "integrity": "sha512-c646jH1avxr+aVpndVMeAfYw7wAa6idufrlN3LPA4PmKS0QEGp6PIC9nwz0WQkkvBGAMEki3pFdtxaF39J9vvg==", "dev": true, "optional": true }, @@ -72876,20 +72899,20 @@ } }, "browserify-sign": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.1.tgz", - "integrity": "sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.2.tgz", + "integrity": "sha512-1rudGyeYY42Dk6texmv7c4VcQ0EsvVbLwZkA+AQB7SxvXxmcD93jcHie8bzecJ+ChDlmAm2Qyu0+Ccg5uhZXCg==", "dev": true, "requires": { - "bn.js": "^5.1.1", - "browserify-rsa": "^4.0.1", + "bn.js": "^5.2.1", + "browserify-rsa": "^4.1.0", "create-hash": "^1.2.0", "create-hmac": "^1.1.7", - "elliptic": "^6.5.3", + "elliptic": "^6.5.4", "inherits": "^2.0.4", - "parse-asn1": "^5.1.5", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" + "parse-asn1": "^5.1.6", + "readable-stream": "^3.6.2", + "safe-buffer": "^5.2.1" }, "dependencies": { "inherits": { @@ -72927,14 +72950,14 @@ } }, "browserslist": { - "version": "4.21.10", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz", - "integrity": "sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==", + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz", + "integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==", "requires": { - "caniuse-lite": "^1.0.30001517", - "electron-to-chromium": "^1.4.477", - "node-releases": "^2.0.13", - "update-browserslist-db": "^1.0.11" + "caniuse-lite": "^1.0.30001565", + "electron-to-chromium": "^1.4.601", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" } }, "bser": { @@ -73290,9 +73313,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001538", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001538.tgz", - "integrity": "sha512-HWJnhnID+0YMtGlzcp3T9drmBJUVDchPJ08tpUGFLs9CYlwWPH2uLgpHn8fND5pCgXVtnGS3H4QR9XLMHVNkHw==" + "version": "1.0.30001579", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001579.tgz", + "integrity": "sha512-u5AUVkixruKHJjw/pj9wISlcMpgFWzSrczLZbrqBSxukQixmg0SJ5sZTpvaFvxU0HoQKd4yoyAogyrAz9pzJnA==" }, "capital-case": { "version": "1.0.4", @@ -74672,20 +74695,20 @@ } }, "core-js": { - "version": "3.31.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.31.0.tgz", - "integrity": "sha512-NIp2TQSGfR6ba5aalZD+ZQ1fSxGhDo/s1w0nx3RYzf2pnJxt7YynxFlFScP6eV7+GZsKO95NSjGxyJsU3DZgeQ==" + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.35.1.tgz", + "integrity": "sha512-IgdsbxNyMskrTFxa9lWHyMwAJU5gXOPP+1yO+K59d50VLVAIDAbs7gIv705KzALModfK3ZrSZTPNpC0PQgIZuw==" }, "core-js-builder": { - "version": "3.31.0", - "resolved": "https://registry.npmjs.org/core-js-builder/-/core-js-builder-3.31.0.tgz", - "integrity": "sha512-qx8vgRM3U4+IjlMqNQl7Vj53ectTm1FpzJ+nJSQuT865StCXvusxCO+HuASWIKlkoc+96AfnFa2MEdqvGep9nA==", + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/core-js-builder/-/core-js-builder-3.35.1.tgz", + "integrity": "sha512-CfIvg+khWyS7ElbRXhuQH9QVUQxRDFl8AUwP08BxubJmYtRZuqwjwvc/DVnwRXQ6sqg8ghfNRgnUDWzwNhU/Rw==", "dev": true, "requires": { - "core-js": "3.31.0", - "core-js-compat": "3.31.0", + "core-js": "3.35.1", + "core-js-compat": "3.35.1", "mkdirp": ">=0.5.5 <1", - "webpack": ">=4.46.0 <5" + "webpack": ">=4.47.0 <5" }, "dependencies": { "ajv": { @@ -74837,9 +74860,9 @@ } }, "webpack": { - "version": "4.46.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.46.0.tgz", - "integrity": "sha512-6jJuJjg8znb/xRItk7bkT0+Q7AHCYjjFnvKIWQPkNIOyRqoCGvkOs0ipeQzrqz4l5FtN5ZI/ukEHroeX/o1/5Q==", + "version": "4.47.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.47.0.tgz", + "integrity": "sha512-td7fYwgLSrky3fI1EuU5cneU4+pbH6GgOfuKNS1tNPcfdGinGELAqsb/BP4nnvZyKSG2i/xFGU7+n2PvZA8HJQ==", "dev": true, "requires": { "@webassemblyjs/ast": "1.9.0", @@ -74880,11 +74903,11 @@ } }, "core-js-compat": { - "version": "3.31.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.31.0.tgz", - "integrity": "sha512-hM7YCu1cU6Opx7MXNu0NuumM0ezNeAeRKadixyiQELWY3vT3De9S4J5ZBMraWV2vZnrE1Cirl0GtFtDtMUXzPw==", + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.35.1.tgz", + "integrity": "sha512-sftHa5qUJY3rs9Zht1WEnmkvXputCyDBczPnr7QDgL8n3qrF3CMXY4VPSYtOLLiOUJcah2WNXREd48iOl6mQIw==", "requires": { - "browserslist": "^4.21.5" + "browserslist": "^4.22.2" } }, "core-js-pure": { @@ -75341,9 +75364,9 @@ } }, "cyclist": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-0.2.2.tgz", - "integrity": "sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.2.tgz", + "integrity": "sha512-0sVXIohTfLqVIW3kb/0n6IiWF3Ifj5nm2XaSrLq2DI6fKIGa2fYAZdk917rUneaeLVpYfFcyXE2ft0fe3remsA==", "dev": true }, "damerau-levenshtein": { @@ -76002,9 +76025,9 @@ "dev": true }, "des.js": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", - "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz", + "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==", "dev": true, "requires": { "inherits": "^2.0.1", @@ -76515,9 +76538,9 @@ } }, "electron-to-chromium": { - "version": "1.4.525", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.525.tgz", - "integrity": "sha512-GIZ620hDK4YmIqAWkscG4W6RwY6gOx1y5J6f4JUQwctiJrqH2oxZYU4mXHi35oV32tr630UcepBzSBGJ/WYcZA==" + "version": "1.4.643", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.643.tgz", + "integrity": "sha512-QHscvvS7gt155PtoRC0dR2ilhL8E9LHhfTQEq1uD5AL0524rBLAwpAREFH06f87/e45B9XkR6Ki5dbhbCsVEIg==" }, "elegant-spinner": { "version": "1.0.1", @@ -78157,9 +78180,9 @@ "dev": true }, "figgy-pudding": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz", - "integrity": "sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz", + "integrity": "sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==", "dev": true }, "figures": { @@ -78496,13 +78519,13 @@ "integrity": "sha512-2hJ5ACYeJCzNtiVULov6pljKOLygy0zddoqSI1fFetM+XRPpRshFdGEijtqlamA1XwyZ+7rhryI6FQFzvtLWUQ==" }, "flush-write-stream": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.0.3.tgz", - "integrity": "sha512-calZMC10u0FMUqoiunI2AiGIIUtUIvifNwkHhNupZH4cbNnW1Itkoh/Nf5HFYmDrwWPjrUxpkZT0KhuCq0jmGw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", + "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", "dev": true, "requires": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.4" + "inherits": "^2.0.3", + "readable-stream": "^2.3.6" } }, "fn.name": { @@ -87175,9 +87198,9 @@ "dev": true }, "nan": { - "version": "2.14.2", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", - "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==", + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz", + "integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==", "dev": true, "optional": true }, @@ -87424,9 +87447,9 @@ "dev": true }, "node-releases": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", - "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==" + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" }, "node-stream-zip": { "version": "1.15.0", @@ -89220,12 +89243,12 @@ "dev": true }, "parallel-transform": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.1.0.tgz", - "integrity": "sha1-1BDwZbBdojCB/NEPKIVMKb2jOwY=", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz", + "integrity": "sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==", "dev": true, "requires": { - "cyclist": "~0.2.2", + "cyclist": "^1.0.1", "inherits": "^2.0.3", "readable-stream": "^2.1.5" } @@ -93765,9 +93788,9 @@ } }, "source-list-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.0.tgz", - "integrity": "sha512-I2UmuJSRr/T8jisiROLU3A3ltr+swpniSmNPI4Ml3ZCX6tVnDsuZzK7F2hl5jTqbZBWCEKlj5HRQiPExXLgE8A==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", "dev": true }, "source-map": { @@ -94105,9 +94128,9 @@ } }, "stream-each": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.2.tgz", - "integrity": "sha512-mc1dbFhGBxvTM3bIWmAAINbqiuAk9TATcfIQC8P+/+HJefgaiTlMn2dHvkX8qlI12KeYKSQ1Ua9RrIqrn1VPoA==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz", + "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==", "dev": true, "requires": { "end-of-stream": "^1.1.0", @@ -95969,9 +95992,9 @@ "optional": true }, "update-browserslist-db": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", - "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", "requires": { "escalade": "^3.1.1", "picocolors": "^1.0.0" diff --git a/package.json b/package.json index 035c8d69cc56f0..e47b3580c2a41e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "17.5.1", + "version": "17.6.0-rc.1", "private": true, "description": "A new WordPress editor experience.", "author": "The WordPress Contributors", @@ -177,14 +177,14 @@ "babel-plugin-react-native-platform-specific-extensions": "1.1.1", "babel-plugin-transform-remove-console": "6.9.4", "benchmark": "2.1.4", - "browserslist": "4.21.10", - "caniuse-lite": "1.0.30001538", + "browserslist": "4.22.2", + "caniuse-lite": "1.0.30001579", "chalk": "4.1.1", "change-case": "4.1.2", "commander": "9.2.0", "concurrently": "3.5.0", "copy-webpack-plugin": "10.2.0", - "core-js-builder": "3.31.0", + "core-js-builder": "3.35.1", "cross-env": "3.2.4", "css-loader": "6.2.0", "cssnano": "6.0.1", diff --git a/packages/a11y/CHANGELOG.md b/packages/a11y/CHANGELOG.md index 7c5c9d79784102..e302cf7514cbf5 100644 --- a/packages/a11y/CHANGELOG.md +++ b/packages/a11y/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 3.50.0 (2024-01-24) + ## 3.49.0 (2024-01-10) ## 3.48.0 (2023-12-13) diff --git a/packages/a11y/package.json b/packages/a11y/package.json index c2d1b666339bf6..bfdee27579a985 100644 --- a/packages/a11y/package.json +++ b/packages/a11y/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/a11y", - "version": "3.49.0", + "version": "3.50.0", "description": "Accessibility (a11y) utilities for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/annotations/CHANGELOG.md b/packages/annotations/CHANGELOG.md index 42c37836527973..df543928a05efb 100644 --- a/packages/annotations/CHANGELOG.md +++ b/packages/annotations/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 2.50.0 (2024-01-24) + ## 2.49.0 (2024-01-10) ## 2.48.0 (2023-12-13) diff --git a/packages/annotations/package.json b/packages/annotations/package.json index 007b02ad29a71d..ebe468f068da7f 100644 --- a/packages/annotations/package.json +++ b/packages/annotations/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/annotations", - "version": "2.49.0", + "version": "2.50.0", "description": "Annotate content in the Gutenberg editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/api-fetch/CHANGELOG.md b/packages/api-fetch/CHANGELOG.md index 67df4dad50c64c..5284234afd2289 100644 --- a/packages/api-fetch/CHANGELOG.md +++ b/packages/api-fetch/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 6.47.0 (2024-01-24) + ## 6.46.0 (2024-01-10) ## 6.45.0 (2023-12-13) diff --git a/packages/api-fetch/package.json b/packages/api-fetch/package.json index 02d0474156bfc1..a7ad0ad752d061 100644 --- a/packages/api-fetch/package.json +++ b/packages/api-fetch/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/api-fetch", - "version": "6.46.0", + "version": "6.47.0", "description": "Utility to make WordPress REST API requests.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/autop/CHANGELOG.md b/packages/autop/CHANGELOG.md index 4e4c9a91bc781d..969a52ded8a7fa 100644 --- a/packages/autop/CHANGELOG.md +++ b/packages/autop/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 3.50.0 (2024-01-24) + ## 3.49.0 (2024-01-10) ## 3.48.0 (2023-12-13) diff --git a/packages/autop/package.json b/packages/autop/package.json index e335e4ac7349b5..09fac4ca6c6609 100644 --- a/packages/autop/package.json +++ b/packages/autop/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/autop", - "version": "3.49.0", + "version": "3.50.0", "description": "WordPress's automatic paragraph functions `autop` and `removep`.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/babel-plugin-import-jsx-pragma/CHANGELOG.md b/packages/babel-plugin-import-jsx-pragma/CHANGELOG.md index 19269095d73d70..8ee06832be474c 100644 --- a/packages/babel-plugin-import-jsx-pragma/CHANGELOG.md +++ b/packages/babel-plugin-import-jsx-pragma/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.33.0 (2024-01-24) + ## 4.32.0 (2024-01-10) ## 4.31.0 (2023-12-13) diff --git a/packages/babel-plugin-import-jsx-pragma/package.json b/packages/babel-plugin-import-jsx-pragma/package.json index 3af7e30b2e2340..c0ccae09f1811a 100644 --- a/packages/babel-plugin-import-jsx-pragma/package.json +++ b/packages/babel-plugin-import-jsx-pragma/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/babel-plugin-import-jsx-pragma", - "version": "4.32.0", + "version": "4.33.0", "description": "Babel transform plugin for automatically injecting an import to be used as the pragma for the React JSX Transform plugin.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/babel-plugin-makepot/CHANGELOG.md b/packages/babel-plugin-makepot/CHANGELOG.md index fd1a0546da3ad0..ef5ce07c677b59 100644 --- a/packages/babel-plugin-makepot/CHANGELOG.md +++ b/packages/babel-plugin-makepot/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 5.34.0 (2024-01-24) + ## 5.33.0 (2024-01-10) ## 5.32.0 (2023-12-13) diff --git a/packages/babel-plugin-makepot/package.json b/packages/babel-plugin-makepot/package.json index 6163ad1df3ff1b..3994df49b320a0 100644 --- a/packages/babel-plugin-makepot/package.json +++ b/packages/babel-plugin-makepot/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/babel-plugin-makepot", - "version": "5.33.0", + "version": "5.34.0", "description": "WordPress Babel internationalization (i18n) plugin.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/babel-preset-default/CHANGELOG.md b/packages/babel-preset-default/CHANGELOG.md index 4149381dce16f8..2ebbc9e4efc24d 100644 --- a/packages/babel-preset-default/CHANGELOG.md +++ b/packages/babel-preset-default/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 7.34.0 (2024-01-24) + ## 7.33.0 (2024-01-10) ## 7.32.0 (2023-12-13) diff --git a/packages/babel-preset-default/bin/index.js b/packages/babel-preset-default/bin/index.js index a2d86233565138..54c35564d43d74 100755 --- a/packages/babel-preset-default/bin/index.js +++ b/packages/babel-preset-default/bin/index.js @@ -8,17 +8,13 @@ const { minify } = require( 'uglify-js' ); const { writeFile } = require( 'fs' ).promises; builder( { - modules: [ 'es', 'web' ], + modules: [ 'es.', 'web.' ], exclude: [ - // core-js is extremely conservative in which polyfills to include. - // Since we don't care about the tiny browser implementation bugs behind its decision - // to polyfill these features, we forcefully prevent them from being included. - // @see https://github.com/WordPress/gutenberg/pull/31279 - 'es.promise', // This is an IE-only feature which we don't use, and don't want to polyfill. // @see https://github.com/WordPress/gutenberg/pull/49234 'web.immediate', ], + summary: { console: { size: true, modules: true } }, targets: require( '@wordpress/browserslist-config' ), filename: './build/polyfill.js', } ) diff --git a/packages/babel-preset-default/package.json b/packages/babel-preset-default/package.json index 66e63b24ff51eb..6110031337c893 100644 --- a/packages/babel-preset-default/package.json +++ b/packages/babel-preset-default/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/babel-preset-default", - "version": "7.33.0", + "version": "7.34.0", "description": "Default Babel preset for WordPress development.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/base-styles/CHANGELOG.md b/packages/base-styles/CHANGELOG.md index a399317b2aef0d..614d83a498a33f 100644 --- a/packages/base-styles/CHANGELOG.md +++ b/packages/base-styles/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.41.0 (2024-01-24) + ## 4.40.0 (2024-01-10) ## 4.39.0 (2023-12-13) diff --git a/packages/base-styles/package.json b/packages/base-styles/package.json index 64d3835a00aca3..f9fe0dbd9230bc 100644 --- a/packages/base-styles/package.json +++ b/packages/base-styles/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/base-styles", - "version": "4.40.0", + "version": "4.41.0", "description": "Base SCSS utilities and variables for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/blob/CHANGELOG.md b/packages/blob/CHANGELOG.md index b2a501021c5aaa..87e1956b5a5569 100644 --- a/packages/blob/CHANGELOG.md +++ b/packages/blob/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 3.50.0 (2024-01-24) + ## 3.49.0 (2024-01-10) ## 3.48.0 (2023-12-13) diff --git a/packages/blob/package.json b/packages/blob/package.json index 53071228e0af1e..aaabbefa579785 100644 --- a/packages/blob/package.json +++ b/packages/blob/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/blob", - "version": "3.49.0", + "version": "3.50.0", "description": "Blob utilities for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/block-directory/CHANGELOG.md b/packages/block-directory/CHANGELOG.md index 608fc4c4f005cf..25162b8b692bf6 100644 --- a/packages/block-directory/CHANGELOG.md +++ b/packages/block-directory/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.27.0 (2024-01-24) + ## 4.26.0 (2024-01-10) ## 4.25.0 (2023-12-13) diff --git a/packages/block-directory/package.json b/packages/block-directory/package.json index 466d1fd783e952..c47c5ef91389b5 100644 --- a/packages/block-directory/package.json +++ b/packages/block-directory/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-directory", - "version": "4.26.0", + "version": "4.27.0", "description": "Extend editor with block directory features to search, download and install blocks.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/block-directory/src/components/downloadable-blocks-panel/index.js b/packages/block-directory/src/components/downloadable-blocks-panel/index.js index 46866003128513..4ceee3ddcb16e6 100644 --- a/packages/block-directory/src/components/downloadable-blocks-panel/index.js +++ b/packages/block-directory/src/components/downloadable-blocks-panel/index.js @@ -3,9 +3,8 @@ */ import { __ } from '@wordpress/i18n'; import { Spinner } from '@wordpress/components'; -import { compose } from '@wordpress/compose'; import { store as coreStore } from '@wordpress/core-data'; -import { withSelect } from '@wordpress/data'; +import { useSelect } from '@wordpress/data'; import { getBlockType } from '@wordpress/blocks'; /** @@ -18,16 +17,71 @@ import { store as blockDirectoryStore } from '../../store'; const EMPTY_ARRAY = []; -function DownloadableBlocksPanel( { - downloadableItems, +const useDownloadableBlocks = ( filterValue ) => + useSelect( + ( select ) => { + const { + getDownloadableBlocks, + isRequestingDownloadableBlocks, + getInstalledBlockTypes, + } = select( blockDirectoryStore ); + + const hasPermission = select( coreStore ).canUser( + 'read', + 'block-directory/search' + ); + + let downloadableBlocks = EMPTY_ARRAY; + if ( hasPermission ) { + downloadableBlocks = getDownloadableBlocks( filterValue ); + + // Filter out blocks that are already installed. + const installedBlockTypes = getInstalledBlockTypes(); + const installableBlocks = downloadableBlocks.filter( + ( { name } ) => { + // Check if the block has just been installed, in which case it + // should still show in the list to avoid suddenly disappearing. + // `installedBlockTypes` only returns blocks stored in state + // immediately after installation, not all installed blocks. + const isJustInstalled = installedBlockTypes.some( + ( blockType ) => blockType.name === name + ); + const isPreviouslyInstalled = getBlockType( name ); + return isJustInstalled || ! isPreviouslyInstalled; + } + ); + + // Keep identity of the `downloadableBlocks` array if nothing was filtered out + if ( installableBlocks.length !== downloadableBlocks.length ) { + downloadableBlocks = installableBlocks; + } + + // Return identical empty array when there are no blocks + if ( downloadableBlocks.length === 0 ) { + downloadableBlocks = EMPTY_ARRAY; + } + } + + return { + hasPermission, + downloadableBlocks, + isLoading: isRequestingDownloadableBlocks( filterValue ), + }; + }, + [ filterValue ] + ); + +export default function DownloadableBlocksPanel( { onSelect, onHover, hasLocalBlocks, - hasPermission, - isLoading, isTyping, + filterValue, } ) { - if ( typeof hasPermission === 'undefined' || isLoading || isTyping ) { + const { hasPermission, downloadableBlocks, isLoading } = + useDownloadableBlocks( filterValue ); + + if ( hasPermission === undefined || isLoading || isTyping ) { return ( <> { hasPermission && ! hasLocalBlocks && ( @@ -55,71 +109,20 @@ function DownloadableBlocksPanel( { return null; } - return !! downloadableItems.length ? ( + if ( downloadableBlocks.length === 0 ) { + return hasLocalBlocks ? null : ; + } + + return ( - ) : ( - ! hasLocalBlocks && ); } - -export default compose( [ - withSelect( ( select, { filterValue } ) => { - const { - getDownloadableBlocks, - isRequestingDownloadableBlocks, - getInstalledBlockTypes, - } = select( blockDirectoryStore ); - - const hasPermission = select( coreStore ).canUser( - 'read', - 'block-directory/search' - ); - - function getInstallableBlocks( term ) { - const downloadableBlocks = getDownloadableBlocks( term ); - const installedBlockTypes = getInstalledBlockTypes(); - // Filter out blocks that are already installed. - const installableBlocks = downloadableBlocks.filter( ( block ) => { - // Check if the block has just been installed, in which case it - // should still show in the list to avoid suddenly disappearing. - // `installedBlockTypes` only returns blocks stored in state - // immediately after installation, not all installed blocks. - const isJustInstalled = !! installedBlockTypes.find( - ( blockType ) => blockType.name === block.name - ); - const isPreviouslyInstalled = getBlockType( block.name ); - return isJustInstalled || ! isPreviouslyInstalled; - } ); - - if ( downloadableBlocks.length === installableBlocks.length ) { - return downloadableBlocks; - } - return installableBlocks; - } - - let downloadableItems = hasPermission - ? getInstallableBlocks( filterValue ) - : []; - - if ( downloadableItems.length === 0 ) { - downloadableItems = EMPTY_ARRAY; - } - - const isLoading = isRequestingDownloadableBlocks( filterValue ); - - return { - downloadableItems, - hasPermission, - isLoading, - }; - } ), -] )( DownloadableBlocksPanel ); diff --git a/packages/block-directory/src/plugins/inserter-menu-downloadable-blocks-panel/index.js b/packages/block-directory/src/plugins/inserter-menu-downloadable-blocks-panel/index.js index badcc407c78cb5..34fc9011b9a1cf 100644 --- a/packages/block-directory/src/plugins/inserter-menu-downloadable-blocks-panel/index.js +++ b/packages/block-directory/src/plugins/inserter-menu-downloadable-blocks-panel/index.js @@ -16,13 +16,7 @@ function InserterMenuDownloadableBlocksPanel() { return ( <__unstableInserterMenuExtension> - { ( { - onSelect, - onHover, - filterValue, - hasItems, - rootClientId, - } ) => { + { ( { onSelect, onHover, filterValue, hasItems } ) => { if ( debouncedFilterValue !== filterValue ) { debouncedSetFilterValue( filterValue ); } @@ -35,7 +29,6 @@ function InserterMenuDownloadableBlocksPanel() { { topLevelLockedBlock: __unstableGetContentLockingParent( _selectedBlockClientId ) || ( getTemplateLock( _selectedBlockClientId ) === 'contentOnly' || - ( _selectedBlockName === 'core/block' && - window.__experimentalPatternPartialSyncing ) + _selectedBlockName === 'core/block' ? _selectedBlockClientId : undefined ), }; @@ -307,6 +306,10 @@ const BlockInspectorSingleBlock = ( { clientId, blockName } ) => { label={ __( 'Background' ) } /> +
diff --git a/packages/block-editor/src/components/block-list/content.scss b/packages/block-editor/src/components/block-list/content.scss index 2b778c0892cfa5..ba1714063fa8fc 100644 --- a/packages/block-editor/src/components/block-list/content.scss +++ b/packages/block-editor/src/components/block-list/content.scss @@ -89,7 +89,7 @@ _::-webkit-full-page-media, _:future, :root .has-multi-selection .block-editor-b .block-editor-block-list__block.is-highlighted, .block-editor-block-list__block.is-highlighted ~ .is-multi-selected, &.is-navigate-mode .block-editor-block-list__block.is-selected, - .block-editor-block-list__block:not([contenteditable]):focus { + .block-editor-block-list__block:not([contenteditable="true"]):focus { outline: none; // We're using a pseudo element to overflow placeholder borders @@ -199,9 +199,6 @@ _::-webkit-full-page-media, _:future, :root .has-multi-selection .block-editor-b width: 100%; .components-notice { - margin-left: 0; - margin-right: 0; - .components-notice__content { font-size: $default-font-size; } diff --git a/packages/block-editor/src/components/block-list/use-in-between-inserter.js b/packages/block-editor/src/components/block-list/use-in-between-inserter.js index bd323ed057d733..044e5b185a2244 100644 --- a/packages/block-editor/src/components/block-list/use-in-between-inserter.js +++ b/packages/block-editor/src/components/block-list/use-in-between-inserter.js @@ -77,8 +77,7 @@ export function useInBetweenInserter() { if ( getTemplateLock( rootClientId ) || getBlockEditingMode( rootClientId ) === 'disabled' || - ( getBlockName( rootClientId ) === 'core/block' && - window.__experimentalPatternPartialSyncing ) + getBlockName( rootClientId ) === 'core/block' ) { return; } diff --git a/packages/block-editor/src/components/block-popover/inbetween.js b/packages/block-editor/src/components/block-popover/inbetween.js index a0175c4d4ae584..afc983b1a4196e 100644 --- a/packages/block-editor/src/components/block-popover/inbetween.js +++ b/packages/block-editor/src/components/block-popover/inbetween.js @@ -33,6 +33,8 @@ function BlockPopoverInbetween( { children, __unstablePopoverSlot, __unstableContentRef, + operation = 'insert', + nearestSide = 'right', ...props } ) { // This is a temporary hack to get the inbetween inserter to recompute properly. @@ -81,7 +83,10 @@ function BlockPopoverInbetween( { return undefined; } - const contextElement = previousElement || nextElement; + const contextElement = + operation === 'group' + ? nextElement || previousElement + : previousElement || nextElement; return { contextElement, @@ -98,7 +103,20 @@ function BlockPopoverInbetween( { let width = 0; let height = 0; - if ( isVertical ) { + if ( operation === 'group' ) { + const targetRect = nextRect || previousRect; + top = targetRect.top; + // No spacing is likely around blocks in this operation. + // So width of the inserter containing rect is set to 0. + width = 0; + height = targetRect.bottom - targetRect.top; + // Popover calculates its distance from mid-block so some + // adjustments are needed to make it appear in the right place. + left = + nearestSide === 'left' + ? targetRect.left - 2 + : targetRect.right - 2; + } else if ( isVertical ) { // vertical top = previousRect ? previousRect.bottom : nextRect.top; width = previousRect ? previousRect.width : nextRect.width; @@ -141,6 +159,8 @@ function BlockPopoverInbetween( { popoverRecomputeCounter, isVertical, isVisible, + operation, + nearestSide, ] ); const popoverScrollRef = usePopoverScroll( __unstableContentRef ); diff --git a/packages/block-editor/src/components/block-removal-warning-modal/index.js b/packages/block-editor/src/components/block-removal-warning-modal/index.js index a6de602bcdda81..846883e48e9cd3 100644 --- a/packages/block-editor/src/components/block-removal-warning-modal/index.js +++ b/packages/block-editor/src/components/block-removal-warning-modal/index.js @@ -50,11 +50,12 @@ export function BlockRemovalWarningModal( { rules } ) {

{ _n( - 'Post or page content will not be displayed if you delete this block.', - 'Post or page content will not be displayed if you delete these blocks.', + 'Deleting this block will stop your post or page content from displaying on this template. It is not recommended.', + 'Deleting these blocks will stop your post or page content from displaying on this template. It is not recommended.', blockNamesForPrompt.length ) }

diff --git a/packages/block-editor/src/components/block-tools/insertion-point.js b/packages/block-editor/src/components/block-tools/insertion-point.js index 1caec5f3aee8e0..19ad39caca336a 100644 --- a/packages/block-editor/src/components/block-tools/insertion-point.js +++ b/packages/block-editor/src/components/block-tools/insertion-point.js @@ -24,6 +24,8 @@ export const InsertionPointOpenRef = createContext(); function InbetweenInsertionPointPopover( { __unstablePopoverSlot, __unstableContentRef, + operation = 'insert', + nearestSide = 'right', } ) { const { selectBlock, hideInsertionPoint } = useDispatch( blockEditorStore ); const openRef = useContext( InsertionPointOpenRef ); @@ -138,9 +140,14 @@ function InbetweenInsertionPointPopover( { return null; } + const orientationClassname = + orientation === 'horizontal' || operation === 'group' + ? 'is-horizontal' + : 'is-vertical'; + const className = classnames( 'block-editor-block-list__insertion-point', - 'is-' + orientation + orientationClassname ); return ( @@ -149,6 +156,8 @@ function InbetweenInsertionPointPopover( { nextClientId={ nextClientId } __unstablePopoverSlot={ __unstablePopoverSlot } __unstableContentRef={ __unstableContentRef } + operation={ operation } + nearestSide={ nearestSide } > ) : ( - + ); } diff --git a/packages/block-editor/src/components/contrast-checker/style.scss b/packages/block-editor/src/components/contrast-checker/style.scss deleted file mode 100644 index b3b08d6230d05d..00000000000000 --- a/packages/block-editor/src/components/contrast-checker/style.scss +++ /dev/null @@ -1,3 +0,0 @@ -.block-editor-contrast-checker > .components-notice { - margin: 0; -} diff --git a/packages/block-editor/src/components/dimensions-tool/aspect-ratio-tool.js b/packages/block-editor/src/components/dimensions-tool/aspect-ratio-tool.js index 988c6b5c286869..5ff35ae0e0c888 100644 --- a/packages/block-editor/src/components/dimensions-tool/aspect-ratio-tool.js +++ b/packages/block-editor/src/components/dimensions-tool/aspect-ratio-tool.js @@ -98,6 +98,7 @@ export default function AspectRatioTool( { onChange = () => {}, options = DEFAULT_ASPECT_RATIO_OPTIONS, defaultValue = DEFAULT_ASPECT_RATIO_OPTIONS[ 0 ].value, + hasValue, isShownByDefault = true, } ) { // Match the CSS default so if the value is used directly in CSS it will look correct in the control. @@ -105,7 +106,9 @@ export default function AspectRatioTool( { return ( displayValue !== defaultValue } + hasValue={ + hasValue ? hasValue : () => displayValue !== defaultValue + } label={ __( 'Aspect ratio' ) } onDeselect={ () => onChange( undefined ) } isShownByDefault={ isShownByDefault } diff --git a/packages/block-editor/src/components/global-styles/dimensions-panel.js b/packages/block-editor/src/components/global-styles/dimensions-panel.js index 47e50aa515e3c6..0d486e29452637 100644 --- a/packages/block-editor/src/components/global-styles/dimensions-panel.js +++ b/packages/block-editor/src/components/global-styles/dimensions-panel.js @@ -27,6 +27,7 @@ import { getValueFromVariable, TOOLSPANEL_DROPDOWNMENU_PROPS } from './utils'; import SpacingSizesControl from '../spacing-sizes-control'; import HeightControl from '../height-control'; import ChildLayoutControl from '../child-layout-control'; +import AspectRatioTool from '../dimensions-tool/aspect-ratio-tool'; import { cleanEmptyObject } from '../../hooks/utils'; import { setImmutably } from '../../utils/object'; @@ -39,6 +40,7 @@ export function useHasDimensionsPanel( settings ) { const hasMargin = useHasMargin( settings ); const hasGap = useHasGap( settings ); const hasMinHeight = useHasMinHeight( settings ); + const hasAspectRatio = useHasAspectRatio( settings ); const hasChildLayout = useHasChildLayout( settings ); return ( @@ -49,6 +51,7 @@ export function useHasDimensionsPanel( settings ) { hasMargin || hasGap || hasMinHeight || + hasAspectRatio || hasChildLayout ) ); } @@ -77,6 +80,10 @@ function useHasMinHeight( settings ) { return settings?.dimensions?.minHeight; } +function useHasAspectRatio( settings ) { + return settings?.dimensions?.aspectRatio; +} + function useHasChildLayout( settings ) { const { type: parentLayoutType = 'default', @@ -192,6 +199,7 @@ const DEFAULT_CONTROLS = { margin: true, blockGap: true, minHeight: true, + aspectRatio: true, childLayout: true, }; @@ -346,8 +354,18 @@ export default function DimensionsPanel( { const showMinHeightControl = useHasMinHeight( settings ); const minHeightValue = decodeValue( inheritedValue?.dimensions?.minHeight ); const setMinHeightValue = ( newValue ) => { + const tempValue = setImmutably( + value, + [ 'dimensions', 'minHeight' ], + newValue + ); + // Apply min-height, while removing any applied aspect ratio. onChange( - setImmutably( value, [ 'dimensions', 'minHeight' ], newValue ) + setImmutably( + tempValue, + [ 'dimensions', 'aspectRatio' ], + undefined + ) ); }; const resetMinHeightValue = () => { @@ -355,6 +373,24 @@ export default function DimensionsPanel( { }; const hasMinHeightValue = () => !! value?.dimensions?.minHeight; + // Aspect Ratio + const showAspectRatioControl = useHasAspectRatio( settings ); + const aspectRatioValue = decodeValue( + inheritedValue?.dimensions?.aspectRatio + ); + const setAspectRatioValue = ( newValue ) => { + const tempValue = setImmutably( + value, + [ 'dimensions', 'aspectRatio' ], + newValue + ); + // Apply aspect-ratio, while removing any applied min-height. + onChange( + setImmutably( tempValue, [ 'dimensions', 'minHeight' ], undefined ) + ); + }; + const hasAspectRatioValue = () => !! value?.dimensions?.aspectRatio; + // Child Layout const showChildLayoutControl = useHasChildLayout( settings ); const childLayout = inheritedValue?.layout; @@ -397,6 +433,7 @@ export default function DimensionsPanel( { dimensions: { ...previousValue?.dimensions, minHeight: undefined, + aspectRatio: undefined, }, }; }, [] ); @@ -617,6 +654,18 @@ export default function DimensionsPanel( { /> ) } + { showAspectRatioControl && ( + + ) } { showChildLayoutControl && ( { - onChange( setImmutably( value, [ 'shadow' ], newValue ) ); + const slug = mergedShadowPresets?.find( + ( { shadow: shadowName } ) => shadowName === newValue + )?.slug; + + onChange( + setImmutably( + value, + [ 'shadow' ], + slug ? `var:preset|shadow|${ slug }` : newValue || undefined + ) + ); }; const hasShadow = () => !! value?.shadow; const resetShadow = () => setShadow( undefined ); diff --git a/packages/block-editor/src/components/global-styles/hooks.js b/packages/block-editor/src/components/global-styles/hooks.js index 2652732807cfd7..7c5cf4cbf3e0fa 100644 --- a/packages/block-editor/src/components/global-styles/hooks.js +++ b/packages/block-editor/src/components/global-styles/hooks.js @@ -49,6 +49,7 @@ const VALID_SETTINGS = [ 'color.palette', 'color.text', 'custom', + 'dimensions.aspectRatio', 'dimensions.minHeight', 'layout.contentSize', 'layout.definitions', @@ -344,12 +345,14 @@ export function useSettingsForBlockElement( } } ); - if ( ! supportedStyles.includes( 'minHeight' ) ) { - updatedSettings.dimensions = { - ...updatedSettings.dimensions, - minHeight: false, - }; - } + [ 'aspectRatio', 'minHeight' ].forEach( ( key ) => { + if ( ! supportedStyles.includes( key ) ) { + updatedSettings.dimensions = { + ...updatedSettings.dimensions, + [ key ]: false, + }; + } + } ); [ 'radius', 'color', 'style', 'width' ].forEach( ( key ) => { if ( diff --git a/packages/block-editor/src/components/global-styles/use-global-styles-output.js b/packages/block-editor/src/components/global-styles/use-global-styles-output.js index 1cd63ef4d03f00..ac51a0ca443db6 100644 --- a/packages/block-editor/src/components/global-styles/use-global-styles-output.js +++ b/packages/block-editor/src/components/global-styles/use-global-styles-output.js @@ -434,6 +434,12 @@ export function getStylesDeclarations( ); } + // For aspect ratio to work, other dimensions rules (and Cover block defaults) must be unset. + // This ensures that a fixed height does not override the aspect ratio. + if ( cssProperty === 'aspect-ratio' ) { + output.push( 'min-height: unset' ); + } + output.push( `${ cssProperty }: ${ ruleValue }` ); } ); diff --git a/packages/block-editor/src/components/index.js b/packages/block-editor/src/components/index.js index 08247d8cdb014a..5263ca3332b250 100644 --- a/packages/block-editor/src/components/index.js +++ b/packages/block-editor/src/components/index.js @@ -152,8 +152,10 @@ export { default as WritingFlow } from './writing-flow'; export { default as useBlockDisplayInformation } from './use-block-display-information'; export { default as __unstableIframe } from './iframe'; export { - RecursionProvider as __experimentalRecursionProvider, - useHasRecursion as __experimentalUseHasRecursion, + RecursionProvider, + DeprecatedExperimentalRecursionProvider as __experimentalRecursionProvider, + useHasRecursion, + DeprecatedExperimentalUseHasRecursion as __experimentalUseHasRecursion, } from './recursion-provider'; export { default as __experimentalBlockPatternsList } from './block-patterns-list'; export { default as __experimentalPublishDateTimePicker } from './publish-date-time-picker'; diff --git a/packages/block-editor/src/components/index.native.js b/packages/block-editor/src/components/index.native.js index 21a9b1114ce5fa..ac4b45af3609ca 100644 --- a/packages/block-editor/src/components/index.native.js +++ b/packages/block-editor/src/components/index.native.js @@ -61,8 +61,10 @@ export { default as PanelColorSettings } from './panel-color-settings'; export { default as __experimentalPanelColorGradientSettings } from './colors-gradients/panel-color-gradient-settings'; export { useSettings, default as useSetting } from './use-settings'; export { - RecursionProvider as __experimentalRecursionProvider, - useHasRecursion as __experimentalUseHasRecursion, + RecursionProvider, + DeprecatedExperimentalRecursionProvider as __experimentalRecursionProvider, + useHasRecursion, + DeprecatedExperimentalUseHasRecursion as __experimentalUseHasRecursion, } from './recursion-provider'; export { default as Warning } from './warning'; export { default as ContrastChecker } from './contrast-checker'; diff --git a/packages/block-editor/src/components/inspector-controls-tabs/styles-tab.js b/packages/block-editor/src/components/inspector-controls-tabs/styles-tab.js index d42b3bffce4ebc..6f24051ea2cfcb 100644 --- a/packages/block-editor/src/components/inspector-controls-tabs/styles-tab.js +++ b/packages/block-editor/src/components/inspector-controls-tabs/styles-tab.js @@ -46,6 +46,7 @@ const StylesTab = ( { blockName, clientId, hasBlockStyles } ) => { label={ __( 'Dimensions' ) } /> + ); diff --git a/packages/block-editor/src/components/inspector-controls-tabs/use-inspector-controls-tabs.js b/packages/block-editor/src/components/inspector-controls-tabs/use-inspector-controls-tabs.js index 2a47ae5267ca4e..ff68be82a829f1 100644 --- a/packages/block-editor/src/components/inspector-controls-tabs/use-inspector-controls-tabs.js +++ b/packages/block-editor/src/components/inspector-controls-tabs/use-inspector-controls-tabs.js @@ -40,6 +40,7 @@ export default function useInspectorControlsTabs( blockName ) { position: positionGroup, styles: stylesGroup, typography: typographyGroup, + effects: effectsGroup, } = InspectorControlsGroups; // List View Tab: If there are any fills for the list group add that tab. @@ -55,6 +56,7 @@ export default function useInspectorControlsTabs( blockName ) { ...( useSlotFills( dimensionsGroup.Slot.__unstableName ) || [] ), ...( useSlotFills( stylesGroup.Slot.__unstableName ) || [] ), ...( useSlotFills( typographyGroup.Slot.__unstableName ) || [] ), + ...( useSlotFills( effectsGroup.Slot.__unstableName ) || [] ), ]; const hasStyleFills = styleFills.length; diff --git a/packages/block-editor/src/components/inspector-controls/groups.js b/packages/block-editor/src/components/inspector-controls/groups.js index b4eada4b6b4be6..9ca1a72b9918a6 100644 --- a/packages/block-editor/src/components/inspector-controls/groups.js +++ b/packages/block-editor/src/components/inspector-controls/groups.js @@ -20,6 +20,7 @@ const InspectorControlsTypography = createSlotFill( ); const InspectorControlsListView = createSlotFill( 'InspectorControlsListView' ); const InspectorControlsStyles = createSlotFill( 'InspectorControlsStyles' ); +const InspectorControlsEffects = createSlotFill( 'InspectorControlsEffects' ); const groups = { default: InspectorControlsDefault, @@ -28,6 +29,7 @@ const groups = { border: InspectorControlsBorder, color: InspectorControlsColor, dimensions: InspectorControlsDimensions, + effects: InspectorControlsEffects, filter: InspectorControlsFilter, list: InspectorControlsListView, position: InspectorControlsPosition, diff --git a/packages/block-editor/src/components/link-control/link-preview.js b/packages/block-editor/src/components/link-control/link-preview.js index 04a67d44789948..17cc851c503a14 100644 --- a/packages/block-editor/src/components/link-control/link-preview.js +++ b/packages/block-editor/src/components/link-control/link-preview.js @@ -10,12 +10,15 @@ import { __ } from '@wordpress/i18n'; import { Button, ExternalLink, - __experimentalText as Text, + __experimentalTruncate as Truncate, Tooltip, } from '@wordpress/components'; +import { useCopyToClipboard } from '@wordpress/compose'; import { filterURLForDisplay, safeDecodeURI } from '@wordpress/url'; -import { Icon, globe, info, linkOff, edit } from '@wordpress/icons'; +import { Icon, globe, info, linkOff, edit, copy } from '@wordpress/icons'; import { __unstableStripHTML as stripHTML } from '@wordpress/dom'; +import { useDispatch } from '@wordpress/data'; +import { store as noticesStore } from '@wordpress/notices'; /** * Internal dependencies @@ -41,7 +44,7 @@ export default function LinkPreview( { const hasRichData = richData && Object.keys( richData ).length; const displayURL = - ( value && filterURLForDisplay( safeDecodeURI( value.url ), 16 ) ) || + ( value && filterURLForDisplay( safeDecodeURI( value.url ), 24 ) ) || ''; // url can be undefined if the href attribute is unset @@ -61,6 +64,14 @@ export default function LinkPreview( { icon = ; } + const { createNotice } = useDispatch( noticesStore ); + const ref = useCopyToClipboard( value.url, () => { + createNotice( 'info', __( 'Copied URL to clipboard.' ), { + isDismissible: true, + type: 'snackbar', + } ); + } ); + return (
{ ! isEmptyURL ? ( <> - + - { displayTitle } + + { displayTitle } + - { value?.url && displayTitle !== displayURL && ( - { displayURL } + + { displayURL } + ) } @@ -119,7 +130,7 @@ export default function LinkPreview( { label={ __( 'Edit' ) } className="block-editor-link-control__search-item-action" onClick={ onEditClick } - iconSize={ 24 } + size="compact" /> { hasUnlinkControl && (
- - { !! ( - ( hasRichData && - ( richData?.image || richData?.description ) ) || - isFetching - ) && ( -
- { ( richData?.image || isFetching ) && ( -
- { richData?.image && ( - - ) } -
- ) } - - { ( richData?.description || isFetching ) && ( -
- { richData?.description && ( - - { richData.description } - - ) } -
- ) } -
- ) } - { additionalControls && additionalControls() } ); diff --git a/packages/block-editor/src/components/link-control/style.scss b/packages/block-editor/src/components/link-control/style.scss index 1a053eabf265ce..7b6470df435437 100644 --- a/packages/block-editor/src/components/link-control/style.scss +++ b/packages/block-editor/src/components/link-control/style.scss @@ -1,5 +1,4 @@ $block-editor-link-control-number-of-actions: 1; -$preview-image-height: 140px; @keyframes loadingpulse { 0% { @@ -180,6 +179,7 @@ $preview-image-height: 140px; flex-direction: row; align-items: flex-start; margin-right: $grid-unit-10; + gap: $grid-unit-10; // Force text to wrap to improve UX when encountering long lines // of text, particular those with no spaces. @@ -188,6 +188,9 @@ $preview-image-height: 140px; overflow-wrap: break-word; .block-editor-link-control__search-item-info { + color: $gray-700; + line-height: 1.1; + font-size: $helptext-font-size; word-break: break-all; } } @@ -206,17 +209,29 @@ $preview-image-height: 140px; word-break: break-all; } + .block-editor-link-control__search-item-details { + display: flex; + flex-direction: column; + justify-content: space-between; + gap: $grid-unit-05; + } + + .block-editor-link-control__search-item-header .block-editor-link-control__search-item-icon { + background-color: $gray-100; + width: $grid-unit-40; + height: $grid-unit-40; + border-radius: $radius-block-ui; + } + .block-editor-link-control__search-item-icon { position: relative; - margin-right: $grid-unit-10; - max-height: 24px; flex-shrink: 0; - width: 24px; display: flex; justify-content: center; + align-items: center; img { - width: 16px; // favicons often have a source of 32px + width: $grid-unit-20; // favicons often have a source of 32px } } @@ -227,10 +242,13 @@ $preview-image-height: 140px; } .block-editor-link-control__search-item-title { - display: block; - font-weight: 500; - position: relative; - line-height: $grid-unit-30; + border-radius: $radius-block-ui; + line-height: 1.1; + + &:focus-visible { + @include block-toolbar-button-style__focus(); + text-decoration: none; + } mark { font-weight: 600; @@ -246,58 +264,6 @@ $preview-image-height: 140px; display: none; // specifically requested to be removed visually as well. } } - - .block-editor-link-control__search-item-description { - padding-top: 12px; - margin: 0; - - &.is-placeholder { - margin-top: 12px; - padding-top: 0; - height: 28px; - display: flex; - flex-direction: column; - justify-content: space-around; - - &::before, - &::after { - display: block; - content: ""; - height: 0.7em; - width: 100%; - background-color: $gray-100; - border-radius: 3px; - } - } - - .components-text { - font-size: 0.9em; - } - } - - .block-editor-link-control__search-item-image { - display: flex; - width: 100%; - background-color: $gray-100; - justify-content: center; - height: $preview-image-height; // limit height - max-height: $preview-image-height; // limit height - overflow: hidden; - border-radius: 2px; - margin-top: 12px; - - &.is-placeholder { - background-color: $gray-100; - border-radius: 3px; - } - - img { - display: block; // remove unwanted space below image - width: 100%; - height: 100%; - object-fit: contain; - } - } } .block-editor-link-control__search-item-top { @@ -307,24 +273,7 @@ $preview-image-height: 140px; align-items: center; } -.block-editor-link-control__search-item-bottom { - transition: opacity 1.5s; - width: 100%; -} - .block-editor-link-control__search-item.is-fetching { - .block-editor-link-control__search-item-description { - &::before, - &::after { - animation: loadingpulse 1s linear infinite; - animation-delay: 0.5s; // avoid animating for fast network responses - } - } - - .block-editor-link-control__search-item-image { - animation: loadingpulse 1s linear infinite; - animation-delay: 0.5s; // avoid animating for fast network responses - } .block-editor-link-control__search-item-icon { svg, diff --git a/packages/block-editor/src/components/link-control/test/index.js b/packages/block-editor/src/components/link-control/test/index.js index 32db57a55d76ed..054f7a3ea87468 100644 --- a/packages/block-editor/src/components/link-control/test/index.js +++ b/packages/block-editor/src/components/link-control/test/index.js @@ -2244,7 +2244,8 @@ describe( 'Rich link previews', () => { const titlePreview = screen.getByText( selectedLink.title ); - expect( titlePreview ).toHaveClass( + // eslint-disable-next-line testing-library/no-node-access + expect( titlePreview.parentElement ).toHaveClass( 'block-editor-link-control__search-item-title' ); } ); diff --git a/packages/block-editor/src/components/list-view/block.js b/packages/block-editor/src/components/list-view/block.js index aab9d444a5948f..6dedabb48f8b85 100644 --- a/packages/block-editor/src/components/list-view/block.js +++ b/packages/block-editor/src/components/list-view/block.js @@ -187,7 +187,7 @@ function ListViewBlock( { selectBlock( undefined, focusClientId, null, null ); } - focusListItem( focusClientId, treeGridElementRef ); + focusListItem( focusClientId, treeGridElementRef?.current ); }, [ selectBlock, treeGridElementRef ] ); diff --git a/packages/block-editor/src/components/list-view/index.js b/packages/block-editor/src/components/list-view/index.js index 5270a7af3a2962..895571755e4fa0 100644 --- a/packages/block-editor/src/components/list-view/index.js +++ b/packages/block-editor/src/components/list-view/index.js @@ -42,6 +42,7 @@ import useListViewExpandSelectedItem from './use-list-view-expand-selected-item' import { store as blockEditorStore } from '../../store'; import { BlockSettingsDropdown } from '../block-settings-menu/block-settings-dropdown'; import { focusListItem } from './utils'; +import useClipboardHandler from './use-clipboard-handler'; const expanded = ( state, action ) => { if ( Array.isArray( action.clientIds ) ) { @@ -137,14 +138,6 @@ function ListViewComponent( const [ expandedState, setExpandedState ] = useReducer( expanded, {} ); - const { ref: dropZoneRef, target: blockDropTarget } = useListViewDropZone( { - dropZoneElement, - expandedState, - setExpandedState, - } ); - const elementRef = useRef(); - const treeGridRef = useMergeRefs( [ elementRef, dropZoneRef, ref ] ); - const [ insertedBlock, setInsertedBlock ] = useState( null ); const { setSelectedTreeId } = useListViewExpandSelectedItem( { @@ -166,11 +159,31 @@ function ListViewComponent( }, [ setSelectedTreeId, updateBlockSelection, onSelect, getBlock ] ); + + const { ref: dropZoneRef, target: blockDropTarget } = useListViewDropZone( { + dropZoneElement, + expandedState, + setExpandedState, + } ); + const elementRef = useRef(); + + // Allow handling of copy, cut, and paste events. + const clipBoardRef = useClipboardHandler( { + selectBlock: selectEditorBlock, + } ); + + const treeGridRef = useMergeRefs( [ + clipBoardRef, + elementRef, + dropZoneRef, + ref, + ] ); + useEffect( () => { // If a blocks are already selected when the list view is initially // mounted, shift focus to the first selected block. if ( selectedClientIds?.length ) { - focusListItem( selectedClientIds[ 0 ], elementRef ); + focusListItem( selectedClientIds[ 0 ], elementRef?.current ); } // Disable reason: Only focus on the selected item when the list view is mounted. // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/packages/block-editor/src/components/list-view/use-clipboard-handler.js b/packages/block-editor/src/components/list-view/use-clipboard-handler.js new file mode 100644 index 00000000000000..cd25c71e9bf7c4 --- /dev/null +++ b/packages/block-editor/src/components/list-view/use-clipboard-handler.js @@ -0,0 +1,199 @@ +/** + * WordPress dependencies + */ +import { useDispatch, useSelect } from '@wordpress/data'; +import { useRefEffect } from '@wordpress/compose'; + +/** + * Internal dependencies + */ +import { store as blockEditorStore } from '../../store'; +import { useNotifyCopy } from '../../utils/use-notify-copy'; +import { focusListItem } from './utils'; +import { getPasteBlocks, setClipboardBlocks } from '../writing-flow/utils'; + +// This hook borrows from useClipboardHandler in ../writing-flow/use-clipboard-handler.js +// and adds behaviour for the list view, while skipping partial selection. +export default function useClipboardHandler( { selectBlock } ) { + const { + getBlockOrder, + getBlockRootClientId, + getBlocksByClientId, + getPreviousBlockClientId, + getSelectedBlockClientIds, + getSettings, + canInsertBlockType, + canRemoveBlocks, + } = useSelect( blockEditorStore ); + const { flashBlock, removeBlocks, replaceBlocks, insertBlocks } = + useDispatch( blockEditorStore ); + const notifyCopy = useNotifyCopy(); + + return useRefEffect( ( node ) => { + function updateFocusAndSelection( focusClientId, shouldSelectBlock ) { + if ( shouldSelectBlock ) { + selectBlock( undefined, focusClientId, null, null ); + } + + focusListItem( focusClientId, node ); + } + + // Determine which blocks to update: + // If the current (focused) block is part of the block selection, use the whole selection. + // If the focused block is not part of the block selection, only update the focused block. + function getBlocksToUpdate( clientId ) { + const selectedBlockClientIds = getSelectedBlockClientIds(); + const isUpdatingSelectedBlocks = + selectedBlockClientIds.includes( clientId ); + const firstBlockClientId = isUpdatingSelectedBlocks + ? selectedBlockClientIds[ 0 ] + : clientId; + const firstBlockRootClientId = + getBlockRootClientId( firstBlockClientId ); + + const blocksToUpdate = isUpdatingSelectedBlocks + ? selectedBlockClientIds + : [ clientId ]; + + return { + blocksToUpdate, + firstBlockClientId, + firstBlockRootClientId, + originallySelectedBlockClientIds: selectedBlockClientIds, + }; + } + + function handler( event ) { + if ( event.defaultPrevented ) { + // This was possibly already handled in rich-text/use-paste-handler.js. + return; + } + + // Only handle events that occur within the list view. + if ( ! node.contains( event.target.ownerDocument.activeElement ) ) { + return; + } + + // Retrieve the block clientId associated with the focused list view row. + // This enables applying copy / cut / paste behavior to the focused block, + // rather than just the blocks that are currently selected. + const listViewRow = + event.target.ownerDocument.activeElement?.closest( + '[role=row]' + ); + const clientId = listViewRow?.dataset?.block; + if ( ! clientId ) { + return; + } + + const { + blocksToUpdate: selectedBlockClientIds, + firstBlockClientId, + firstBlockRootClientId, + originallySelectedBlockClientIds, + } = getBlocksToUpdate( clientId ); + + if ( selectedBlockClientIds.length === 0 ) { + return; + } + + event.preventDefault(); + + if ( event.type === 'copy' || event.type === 'cut' ) { + if ( selectedBlockClientIds.length === 1 ) { + flashBlock( selectedBlockClientIds[ 0 ] ); + } + + notifyCopy( event.type, selectedBlockClientIds ); + const blocks = getBlocksByClientId( selectedBlockClientIds ); + setClipboardBlocks( event, blocks ); + } + + if ( event.type === 'cut' ) { + // Don't update the selection if the blocks cannot be deleted. + if ( + ! canRemoveBlocks( + selectedBlockClientIds, + firstBlockRootClientId + ) + ) { + return; + } + + let blockToFocus = + getPreviousBlockClientId( firstBlockClientId ) ?? + // If the previous block is not found (when the first block is deleted), + // fallback to focus the parent block. + firstBlockRootClientId; + + // Remove blocks, but don't update selection, and it will be handled below. + removeBlocks( selectedBlockClientIds, false ); + + // Update the selection if the original selection has been removed. + const shouldUpdateSelection = + originallySelectedBlockClientIds.length > 0 && + getSelectedBlockClientIds().length === 0; + + // If there's no previous block nor parent block, focus the first block. + if ( ! blockToFocus ) { + blockToFocus = getBlockOrder()[ 0 ]; + } + + updateFocusAndSelection( blockToFocus, shouldUpdateSelection ); + } else if ( event.type === 'paste' ) { + const { + __experimentalCanUserUseUnfilteredHTML: + canUserUseUnfilteredHTML, + } = getSettings(); + const blocks = getPasteBlocks( + event, + canUserUseUnfilteredHTML + ); + + if ( selectedBlockClientIds.length === 1 ) { + const [ selectedBlockClientId ] = selectedBlockClientIds; + + // If a single block is focused, and the blocks to be posted can + // be inserted within the block, then append the pasted blocks + // within the focused block. For example, if you have copied a paragraph + // block and paste it within a single Group block, this will append + // the paragraph block within the Group block. + if ( + blocks.every( ( block ) => + canInsertBlockType( + block.name, + selectedBlockClientId + ) + ) + ) { + insertBlocks( + blocks, + undefined, + selectedBlockClientId + ); + updateFocusAndSelection( blocks[ 0 ]?.clientId, false ); + return; + } + } + + replaceBlocks( + selectedBlockClientIds, + blocks, + blocks.length - 1, + -1 + ); + updateFocusAndSelection( blocks[ 0 ]?.clientId, false ); + } + } + + node.ownerDocument.addEventListener( 'copy', handler ); + node.ownerDocument.addEventListener( 'cut', handler ); + node.ownerDocument.addEventListener( 'paste', handler ); + + return () => { + node.ownerDocument.removeEventListener( 'copy', handler ); + node.ownerDocument.removeEventListener( 'cut', handler ); + node.ownerDocument.removeEventListener( 'paste', handler ); + }; + }, [] ); +} diff --git a/packages/block-editor/src/components/list-view/utils.js b/packages/block-editor/src/components/list-view/utils.js index ed7a321dea0c86..c91376b0472116 100644 --- a/packages/block-editor/src/components/list-view/utils.js +++ b/packages/block-editor/src/components/list-view/utils.js @@ -63,12 +63,12 @@ export function getCommonDepthClientIds( * * @typedef {import('@wordpress/element').RefObject} RefObject * - * @param {string} focusClientId The client ID of the block to focus. - * @param {RefObject} treeGridElementRef The container element to search within. + * @param {string} focusClientId The client ID of the block to focus. + * @param {?HTMLElement} treeGridElement The container element to search within. */ -export function focusListItem( focusClientId, treeGridElementRef ) { +export function focusListItem( focusClientId, treeGridElement ) { const getFocusElement = () => { - const row = treeGridElementRef.current?.querySelector( + const row = treeGridElement?.querySelector( `[role=row][data-block="${ focusClientId }"]` ); if ( ! row ) return null; diff --git a/packages/block-editor/src/components/recursion-provider/README.md b/packages/block-editor/src/components/recursion-provider/README.md index 4538fd6a7d3507..37af15d75c1914 100644 --- a/packages/block-editor/src/components/recursion-provider/README.md +++ b/packages/block-editor/src/components/recursion-provider/README.md @@ -11,8 +11,8 @@ To help with detecting infinite loops on the client, the `RecursionProvider` com * WordPress dependencies */ import { - __experimentalRecursionProvider as RecursionProvider, - __experimentalUseHasRecursion as useHasRecursion, + RecursionProvider, + useHasRecursion, useBlockProps, Warning, } from '@wordpress/block-editor'; diff --git a/packages/block-editor/src/components/recursion-provider/index.js b/packages/block-editor/src/components/recursion-provider/index.js index 2c38087a8731d4..4f462cb33ef2a3 100644 --- a/packages/block-editor/src/components/recursion-provider/index.js +++ b/packages/block-editor/src/components/recursion-provider/index.js @@ -2,6 +2,7 @@ * WordPress dependencies */ import { createContext, useContext, useMemo } from '@wordpress/element'; +import deprecated from '@wordpress/deprecated'; /** * Internal dependencies @@ -82,3 +83,19 @@ export function useHasRecursion( uniqueId, blockName = '' ) { blockName = blockName || name; return Boolean( previouslyRenderedBlocks[ blockName ]?.has( uniqueId ) ); } + +export const DeprecatedExperimentalRecursionProvider = ( props ) => { + deprecated( 'wp.blockEditor.__experimentalRecursionProvider', { + since: '6.5', + alternative: 'wp.blockEditor.RecursionProvider', + } ); + return ; +}; + +export const DeprecatedExperimentalUseHasRecursion = ( props ) => { + deprecated( 'wp.blockEditor.__experimentalUseHasRecursion', { + since: '6.5', + alternative: 'wp.blockEditor.useHasRecursion', + } ); + return useHasRecursion( ...props ); +}; diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index 51a70677a5edc7..69b04fe4c4904e 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -19,6 +19,7 @@ import { removeFormat, } from '@wordpress/rich-text'; import { Popover } from '@wordpress/components'; +import { getBlockType } from '@wordpress/blocks'; /** * Internal dependencies @@ -44,6 +45,7 @@ import FormatEdit from './format-edit'; import { getAllowedFormats } from './utils'; import { Content } from './content'; import { withDeprecations } from './with-deprecations'; +import { unlock } from '../../lock-unlock'; export const keyboardShortcutContext = createContext(); export const inputEventContext = createContext(); @@ -113,7 +115,11 @@ export function RichTextWrapper( props = removeNativeProps( props ); const anchorRef = useRef(); - const { clientId, isSelected: isBlockSelected } = useBlockEditContext(); + const { + clientId, + isSelected: isBlockSelected, + name: blockName, + } = useBlockEditContext(); const selector = ( select ) => { // Avoid subscribing to the block editor store if the block is not // selected. @@ -121,10 +127,12 @@ export function RichTextWrapper( return { isSelected: false }; } - const { getSelectionStart, getSelectionEnd } = + const { getSelectionStart, getSelectionEnd, getBlockAttributes } = select( blockEditorStore ); const selectionStart = getSelectionStart(); const selectionEnd = getSelectionEnd(); + const blockBindings = + getBlockAttributes( clientId )?.metadata?.bindings; let isSelected; @@ -137,18 +145,44 @@ export function RichTextWrapper( isSelected = selectionStart.clientId === clientId; } + // Disable Rich Text editing if block bindings specify that. + let shouldDisableEditing = false; + if ( blockBindings ) { + const blockTypeAttributes = getBlockType( blockName ).attributes; + const { getBlockBindingsSource } = unlock( + select( blockEditorStore ) + ); + for ( const [ attribute, args ] of Object.entries( + blockBindings + ) ) { + // If any of the attributes with source "rich-text" is part of the bindings, + // has a source with `lockAttributesEditing`, disable it. + if ( + blockTypeAttributes?.[ attribute ]?.source === + 'rich-text' && + getBlockBindingsSource( args.source.name ) + ?.lockAttributesEditing + ) { + shouldDisableEditing = true; + break; + } + } + } + return { selectionStart: isSelected ? selectionStart.offset : undefined, selectionEnd: isSelected ? selectionEnd.offset : undefined, isSelected, + shouldDisableEditing, }; }; - const { selectionStart, selectionEnd, isSelected } = useSelect( selector, [ - clientId, - identifier, - originalIsSelected, - isBlockSelected, - ] ); + const { selectionStart, selectionEnd, isSelected, shouldDisableEditing } = + useSelect( selector, [ + clientId, + identifier, + originalIsSelected, + isBlockSelected, + ] ); const { getSelectionStart, getSelectionEnd, getBlockRootClientId } = useSelect( blockEditorStore ); const { selectionChange } = useDispatch( blockEditorStore ); @@ -376,7 +410,7 @@ export function RichTextWrapper( useFirefoxCompat(), anchorRef, ] ) } - contentEditable={ true } + contentEditable={ ! shouldDisableEditing } suppressContentEditableWarning={ true } className={ classnames( 'block-editor-rich-text__editable', @@ -389,7 +423,11 @@ export function RichTextWrapper( // select blocks when Shift Clicking into an element with // tabIndex because Safari will focus the element. However, // Safari will correctly ignore nested contentEditable elements. - tabIndex={ props.tabIndex === 0 ? null : props.tabIndex } + tabIndex={ + props.tabIndex === 0 && ! shouldDisableEditing + ? null + : props.tabIndex + } data-wp-block-attribute-key={ identifier } /> diff --git a/packages/block-editor/src/components/use-block-drop-zone/index.js b/packages/block-editor/src/components/use-block-drop-zone/index.js index 20bec8af76dff8..b1573133dcd056 100644 --- a/packages/block-editor/src/components/use-block-drop-zone/index.js +++ b/packages/block-editor/src/components/use-block-drop-zone/index.js @@ -20,6 +20,7 @@ import useOnBlockDrop from '../use-on-block-drop'; import { getDistanceToNearestEdge, isPointContainedByRect, + isPointWithinTopAndBottomBoundariesOfRect, } from '../../utils/math'; import { store as blockEditorStore } from '../../store'; @@ -72,6 +73,8 @@ export function getDropTargetPosition( let nearestIndex = 0; let insertPosition = 'before'; let minDistance = Infinity; + let targetBlockIndex = null; + let nearestSide = 'right'; const { dropZoneElement, @@ -136,7 +139,12 @@ export function getDropTargetPosition( } blocksData.forEach( - ( { isUnmodifiedDefaultBlock, getBoundingClientRect, blockIndex } ) => { + ( { + isUnmodifiedDefaultBlock, + getBoundingClientRect, + blockIndex, + blockOrientation, + } ) => { const rect = getBoundingClientRect(); let [ distance, edge ] = getDistanceToNearestEdge( @@ -144,12 +152,35 @@ export function getDropTargetPosition( rect, allowedEdges ); + // If the the point is close to a side, prioritize that side. + const [ sideDistance, sideEdge ] = getDistanceToNearestEdge( + position, + rect, + [ 'left', 'right' ] + ); + + const isPointInsideRect = isPointContainedByRect( position, rect ); + // Prioritize the element if the point is inside of an unmodified default block. - if ( - isUnmodifiedDefaultBlock && - isPointContainedByRect( position, rect ) - ) { + if ( isUnmodifiedDefaultBlock && isPointInsideRect ) { distance = 0; + } else if ( + orientation === 'vertical' && + blockOrientation !== 'horizontal' && + ( ( isPointInsideRect && sideDistance < THRESHOLD_DISTANCE ) || + ( ! isPointInsideRect && + isPointWithinTopAndBottomBoundariesOfRect( + position, + rect + ) ) ) + ) { + /** + * This condition should only apply when the layout is vertical (otherwise there's + * no need to create a Row) and dropzones should only activate when the block is + * either within and close to the sides of the target block or on its outer sides. + */ + targetBlockIndex = blockIndex; + nearestSide = sideEdge; } if ( distance < minDistance ) { @@ -175,6 +206,10 @@ export function getDropTargetPosition( const isAdjacentBlockUnmodifiedDefaultBlock = !! blocksData[ adjacentIndex ]?.isUnmodifiedDefaultBlock; + // If the target index is set then group with the block at that index. + if ( targetBlockIndex !== null ) { + return [ targetBlockIndex, 'group', nearestSide ]; + } // If both blocks are not unmodified default blocks then just insert between them. if ( ! isNearestBlockUnmodifiedDefaultBlock && @@ -284,6 +319,7 @@ export default function useBlockDropZone( { dropTarget.index, { operation: dropTarget.operation, + nearestSide: dropTarget.nearestSide, } ); const throttled = useThrottle( @@ -333,28 +369,32 @@ export default function useBlockDropZone( { .getElementById( `block-${ clientId }` ) .getBoundingClientRect(), blockIndex: getBlockIndex( clientId ), + blockOrientation: + getBlockListSettings( clientId )?.orientation, }; } ); - const [ targetIndex, operation ] = getDropTargetPosition( - blocksData, - { x: event.clientX, y: event.clientY }, - getBlockListSettings( targetRootClientId )?.orientation, - { - dropZoneElement, - parentBlockClientId, - parentBlockOrientation: parentBlockClientId - ? getBlockListSettings( parentBlockClientId ) - ?.orientation - : undefined, - rootBlockIndex: getBlockIndex( targetRootClientId ), - } - ); + const [ targetIndex, operation, nearestSide ] = + getDropTargetPosition( + blocksData, + { x: event.clientX, y: event.clientY }, + getBlockListSettings( targetRootClientId )?.orientation, + { + dropZoneElement, + parentBlockClientId, + parentBlockOrientation: parentBlockClientId + ? getBlockListSettings( parentBlockClientId ) + ?.orientation + : undefined, + rootBlockIndex: getBlockIndex( targetRootClientId ), + } + ); registry.batch( () => { setDropTarget( { index: targetIndex, operation, + nearestSide, } ); const insertionPointClientId = [ @@ -366,6 +406,7 @@ export default function useBlockDropZone( { showInsertionPoint( insertionPointClientId, targetIndex, { operation, + nearestSide, } ); } ); }, diff --git a/packages/block-editor/src/components/use-block-drop-zone/test/index.js b/packages/block-editor/src/components/use-block-drop-zone/test/index.js index f5560c1cfdf13a..e6614b3fafc133 100644 --- a/packages/block-editor/src/components/use-block-drop-zone/test/index.js +++ b/packages/block-editor/src/components/use-block-drop-zone/test/index.js @@ -22,13 +22,6 @@ const elementData = [ bottom: 900, right: 400, }, - // Fourth block wraps to the next row/column. - { - top: 0, - left: 400, - bottom: 300, - right: 800, - }, ]; const mapElements = @@ -73,7 +66,7 @@ describe( 'getDropTargetPosition', () => { const orientation = 'vertical'; it( 'returns `0` when the position is nearest to the start of the first block', () => { - const position = { x: 0, y: 0 }; + const position = { x: 32, y: 0 }; const result = getDropTargetPosition( verticalBlocksData, @@ -85,7 +78,7 @@ describe( 'getDropTargetPosition', () => { } ); it( 'returns `1` when the position is nearest to the end of the first block', () => { - const position = { x: 0, y: 190 }; + const position = { x: 32, y: 190 }; const result = getDropTargetPosition( verticalBlocksData, @@ -97,7 +90,7 @@ describe( 'getDropTargetPosition', () => { } ); it( 'returns `1` when the position is nearest to the start of the second block', () => { - const position = { x: 0, y: 210 }; + const position = { x: 32, y: 210 }; const result = getDropTargetPosition( verticalBlocksData, @@ -109,7 +102,7 @@ describe( 'getDropTargetPosition', () => { } ); it( 'returns `2` when the position is nearest to the end of the second block', () => { - const position = { x: 0, y: 450 }; + const position = { x: 32, y: 450 }; const result = getDropTargetPosition( verticalBlocksData, @@ -121,7 +114,7 @@ describe( 'getDropTargetPosition', () => { } ); it( 'returns `2` when the position is nearest to the start of the third block', () => { - const position = { x: 0, y: 510 }; + const position = { x: 32, y: 510 }; const result = getDropTargetPosition( verticalBlocksData, @@ -133,7 +126,7 @@ describe( 'getDropTargetPosition', () => { } ); it( 'returns `3` when the position is nearest to the end of the third block', () => { - const position = { x: 0, y: 880 }; + const position = { x: 32, y: 880 }; const result = getDropTargetPosition( verticalBlocksData, @@ -145,7 +138,7 @@ describe( 'getDropTargetPosition', () => { } ); it( 'returns `3` when the position is past the end of the third block', () => { - const position = { x: 0, y: 920 }; + const position = { x: 32, y: 920 }; const result = getDropTargetPosition( verticalBlocksData, @@ -155,9 +148,8 @@ describe( 'getDropTargetPosition', () => { expect( result ).toEqual( [ 3, 'insert' ] ); } ); - - it( 'returns `4` when the position is nearest to the start of the fourth block', () => { - const position = { x: 401, y: 0 }; + it( 'returns group with index 0 when position is close to the right of the first block', () => { + const position = { x: 372, y: 0 }; const result = getDropTargetPosition( verticalBlocksData, @@ -165,11 +157,10 @@ describe( 'getDropTargetPosition', () => { orientation ); - expect( result ).toEqual( [ 3, 'insert' ] ); + expect( result ).toEqual( [ 0, 'group', 'right' ] ); } ); - - it( 'returns `5` when the position is nearest to the end of the fourth block', () => { - const position = { x: 401, y: 300 }; + it( 'returns group with index 1 when position is close to the left of the second block', () => { + const position = { x: 12, y: 212 }; const result = getDropTargetPosition( verticalBlocksData, @@ -177,7 +168,7 @@ describe( 'getDropTargetPosition', () => { orientation ); - expect( result ).toEqual( [ 4, 'insert' ] ); + expect( result ).toEqual( [ 1, 'group', 'left' ] ); } ); } ); @@ -267,30 +258,6 @@ describe( 'getDropTargetPosition', () => { expect( result ).toEqual( [ 3, 'insert' ] ); } ); - - it( 'returns `3` when the position is nearest to the start of the last block', () => { - const position = { x: 0, y: 401 }; - - const result = getDropTargetPosition( - horizontalBlocksData, - position, - orientation - ); - - expect( result ).toEqual( [ 3, 'insert' ] ); - } ); - - it( 'returns `4` when the position is nearest to the end of the last block', () => { - const position = { x: 300, y: 401 }; - - const result = getDropTargetPosition( - horizontalBlocksData, - position, - orientation - ); - - expect( result ).toEqual( [ 4, 'insert' ] ); - } ); } ); describe( 'Unmodified default blocks', () => { @@ -316,14 +283,18 @@ describe( 'getDropTargetPosition', () => { // Dropping above the first block. expect( - getDropTargetPosition( blocksData, { x: 0, y: 0 }, orientation ) + getDropTargetPosition( + blocksData, + { x: 32, y: 0 }, + orientation + ) ).toEqual( [ 0, 'replace' ] ); // Dropping on the top half of the first block. expect( getDropTargetPosition( blocksData, - { x: 0, y: 20 }, + { x: 32, y: 20 }, orientation ) ).toEqual( [ 0, 'replace' ] ); @@ -332,7 +303,7 @@ describe( 'getDropTargetPosition', () => { expect( getDropTargetPosition( blocksData, - { x: 0, y: 200 }, + { x: 32, y: 200 }, orientation ) ).toEqual( [ 0, 'replace' ] ); @@ -341,7 +312,7 @@ describe( 'getDropTargetPosition', () => { expect( getDropTargetPosition( blocksData, - { x: 0, y: 211 }, + { x: 32, y: 211 }, orientation ) ).toEqual( [ 0, 'replace' ] ); @@ -350,7 +321,7 @@ describe( 'getDropTargetPosition', () => { expect( getDropTargetPosition( blocksData, - { x: 0, y: 219 }, + { x: 32, y: 219 }, orientation ) ).toEqual( [ 0, 'replace' ] ); @@ -359,7 +330,7 @@ describe( 'getDropTargetPosition', () => { expect( getDropTargetPosition( blocksData, - { x: 0, y: 230 }, + { x: 32, y: 230 }, orientation ) ).toEqual( [ 0, 'replace' ] ); @@ -368,7 +339,7 @@ describe( 'getDropTargetPosition', () => { expect( getDropTargetPosition( blocksData, - { x: 0, y: 410 }, + { x: 32, y: 410 }, orientation ) ).toEqual( [ 2, 'insert' ] ); @@ -377,7 +348,7 @@ describe( 'getDropTargetPosition', () => { expect( getDropTargetPosition( blocksData, - { x: 0, y: 421 }, + { x: 32, y: 421 }, orientation ) ).toEqual( [ 2, 'insert' ] ); @@ -410,7 +381,7 @@ describe( 'getDropTargetPosition', () => { expect( getDropTargetPosition( blocksData, - { x: 0, y: 20 }, + { x: 32, y: 20 }, orientation ) ).toEqual( [ 0, 'insert' ] ); @@ -419,7 +390,7 @@ describe( 'getDropTargetPosition', () => { expect( getDropTargetPosition( blocksData, - { x: 0, y: 200 }, + { x: 32, y: 200 }, orientation ) ).toEqual( [ 1, 'replace' ] ); @@ -428,7 +399,7 @@ describe( 'getDropTargetPosition', () => { expect( getDropTargetPosition( blocksData, - { x: 0, y: 211 }, + { x: 32, y: 211 }, orientation ) ).toEqual( [ 1, 'replace' ] ); @@ -437,7 +408,7 @@ describe( 'getDropTargetPosition', () => { expect( getDropTargetPosition( blocksData, - { x: 0, y: 219 }, + { x: 32, y: 219 }, orientation ) ).toEqual( [ 1, 'replace' ] ); @@ -446,7 +417,7 @@ describe( 'getDropTargetPosition', () => { expect( getDropTargetPosition( blocksData, - { x: 0, y: 230 }, + { x: 32, y: 230 }, orientation ) ).toEqual( [ 1, 'replace' ] ); @@ -455,7 +426,7 @@ describe( 'getDropTargetPosition', () => { expect( getDropTargetPosition( blocksData, - { x: 0, y: 410 }, + { x: 32, y: 410 }, orientation ) ).toEqual( [ 1, 'replace' ] ); @@ -464,7 +435,7 @@ describe( 'getDropTargetPosition', () => { expect( getDropTargetPosition( blocksData, - { x: 0, y: 421 }, + { x: 32, y: 421 }, orientation ) ).toEqual( [ 1, 'replace' ] ); diff --git a/packages/block-editor/src/components/use-on-block-drop/index.js b/packages/block-editor/src/components/use-on-block-drop/index.js index 701cc9f4f8451f..80e83c01b4b9ae 100644 --- a/packages/block-editor/src/components/use-on-block-drop/index.js +++ b/packages/block-editor/src/components/use-on-block-drop/index.js @@ -4,9 +4,11 @@ import { useCallback } from '@wordpress/element'; import { cloneBlock, + createBlock, findTransform, getBlockTransforms, pasteHandler, + store as blocksStore, } from '@wordpress/blocks'; import { useDispatch, useSelect, useRegistry } from '@wordpress/data'; import { getFilesFromDataTransfer } from '@wordpress/dom'; @@ -61,6 +63,8 @@ export function parseDropEvent( event ) { * @param {Function} moveBlocks A function that moves blocks. * @param {Function} insertOrReplaceBlocks A function that inserts or replaces blocks. * @param {Function} clearSelectedBlock A function that clears block selection. + * @param {string} operation The type of operation to perform on drop. Could be `insert` or `replace` or `group`. + * @param {Function} getBlock A function that returns a block given its client id. * @return {Function} The event handler for a block drop event. */ export function onBlockDrop( @@ -70,7 +74,9 @@ export function onBlockDrop( getClientIdsOfDescendants, moveBlocks, insertOrReplaceBlocks, - clearSelectedBlock + clearSelectedBlock, + operation, + getBlock ) { return ( event ) => { const { @@ -113,6 +119,21 @@ export function onBlockDrop( return; } + // If the user is dropping a block over another block, replace both blocks + // with a group block containing them + if ( operation === 'group' ) { + const blocksToInsert = sourceClientIds.map( ( clientId ) => + getBlock( clientId ) + ); + insertOrReplaceBlocks( + blocksToInsert, + true, + null, + sourceClientIds + ); + return; + } + const isAtSameLevel = sourceRootClientId === targetRootClientId; const draggedBlockCount = sourceClientIds.length; @@ -202,7 +223,7 @@ export default function useOnBlockDrop( targetBlockIndex, options = {} ) { - const { operation = 'insert' } = options; + const { operation = 'insert', nearestSide = 'right' } = options; const { canInsertBlockType, getBlockIndex, @@ -210,7 +231,10 @@ export default function useOnBlockDrop( getBlockOrder, getBlocksByClientId, getSettings, + getBlock, + isGroupable, } = useSelect( blockEditorStore ); + const { getBlockType, getGroupingBlockName } = useSelect( blocksStore ); const { insertBlocks, moveBlocksToPosition, @@ -222,12 +246,63 @@ export default function useOnBlockDrop( const registry = useRegistry(); const insertOrReplaceBlocks = useCallback( - ( blocks, updateSelection = true, initialPosition = 0 ) => { + ( + blocks, + updateSelection = true, + initialPosition = 0, + clientIdsToReplace = [] + ) => { + const clientIds = getBlockOrder( targetRootClientId ); + const clientId = clientIds[ targetBlockIndex ]; + const blocksClientIds = blocks.map( ( block ) => block.clientId ); + const areGroupableBlocks = isGroupable( [ + ...blocksClientIds, + clientId, + ] ); if ( operation === 'replace' ) { - const clientIds = getBlockOrder( targetRootClientId ); - const clientId = clientIds[ targetBlockIndex ]; - replaceBlocks( clientId, blocks, undefined, initialPosition ); + } else if ( operation === 'group' && areGroupableBlocks ) { + const targetBlock = getBlock( clientId ); + if ( nearestSide === 'left' ) { + blocks.push( targetBlock ); + } else { + blocks.unshift( targetBlock ); + } + + const groupInnerBlocks = blocks.map( ( block ) => { + return createBlock( + block.name, + block.attributes, + block.innerBlocks + ); + } ); + + const areAllImages = blocks.every( ( block ) => { + return block.name === 'core/image'; + } ); + + const galleryBlock = !! getBlockType( 'core/gallery' ); + + const wrappedBlocks = createBlock( + areAllImages && galleryBlock + ? 'core/gallery' + : getGroupingBlockName(), + { + layout: { + type: 'flex', + flexWrap: areAllImages ? null : 'nowrap', + }, + }, + groupInnerBlocks + ); + // Need to make sure both the target block and the block being dragged are replaced + // otherwise the dragged block will be duplicated. + replaceBlocks( + [ clientId, ...clientIdsToReplace ], + wrappedBlocks, + undefined, + initialPosition + ); } else { insertBlocks( blocks, @@ -239,12 +314,16 @@ export default function useOnBlockDrop( } }, [ - operation, getBlockOrder, - insertBlocks, - replaceBlocks, - targetBlockIndex, targetRootClientId, + targetBlockIndex, + operation, + replaceBlocks, + getBlock, + nearestSide, + getBlockType, + getGroupingBlockName, + insertBlocks, ] ); @@ -297,7 +376,9 @@ export default function useOnBlockDrop( getClientIdsOfDescendants, moveBlocks, insertOrReplaceBlocks, - clearSelectedBlock + clearSelectedBlock, + operation, + getBlock ); const _onFilesDrop = onFilesDrop( targetRootClientId, diff --git a/packages/block-editor/src/components/writing-flow/use-clipboard-handler.js b/packages/block-editor/src/components/writing-flow/use-clipboard-handler.js index 5b78d2f8656b61..8528655c1dcc9e 100644 --- a/packages/block-editor/src/components/writing-flow/use-clipboard-handler.js +++ b/packages/block-editor/src/components/writing-flow/use-clipboard-handler.js @@ -1,17 +1,9 @@ /** * WordPress dependencies */ -import { - serialize, - pasteHandler, - createBlock, - findTransform, - getBlockTransforms, -} from '@wordpress/blocks'; import { documentHasSelection, documentHasUncollapsedSelection, - __unstableStripHTML as stripHTML, } from '@wordpress/dom'; import { useDispatch, useSelect } from '@wordpress/data'; import { useRefEffect } from '@wordpress/compose'; @@ -19,9 +11,9 @@ import { useRefEffect } from '@wordpress/compose'; /** * Internal dependencies */ -import { getPasteEventData } from '../../utils/pasting'; import { store as blockEditorStore } from '../../store'; import { useNotifyCopy } from '../../utils/use-notify-copy'; +import { getPasteBlocks, setClipboardBlocks } from './utils'; export default function useClipboardHandler() { const { @@ -112,29 +104,7 @@ export default function useClipboardHandler() { blocks = [ head, ...inBetweenBlocks, tail ]; } - const wrapperBlockName = event.clipboardData.getData( - '__unstableWrapperBlockName' - ); - - if ( wrapperBlockName ) { - blocks = createBlock( - wrapperBlockName, - JSON.parse( - event.clipboardData.getData( - '__unstableWrapperBlockAttributes' - ) - ), - blocks - ); - } - - const serialized = serialize( blocks ); - - event.clipboardData.setData( - 'text/plain', - toPlainText( serialized ) - ); - event.clipboardData.setData( 'text/html', serialized ); + setClipboardBlocks( event, blocks ); } } @@ -153,35 +123,10 @@ export default function useClipboardHandler() { __experimentalCanUserUseUnfilteredHTML: canUserUseUnfilteredHTML, } = getSettings(); - const { plainText, html, files } = getPasteEventData( event ); - let blocks = []; - - if ( files.length ) { - const fromTransforms = getBlockTransforms( 'from' ); - blocks = files - .reduce( ( accumulator, file ) => { - const transformation = findTransform( - fromTransforms, - ( transform ) => - transform.type === 'files' && - transform.isMatch( [ file ] ) - ); - if ( transformation ) { - accumulator.push( - transformation.transform( [ file ] ) - ); - } - return accumulator; - }, [] ) - .flat(); - } else { - blocks = pasteHandler( { - HTML: html, - plainText, - mode: 'BLOCKS', - canUserUseUnfilteredHTML, - } ); - } + const blocks = getPasteBlocks( + event, + canUserUseUnfilteredHTML + ); if ( selectedBlockClientIds.length === 1 ) { const [ selectedBlockClientId ] = selectedBlockClientIds; @@ -223,20 +168,3 @@ export default function useClipboardHandler() { }; }, [] ); } - -/** - * Given a string of HTML representing serialized blocks, returns the plain - * text extracted after stripping the HTML of any tags and fixing line breaks. - * - * @param {string} html Serialized blocks. - * @return {string} The plain-text content with any html removed. - */ -function toPlainText( html ) { - // Manually handle BR tags as line breaks prior to `stripHTML` call - html = html.replace( /
/g, '\n' ); - - const plainText = stripHTML( html ).trim(); - - // Merge any consecutive line breaks - return plainText.replace( /\n\n+/g, '\n\n' ); -} diff --git a/packages/block-editor/src/components/writing-flow/utils.js b/packages/block-editor/src/components/writing-flow/utils.js new file mode 100644 index 00000000000000..ef1827077ccbf1 --- /dev/null +++ b/packages/block-editor/src/components/writing-flow/utils.js @@ -0,0 +1,103 @@ +/** + * WordPress dependencies + */ +import { __unstableStripHTML as stripHTML } from '@wordpress/dom'; +import { + serialize, + createBlock, + pasteHandler, + findTransform, + getBlockTransforms, +} from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import { getPasteEventData } from '../../utils/pasting'; + +/** + * Sets the clipboard data for the provided blocks, with both HTML and plain + * text representations. + * + * @param {ClipboardEvent} event Clipboard event. + * @param {WPBlock[]} blocks Blocks to set as clipboard data. + */ +export function setClipboardBlocks( event, blocks ) { + let _blocks = blocks; + const wrapperBlockName = event.clipboardData.getData( + '__unstableWrapperBlockName' + ); + + if ( wrapperBlockName ) { + _blocks = createBlock( + wrapperBlockName, + JSON.parse( + event.clipboardData.getData( + '__unstableWrapperBlockAttributes' + ) + ), + _blocks + ); + } + + const serialized = serialize( _blocks ); + + event.clipboardData.setData( 'text/plain', toPlainText( serialized ) ); + event.clipboardData.setData( 'text/html', serialized ); +} + +/** + * Returns the blocks to be pasted from the clipboard event. + * + * @param {ClipboardEvent} event The clipboard event. + * @param {boolean} canUserUseUnfilteredHTML Whether the user can or can't post unfiltered HTML. + * @return {Array|string} A list of blocks or a string, depending on `handlerMode`. + */ +export function getPasteBlocks( event, canUserUseUnfilteredHTML ) { + const { plainText, html, files } = getPasteEventData( event ); + let blocks = []; + + if ( files.length ) { + const fromTransforms = getBlockTransforms( 'from' ); + blocks = files + .reduce( ( accumulator, file ) => { + const transformation = findTransform( + fromTransforms, + ( transform ) => + transform.type === 'files' && + transform.isMatch( [ file ] ) + ); + if ( transformation ) { + accumulator.push( transformation.transform( [ file ] ) ); + } + return accumulator; + }, [] ) + .flat(); + } else { + blocks = pasteHandler( { + HTML: html, + plainText, + mode: 'BLOCKS', + canUserUseUnfilteredHTML, + } ); + } + + return blocks; +} + +/** + * Given a string of HTML representing serialized blocks, returns the plain + * text extracted after stripping the HTML of any tags and fixing line breaks. + * + * @param {string} html Serialized blocks. + * @return {string} The plain-text content with any html removed. + */ +function toPlainText( html ) { + // Manually handle BR tags as line breaks prior to `stripHTML` call + html = html.replace( /
/g, '\n' ); + + const plainText = stripHTML( html ).trim(); + + // Merge any consecutive line breaks + return plainText.replace( /\n\n+/g, '\n\n' ); +} diff --git a/packages/block-editor/src/hooks/block-hooks.js b/packages/block-editor/src/hooks/block-hooks.js index f9d7026e48545f..1e0b8e894d2067 100644 --- a/packages/block-editor/src/hooks/block-hooks.js +++ b/packages/block-editor/src/hooks/block-hooks.js @@ -55,8 +55,7 @@ function BlockHooksControlPure( { name, clientId } ) { const _hookedBlockClientIds = hookedBlocksForCurrentBlock.reduce( ( clientIds, block ) => { // If the block doesn't exist anywhere in the block tree, - // we know that we have to display the toggle for it, and set - // it to disabled. + // we know that we have to set the toggle to disabled. if ( getGlobalBlockCount( block.name ) === 0 ) { return clientIds; } @@ -96,13 +95,8 @@ function BlockHooksControlPure( { name, clientId } ) { } // If no hooked block was found in any of its designated locations, - // but it exists elsewhere in the block tree, we consider it manually inserted. - // In this case, we take note and will remove the corresponding toggle from the - // block inspector panel. - return { - ...clientIds, - [ block.name ]: false, - }; + // we set the toggle to disabled. + return clientIds; }, {} ); @@ -118,13 +112,7 @@ function BlockHooksControlPure( { name, clientId } ) { const { insertBlock, removeBlock } = useDispatch( blockEditorStore ); - // Remove toggle if block isn't present in the designated location but elsewhere in the block tree. - const hookedBlocksForCurrentBlockIfNotPresentElsewhere = - hookedBlocksForCurrentBlock?.filter( - ( block ) => hookedBlockClientIds?.[ block.name ] !== false - ); - - if ( ! hookedBlocksForCurrentBlockIfNotPresentElsewhere.length ) { + if ( ! hookedBlocksForCurrentBlock.length ) { return null; } diff --git a/packages/block-editor/src/hooks/dimensions.js b/packages/block-editor/src/hooks/dimensions.js index bbf5b12ca27cf8..6f03ddac2c6504 100644 --- a/packages/block-editor/src/hooks/dimensions.js +++ b/packages/block-editor/src/hooks/dimensions.js @@ -1,7 +1,12 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + /** * WordPress dependencies */ -import { useState, useEffect, useCallback } from '@wordpress/element'; +import { Platform, useState, useEffect, useCallback } from '@wordpress/element'; import { useDispatch, useSelect } from '@wordpress/data'; import { getBlockSupport } from '@wordpress/blocks'; import deprecated from '@wordpress/deprecated'; @@ -18,8 +23,7 @@ import { MarginVisualizer } from './margin'; import { PaddingVisualizer } from './padding'; import { store as blockEditorStore } from '../store'; import { unlock } from '../lock-unlock'; - -import { cleanEmptyObject } from './utils'; +import { cleanEmptyObject, shouldSkipSerialization } from './utils'; export const DIMENSIONS_SUPPORT_KEY = 'dimensions'; export const SPACING_SUPPORT_KEY = 'spacing'; @@ -125,6 +129,73 @@ export function DimensionsPanel( { clientId, name, setAttributes, settings } ) { ); } +/** + * Determine whether there is block support for dimensions. + * + * @param {string} blockName Block name. + * @param {string} feature Background image feature to check for. + * + * @return {boolean} Whether there is support. + */ +export function hasDimensionsSupport( blockName, feature = 'any' ) { + if ( Platform.OS !== 'web' ) { + return false; + } + + const support = getBlockSupport( blockName, DIMENSIONS_SUPPORT_KEY ); + + if ( support === true ) { + return true; + } + + if ( feature === 'any' ) { + return !! ( support?.aspectRatio || !! support?.minHeight ); + } + + return !! support?.[ feature ]; +} + +export default { + useBlockProps, + attributeKeys: [ 'minHeight', 'style' ], + hasSupport( name ) { + return hasDimensionsSupport( name, 'aspectRatio' ); + }, +}; + +function useBlockProps( { name, minHeight, style } ) { + if ( + ! hasDimensionsSupport( name, 'aspectRatio' ) || + shouldSkipSerialization( name, DIMENSIONS_SUPPORT_KEY, 'aspectRatio' ) + ) { + return {}; + } + + const className = classnames( { + 'has-aspect-ratio': !! style?.dimensions?.aspectRatio, + } ); + + // Allow dimensions-based inline style overrides to override any global styles rules that + // might be set for the block, and therefore affect the display of the aspect ratio. + const inlineStyleOverrides = {}; + + // Apply rules to unset incompatible styles. + // Note that a set `aspectRatio` will win out if both an aspect ratio and a minHeight are set. + // This is because the aspect ratio is a newer block support, so (in theory) any aspect ratio + // that is set should be intentional and should override any existing minHeight. The Cover block + // and dimensions controls have logic that will manually clear the aspect ratio if a minHeight + // is set. + if ( style?.dimensions?.aspectRatio ) { + // To ensure the aspect ratio does not get overridden by `minHeight` unset any existing rule. + inlineStyleOverrides.minHeight = 'unset'; + } else if ( minHeight || style?.dimensions?.minHeight ) { + // To ensure the minHeight does not get overridden by `aspectRatio` unset any existing rule. + inlineStyleOverrides.aspectRatio = 'unset'; + } + + return { className, style: inlineStyleOverrides }; +} + /** * @deprecated */ diff --git a/packages/block-editor/src/hooks/effects.js b/packages/block-editor/src/hooks/effects.js new file mode 100644 index 00000000000000..74d2aa46019d7b --- /dev/null +++ b/packages/block-editor/src/hooks/effects.js @@ -0,0 +1,57 @@ +/** + * WordPress dependencies + */ +import { hasBlockSupport } from '@wordpress/blocks'; +import { useSelect } from '@wordpress/data'; +/** + * Internal dependencies + */ +import StylesEffectsPanel, { + useHasEffectsPanel, +} from '../components/global-styles/effects-panel'; +import { InspectorControls } from '../components'; +import { store as blockEditorStore } from '../store'; +import { cleanEmptyObject } from './utils'; + +export const SHADOW_SUPPORT_KEY = 'shadow'; +export const EFFECTS_SUPPORT_KEYS = [ SHADOW_SUPPORT_KEY ]; + +export function hasEffectsSupport( blockName ) { + return EFFECTS_SUPPORT_KEYS.some( ( key ) => + hasBlockSupport( blockName, key ) + ); +} + +function EffectsInspectorControl( { children, resetAllFilter } ) { + return ( + + { children } + + ); +} +export function EffectsPanel( { clientId, setAttributes, settings } ) { + const isEnabled = useHasEffectsPanel( settings ); + const value = useSelect( + ( select ) => + select( blockEditorStore ).getBlockAttributes( clientId )?.style, + [ clientId ] + ); + + const onChange = ( newStyle ) => { + setAttributes( { style: cleanEmptyObject( newStyle ) } ); + }; + + if ( ! isEnabled ) { + return null; + } + + return ( + + ); +} diff --git a/packages/block-editor/src/hooks/index.js b/packages/block-editor/src/hooks/index.js index f17c0a22166e4e..e3c0a7580aab3c 100644 --- a/packages/block-editor/src/hooks/index.js +++ b/packages/block-editor/src/hooks/index.js @@ -16,6 +16,7 @@ import './generated-class-name'; import style from './style'; import './settings'; import color from './color'; +import dimensions from './dimensions'; import duotone from './duotone'; import fontFamily from './font-family'; import fontSize from './font-size'; @@ -27,6 +28,7 @@ import contentLockUI from './content-lock-ui'; import './metadata'; import blockHooks from './block-hooks'; import blockRenaming from './block-renaming'; +import './use-bindings-attributes'; createBlockEditFilter( [ @@ -46,6 +48,7 @@ createBlockListBlockFilter( [ align, style, color, + dimensions, duotone, fontFamily, fontSize, @@ -68,6 +71,7 @@ createBlockSaveFilter( [ export { useCustomSides } from './dimensions'; export { useLayoutClasses, useLayoutStyles } from './layout'; export { getBorderClassesAndStyles, useBorderProps } from './use-border-props'; +export { getShadowClassesAndStyles, useShadowProps } from './use-shadow-props'; export { getColorClassesAndStyles, useColorProps } from './use-color-props'; export { getSpacingClassesAndStyles } from './use-spacing-props'; export { getTypographyClassesAndStyles } from './use-typography-props'; diff --git a/packages/block-editor/src/hooks/index.native.js b/packages/block-editor/src/hooks/index.native.js index 55ae7e19df7037..6e4c1c6a17ba54 100644 --- a/packages/block-editor/src/hooks/index.native.js +++ b/packages/block-editor/src/hooks/index.native.js @@ -28,6 +28,7 @@ createBlockSaveFilter( [ ] ); export { getBorderClassesAndStyles, useBorderProps } from './use-border-props'; +export { getShadowClassesAndStyles, useShadowProps } from './use-shadow-props'; export { getColorClassesAndStyles, useColorProps } from './use-color-props'; export { getSpacingClassesAndStyles } from './use-spacing-props'; export { useCachedTruthy } from './use-cached-truthy'; diff --git a/packages/block-editor/src/hooks/style.js b/packages/block-editor/src/hooks/style.js index 7221de63456cd5..52fd990a3cac44 100644 --- a/packages/block-editor/src/hooks/style.js +++ b/packages/block-editor/src/hooks/style.js @@ -27,6 +27,11 @@ import { SPACING_SUPPORT_KEY, DimensionsPanel, } from './dimensions'; +import { + EFFECTS_SUPPORT_KEYS, + SHADOW_SUPPORT_KEY, + EffectsPanel, +} from './effects'; import { shouldSkipSerialization, useStyleOverride, @@ -37,6 +42,7 @@ import { useBlockEditingMode } from '../components/block-editing-mode'; const styleSupportKeys = [ ...TYPOGRAPHY_SUPPORT_KEYS, + ...EFFECTS_SUPPORT_KEYS, BORDER_SUPPORT_KEY, COLOR_SUPPORT_KEY, DIMENSIONS_SUPPORT_KEY, @@ -110,6 +116,7 @@ const skipSerializationPathsEdit = { [ `${ SPACING_SUPPORT_KEY }.__experimentalSkipSerialization` ]: [ SPACING_SUPPORT_KEY, ], + [ `${ SHADOW_SUPPORT_KEY }` ]: [ SHADOW_SUPPORT_KEY ], }; /** @@ -126,10 +133,14 @@ const skipSerializationPathsEdit = { */ const skipSerializationPathsSave = { ...skipSerializationPathsEdit, + [ `${ DIMENSIONS_SUPPORT_KEY }.aspectRatio` ]: [ + `${ DIMENSIONS_SUPPORT_KEY }.aspectRatio`, + ], // Skip serialization of aspect ratio in save mode. [ `${ BACKGROUND_SUPPORT_KEY }` ]: [ BACKGROUND_SUPPORT_KEY ], // Skip serialization of background support in save mode. }; const skipSerializationPathsSaveChecks = { + [ `${ DIMENSIONS_SUPPORT_KEY }.aspectRatio` ]: true, [ `${ BACKGROUND_SUPPORT_KEY }` ]: true, }; @@ -336,6 +347,7 @@ function BlockStyleControls( { + ); } diff --git a/packages/block-editor/src/hooks/supports.js b/packages/block-editor/src/hooks/supports.js index 2cf08d46fa8fe2..4e116494029bf1 100644 --- a/packages/block-editor/src/hooks/supports.js +++ b/packages/block-editor/src/hooks/supports.js @@ -59,8 +59,10 @@ const TYPOGRAPHY_SUPPORT_KEYS = [ WRITING_MODE_SUPPORT_KEY, LETTER_SPACING_SUPPORT_KEY, ]; +const EFFECTS_SUPPORT_KEYS = [ 'shadow' ]; const SPACING_SUPPORT_KEY = 'spacing'; const styleSupportKeys = [ + ...EFFECTS_SUPPORT_KEYS, ...TYPOGRAPHY_SUPPORT_KEYS, BORDER_SUPPORT_KEY, COLOR_SUPPORT_KEY, diff --git a/packages/block-editor/src/hooks/test/effects.js b/packages/block-editor/src/hooks/test/effects.js new file mode 100644 index 00000000000000..b4fe61745744b1 --- /dev/null +++ b/packages/block-editor/src/hooks/test/effects.js @@ -0,0 +1,39 @@ +/** + * Internal dependencies + */ +import { hasEffectsSupport } from '../effects'; + +describe( 'effects', () => { + describe( 'hasEffectsSupport', () => { + it( 'should return false if the block does not support effects', () => { + const settings = { + supports: { + shadow: false, + }, + }; + + expect( hasEffectsSupport( settings ) ).toBe( false ); + } ); + + it( 'should return true if the block supports effects', () => { + const settings = { + supports: { + shadow: true, + }, + }; + + expect( hasEffectsSupport( settings ) ).toBe( true ); + } ); + + it( 'should return true if the block supports effects and other features', () => { + const settings = { + supports: { + shadow: true, + align: true, + }, + }; + + expect( hasEffectsSupport( settings ) ).toBe( true ); + } ); + } ); +} ); diff --git a/packages/block-editor/src/hooks/use-bindings-attributes.js b/packages/block-editor/src/hooks/use-bindings-attributes.js new file mode 100644 index 00000000000000..94aac654097e5d --- /dev/null +++ b/packages/block-editor/src/hooks/use-bindings-attributes.js @@ -0,0 +1,148 @@ +/** + * WordPress dependencies + */ +import { getBlockType } from '@wordpress/blocks'; +import { createHigherOrderComponent } from '@wordpress/compose'; +import { useRegistry, useSelect } from '@wordpress/data'; +import { addFilter } from '@wordpress/hooks'; +/** + * Internal dependencies + */ +import { store as blockEditorStore } from '../store'; +import { useBlockEditContext } from '../components/block-edit/context'; +import { unlock } from '../lock-unlock'; + +/** @typedef {import('@wordpress/compose').WPHigherOrderComponent} WPHigherOrderComponent */ +/** @typedef {import('@wordpress/blocks').WPBlockSettings} WPBlockSettings */ + +/** + * Given a binding of block attributes, returns a higher order component that + * overrides its `attributes` and `setAttributes` props to sync any changes needed. + * + * @return {WPHigherOrderComponent} Higher-order component. + */ + +const BLOCK_BINDINGS_ALLOWED_BLOCKS = { + 'core/paragraph': [ 'content' ], + 'core/heading': [ 'content' ], + 'core/image': [ 'url', 'title', 'alt' ], + 'core/button': [ 'url', 'text', 'linkTarget' ], +}; + +const createEditFunctionWithBindingsAttribute = () => + createHigherOrderComponent( + ( BlockEdit ) => ( props ) => { + const { clientId, name: blockName } = useBlockEditContext(); + const { getBlockBindingsSource } = unlock( + useSelect( blockEditorStore ) + ); + const { getBlockAttributes, updateBlockAttributes } = + useSelect( blockEditorStore ); + + const updatedAttributes = getBlockAttributes( clientId ); + if ( updatedAttributes?.metadata?.bindings ) { + Object.entries( updatedAttributes.metadata.bindings ).forEach( + ( [ attributeName, settings ] ) => { + const source = getBlockBindingsSource( + settings.source.name + ); + + if ( source ) { + // Second argument (`updateMetaValue`) will be used to update the value in the future. + const { + placeholder, + useValue: [ metaValue = null ] = [], + } = source.useSource( + props, + settings.source.attributes + ); + + if ( placeholder && ! metaValue ) { + // If the attribute is `src` or `href`, a placeholder can't be used because it is not a valid url. + // Adding this workaround until attributes and metadata fields types are improved and include `url`. + const htmlAttribute = + getBlockType( blockName ).attributes[ + attributeName + ].attribute; + if ( + htmlAttribute === 'src' || + htmlAttribute === 'href' + ) { + updatedAttributes[ attributeName ] = null; + } else { + updatedAttributes[ attributeName ] = + placeholder; + } + } + + if ( metaValue ) { + updatedAttributes[ attributeName ] = metaValue; + } + } + } + ); + } + + const registry = useRegistry(); + + return ( + <> + + registry.batch( () => + updateBlockAttributes( blockId, newAttributes ) + ) + } + { ...props } + /> + + ); + }, + 'useBoundAttributes' + ); + +/** + * Filters a registered block's settings to enhance a block's `edit` component + * to upgrade bound attributes. + * + * @param {WPBlockSettings} settings Registered block settings. + * + * @return {WPBlockSettings} Filtered block settings. + */ +function shimAttributeSource( settings ) { + if ( ! ( settings.name in BLOCK_BINDINGS_ALLOWED_BLOCKS ) ) { + return settings; + } + settings.edit = createEditFunctionWithBindingsAttribute()( settings.edit ); + + return settings; +} + +addFilter( + 'blocks.registerBlockType', + 'core/editor/custom-sources-backwards-compatibility/shim-attribute-source', + shimAttributeSource +); + +// Add the context to all blocks. +addFilter( + 'blocks.registerBlockType', + 'core/block-bindings-ui', + ( settings, name ) => { + if ( ! ( name in BLOCK_BINDINGS_ALLOWED_BLOCKS ) ) { + return settings; + } + const contextItems = [ 'postId', 'postType', 'queryId' ]; + const usesContextArray = settings.usesContext; + const oldUsesContextArray = new Set( usesContextArray ); + contextItems.forEach( ( item ) => { + if ( ! oldUsesContextArray.has( item ) ) { + usesContextArray.push( item ); + } + } ); + settings.usesContext = usesContextArray; + return settings; + } +); diff --git a/packages/block-editor/src/hooks/use-shadow-props.js b/packages/block-editor/src/hooks/use-shadow-props.js new file mode 100644 index 00000000000000..fdc601366245c9 --- /dev/null +++ b/packages/block-editor/src/hooks/use-shadow-props.js @@ -0,0 +1,37 @@ +/** + * Internal dependencies + */ +import { getInlineStyles } from './style'; + +// This utility is intended to assist where the serialization of the shadow +// block support is being skipped for a block but the shadow related CSS classes +// & styles still need to be generated so they can be applied to inner elements. + +/** + * Provides the CSS class names and inline styles for a block's shadow support + * attributes. + * + * @param {Object} attributes Block attributes. + * @return {Object} Shadow block support derived CSS classes & styles. + */ +export function getShadowClassesAndStyles( attributes ) { + const shadow = attributes.style?.shadow || ''; + + return { + className: undefined, + style: getInlineStyles( { shadow } ), + }; +} + +/** + * Derives the shadow related props for a block from its shadow block support + * attributes. + * + * @param {Object} attributes Block attributes. + * + * @return {Object} ClassName & style props from shadow block support. + */ +export function useShadowProps( attributes ) { + const shadowProps = getShadowClassesAndStyles( attributes ); + return shadowProps; +} diff --git a/packages/block-editor/src/hooks/utils.js b/packages/block-editor/src/hooks/utils.js index e63029e4e34e81..f13963933e5225 100644 --- a/packages/block-editor/src/hooks/utils.js +++ b/packages/block-editor/src/hooks/utils.js @@ -195,6 +195,7 @@ export function useBlockSettings( name, parentLayout ) { blockGap, spacingSizes, units, + aspectRatio, minHeight, layout, borderColor, @@ -221,6 +222,7 @@ export function useBlockSettings( name, parentLayout ) { isTextEnabled, isHeadingEnabled, isButtonEnabled, + shadow, ] = useSettings( 'background.backgroundImage', 'background.backgroundSize', @@ -243,6 +245,7 @@ export function useBlockSettings( name, parentLayout ) { 'spacing.blockGap', 'spacing.spacingSizes', 'spacing.units', + 'dimensions.aspectRatio', 'dimensions.minHeight', 'layout', 'border.color', @@ -268,7 +271,8 @@ export function useBlockSettings( name, parentLayout ) { 'color.link', 'color.text', 'color.heading', - 'color.button' + 'color.button', + 'shadow' ); const rawSettings = useMemo( () => { @@ -341,10 +345,12 @@ export function useBlockSettings( name, parentLayout ) { width: borderWidth, }, dimensions: { + aspectRatio, minHeight, }, layout, parentLayout, + shadow, }; }, [ backgroundImage, @@ -368,6 +374,7 @@ export function useBlockSettings( name, parentLayout ) { blockGap, spacingSizes, units, + aspectRatio, minHeight, layout, parentLayout, @@ -395,6 +402,7 @@ export function useBlockSettings( name, parentLayout ) { isTextEnabled, isHeadingEnabled, isButtonEnabled, + shadow, ] ); return useSettingsForBlockElement( rawSettings, name ); diff --git a/packages/block-editor/src/index.js b/packages/block-editor/src/index.js index 1dbc4501e92180..83475b9358723e 100644 --- a/packages/block-editor/src/index.js +++ b/packages/block-editor/src/index.js @@ -11,6 +11,8 @@ export { useCustomSides as __experimentalUseCustomSides, getSpacingClassesAndStyles as __experimentalGetSpacingClassesAndStyles, getGapCSSValue as __experimentalGetGapCSSValue, + getShadowClassesAndStyles as __experimentalGetShadowClassesAndStyles, + useShadowProps as __experimentalUseShadowProps, useCachedTruthy, } from './hooks'; export * from './components'; diff --git a/packages/block-editor/src/store/actions.js b/packages/block-editor/src/store/actions.js index 47d530c8319a23..6adbafe28341c6 100644 --- a/packages/block-editor/src/store/actions.js +++ b/packages/block-editor/src/store/actions.js @@ -640,13 +640,15 @@ export function showInsertionPoint( index, __unstableOptions = {} ) { - const { __unstableWithInserter, operation } = __unstableOptions; + const { __unstableWithInserter, operation, nearestSide } = + __unstableOptions; return { type: 'SHOW_INSERTION_POINT', rootClientId, index, __unstableWithInserter, operation, + nearestSide, }; } /** diff --git a/packages/block-editor/src/store/index.js b/packages/block-editor/src/store/index.js index 0bcc00cb5f6ae8..10e16a0779cd63 100644 --- a/packages/block-editor/src/store/index.js +++ b/packages/block-editor/src/store/index.js @@ -10,6 +10,7 @@ import reducer from './reducer'; import * as selectors from './selectors'; import * as privateActions from './private-actions'; import * as privateSelectors from './private-selectors'; +import * as resolvers from './resolvers'; import * as actions from './actions'; import { STORE_NAME } from './constants'; import { unlock } from '../lock-unlock'; @@ -22,6 +23,7 @@ import { unlock } from '../lock-unlock'; export const storeConfig = { reducer, selectors, + resolvers, actions, }; diff --git a/packages/block-editor/src/store/private-actions.js b/packages/block-editor/src/store/private-actions.js index a31455a0b7e7b3..aea3613884bb69 100644 --- a/packages/block-editor/src/store/private-actions.js +++ b/packages/block-editor/src/store/private-actions.js @@ -360,3 +360,13 @@ export function stopEditingAsBlocks( clientId ) { dispatch.__unstableSetTemporarilyEditingAsBlocks(); }; } + +export function registerBlockBindingsSource( source ) { + return { + type: 'REGISTER_BLOCK_BINDINGS_SOURCE', + sourceName: source.name, + sourceLabel: source.label, + useSource: source.useSource, + lockAttributesEditing: source.lockAttributesEditing, + }; +} diff --git a/packages/block-editor/src/store/private-selectors.js b/packages/block-editor/src/store/private-selectors.js index e8230eea89daa3..caca9507f07706 100644 --- a/packages/block-editor/src/store/private-selectors.js +++ b/packages/block-editor/src/store/private-selectors.js @@ -3,6 +3,11 @@ */ import createSelector from 'rememo'; +/** + * WordPress dependencies + */ +import { createRegistrySelector } from '@wordpress/data'; + /** * Internal dependencies */ @@ -11,11 +16,12 @@ import { getBlockParents, getBlockEditingMode, getSettings, - __experimentalGetParsedPattern, canInsertBlockType, - __experimentalGetAllowedPatterns, } from './selectors'; -import { getAllPatterns, checkAllowListRecursive } from './utils'; +import { checkAllowListRecursive, getAllPatternsDependants } from './utils'; +import { INSERTER_PATTERN_TYPES } from '../components/inserter/block-patterns-tab/utils'; +import { store } from './'; +import { unlock } from '../lock-unlock'; /** * Returns true if the block interface is hidden, or false otherwise. @@ -242,6 +248,10 @@ export const getInserterMediaCategories = createSelector( ] ); +export function getFetchedPatterns( state ) { + return state.blockPatterns; +} + /** * Returns whether there is at least one allowed pattern for inner blocks children. * This is useful for deferring the parsing of all patterns until needed. @@ -251,29 +261,74 @@ export const getInserterMediaCategories = createSelector( * * @return {boolean} If there is at least one allowed pattern. */ -export const hasAllowedPatterns = createSelector( - ( state, rootClientId = null ) => { - const patterns = getAllPatterns( state ); - const { allowedBlockTypes } = getSettings( state ); - return patterns.some( ( { name, inserter = true } ) => { - if ( ! inserter ) { - return false; - } - const { blocks } = __experimentalGetParsedPattern( state, name ); - return ( - checkAllowListRecursive( blocks, allowedBlockTypes ) && - blocks.every( ( { name: blockName } ) => - canInsertBlockType( state, blockName, rootClientId ) - ) +export const hasAllowedPatterns = createRegistrySelector( ( select ) => + createSelector( + ( state, rootClientId = null ) => { + const { getAllPatterns, __experimentalGetParsedPattern } = unlock( + select( store ) ); - } ); - }, - ( state, rootClientId ) => [ - ...__experimentalGetAllowedPatterns.getDependants( - state, - rootClientId - ), - ] + const patterns = getAllPatterns(); + const { allowedBlockTypes } = getSettings( state ); + return patterns.some( ( { name, inserter = true } ) => { + if ( ! inserter ) { + return false; + } + const { blocks } = __experimentalGetParsedPattern( name ); + return ( + checkAllowListRecursive( blocks, allowedBlockTypes ) && + blocks.every( ( { name: blockName } ) => + canInsertBlockType( state, blockName, rootClientId ) + ) + ); + } ); + }, + ( state, rootClientId ) => [ + getAllPatternsDependants( state ), + state.settings.allowedBlockTypes, + state.settings.templateLock, + state.blockListSettings[ rootClientId ], + state.blocks.byClientId.get( rootClientId ), + ] + ) +); + +export const getAllPatterns = createRegistrySelector( ( select ) => + createSelector( ( state ) => { + // This setting is left for back compat. + const { + __experimentalBlockPatterns = [], + __experimentalUserPatternCategories = [], + __experimentalReusableBlocks = [], + } = state.settings; + const userPatterns = ( __experimentalReusableBlocks ?? [] ).map( + ( userPattern ) => { + return { + name: `core/block/${ userPattern.id }`, + id: userPattern.id, + type: INSERTER_PATTERN_TYPES.user, + title: userPattern.title.raw, + categories: userPattern.wp_pattern_category.map( + ( catId ) => { + const category = ( + __experimentalUserPatternCategories ?? [] + ).find( ( { id } ) => id === catId ); + return category ? category.slug : catId; + } + ), + content: userPattern.content.raw, + syncStatus: userPattern.wp_pattern_sync_status, + }; + } + ); + return [ + ...userPatterns, + ...__experimentalBlockPatterns, + ...unlock( select( store ) ).getFetchedPatterns(), + ].filter( + ( x, index, arr ) => + index === arr.findIndex( ( y ) => x.name === y.name ) + ); + }, getAllPatternsDependants ) ); /** @@ -286,3 +341,11 @@ export const hasAllowedPatterns = createSelector( export function getLastFocus( state ) { return state.lastFocus; } + +export function getAllBlockBindingsSources( state ) { + return state.blockBindingsSources; +} + +export function getBlockBindingsSource( state, sourceName ) { + return state.blockBindingsSources[ sourceName ]; +} diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js index fa6c8942e66add..dc69a4da609a4d 100644 --- a/packages/block-editor/src/store/reducer.js +++ b/packages/block-editor/src/store/reducer.js @@ -1599,13 +1599,19 @@ export function blocksMode( state = {}, action ) { export function insertionPoint( state = null, action ) { switch ( action.type ) { case 'SHOW_INSERTION_POINT': { - const { rootClientId, index, __unstableWithInserter, operation } = - action; + const { + rootClientId, + index, + __unstableWithInserter, + operation, + nearestSide, + } = action; const nextState = { rootClientId, index, __unstableWithInserter, operation, + nearestSide, }; // Bail out updates if the states are the same. @@ -2017,6 +2023,29 @@ export function lastFocus( state = false, action ) { return state; } +function blockBindingsSources( state = {}, action ) { + if ( action.type === 'REGISTER_BLOCK_BINDINGS_SOURCE' ) { + return { + ...state, + [ action.sourceName ]: { + label: action.sourceLabel, + useSource: action.useSource, + lockAttributesEditing: action.lockAttributesEditing, + }, + }; + } + return state; +} + +function blockPatterns( state = [], action ) { + switch ( action.type ) { + case 'RECEIVE_BLOCK_PATTERNS': + return action.patterns; + } + + return state; +} + const combinedReducers = combineReducers( { blocks, isTyping, @@ -2047,6 +2076,8 @@ const combinedReducers = combineReducers( { blockRemovalRules, openedBlockSettingsMenu, registeredInserterMediaCategories, + blockBindingsSources, + blockPatterns, } ); function withAutomaticChangeReset( reducer ) { diff --git a/packages/block-editor/src/store/resolvers.js b/packages/block-editor/src/store/resolvers.js new file mode 100644 index 00000000000000..40c51d241ac676 --- /dev/null +++ b/packages/block-editor/src/store/resolvers.js @@ -0,0 +1,17 @@ +export const getFetchedPatterns = + () => + async ( { dispatch, select } ) => { + const { __experimentalFetchBlockPatterns } = select.getSettings(); + if ( ! __experimentalFetchBlockPatterns ) { + return []; + } + const patterns = await __experimentalFetchBlockPatterns(); + dispatch( { type: 'RECEIVE_BLOCK_PATTERNS', patterns } ); + }; + +getFetchedPatterns.shouldInvalidate = ( action ) => { + return ( + action.type === 'UPDATE_SETTINGS' && + !! action.settings.__experimentalFetchBlockPatterns + ); +}; diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 5e47e966ef3345..099c6b30222efc 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -27,11 +27,13 @@ import { createRegistrySelector } from '@wordpress/data'; * Internal dependencies */ import { - getAllPatterns, checkAllowListRecursive, checkAllowList, + getAllPatternsDependants, } from './utils'; import { orderBy } from '../utils/sorting'; +import { STORE_NAME } from './constants'; +import { unlock } from '../lock-unlock'; /** * A block selection object. @@ -318,14 +320,14 @@ export const getGlobalBlockCount = createSelector( ); /** - * Returns all global blocks that match a blockName. Results include nested blocks. + * Returns all blocks that match a blockName. Results include nested blocks. * * @param {Object} state Global application state. * @param {?string} blockName Optional block name, if not specified, returns an empty array. * * @return {Array} Array of clientIds of blocks with name equal to blockName. */ -export const __experimentalGetGlobalBlocksByName = createSelector( +export const getBlocksByName = createSelector( ( state, blockName ) => { if ( ! blockName ) { return EMPTY_ARRAY; @@ -343,6 +345,27 @@ export const __experimentalGetGlobalBlocksByName = createSelector( ( state ) => [ state.blocks.order, state.blocks.byClientId ] ); +/** + * Returns all global blocks that match a blockName. Results include nested blocks. + * + * @deprecated + * + * @param {Object} state Global application state. + * @param {?string} blockName Optional block name, if not specified, returns an empty array. + * + * @return {Array} Array of clientIds of blocks with name equal to blockName. + */ +export function __experimentalGetGlobalBlocksByName( state, blockName ) { + deprecated( + "wp.data.select( 'core/block-editor' ).__experimentalGetGlobalBlocksByName", + { + since: '6.5', + alternative: `wp.data.select( 'core/block-editor' ).getBlocksByName`, + } + ); + return getBlocksByName( state, blockName ); +} + /** * Given an array of block client IDs, returns the corresponding array of block * objects. @@ -2239,41 +2262,36 @@ export const __experimentalGetDirectInsertBlock = createSelector( ] ); -export const __experimentalGetParsedPattern = createSelector( - ( state, patternName ) => { - const patterns = getAllPatterns( state ); - const pattern = patterns.find( ( { name } ) => name === patternName ); - if ( ! pattern ) { - return null; - } - return { - ...pattern, - blocks: parse( pattern.content, { - __unstableSkipMigrationLogs: true, - } ), - }; - }, - ( state ) => [ getAllPatterns( state ) ] -); - -const getAllAllowedPatterns = createSelector( - ( state ) => { - const patterns = getAllPatterns( state ); - const { allowedBlockTypes } = getSettings( state ); - - const parsedPatterns = patterns - .filter( ( { inserter = true } ) => !! inserter ) - .map( ( { name } ) => - __experimentalGetParsedPattern( state, name ) +export const __experimentalGetParsedPattern = createRegistrySelector( + ( select ) => + createSelector( ( state, patternName ) => { + const { getAllPatterns } = unlock( select( STORE_NAME ) ); + const patterns = getAllPatterns(); + const pattern = patterns.find( + ( { name } ) => name === patternName ); - const allowedPatterns = parsedPatterns.filter( ( { blocks } ) => - checkAllowListRecursive( blocks, allowedBlockTypes ) - ); - return allowedPatterns; - }, - ( state ) => [ getAllPatterns( state ), state.settings.allowedBlockTypes ] + if ( ! pattern ) { + return null; + } + return { + ...pattern, + blocks: parse( pattern.content, { + __unstableSkipMigrationLogs: true, + } ), + }; + }, getAllPatternsDependants ) ); +const getAllowedPatternsDependants = ( state, rootClientId ) => { + return [ + ...getAllPatternsDependants( state ), + state.settings.allowedBlockTypes, + state.settings.templateLock, + state.blockListSettings[ rootClientId ], + state.blocks.byClientId.get( rootClientId ), + ]; +}; + /** * Returns the list of allowed patterns for inner blocks children. * @@ -2282,24 +2300,33 @@ const getAllAllowedPatterns = createSelector( * * @return {Array?} The list of allowed patterns. */ -export const __experimentalGetAllowedPatterns = createSelector( - ( state, rootClientId = null ) => { - const availableParsedPatterns = getAllAllowedPatterns( state ); - const patternsAllowed = availableParsedPatterns.filter( - ( { blocks } ) => - blocks.every( ( { name } ) => - canInsertBlockType( state, name, rootClientId ) - ) - ); +export const __experimentalGetAllowedPatterns = createRegistrySelector( + ( select ) => { + return createSelector( ( state, rootClientId = null ) => { + const { + getAllPatterns, + __experimentalGetParsedPattern: getParsedPattern, + } = unlock( select( STORE_NAME ) ); + const patterns = getAllPatterns(); + const { allowedBlockTypes } = getSettings( state ); + + const parsedPatterns = patterns + .filter( ( { inserter = true } ) => !! inserter ) + .map( ( { name } ) => getParsedPattern( name ) ); + const availableParsedPatterns = parsedPatterns.filter( + ( { blocks } ) => + checkAllowListRecursive( blocks, allowedBlockTypes ) + ); + const patternsAllowed = availableParsedPatterns.filter( + ( { blocks } ) => + blocks.every( ( { name } ) => + canInsertBlockType( state, name, rootClientId ) + ) + ); - return patternsAllowed; - }, - ( state, rootClientId ) => [ - getAllAllowedPatterns( state ), - state.settings.templateLock, - state.blockListSettings[ rootClientId ], - state.blocks.byClientId.get( rootClientId ), - ] + return patternsAllowed; + }, getAllowedPatternsDependants ); + } ); /** @@ -2315,36 +2342,34 @@ export const __experimentalGetAllowedPatterns = createSelector( * * @return {Array} The list of matched block patterns based on declared `blockTypes` and block name. */ -export const getPatternsByBlockTypes = createSelector( - ( state, blockNames, rootClientId = null ) => { - if ( ! blockNames ) return EMPTY_ARRAY; - const patterns = __experimentalGetAllowedPatterns( - state, - rootClientId - ); - const normalizedBlockNames = Array.isArray( blockNames ) - ? blockNames - : [ blockNames ]; - const filteredPatterns = patterns.filter( ( pattern ) => - pattern?.blockTypes?.some?.( ( blockName ) => - normalizedBlockNames.includes( blockName ) - ) - ); - if ( filteredPatterns.length === 0 ) { - return EMPTY_ARRAY; - } - return filteredPatterns; - }, - ( state, blockNames, rootClientId ) => [ - ...__experimentalGetAllowedPatterns.getDependants( - state, - rootClientId - ), - ] +export const getPatternsByBlockTypes = createRegistrySelector( ( select ) => + createSelector( + ( state, blockNames, rootClientId = null ) => { + if ( ! blockNames ) return EMPTY_ARRAY; + const patterns = + select( STORE_NAME ).__experimentalGetAllowedPatterns( + rootClientId + ); + const normalizedBlockNames = Array.isArray( blockNames ) + ? blockNames + : [ blockNames ]; + const filteredPatterns = patterns.filter( ( pattern ) => + pattern?.blockTypes?.some?.( ( blockName ) => + normalizedBlockNames.includes( blockName ) + ) + ); + if ( filteredPatterns.length === 0 ) { + return EMPTY_ARRAY; + } + return filteredPatterns; + }, + ( state, blockNames, rootClientId ) => + getAllowedPatternsDependants( state, rootClientId ) + ) ); -export const __experimentalGetPatternsByBlockTypes = createSelector( - ( state, blockNames, rootClientId = null ) => { +export const __experimentalGetPatternsByBlockTypes = createRegistrySelector( + ( select ) => { deprecated( 'wp.data.select( "core/block-editor" ).__experimentalGetPatternsByBlockTypes', { @@ -2354,14 +2379,8 @@ export const __experimentalGetPatternsByBlockTypes = createSelector( version: '6.4', } ); - return getPatternsByBlockTypes( state, blockNames, rootClientId ); - }, - ( state, blockNames, rootClientId ) => [ - ...__experimentalGetAllowedPatterns.getDependants( - state, - rootClientId - ), - ] + return select( STORE_NAME ).getPatternsByBlockTypes; + } ); /** @@ -2381,45 +2400,46 @@ export const __experimentalGetPatternsByBlockTypes = createSelector( * * @return {WPBlockPattern[]} Items that are eligible for a pattern transformation. */ -export const __experimentalGetPatternTransformItems = createSelector( - ( state, blocks, rootClientId = null ) => { - if ( ! blocks ) return EMPTY_ARRAY; - /** - * For now we only handle blocks without InnerBlocks and take into account - * the `__experimentalRole` property of blocks' attributes for the transformation. - * Note that the blocks have been retrieved through `getBlock`, which doesn't - * return the inner blocks of an inner block controller, so we still need - * to check for this case too. - */ - if ( - blocks.some( - ( { clientId, innerBlocks } ) => - innerBlocks.length || - areInnerBlocksControlled( state, clientId ) - ) - ) { - return EMPTY_ARRAY; - } - - // Create a Set of the selected block names that is used in patterns filtering. - const selectedBlockNames = Array.from( - new Set( blocks.map( ( { name } ) => name ) ) - ); - /** - * Here we will return first set of possible eligible block patterns, - * by checking the `blockTypes` property. We still have to recurse through - * block pattern's blocks and try to find matches from the selected blocks. - * Now this happens in the consumer to avoid heavy operations in the selector. - */ - return getPatternsByBlockTypes( - state, - selectedBlockNames, - rootClientId - ); - }, - ( state, blocks, rootClientId ) => [ - ...getPatternsByBlockTypes.getDependants( state, rootClientId ), - ] +export const __experimentalGetPatternTransformItems = createRegistrySelector( + ( select ) => + createSelector( + ( state, blocks, rootClientId = null ) => { + if ( ! blocks ) return EMPTY_ARRAY; + /** + * For now we only handle blocks without InnerBlocks and take into account + * the `__experimentalRole` property of blocks' attributes for the transformation. + * Note that the blocks have been retrieved through `getBlock`, which doesn't + * return the inner blocks of an inner block controller, so we still need + * to check for this case too. + */ + if ( + blocks.some( + ( { clientId, innerBlocks } ) => + innerBlocks.length || + areInnerBlocksControlled( state, clientId ) + ) + ) { + return EMPTY_ARRAY; + } + + // Create a Set of the selected block names that is used in patterns filtering. + const selectedBlockNames = Array.from( + new Set( blocks.map( ( { name } ) => name ) ) + ); + /** + * Here we will return first set of possible eligible block patterns, + * by checking the `blockTypes` property. We still have to recurse through + * block pattern's blocks and try to find matches from the selected blocks. + * Now this happens in the consumer to avoid heavy operations in the selector. + */ + return select( STORE_NAME ).getPatternsByBlockTypes( + selectedBlockNames, + rootClientId + ); + }, + ( state, blocks, rootClientId ) => + getAllowedPatternsDependants( state, rootClientId ) + ) ); /** @@ -2721,8 +2741,7 @@ export const __unstableGetContentLockingParent = createSelector( current = state.blocks.parents.get( current ); if ( ( current && - getBlockName( state, current ) === 'core/block' && - window.__experimentalPatternPartialSyncing ) || + getBlockName( state, current ) === 'core/block' ) || ( current && getTemplateLock( state, current ) === 'contentOnly' ) ) { diff --git a/packages/block-editor/src/store/test/registry-selectors.js b/packages/block-editor/src/store/test/registry-selectors.js new file mode 100644 index 00000000000000..89115c75f05144 --- /dev/null +++ b/packages/block-editor/src/store/test/registry-selectors.js @@ -0,0 +1,431 @@ +/** + * WordPress dependencies + */ +import { registerBlockType, unregisterBlockType } from '@wordpress/blocks'; +import { select, dispatch } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { store } from '../'; + +describe( 'selectors', () => { + beforeEach( () => { + registerBlockType( 'core/test-block-a', { + save: ( props ) => props.attributes.text, + category: 'design', + title: 'Test Block A', + icon: 'test', + keywords: [ 'testing' ], + } ); + + registerBlockType( 'core/test-block-b', { + save: ( props ) => props.attributes.text, + category: 'text', + title: 'Test Block B', + icon: 'test', + keywords: [ 'testing' ], + supports: { + multiple: false, + }, + } ); + } ); + + afterEach( async () => { + unregisterBlockType( 'core/test-block-a' ); + unregisterBlockType( 'core/test-block-b' ); + } ); + + describe( '__experimentalGetAllowedPatterns', () => { + beforeAll( async () => { + await dispatch( store ).resetBlocks( [ + { + clientId: 'block1', + name: 'core/test-block-a', + innerBlocks: [], + }, + { + clientId: 'block2', + name: 'core/test-block-b', + innerBlocks: [], + }, + ] ); + await dispatch( store ).updateSettings( { + __experimentalBlockPatterns: [ + { + name: 'pattern-a', + title: 'pattern with a', + content: ``, + }, + { + name: 'pattern-b', + title: 'pattern with b', + content: + '', + }, + { + name: 'pattern-c', + title: 'pattern hidden from UI', + inserter: false, + content: + '', + }, + ], + } ); + await dispatch( store ).updateBlockListSettings( 'block1', { + allowedBlocks: [ 'core/test-block-b' ], + } ); + } ); + + afterAll( async () => { + await dispatch( store ).resetBlocks( [] ); + await dispatch( store ).updateSettings( { + __experimentalBlockPatterns: [], + } ); + await dispatch( store ).updateBlockListSettings( 'block1', {} ); + } ); + + it( 'should return all patterns for root level', () => { + expect( + select( store ).__experimentalGetAllowedPatterns( null ) + ).toHaveLength( 2 ); + } ); + it( 'should return patterns that consists of blocks allowed for the specified client ID', () => { + expect( + select( store ).__experimentalGetAllowedPatterns( 'block1' ) + ).toHaveLength( 1 ); + expect( + select( store ).__experimentalGetAllowedPatterns( 'block2' ) + ).toHaveLength( 0 ); + } ); + it( 'should return empty array if only patterns hidden from UI exist', () => { + expect( + select( store ).__experimentalGetAllowedPatterns( { + blocks: { byClientId: new Map() }, + blockListSettings: {}, + settings: { + __experimentalBlockPatterns: [ + { + name: 'pattern-c', + title: 'pattern hidden from UI', + inserter: false, + content: + '', + }, + ], + }, + } ) + ).toHaveLength( 0 ); + } ); + } ); + + describe( '__experimentalGetParsedPattern', () => { + beforeAll( async () => { + await dispatch( store ).updateSettings( { + __experimentalBlockPatterns: [ + { + name: 'pattern-a', + title: 'pattern with a', + content: ``, + }, + { + name: 'pattern-hidden-from-ui', + title: 'pattern hidden from UI', + inserter: false, + content: + '', + }, + ], + } ); + } ); + + afterAll( async () => { + await dispatch( store ).updateSettings( { + __experimentalBlockPatterns: [], + } ); + } ); + + it( 'should return proper results when pattern does not exist', () => { + expect( + select( store ).__experimentalGetParsedPattern( 'not there' ) + ).toBeNull(); + } ); + it( 'should return existing pattern properly parsed', () => { + const { name, blocks } = + select( store ).__experimentalGetParsedPattern( 'pattern-a' ); + expect( name ).toEqual( 'pattern-a' ); + expect( blocks ).toHaveLength( 1 ); + expect( blocks[ 0 ] ).toEqual( + expect.objectContaining( { + name: 'core/test-block-a', + } ) + ); + } ); + it( 'should return hidden from UI pattern when requested', () => { + const { name, blocks, inserter } = select( + store + ).__experimentalGetParsedPattern( 'pattern-hidden-from-ui' ); + expect( name ).toEqual( 'pattern-hidden-from-ui' ); + expect( inserter ).toBeFalsy(); + expect( blocks ).toHaveLength( 2 ); + expect( blocks[ 0 ] ).toEqual( + expect.objectContaining( { + name: 'core/test-block-a', + } ) + ); + } ); + } ); + + describe( 'getPatternsByBlockTypes', () => { + beforeAll( async () => { + await dispatch( store ).resetBlocks( [ + { + clientId: 'block1', + name: 'core/test-block-a', + innerBlocks: [], + }, + ] ); + await dispatch( store ).updateSettings( { + __experimentalBlockPatterns: [ + { + name: 'pattern-a', + blockTypes: [ 'test/block-a' ], + title: 'pattern a', + content: + '', + }, + { + name: 'pattern-b', + blockTypes: [ 'test/block-b' ], + title: 'pattern b', + content: + '', + }, + { + title: 'pattern c', + blockTypes: [ 'test/block-a' ], + content: + '', + }, + ], + } ); + await dispatch( store ).updateBlockListSettings( 'block1', { + allowedBlocks: [ 'core/test-block-b' ], + } ); + } ); + + afterAll( async () => { + await dispatch( store ).resetBlocks( [] ); + await dispatch( store ).updateSettings( { + __experimentalBlockPatterns: [], + } ); + await dispatch( store ).updateBlockListSettings( 'block1', {} ); + } ); + + it( 'should return empty array if no block name is provided', () => { + expect( select( store ).getPatternsByBlockTypes() ).toEqual( [] ); + } ); + it( 'should return empty array if no match is found', () => { + const patterns = select( store ).getPatternsByBlockTypes( + 'test/block-not-exists' + ); + expect( patterns ).toEqual( [] ); + } ); + it( 'should return the same empty array in both empty array cases', () => { + const patterns1 = select( store ).getPatternsByBlockTypes(); + const patterns2 = select( store ).getPatternsByBlockTypes( + 'test/block-not-exists' + ); + expect( patterns1 ).toBe( patterns2 ); + } ); + it( 'should return proper results when there are matched block patterns', () => { + const patterns = + select( store ).getPatternsByBlockTypes( 'test/block-a' ); + expect( patterns ).toHaveLength( 2 ); + expect( patterns ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { title: 'pattern a' } ), + expect.objectContaining( { title: 'pattern c' } ), + ] ) + ); + } ); + it( 'should return proper result with matched patterns and allowed blocks from rootClientId', () => { + const patterns = select( store ).getPatternsByBlockTypes( + 'test/block-a', + 'block1' + ); + expect( patterns ).toHaveLength( 1 ); + expect( patterns[ 0 ] ).toEqual( + expect.objectContaining( { title: 'pattern c' } ) + ); + } ); + } ); + + describe( '__experimentalGetPatternTransformItems', () => { + beforeAll( async () => { + await dispatch( store ).resetBlocks( [ + { + clientId: 'block1', + name: 'core/test-block-a', + innerBlocks: [], + }, + { + clientId: 'block2', + name: 'core/test-block-b', + innerBlocks: [], + }, + ] ); + await dispatch( store ).setHasControlledInnerBlocks( + 'block2-clientId', + true + ); + await dispatch( store ).updateSettings( { + __experimentalBlockPatterns: [ + { + name: 'pattern-a', + blockTypes: [ 'test/block-a' ], + title: 'pattern a', + content: + '', + }, + { + name: 'pattern-b', + blockTypes: [ 'test/block-b' ], + title: 'pattern b', + content: + '', + }, + { + name: 'pattern-c', + title: 'pattern c', + blockTypes: [ 'test/block-a' ], + content: + '', + }, + { + name: 'pattern-mix', + title: 'pattern mix', + blockTypes: [ + 'core/test-block-a', + 'core/test-block-b', + ], + content: + '', + }, + ], + } ); + } ); + + afterAll( async () => { + await dispatch( store ).resetBlocks( [] ); + await dispatch( store ).updateSettings( { + __experimentalBlockPatterns: [], + } ); + await dispatch( store ).updateBlockListSettings( 'block1', {} ); + } ); + + describe( 'should return empty array', () => { + it( 'when no blocks are selected', () => { + expect( + select( store ).__experimentalGetPatternTransformItems() + ).toEqual( [] ); + } ); + it( 'when a selected block has inner blocks', () => { + const blocks = [ + { name: 'core/test-block-a', innerBlocks: [] }, + { + name: 'core/test-block-b', + innerBlocks: [ { name: 'some inner block' } ], + }, + ]; + expect( + select( store ).__experimentalGetPatternTransformItems( + blocks + ) + ).toEqual( [] ); + } ); + it( 'when a selected block has controlled inner blocks', () => { + const blocks = [ + { name: 'core/test-block-a', innerBlocks: [] }, + { + name: 'core/test-block-b', + clientId: 'block2-clientId', + innerBlocks: [], + }, + ]; + expect( + select( store ).__experimentalGetPatternTransformItems( + blocks + ) + ).toEqual( [] ); + } ); + it( 'when no patterns are available based on the selected blocks', () => { + const blocks = [ + { name: 'block-with-no-patterns', innerBlocks: [] }, + ]; + expect( + select( store ).__experimentalGetPatternTransformItems( + blocks + ) + ).toEqual( [] ); + } ); + } ); + describe( 'should return proper results', () => { + it( 'when a single block is selected', () => { + const blocks = [ + { name: 'core/test-block-b', innerBlocks: [] }, + ]; + const patterns = + select( store ).__experimentalGetPatternTransformItems( + blocks + ); + expect( patterns ).toHaveLength( 1 ); + expect( patterns[ 0 ] ).toEqual( + expect.objectContaining( { + name: 'pattern-mix', + } ) + ); + } ); + it( 'when different multiple blocks are selected', () => { + const blocks = [ + { name: 'core/test-block-b', innerBlocks: [] }, + { name: 'test/block-b', innerBlocks: [] }, + { name: 'some other block', innerBlocks: [] }, + ]; + const patterns = + select( store ).__experimentalGetPatternTransformItems( + blocks + ); + expect( patterns ).toHaveLength( 2 ); + expect( patterns ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + name: 'pattern-mix', + } ), + expect.objectContaining( { + name: 'pattern-b', + } ), + ] ) + ); + } ); + it( 'when multiple blocks are selected containing multiple times the same block', () => { + const blocks = [ + { name: 'core/test-block-b', innerBlocks: [] }, + { name: 'some other block', innerBlocks: [] }, + { name: 'core/test-block-a', innerBlocks: [] }, + { name: 'core/test-block-b', innerBlocks: [] }, + ]; + const patterns = + select( store ).__experimentalGetPatternTransformItems( + blocks + ); + expect( patterns ).toHaveLength( 1 ); + expect( patterns[ 0 ] ).toEqual( + expect.objectContaining( { + name: 'pattern-mix', + } ) + ); + } ); + } ); + } ); +} ); diff --git a/packages/block-editor/src/store/test/selectors.js b/packages/block-editor/src/store/test/selectors.js index 5046b6e5b83d68..29833611b17f4a 100644 --- a/packages/block-editor/src/store/test/selectors.js +++ b/packages/block-editor/src/store/test/selectors.js @@ -65,14 +65,10 @@ const { __experimentalGetLastBlockAttributeChanges, getLowestCommonAncestorWithSelectedBlock, __experimentalGetActiveBlockIdByBlockNames: getActiveBlockIdByBlockNames, - __experimentalGetAllowedPatterns, - __experimentalGetParsedPattern, - getPatternsByBlockTypes, __unstableGetClientIdWithClientIdsTree, __unstableGetClientIdsTree, - __experimentalGetPatternTransformItems, wasBlockJustInserted, - __experimentalGetGlobalBlocksByName, + getBlocksByName, getBlockEditingMode, } = selectors; @@ -975,7 +971,7 @@ describe( 'selectors', () => { } ); } ); - describe( '__experimentalGetGlobalBlocksByName', () => { + describe( 'getBlocksByName', () => { const state = { blocks: { byClientId: new Map( @@ -1017,31 +1013,25 @@ describe( 'selectors', () => { }; it( 'should return the clientIds of blocks of a given type', () => { - expect( - __experimentalGetGlobalBlocksByName( state, 'core/heading' ) - ).toStrictEqual( [ '123' ] ); + expect( getBlocksByName( state, 'core/heading' ) ).toStrictEqual( [ + '123', + ] ); } ); it( 'should return the clientIds of blocks of a given type even if blocks are nested', () => { - expect( - __experimentalGetGlobalBlocksByName( state, 'core/paragraph' ) - ).toStrictEqual( [ '456', '1415', '1213' ] ); + expect( getBlocksByName( state, 'core/paragraph' ) ).toStrictEqual( + [ '456', '1415', '1213' ] + ); } ); it( 'Should return empty array if no blocks match. The empty array should be the same reference', () => { - const result = __experimentalGetGlobalBlocksByName( - state, - 'test/missing' + const result = getBlocksByName( state, 'test/missing' ); + expect( getBlocksByName( state, 'test/missing' ) ).toStrictEqual( + [] + ); + expect( getBlocksByName( state, 'test/missing2' ) === result ).toBe( + true ); - expect( - __experimentalGetGlobalBlocksByName( state, 'test/missing' ) - ).toStrictEqual( [] ); - expect( - __experimentalGetGlobalBlocksByName( - state, - 'test/missing2' - ) === result - ).toBe( true ); } ); } ); @@ -4205,382 +4195,6 @@ describe( 'selectors', () => { } ); } ); - describe( '__experimentalGetAllowedPatterns', () => { - const state = { - blocks: { - byClientId: new Map( - Object.entries( { - block1: { name: 'core/test-block-a' }, - block2: { name: 'core/test-block-b' }, - } ) - ), - attributes: new Map( - Object.entries( { - block1: {}, - block2: {}, - } ) - ), - parents: new Map( - Object.entries( { - block1: '', - block2: '', - } ) - ), - }, - blockListSettings: { - block1: { - allowedBlocks: [ 'core/test-block-b' ], - }, - block2: { - allowedBlocks: [], - }, - }, - settings: { - __experimentalBlockPatterns: [ - { - name: 'pattern-a', - title: 'pattern with a', - content: ``, - }, - { - name: 'pattern-b', - title: 'pattern with b', - content: - '', - }, - { - name: 'pattern-c', - title: 'pattern hidden from UI', - inserter: false, - content: - '', - }, - ], - }, - blockEditingModes: new Map(), - }; - - it( 'should return all patterns for root level', () => { - expect( - __experimentalGetAllowedPatterns( state, null ) - ).toHaveLength( 2 ); - } ); - - it( 'should return patterns that consists of blocks allowed for the specified client ID', () => { - expect( - __experimentalGetAllowedPatterns( state, 'block1' ) - ).toHaveLength( 1 ); - - expect( - __experimentalGetAllowedPatterns( state, 'block2' ) - ).toHaveLength( 0 ); - } ); - it( 'should return empty array if only patterns hidden from UI exist', () => { - expect( - __experimentalGetAllowedPatterns( { - blocks: { byClientId: new Map() }, - blockListSettings: {}, - settings: { - __experimentalBlockPatterns: [ - { - name: 'pattern-c', - title: 'pattern hidden from UI', - inserter: false, - content: - '', - }, - ], - }, - } ) - ).toHaveLength( 0 ); - } ); - } ); - describe( '__experimentalGetParsedPattern', () => { - const state = { - settings: { - __experimentalBlockPatterns: [ - { - name: 'pattern-a', - title: 'pattern with a', - content: ``, - }, - { - name: 'pattern-hidden-from-ui', - title: 'pattern hidden from UI', - inserter: false, - content: - '', - }, - ], - }, - }; - it( 'should return proper results when pattern does not exist', () => { - expect( - __experimentalGetParsedPattern( state, 'not there' ) - ).toBeNull(); - } ); - it( 'should return existing pattern properly parsed', () => { - const { name, blocks } = __experimentalGetParsedPattern( - state, - 'pattern-a' - ); - expect( name ).toEqual( 'pattern-a' ); - expect( blocks ).toHaveLength( 1 ); - expect( blocks[ 0 ] ).toEqual( - expect.objectContaining( { - name: 'core/test-block-a', - } ) - ); - } ); - it( 'should return hidden from UI pattern when requested', () => { - const { name, blocks, inserter } = __experimentalGetParsedPattern( - state, - 'pattern-hidden-from-ui' - ); - expect( name ).toEqual( 'pattern-hidden-from-ui' ); - expect( inserter ).toBeFalsy(); - expect( blocks ).toHaveLength( 2 ); - expect( blocks[ 0 ] ).toEqual( - expect.objectContaining( { - name: 'core/test-block-a', - } ) - ); - } ); - } ); - describe( 'getPatternsByBlockTypes', () => { - const state = { - blocks: { - byClientId: new Map( - Object.entries( { - block1: { name: 'core/test-block-a' }, - } ) - ), - parents: new Map(), - }, - blockListSettings: { - block1: { - allowedBlocks: [ 'core/test-block-b' ], - }, - }, - settings: { - __experimentalBlockPatterns: [ - { - name: 'pattern-a', - blockTypes: [ 'test/block-a' ], - title: 'pattern a', - content: - '', - }, - { - name: 'pattern-b', - blockTypes: [ 'test/block-b' ], - title: 'pattern b', - content: - '', - }, - { - title: 'pattern c', - blockTypes: [ 'test/block-a' ], - content: - '', - }, - ], - }, - blockEditingModes: new Map(), - }; - it( 'should return empty array if no block name is provided', () => { - expect( getPatternsByBlockTypes( state ) ).toEqual( [] ); - } ); - it( 'should return empty array if no match is found', () => { - const patterns = getPatternsByBlockTypes( - state, - 'test/block-not-exists' - ); - expect( patterns ).toEqual( [] ); - } ); - it( 'should return the same empty array in both empty array cases', () => { - const patterns1 = getPatternsByBlockTypes( state ); - const patterns2 = getPatternsByBlockTypes( - state, - 'test/block-not-exists' - ); - expect( patterns1 ).toBe( patterns2 ); - } ); - it( 'should return proper results when there are matched block patterns', () => { - const patterns = getPatternsByBlockTypes( state, 'test/block-a' ); - expect( patterns ).toHaveLength( 2 ); - expect( patterns ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { title: 'pattern a' } ), - expect.objectContaining( { title: 'pattern c' } ), - ] ) - ); - } ); - it( 'should return proper result with matched patterns and allowed blocks from rootClientId', () => { - const patterns = getPatternsByBlockTypes( - state, - 'test/block-a', - 'block1' - ); - expect( patterns ).toHaveLength( 1 ); - expect( patterns[ 0 ] ).toEqual( - expect.objectContaining( { title: 'pattern c' } ) - ); - } ); - } ); - describe( '__experimentalGetPatternTransformItems', () => { - const state = { - blocks: { - byClientId: new Map( - Object.entries( { - block1: { name: 'core/test-block-a' }, - block2: { name: 'core/test-block-b' }, - } ) - ), - parents: new Map(), - controlledInnerBlocks: { 'block2-clientId': true }, - }, - blockListSettings: { - block1: { - allowedBlocks: [ 'core/test-block-b' ], - }, - }, - settings: { - __experimentalBlockPatterns: [ - { - name: 'pattern-a', - blockTypes: [ 'test/block-a' ], - title: 'pattern a', - content: - '', - }, - { - name: 'pattern-b', - blockTypes: [ 'test/block-b' ], - title: 'pattern b', - content: - '', - }, - { - name: 'pattern-c', - title: 'pattern c', - blockTypes: [ 'test/block-a' ], - content: - '', - }, - { - name: 'pattern-mix', - title: 'pattern mix', - blockTypes: [ - 'core/test-block-a', - 'core/test-block-b', - ], - content: - '', - }, - ], - }, - blockEditingModes: new Map(), - }; - describe( 'should return empty array', () => { - it( 'when no blocks are selected', () => { - expect( - __experimentalGetPatternTransformItems( state ) - ).toEqual( [] ); - } ); - it( 'when a selected block has inner blocks', () => { - const blocks = [ - { name: 'core/test-block-a', innerBlocks: [] }, - { - name: 'core/test-block-b', - innerBlocks: [ { name: 'some inner block' } ], - }, - ]; - expect( - __experimentalGetPatternTransformItems( state, blocks ) - ).toEqual( [] ); - } ); - it( 'when a selected block has controlled inner blocks', () => { - const blocks = [ - { name: 'core/test-block-a', innerBlocks: [] }, - { - name: 'core/test-block-b', - clientId: 'block2-clientId', - innerBlocks: [], - }, - ]; - expect( - __experimentalGetPatternTransformItems( state, blocks ) - ).toEqual( [] ); - } ); - it( 'when no patterns are available based on the selected blocks', () => { - const blocks = [ - { name: 'block-with-no-patterns', innerBlocks: [] }, - ]; - expect( - __experimentalGetPatternTransformItems( state, blocks ) - ).toEqual( [] ); - } ); - } ); - describe( 'should return proper results', () => { - it( 'when a single block is selected', () => { - const blocks = [ - { name: 'core/test-block-b', innerBlocks: [] }, - ]; - const patterns = __experimentalGetPatternTransformItems( - state, - blocks - ); - expect( patterns ).toHaveLength( 1 ); - expect( patterns[ 0 ] ).toEqual( - expect.objectContaining( { - name: 'pattern-mix', - } ) - ); - } ); - it( 'when different multiple blocks are selected', () => { - const blocks = [ - { name: 'core/test-block-b', innerBlocks: [] }, - { name: 'test/block-b', innerBlocks: [] }, - { name: 'some other block', innerBlocks: [] }, - ]; - const patterns = __experimentalGetPatternTransformItems( - state, - blocks - ); - expect( patterns ).toHaveLength( 2 ); - expect( patterns ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - name: 'pattern-mix', - } ), - expect.objectContaining( { - name: 'pattern-b', - } ), - ] ) - ); - } ); - it( 'when multiple blocks are selected containing multiple times the same block', () => { - const blocks = [ - { name: 'core/test-block-b', innerBlocks: [] }, - { name: 'some other block', innerBlocks: [] }, - { name: 'core/test-block-a', innerBlocks: [] }, - { name: 'core/test-block-b', innerBlocks: [] }, - ]; - const patterns = __experimentalGetPatternTransformItems( - state, - blocks - ); - expect( patterns ).toHaveLength( 1 ); - expect( patterns[ 0 ] ).toEqual( - expect.objectContaining( { - name: 'pattern-mix', - } ) - ); - } ); - } ); - } ); - describe( 'wasBlockJustInserted', () => { it( 'should return true if the client id passed to wasBlockJustInserted is found within the state', () => { const expectedClientId = '62bfef6e-d5e9-43ba-b7f9-c77cf354141f'; diff --git a/packages/block-editor/src/store/utils.js b/packages/block-editor/src/store/utils.js index 7587dcdf56fd79..6cde56da1b55a7 100644 --- a/packages/block-editor/src/store/utils.js +++ b/packages/block-editor/src/store/utils.js @@ -1,53 +1,3 @@ -/** - * External dependencies - */ -import createSelector from 'rememo'; - -/** - * Internal dependencies - */ -import { INSERTER_PATTERN_TYPES } from '../components/inserter/block-patterns-tab/utils'; - -export const getUserPatterns = createSelector( - ( state ) => { - const userPatterns = state.settings.__experimentalReusableBlocks ?? []; - const userPatternCategories = - state.settings.__experimentalUserPatternCategories ?? []; - return userPatterns.map( ( userPattern ) => { - return { - name: `core/block/${ userPattern.id }`, - id: userPattern.id, - type: INSERTER_PATTERN_TYPES.user, - title: userPattern.title.raw, - categories: userPattern.wp_pattern_category.map( ( catId ) => { - const category = userPatternCategories.find( - ( { id } ) => id === catId - ); - return category ? category.slug : catId; - } ), - content: userPattern.content.raw, - syncStatus: userPattern.wp_pattern_sync_status, - }; - } ); - }, - ( state ) => [ - state.settings.__experimentalReusableBlocks, - state.settings.__experimentalUserPatternCategories, - ] -); - -export const getAllPatterns = createSelector( - ( state ) => { - const patterns = state.settings.__experimentalBlockPatterns; - const userPatterns = getUserPatterns( state ); - return [ ...userPatterns, ...patterns ]; - }, - ( state ) => [ - state.settings.__experimentalBlockPatterns, - getUserPatterns( state ), - ] -); - export const checkAllowList = ( list, item, defaultResult = null ) => { if ( typeof list === 'boolean' ) { return list; @@ -89,3 +39,13 @@ export const checkAllowListRecursive = ( blocks, allowedBlockTypes ) => { return true; }; + +export const getAllPatternsDependants = ( state ) => { + return [ + state.settings.__experimentalBlockPatterns, + state.settings.__experimentalUserPatternCategories, + state.settings.__experimentalReusableBlocks, + state.settings.__experimentalFetchBlockPatterns, + state.blockPatterns, + ]; +}; diff --git a/packages/block-editor/src/style.scss b/packages/block-editor/src/style.scss index 2d7b1547394452..43fb047710b8d1 100644 --- a/packages/block-editor/src/style.scss +++ b/packages/block-editor/src/style.scss @@ -24,7 +24,6 @@ @import "./components/block-variation-transforms/style.scss"; @import "./components/border-radius-control/style.scss"; @import "./components/colors-gradients/style.scss"; -@import "./components/contrast-checker/style.scss"; @import "./components/date-format-picker/style.scss"; @import "./components/duotone-control/style.scss"; @import "./components/font-appearance-control/style.scss"; diff --git a/packages/block-editor/src/utils/block-variation-transforms.js b/packages/block-editor/src/utils/block-variation-transforms.js deleted file mode 100644 index 15b644bd235ee9..00000000000000 --- a/packages/block-editor/src/utils/block-variation-transforms.js +++ /dev/null @@ -1,38 +0,0 @@ -/** @typedef {import('@wordpress/blocks').WPBlockVariation} WPBlockVariation */ - -function matchesAttributes( blockAttributes, variation ) { - return Object.entries( variation ).every( ( [ key, value ] ) => { - if ( - typeof value === 'object' && - typeof blockAttributes[ key ] === 'object' - ) { - return matchesAttributes( blockAttributes[ key ], value ); - } - return blockAttributes[ key ] === value; - } ); -} - -/** - * Matches the provided block variations with a block's attributes. If no match - * or more than one matches are found it returns `undefined`. If a single match is - * found it returns it. - * - * This is a simple implementation for now as it takes into account only the attributes - * of a block variation and not `InnerBlocks`. - * - * @param {Object} blockAttributes - The block attributes to try to find a match. - * @param {WPBlockVariation[]} variations - A list of block variations to test for a match. - * @return {WPBlockVariation | undefined} - If a match is found returns it. If not or more than one matches are found returns `undefined`. - */ -export const __experimentalGetMatchingVariation = ( - blockAttributes, - variations -) => { - if ( ! variations || ! blockAttributes ) return; - const matches = variations.filter( ( { attributes } ) => { - if ( ! attributes || ! Object.keys( attributes ).length ) return false; - return matchesAttributes( blockAttributes, attributes ); - } ); - if ( matches.length !== 1 ) return; - return matches[ 0 ]; -}; diff --git a/packages/block-editor/src/utils/index.js b/packages/block-editor/src/utils/index.js index ee3b2692b369a8..6f53ba585e5ecb 100644 --- a/packages/block-editor/src/utils/index.js +++ b/packages/block-editor/src/utils/index.js @@ -1,3 +1,2 @@ export { default as transformStyles } from './transform-styles'; -export * from './block-variation-transforms'; export { default as getPxFromCssUnit } from './get-px-from-css-unit'; diff --git a/packages/block-editor/src/utils/math.js b/packages/block-editor/src/utils/math.js index 128972e8a400e1..5d8ed97ffefb29 100644 --- a/packages/block-editor/src/utils/math.js +++ b/packages/block-editor/src/utils/math.js @@ -106,3 +106,15 @@ export function isPointContainedByRect( point, rect ) { rect.bottom >= point.y ); } + +/** + * Is the point within the top and bottom boundaries of the rectangle. + * + * @param {WPPoint} point The point. + * @param {DOMRect} rect The rectangle. + * + * @return {boolean} True if the point is within top and bottom of rectangle, false otherwise. + */ +export function isPointWithinTopAndBottomBoundariesOfRect( point, rect ) { + return rect.top <= point.y && rect.bottom >= point.y; +} diff --git a/packages/block-editor/src/utils/test/block-variation-transforms.js b/packages/block-editor/src/utils/test/block-variation-transforms.js deleted file mode 100644 index 47844ba0309426..00000000000000 --- a/packages/block-editor/src/utils/test/block-variation-transforms.js +++ /dev/null @@ -1,94 +0,0 @@ -/** - * Internal dependencies - */ -import { __experimentalGetMatchingVariation as getMatchingVariation } from '../block-variation-transforms'; - -describe( 'getMatchingVariation', () => { - describe( 'should not find a match', () => { - it( 'when no variations or attributes passed', () => { - expect( - getMatchingVariation( null, { content: 'hi' } ) - ).toBeUndefined(); - expect( getMatchingVariation( {} ) ).toBeUndefined(); - } ); - it( 'when no variation matched', () => { - const variations = [ - { name: 'one', attributes: { level: 1 } }, - { name: 'two', attributes: { level: 2 } }, - ]; - expect( - getMatchingVariation( { level: 4 }, variations ) - ).toBeUndefined(); - } ); - it( 'when more than one match found', () => { - const variations = [ - { name: 'one', attributes: { level: 1 } }, - { name: 'two', attributes: { level: 1, content: 'hi' } }, - ]; - expect( - getMatchingVariation( - { level: 1, content: 'hi', other: 'prop' }, - variations - ) - ).toBeUndefined(); - } ); - it( 'when variation is a superset of attributes', () => { - const variations = [ - { name: 'one', attributes: { level: 1, content: 'hi' } }, - ]; - expect( - getMatchingVariation( { level: 1, other: 'prop' }, variations ) - ).toBeUndefined(); - } ); - it( 'when variation has a nested attribute', () => { - const variations = [ - { name: 'one', attributes: { query: { author: 'somebody' } } }, - { name: 'two', attributes: { query: { author: 'nobody' } } }, - ]; - expect( - getMatchingVariation( - { query: { author: 'foobar' }, other: 'prop' }, - variations - ) - ).toBeUndefined(); - } ); - } ); - describe( 'should find a match', () => { - it( 'when variation has one attribute', () => { - const variations = [ - { name: 'one', attributes: { level: 1 } }, - { name: 'two', attributes: { level: 2 } }, - ]; - expect( - getMatchingVariation( - { level: 2, content: 'hi', other: 'prop' }, - variations - ).name - ).toEqual( 'two' ); - } ); - it( 'when variation has many attributes', () => { - const variations = [ - { name: 'one', attributes: { level: 1, content: 'hi' } }, - { name: 'two', attributes: { level: 2 } }, - ]; - expect( - getMatchingVariation( - { level: 1, content: 'hi', other: 'prop' }, - variations - ).name - ).toEqual( 'one' ); - } ); - it( 'when variation has a nested attribute', () => { - const variations = [ - { name: 'one', attributes: { query: { author: 'somebody' } } }, - { name: 'two', attributes: { query: { author: 'nobody' } } }, - ]; - expect( - getMatchingVariation( - { query: { author: 'somebody' }, other: 'prop' }, - variations - ).name - ).toEqual( 'one' ); - } ); - } ); -} ); diff --git a/packages/block-library/CHANGELOG.md b/packages/block-library/CHANGELOG.md index d17ee65991c0a3..fd547071ca01fa 100644 --- a/packages/block-library/CHANGELOG.md +++ b/packages/block-library/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 8.27.0 (2024-01-24) + ## 8.26.0 (2024-01-10) ## 8.25.0 (2023-12-13) diff --git a/packages/block-library/package.json b/packages/block-library/package.json index 12f4e23d9da4d0..50d78be299b224 100644 --- a/packages/block-library/package.json +++ b/packages/block-library/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-library", - "version": "8.26.0", + "version": "8.27.0", "description": "Block library for the WordPress editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", @@ -51,6 +51,7 @@ "@wordpress/i18n": "file:../i18n", "@wordpress/icons": "file:../icons", "@wordpress/interactivity": "file:../interactivity", + "@wordpress/interactivity-router": "file:../interactivity-router", "@wordpress/keycodes": "file:../keycodes", "@wordpress/notices": "file:../notices", "@wordpress/patterns": "file:../patterns", diff --git a/packages/block-library/src/avatar/edit.js b/packages/block-library/src/avatar/edit.js index 8b326f4e72d88a..8726d0cf2c0df2 100644 --- a/packages/block-library/src/avatar/edit.js +++ b/packages/block-library/src/avatar/edit.js @@ -192,22 +192,12 @@ const UserEdit = ( { attributes, context, setAttributes, isSelected } ) => { avatar={ avatar } setAttributes={ setAttributes } /> -
- { attributes.isLink ? ( - event.preventDefault() } - > - - - ) : ( + { attributes.isLink ? ( + event.preventDefault() } + > { isSelected={ isSelected } setAttributes={ setAttributes } /> - ) } -
+ + ) : ( + + ) } ); }; diff --git a/packages/block-library/src/block/block.json b/packages/block-library/src/block/block.json index aeccdbfc1051db..b30c865e57a7f6 100644 --- a/packages/block-library/src/block/block.json +++ b/packages/block-library/src/block/block.json @@ -10,6 +10,9 @@ "attributes": { "ref": { "type": "number" + }, + "overrides": { + "type": "object" } }, "supports": { diff --git a/packages/block-library/src/block/edit.js b/packages/block-library/src/block/edit.js index c53f52bfb703e9..2d517983c3fe12 100644 --- a/packages/block-library/src/block/edit.js +++ b/packages/block-library/src/block/edit.js @@ -18,8 +18,8 @@ import { import { __ } from '@wordpress/i18n'; import { useInnerBlocksProps, - __experimentalRecursionProvider as RecursionProvider, - __experimentalUseHasRecursion as useHasRecursion, + RecursionProvider, + useHasRecursion, InnerBlocks, useBlockProps, Warning, @@ -88,6 +88,26 @@ const useInferredLayout = ( blocks, parentLayout ) => { }, [ blocks, parentLayout ] ); }; +/** + * Enum for patch operations. + * We use integers here to minimize the size of the serialized data. + * This has to be deserialized accordingly on the server side. + * See block-bindings/sources/pattern.php + */ +const PATCH_OPERATIONS = { + /** @type {0} */ + Remove: 0, + /** @type {1} */ + Replace: 1, + // Other operations are reserved for future use. (e.g. Add) +}; + +/** + * @typedef {[typeof PATCH_OPERATIONS.Remove]} RemovePatch + * @typedef {[typeof PATCH_OPERATIONS.Replace, unknown]} ReplacePatch + * @typedef {RemovePatch | ReplacePatch} OverridePatch + */ + function applyInitialOverrides( blocks, overrides = {}, defaultValues ) { return blocks.map( ( block ) => { const innerBlocks = applyInitialOverrides( @@ -104,9 +124,15 @@ function applyInitialOverrides( blocks, overrides = {}, defaultValues ) { defaultValues[ blockId ] ??= {}; defaultValues[ blockId ][ attributeKey ] = block.attributes[ attributeKey ]; - if ( overrides[ blockId ]?.[ attributeKey ] !== undefined ) { - newAttributes[ attributeKey ] = - overrides[ blockId ][ attributeKey ]; + /** @type {OverridePatch} */ + const overrideAttribute = overrides[ blockId ]?.[ attributeKey ]; + if ( ! overrideAttribute ) { + continue; + } + if ( overrideAttribute[ 0 ] === PATCH_OPERATIONS.Remove ) { + delete newAttributes[ attributeKey ]; + } else if ( overrideAttribute[ 0 ] === PATCH_OPERATIONS.Replace ) { + newAttributes[ attributeKey ] = overrideAttribute[ 1 ]; } } return { @@ -118,13 +144,14 @@ function applyInitialOverrides( blocks, overrides = {}, defaultValues ) { } function getOverridesFromBlocks( blocks, defaultValues ) { - /** @type {Record>} */ + /** @type {Record>} */ const overrides = {}; for ( const block of blocks ) { Object.assign( overrides, getOverridesFromBlocks( block.innerBlocks, defaultValues ) ); + /** @type {string} */ const blockId = block.attributes.metadata?.id; if ( ! isPartiallySynced( block ) || ! blockId ) continue; const attributes = getPartiallySyncedAttributes( block ); @@ -134,10 +161,23 @@ function getOverridesFromBlocks( blocks, defaultValues ) { defaultValues[ blockId ][ attributeKey ] ) { overrides[ blockId ] ??= {}; - // TODO: We need a way to represent `undefined` in the serialized overrides. - // Also see: https://github.com/WordPress/gutenberg/pull/57249#discussion_r1452987871 - overrides[ blockId ][ attributeKey ] = - block.attributes[ attributeKey ]; + /** + * Create a patch operation for the binding attribute. + * We use a tuple here to minimize the size of the serialized data. + * The first item is the operation type, the second item is the value if any. + */ + if ( block.attributes[ attributeKey ] === undefined ) { + /** @type {RemovePatch} */ + overrides[ blockId ][ attributeKey ] = [ + PATCH_OPERATIONS.Remove, + ]; + } else { + /** @type {ReplacePatch} */ + overrides[ blockId ][ attributeKey ] = [ + PATCH_OPERATIONS.Replace, + block.attributes[ attributeKey ], + ]; + } } } } diff --git a/packages/block-library/src/block/v1/edit.native.js b/packages/block-library/src/block/edit.native.js similarity index 96% rename from packages/block-library/src/block/v1/edit.native.js rename to packages/block-library/src/block/edit.native.js index 3a649921b3dda1..ae8c8315aa2e88 100644 --- a/packages/block-library/src/block/v1/edit.native.js +++ b/packages/block-library/src/block/edit.native.js @@ -27,8 +27,8 @@ import { import { useSelect, useDispatch } from '@wordpress/data'; import { __, sprintf } from '@wordpress/i18n'; import { - __experimentalRecursionProvider as RecursionProvider, - __experimentalUseHasRecursion as useHasRecursion, + RecursionProvider, + useHasRecursion, InnerBlocks, Warning, store as blockEditorStore, @@ -42,8 +42,8 @@ import { store as noticesStore } from '@wordpress/notices'; /** * Internal dependencies */ -import styles from '../editor.scss'; -import EditTitle from '../edit-title'; +import styles from './editor.scss'; +import EditTitle from './edit-title'; export default function ReusableBlockEdit( { attributes: { ref }, diff --git a/packages/block-library/src/block/index.js b/packages/block-library/src/block/index.js index 0d117e6f3938ab..95e090f0afd6ad 100644 --- a/packages/block-library/src/block/index.js +++ b/packages/block-library/src/block/index.js @@ -8,15 +8,14 @@ import { symbol as icon } from '@wordpress/icons'; */ import initBlock from '../utils/init-block'; import metadata from './block.json'; -import editV1 from './v1/edit'; -import editV2 from './edit'; +import edit from './edit'; const { name } = metadata; export { metadata, name }; export const settings = { - edit: window.__experimentalPatternPartialSyncing ? editV2 : editV1, + edit, icon, }; diff --git a/packages/block-library/src/block/index.php b/packages/block-library/src/block/index.php index 54b54fad139ff3..444001fa498595 100644 --- a/packages/block-library/src/block/index.php +++ b/packages/block-library/src/block/index.php @@ -46,17 +46,14 @@ function render_block_core_block( $attributes ) { $content = $wp_embed->run_shortcode( $reusable_block->post_content ); $content = $wp_embed->autoembed( $content ); - $gutenberg_experiments = get_option( 'gutenberg-experiments' ); - $has_partial_synced_overrides = $gutenberg_experiments - && array_key_exists( 'gutenberg-pattern-partial-syncing', $gutenberg_experiments ) - && isset( $attributes['overrides'] ); + $has_pattern_overrides = isset( $attributes['overrides'] ); /** * We set the `pattern/overrides` context through the `render_block_context` * filter so that it is available when a pattern's inner blocks are * rendering via do_blocks given it only receives the inner content. */ - if ( $has_partial_synced_overrides ) { + if ( $has_pattern_overrides ) { $filter_block_context = static function ( $context ) use ( $attributes ) { $context['pattern/overrides'] = $attributes['overrides']; return $context; @@ -67,7 +64,7 @@ function render_block_core_block( $attributes ) { $content = do_blocks( $content ); unset( $seen_refs[ $attributes['ref'] ] ); - if ( $has_partial_synced_overrides ) { + if ( $has_pattern_overrides ) { remove_filter( 'render_block_context', $filter_block_context, 1 ); } @@ -86,28 +83,3 @@ function register_block_core_block() { ); } add_action( 'init', 'register_block_core_block' ); - -$gutenberg_experiments = get_option( 'gutenberg-experiments' ); -if ( $gutenberg_experiments && array_key_exists( 'gutenberg-pattern-partial-syncing', $gutenberg_experiments ) ) { - /** - * Registers the overrides attribute for core/block. - * - * @param array $args Array of arguments for registering a block type. - * @param string $block_name Block name including namespace. - * @return array $args - */ - function register_block_core_block_args( $args, $block_name ) { - if ( 'core/block' === $block_name ) { - $args['attributes'] = array_merge( - $args['attributes'], - array( - 'overrides' => array( - 'type' => 'object', - ), - ) - ); - } - return $args; - } - add_filter( 'register_block_type_args', 'register_block_core_block_args', 10, 2 ); -} diff --git a/packages/block-library/src/block/v1/edit.js b/packages/block-library/src/block/v1/edit.js deleted file mode 100644 index 5975711376c650..00000000000000 --- a/packages/block-library/src/block/v1/edit.js +++ /dev/null @@ -1,163 +0,0 @@ -/** - * External dependencies - */ -import classnames from 'classnames'; - -/** - * WordPress dependencies - */ -import { - useEntityBlockEditor, - useEntityProp, - useEntityRecord, -} from '@wordpress/core-data'; -import { - Placeholder, - Spinner, - TextControl, - PanelBody, -} from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; -import { - useInnerBlocksProps, - __experimentalRecursionProvider as RecursionProvider, - __experimentalUseHasRecursion as useHasRecursion, - InnerBlocks, - InspectorControls, - useBlockProps, - Warning, - privateApis as blockEditorPrivateApis, -} from '@wordpress/block-editor'; -import { useRef, useMemo } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import { unlock } from '../../lock-unlock'; - -const { useLayoutClasses } = unlock( blockEditorPrivateApis ); -const fullAlignments = [ 'full', 'wide', 'left', 'right' ]; - -const useInferredLayout = ( blocks, parentLayout ) => { - const initialInferredAlignmentRef = useRef(); - - return useMemo( () => { - // Exit early if the pattern's blocks haven't loaded yet. - if ( ! blocks?.length ) { - return {}; - } - - let alignment = initialInferredAlignmentRef.current; - - // Only track the initial alignment so that temporarily removed - // alignments can be reapplied. - if ( alignment === undefined ) { - const isConstrained = parentLayout?.type === 'constrained'; - const hasFullAlignment = blocks.some( ( block ) => - fullAlignments.includes( block.attributes.align ) - ); - - alignment = isConstrained && hasFullAlignment ? 'full' : null; - initialInferredAlignmentRef.current = alignment; - } - - const layout = alignment ? parentLayout : undefined; - - return { alignment, layout }; - }, [ blocks, parentLayout ] ); -}; - -export default function ReusableBlockEdit( { - name, - attributes: { ref }, - __unstableParentLayout: parentLayout, -} ) { - const hasAlreadyRendered = useHasRecursion( ref ); - const { record, hasResolved } = useEntityRecord( - 'postType', - 'wp_block', - ref - ); - const isMissing = hasResolved && ! record; - - const [ blocks, onInput, onChange ] = useEntityBlockEditor( - 'postType', - 'wp_block', - { id: ref } - ); - - const [ title, setTitle ] = useEntityProp( - 'postType', - 'wp_block', - 'title', - ref - ); - - const { alignment, layout } = useInferredLayout( blocks, parentLayout ); - const layoutClasses = useLayoutClasses( { layout }, name ); - - const blockProps = useBlockProps( { - className: classnames( - 'block-library-block__reusable-block-container', - layout && layoutClasses, - { [ `align${ alignment }` ]: alignment } - ), - } ); - - const innerBlocksProps = useInnerBlocksProps( blockProps, { - value: blocks, - layout, - onInput, - onChange, - renderAppender: blocks?.length - ? undefined - : InnerBlocks.ButtonBlockAppender, - } ); - - let children = null; - - if ( hasAlreadyRendered ) { - children = ( - - { __( 'Block cannot be rendered inside itself.' ) } - - ); - } - - if ( isMissing ) { - children = ( - - { __( 'Block has been deleted or is unavailable.' ) } - - ); - } - - if ( ! hasResolved ) { - children = ( - - - - ); - } - - return ( - - - - - - - { children === null ? ( -
- ) : ( -
{ children }
- ) } - - ); -} diff --git a/packages/block-library/src/button/block.json b/packages/block-library/src/button/block.json index 3c232700a876e6..f04d4642bb98e8 100644 --- a/packages/block-library/src/button/block.json +++ b/packages/block-library/src/button/block.json @@ -8,6 +8,7 @@ "description": "Prompt visitors to take action with a button-style link.", "keywords": [ "link" ], "textdomain": "default", + "usesContext": [ "pattern/overrides" ], "attributes": { "tagName": { "type": "string", diff --git a/packages/block-library/src/button/edit.js b/packages/block-library/src/button/edit.js index b46e145d760ad5..7dffdfb5c1b669 100644 --- a/packages/block-library/src/button/edit.js +++ b/packages/block-library/src/button/edit.js @@ -9,6 +9,7 @@ import classnames from 'classnames'; import { NEW_TAB_TARGET, NOFOLLOW_REL } from './constants'; import { getUpdatedLinkAttributes } from './get-updated-link-attributes'; import removeAnchorTag from '../utils/remove-anchor-tag'; +import { unlock } from '../lock-unlock'; /** * WordPress dependencies @@ -32,6 +33,7 @@ import { __experimentalUseBorderProps as useBorderProps, __experimentalUseColorProps as useColorProps, __experimentalGetSpacingClassesAndStyles as useSpacingProps, + __experimentalUseShadowProps as useShadowProps, __experimentalLinkControl as LinkControl, __experimentalGetElementClassName, store as blockEditorStore, @@ -164,6 +166,7 @@ function ButtonEdit( props ) { text, url, width, + metadata, } = attributes; const TagName = tagName || 'a'; @@ -184,6 +187,7 @@ function ButtonEdit( props ) { const borderProps = useBorderProps( attributes ); const colorProps = useColorProps( attributes ); const spacingProps = useSpacingProps( attributes ); + const shadowProps = useShadowProps( attributes ); const ref = useRef(); const richTextRef = useRef(); const blockProps = useBlockProps( { @@ -228,6 +232,27 @@ function ButtonEdit( props ) { const useEnterRef = useEnter( { content: text, clientId } ); const mergedRef = useMergeRefs( [ useEnterRef, richTextRef ] ); + const { lockUrlControls = false } = useSelect( + ( select ) => { + if ( ! isSelected ) { + return {}; + } + + const { getBlockBindingsSource } = unlock( + select( blockEditorStore ) + ); + + return { + lockUrlControls: + !! metadata?.bindings?.url && + getBlockBindingsSource( + metadata?.bindings?.url?.source?.name + )?.lockAttributesEditing === true, + }; + }, + [ isSelected ] + ); + return ( <>
createBlock( 'core/button', { @@ -287,7 +313,7 @@ function ButtonEdit( props ) { } } /> ) } - { ! isURLSet && isLinkTag && ( + { ! isURLSet && isLinkTag && ! lockUrlControls && ( ) } - { isURLSet && isLinkTag && ( + { isURLSet && isLinkTag && ! lockUrlControls && ( ) } - { isLinkTag && isSelected && ( isEditingURL || isURLSet ) && ( - { - setIsEditingURL( false ); - richTextRef.current?.focus(); - } } - anchor={ popoverAnchor } - focusOnMount={ isEditingURL ? 'firstElement' : false } - __unstableSlotName={ '__unstable-block-tools-after' } - shift - > - - setAttributes( - getUpdatedLinkAttributes( { - rel, - url: newURL, - opensInNewTab: newOpensInNewTab, - nofollow: newNofollow, - } ) - ) - } - onRemove={ () => { - unlink(); + { isLinkTag && + isSelected && + ( isEditingURL || isURLSet ) && + ! lockUrlControls && ( + { + setIsEditingURL( false ); richTextRef.current?.focus(); } } - forceIsEditingLink={ isEditingURL } - settings={ LINK_SETTINGS } - /> - - ) } + anchor={ popoverAnchor } + focusOnMount={ isEditingURL ? 'firstElement' : false } + __unstableSlotName={ '__unstable-block-tools-after' } + shift + > + + setAttributes( + getUpdatedLinkAttributes( { + rel, + url: newURL, + opensInNewTab: newOpensInNewTab, + nofollow: newNofollow, + } ) + ) + } + onRemove={ () => { + unlink(); + richTextRef.current?.focus(); + } } + forceIsEditingLink={ isEditingURL } + settings={ LINK_SETTINGS } + /> + + ) } - setAttributes( { minHeight: newMinHeight } ) + setAttributes( { + minHeight: newMinHeight, + style: cleanEmptyObject( { + ...attributes?.style, + dimensions: { + ...attributes?.style?.dimensions, + aspectRatio: undefined, // Reset aspect ratio when minHeight is set. + }, + } ), + } ) } onUnitChange={ ( nextUnit ) => setAttributes( { diff --git a/packages/block-library/src/cover/style.scss b/packages/block-library/src/cover/style.scss index 837e3834e2e1ba..3b4eac41a0d3b4 100644 --- a/packages/block-library/src/cover/style.scss +++ b/packages/block-library/src/cover/style.scss @@ -8,10 +8,9 @@ align-items: center; padding: 1em; // Prevent the `wp-block-cover__background` span from overflowing the container when border-radius is applied. - // `overflow: hidden` is provided as a fallback for browsers that don't support `overflow: clip`. - overflow: hidden; // Use clip instead of overflow: hidden so that sticky position works on child elements. - overflow: clip; + // Use overflow-x instead of overflow so that aspect-ratio allows content to expand the area of the cover block. + overflow-x: clip; // This block has customizable padding, border-box makes that more predictable. box-sizing: border-box; // Keep the flex layout direction to the physical direction (LTR) in RTL languages. diff --git a/packages/block-library/src/file/block.json b/packages/block-library/src/file/block.json index 9dc6677e4adce3..fd5da67d284f49 100644 --- a/packages/block-library/src/file/block.json +++ b/packages/block-library/src/file/block.json @@ -72,7 +72,6 @@ }, "interactivity": true }, - "viewScript": "file:./view.min.js", "editorStyle": "wp-block-file-editor", "style": "wp-block-file" } diff --git a/packages/block-library/src/file/index.php b/packages/block-library/src/file/index.php index 5910a63e6cf182..24eaff8bac622e 100644 --- a/packages/block-library/src/file/index.php +++ b/packages/block-library/src/file/index.php @@ -14,35 +14,8 @@ * * @return string Returns the block content. */ -function render_block_core_file( $attributes, $content, $block ) { - $is_gutenberg_plugin = defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN; - $should_load_view_script = ! empty( $attributes['displayPreview'] ); - $view_js_file = 'wp-block-file-view'; - $script_handles = $block->block_type->view_script_handles; - - if ( $is_gutenberg_plugin ) { - if ( $should_load_view_script ) { - gutenberg_enqueue_module( '@wordpress/block-library/file-block' ); - } - // Remove the view script because we are using the module. - $block->block_type->view_script_handles = array_diff( $script_handles, array( $view_js_file ) ); - } else { - // If the script already exists, there is no point in removing it from viewScript. - if ( ! wp_script_is( $view_js_file ) ) { - - // If the script is not needed, and it is still in the `view_script_handles`, remove it. - if ( ! $should_load_view_script && in_array( $view_js_file, $script_handles, true ) ) { - $block->block_type->view_script_handles = array_diff( $script_handles, array( $view_js_file ) ); - } - // If the script is needed, but it was previously removed, add it again. - if ( $should_load_view_script && ! in_array( $view_js_file, $script_handles, true ) ) { - $block->block_type->view_script_handles = array_merge( $script_handles, array( $view_js_file ) ); - } - } - } - +function render_block_core_file( $attributes, $content ) { // Update object's aria-label attribute if present in block HTML. - // Match an aria-label attribute from an object tag. $pattern = '@aria-label="(?[^"]+)?")@i'; $content = preg_replace_callback( @@ -63,8 +36,10 @@ static function ( $matches ) { $content ); - // If it uses the Interactivity API, add the directives. - if ( $should_load_view_script ) { + // If it's interactive, enqueue the script module and add the directives. + if ( ! empty( $attributes['displayPreview'] ) ) { + wp_enqueue_script_module( '@wordpress/block-library/file-block' ); + $processor = new WP_HTML_Tag_Processor( $content ); $processor->next_tag(); $processor->set_attribute( 'data-wp-interactive', '{"namespace":"core/file"}' ); @@ -77,25 +52,6 @@ static function ( $matches ) { return $content; } -/** - * Ensure that the view script has the `wp-interactivity` dependency. - * - * @since 6.4.0 - * - * @global WP_Scripts $wp_scripts - */ -function block_core_file_ensure_interactivity_dependency() { - global $wp_scripts; - if ( - isset( $wp_scripts->registered['wp-block-file-view'] ) && - ! in_array( 'wp-interactivity', $wp_scripts->registered['wp-block-file-view']->deps, true ) - ) { - $wp_scripts->registered['wp-block-file-view']->deps[] = 'wp-interactivity'; - } -} - -add_action( 'wp_print_scripts', 'block_core_file_ensure_interactivity_dependency' ); - /** * Registers the `core/file` block on server. */ @@ -107,13 +63,11 @@ function register_block_core_file() { ) ); - if ( defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN ) { - gutenberg_register_module( - '@wordpress/block-library/file-block', - gutenberg_url( '/build/interactivity/file.min.js' ), - array( '@wordpress/interactivity' ), - defined( 'GUTENBERG_VERSION' ) ? GUTENBERG_VERSION : get_bloginfo( 'version' ) - ); - } + wp_register_script_module( + '@wordpress/block-library/file-block', + gutenberg_url( '/build/interactivity/file.min.js' ), + array( '@wordpress/interactivity' ), + defined( 'GUTENBERG_VERSION' ) ? GUTENBERG_VERSION : get_bloginfo( 'version' ) + ); } add_action( 'init', 'register_block_core_file' ); diff --git a/packages/block-library/src/footnotes/format.js b/packages/block-library/src/footnotes/format.js index eda45ac0d30f39..def721c21eecd4 100644 --- a/packages/block-library/src/footnotes/format.js +++ b/packages/block-library/src/footnotes/format.js @@ -15,7 +15,7 @@ import { privateApis, } from '@wordpress/block-editor'; import { useSelect, useDispatch, useRegistry } from '@wordpress/data'; -import { useEntityProp } from '@wordpress/core-data'; +import { store as coreDataStore } from '@wordpress/core-data'; import { createBlock, store as blocksStore } from '@wordpress/blocks'; /** @@ -54,43 +54,42 @@ export const format = { getBlockName, getBlockParentsByBlockName, } = registry.select( blockEditorStore ); - const hasFootnotesBlockType = useSelect( - ( select ) => - !! select( blocksStore ).getBlockType( 'core/footnotes' ), - [] + const isFootnotesSupported = useSelect( + ( select ) => { + if ( + ! select( blocksStore ).getBlockType( 'core/footnotes' ) + ) { + return false; + } + + const entityRecord = select( coreDataStore ).getEntityRecord( + 'postType', + postType, + postId + ); + + if ( 'string' !== typeof entityRecord?.meta?.footnotes ) { + return false; + } + + // Checks if the selected block lives within a pattern. + const { + getBlockParentsByBlockName: _getBlockParentsByBlockName, + getSelectedBlockClientId: _getSelectedBlockClientId, + } = select( blockEditorStore ); + const parentCoreBlocks = _getBlockParentsByBlockName( + _getSelectedBlockClientId(), + SYNCED_PATTERN_BLOCK_NAME + ); + return ! parentCoreBlocks || parentCoreBlocks.length === 0; + }, + [ postType, postId ] ); - /* - * This useSelect exists because we need to use its return value - * outside the event callback. - */ - const isBlockWithinPattern = useSelect( ( select ) => { - const { - getBlockParentsByBlockName: _getBlockParentsByBlockName, - getSelectedBlockClientId: _getSelectedBlockClientId, - } = select( blockEditorStore ); - const parentCoreBlocks = _getBlockParentsByBlockName( - _getSelectedBlockClientId(), - SYNCED_PATTERN_BLOCK_NAME - ); - return parentCoreBlocks && parentCoreBlocks.length > 0; - }, [] ); - - const [ meta ] = useEntityProp( 'postType', postType, 'meta', postId ); - const footnotesSupported = 'string' === typeof meta?.footnotes; const { selectionChange, insertBlock } = useDispatch( blockEditorStore ); - if ( ! hasFootnotesBlockType ) { - return null; - } - - if ( ! footnotesSupported ) { - return null; - } - - // Checks if the selected block lives within a pattern. - if ( isBlockWithinPattern ) { + if ( ! isFootnotesSupported ) { return null; } diff --git a/packages/block-library/src/group/block.json b/packages/block-library/src/group/block.json index df59c25a7751fb..674b0645f50215 100644 --- a/packages/block-library/src/group/block.json +++ b/packages/block-library/src/group/block.json @@ -55,6 +55,7 @@ } }, "dimensions": { + "aspectRatio": true, "minHeight": true }, "__experimentalBorder": { diff --git a/packages/block-library/src/heading/block.json b/packages/block-library/src/heading/block.json index 72cc67caddd9ea..a1eb3fce32ef15 100644 --- a/packages/block-library/src/heading/block.json +++ b/packages/block-library/src/heading/block.json @@ -7,6 +7,7 @@ "description": "Introduce new sections and organize content to help visitors (and search engines) understand the structure of your content.", "keywords": [ "title", "subtitle" ], "textdomain": "default", + "usesContext": [ "pattern/overrides" ], "attributes": { "textAlign": { "type": "string" diff --git a/packages/block-library/src/image/block.json b/packages/block-library/src/image/block.json index c5191e3dd86543..d60bcadf0eec74 100644 --- a/packages/block-library/src/image/block.json +++ b/packages/block-library/src/image/block.json @@ -4,7 +4,12 @@ "name": "core/image", "title": "Image", "category": "media", - "usesContext": [ "allowResize", "imageCrop", "fixedHeight" ], + "usesContext": [ + "allowResize", + "imageCrop", + "fixedHeight", + "pattern/overrides" + ], "description": "Insert an image to make a visual statement.", "keywords": [ "img", "photo", "picture" ], "textdomain": "default", @@ -129,6 +134,5 @@ { "name": "rounded", "label": "Rounded" } ], "editorStyle": "wp-block-image-editor", - "style": "wp-block-image", - "viewScript": "file:./view.min.js" + "style": "wp-block-image" } diff --git a/packages/block-library/src/image/edit.js b/packages/block-library/src/image/edit.js index d189af32efcbec..723c8c27508ded 100644 --- a/packages/block-library/src/image/edit.js +++ b/packages/block-library/src/image/edit.js @@ -19,7 +19,7 @@ import { } from '@wordpress/block-editor'; import { useEffect, useRef, useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; -import { image as icon } from '@wordpress/icons'; +import { image as icon, plugins as pluginsIcon } from '@wordpress/icons'; import { store as noticesStore } from '@wordpress/notices'; /** @@ -111,6 +111,7 @@ export function ImageEdit( { aspectRatio, scale, align, + metadata, } = attributes; const [ temporaryURL, setTemporaryURL ] = useState(); @@ -332,6 +333,7 @@ export function ImageEdit( { } ); // Much of this description is duplicated from MediaPlaceholder. + const isUrlAttributeConnected = !! metadata?.bindings?.url; const placeholder = ( content ) => { return ( - { content } + { isUrlAttributeConnected ? ( + + { __( 'Connected to a custom field' ) } + + ) : ( + content + ) } ); }; diff --git a/packages/block-library/src/image/editor.scss b/packages/block-library/src/image/editor.scss index 934682ed91b7de..ded3768dfa7d38 100644 --- a/packages/block-library/src/image/editor.scss +++ b/packages/block-library/src/image/editor.scss @@ -27,6 +27,12 @@ opacity: 0; } } + .block-bindings-media-placeholder-message { + opacity: 0; + } + &.is-selected .block-bindings-media-placeholder-message { + opacity: 1; + } // Remove the transition while we still have a legacy placeholder style. // Otherwise the content jumps between the 1px placeholder border, and any inherited custom @@ -38,7 +44,6 @@ } } - figure.wp-block-image:not(.wp-block) { margin: 0; } diff --git a/packages/block-library/src/image/image.js b/packages/block-library/src/image/image.js index d8788fde4844f6..d6d259c428b6ea 100644 --- a/packages/block-library/src/image/image.js +++ b/packages/block-library/src/image/image.js @@ -124,6 +124,7 @@ export default function Image( { linkTarget, sizeSlug, lightbox, + metadata, } = attributes; // The only supported unit is px, so we can parseInt to strip the px here. @@ -355,7 +356,8 @@ export default function Image( { const lightboxChecked = !! lightbox?.enabled || ( ! lightbox && !! lightboxSetting?.enabled ); - const lightboxToggleDisabled = linkDestination !== 'none'; + const lightboxToggleDisabled = + linkDestination && linkDestination !== 'none'; const dimensionsControl = ( ); + const { + lockUrlControls = false, + lockAltControls = false, + lockTitleControls = false, + } = useSelect( + ( select ) => { + if ( ! isSelected ) { + return {}; + } + + const { getBlockBindingsSource } = unlock( + select( blockEditorStore ) + ); + const { + url: urlBinding, + alt: altBinding, + title: titleBinding, + } = metadata?.bindings || {}; + return { + lockUrlControls: + !! urlBinding && + getBlockBindingsSource( urlBinding?.source?.name ) + ?.lockAttributesEditing === true, + lockAltControls: + !! altBinding && + getBlockBindingsSource( altBinding?.source?.name ) + ?.lockAttributesEditing === true, + lockTitleControls: + !! titleBinding && + getBlockBindingsSource( titleBinding?.source?.name ) + ?.lockAttributesEditing === true, + }; + }, + [ isSelected ] + ); + const controls = ( <> - { ! multiImageSelection && ! isEditingImage && ( - - ) } + { ! multiImageSelection && + ! isEditingImage && + ! lockUrlControls && ( + + ) } { allowCrop && ( setIsEditingImage( true ) } @@ -440,19 +480,21 @@ export default function Image( { /> ) } - { ! multiImageSelection && ! isEditingImage && ( - - - - ) } + { ! multiImageSelection && + ! isEditingImage && + ! lockUrlControls && ( + + + + ) } { ! multiImageSelection && externalBlob && ( @@ -483,16 +525,27 @@ export default function Image( { label={ __( 'Alternative text' ) } value={ alt || '' } onChange={ updateAlt } + disabled={ lockAltControls } help={ - <> - + lockAltControls ? ( + <> + { __( + 'Connected to a custom field' + ) } + + ) : ( + <> + + { __( + 'Describe the purpose of the image.' + ) } + +
{ __( - 'Describe the purpose of the image.' + 'Leave empty if decorative.' ) } -
-
- { __( 'Leave empty if decorative.' ) } - + + ) } __nextHasNoMarginBottom /> @@ -542,17 +595,22 @@ export default function Image( { label={ __( 'Title attribute' ) } value={ title || '' } onChange={ onSetTitle } + disabled={ lockTitleControls } help={ - <> - { __( - 'Describe the role of this image on the page.' - ) } - + lockTitleControls ? ( + <>{ __( 'Connected to a custom field' ) } + ) : ( + <> { __( - '(Note: many devices and browsers do not display this text.)' + 'Describe the role of this image on the page.' ) } - - + + { __( + '(Note: many devices and browsers do not display this text.)' + ) } + + + ) } />
@@ -747,7 +805,8 @@ export default function Image( { } if ( ! url && ! temporaryURL ) { - return sizeControls; + // Add all controls if the image attributes are connected. + return metadata?.bindings ? controls : sizeControls; } return ( diff --git a/packages/block-library/src/image/index.php b/packages/block-library/src/image/index.php index add8e5989ab7de..3297c57d9eab35 100644 --- a/packages/block-library/src/image/index.php +++ b/packages/block-library/src/image/index.php @@ -37,10 +37,6 @@ function render_block_core_image( $attributes, $content, $block ) { $link_destination = isset( $attributes['linkDestination'] ) ? $attributes['linkDestination'] : 'none'; $lightbox_settings = block_core_image_get_lightbox_settings( $block->parsed_block ); - $is_gutenberg_plugin = defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN; - $view_js_file_handle = 'wp-block-image-view'; - $script_handles = $block->block_type->view_script_handles; - /* * If the lightbox is enabled and the image is not linked, add the filter * and the JavaScript view file. @@ -51,34 +47,22 @@ function render_block_core_image( $attributes, $content, $block ) { isset( $lightbox_settings['enabled'] ) && true === $lightbox_settings['enabled'] ) { - if ( $is_gutenberg_plugin ) { - gutenberg_enqueue_module( '@wordpress/block-library/image' ); - // Remove the view script because we are using the module. - $block->block_type->view_script_handles = array_diff( $script_handles, array( $view_js_file_handle ) ); - } elseif ( ! in_array( $view_js_file_handle, $script_handles, true ) ) { - $block->block_type->view_script_handles = array_merge( $script_handles, array( $view_js_file_handle ) ); - } + wp_enqueue_script_module( '@wordpress/block-library/image' ); /* - * This render needs to happen in a filter with priority 15 to ensure - * that it runs after the duotone filter and that duotone styles are - * applied to the image in the lightbox. We also need to ensure that the - * lightbox works with any plugins that might use filters as well. We - * can consider removing this in the future if the way the blocks are - * rendered changes, or if a new kind of filter is introduced. + * This render needs to happen in a filter with priority 15 to ensure that + * it runs after the duotone filter and that duotone styles are applied to + * the image in the lightbox. Lightbox has to work with any plugins that + * might use filters as well. Removing this can be considered in the + * future if the way the blocks are rendered changes, or if a + * new kind of filter is introduced. */ add_filter( 'render_block_core/image', 'block_core_image_render_lightbox', 15, 2 ); } else { /* - * Remove the filter and the JavaScript view file if previously added by - * other Image blocks. + * Remove the filter if previously added by other Image blocks. */ remove_filter( 'render_block_core/image', 'block_core_image_render_lightbox', 15 ); - - // If the script is not needed, and it is still in the `view_script_handles`, remove it. - if ( in_array( $view_js_file_handle, $script_handles, true ) ) { - $block->block_type->view_script_handles = array_diff( $script_handles, array( $view_js_file_handle ) ); - } } return $processor->get_updated_html(); @@ -328,25 +312,6 @@ class="lightbox-trigger" return str_replace( '', $lightbox_html . '', $body_content ); } -/** - * Ensures that the view script has the `wp-interactivity` dependency. - * - * @since 6.4.0 - * - * @global WP_Scripts $wp_scripts - */ -function block_core_image_ensure_interactivity_dependency() { - global $wp_scripts; - if ( - isset( $wp_scripts->registered['wp-block-image-view'] ) && - ! in_array( 'wp-interactivity', $wp_scripts->registered['wp-block-image-view']->deps, true ) - ) { - $wp_scripts->registered['wp-block-image-view']->deps[] = 'wp-interactivity'; - } -} - -add_action( 'wp_print_scripts', 'block_core_image_ensure_interactivity_dependency' ); - /** * Registers the `core/image` block on server. */ @@ -358,13 +323,11 @@ function register_block_core_image() { ) ); - if ( defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN ) { - gutenberg_register_module( - '@wordpress/block-library/image', - gutenberg_url( '/build/interactivity/image.min.js' ), - array( '@wordpress/interactivity' ), - defined( 'GUTENBERG_VERSION' ) ? GUTENBERG_VERSION : get_bloginfo( 'version' ) - ); - } + wp_register_script_module( + '@wordpress/block-library/image', + gutenberg_url( '/build/interactivity/image.min.js' ), + array( '@wordpress/interactivity' ), + defined( 'GUTENBERG_VERSION' ) ? GUTENBERG_VERSION : get_bloginfo( 'version' ) + ); } add_action( 'init', 'register_block_core_image' ); diff --git a/packages/block-library/src/more/index.js b/packages/block-library/src/more/index.js index 4c1fad3cb67f48..b40bb2123bc727 100644 --- a/packages/block-library/src/more/index.js +++ b/packages/block-library/src/more/index.js @@ -20,6 +20,12 @@ export const settings = { icon, example: {}, __experimentalLabel( attributes, { context } ) { + const customName = attributes?.metadata?.name; + + if ( context === 'list-view' && customName ) { + return customName; + } + if ( context === 'accessibility' ) { return attributes.customText; } diff --git a/packages/block-library/src/navigation/block.json b/packages/block-library/src/navigation/block.json index 9ec919ae38d1fa..36817a5e1c35b1 100644 --- a/packages/block-library/src/navigation/block.json +++ b/packages/block-library/src/navigation/block.json @@ -136,7 +136,6 @@ "interactivity": true, "renaming": false }, - "viewScript": "file:./view.min.js", "editorStyle": "wp-block-navigation-editor", "style": "wp-block-navigation" } diff --git a/packages/block-library/src/navigation/edit/index.js b/packages/block-library/src/navigation/edit/index.js index 5589e8ea9e60f0..14ef6fc73d48f0 100644 --- a/packages/block-library/src/navigation/edit/index.js +++ b/packages/block-library/src/navigation/edit/index.js @@ -16,8 +16,8 @@ import { import { InspectorControls, useBlockProps, - __experimentalRecursionProvider as RecursionProvider, - __experimentalUseHasRecursion as useHasRecursion, + RecursionProvider, + useHasRecursion, store as blockEditorStore, withColors, ContrastChecker, diff --git a/packages/block-library/src/navigation/index.php b/packages/block-library/src/navigation/index.php index 3af85afd92522f..0f67a63e7acf27 100644 --- a/packages/block-library/src/navigation/index.php +++ b/packages/block-library/src/navigation/index.php @@ -5,6 +5,648 @@ * @package WordPress */ +/** + * Helper functions used to render the navigation block. + */ +class WP_Navigation_Block_Renderer { + /** + * Used to determine which blocks are wrapped in an
  • . + * + * @var array + */ + private static $nav_blocks_wrapped_in_list_item = array( + 'core/navigation-link', + 'core/home-link', + 'core/site-title', + 'core/site-logo', + 'core/navigation-submenu', + ); + + /** + * Used to determine which blocks need an
  • wrapper. + * + * @var array + */ + private static $needs_list_item_wrapper = array( + 'core/site-title', + 'core/site-logo', + ); + + /** + * Keeps track of all the navigation names that have been seen. + * + * @var array + */ + private static $seen_menu_names = array(); + + /** + * Returns whether or not this is responsive navigation. + * + * @param array $attributes The block attributes. + * @return bool Returns whether or not this is responsive navigation. + */ + private static function is_responsive( $attributes ) { + /** + * This is for backwards compatibility after the `isResponsive` attribute was been removed. + */ + + $has_old_responsive_attribute = ! empty( $attributes['isResponsive'] ) && $attributes['isResponsive']; + return isset( $attributes['overlayMenu'] ) && 'never' !== $attributes['overlayMenu'] || $has_old_responsive_attribute; + } + + /** + * Returns whether or not a navigation has a submenu. + * + * @param WP_Block_List $inner_blocks The list of inner blocks. + * @return bool Returns whether or not a navigation has a submenu. + */ + private static function has_submenus( $inner_blocks ) { + foreach ( $inner_blocks as $inner_block ) { + $inner_block_content = $inner_block->render(); + $p = new WP_HTML_Tag_Processor( $inner_block_content ); + if ( $p->next_tag( + array( + 'name' => 'LI', + 'class_name' => 'has-child', + ) + ) ) { + return true; + } + } + return false; + } + + /** + * Determine whether the navigation blocks is interactive. + * + * @param array $attributes The block attributes. + * @param WP_Block_List $inner_blocks The list of inner blocks. + * @return bool Returns whether or not to load the view script. + */ + private static function is_interactive( $attributes, $inner_blocks ) { + $has_submenus = static::has_submenus( $inner_blocks ); + $is_responsive_menu = static::is_responsive( $attributes ); + return ( $has_submenus && ( $attributes['openSubmenusOnClick'] || $attributes['showSubmenuIcon'] ) ) || $is_responsive_menu; + } + + /** + * Returns whether or not a block needs a list item wrapper. + * + * @param WP_Block $block The block. + * @return bool Returns whether or not a block needs a list item wrapper. + */ + private static function does_block_need_a_list_item_wrapper( $block ) { + return in_array( $block->name, static::$needs_list_item_wrapper, true ); + } + + /** + * Returns the markup for a single inner block. + * + * @param WP_Block $inner_block The inner block. + * @return string Returns the markup for a single inner block. + */ + private static function get_markup_for_inner_block( $inner_block ) { + $inner_block_content = $inner_block->render(); + if ( ! empty( $inner_block_content ) ) { + if ( static::does_block_need_a_list_item_wrapper( $inner_block ) ) { + return '
  • ' . $inner_block_content . '
  • '; + } + + return $inner_block_content; + } + } + + /** + * Returns the html for the inner blocks of the navigation block. + * + * @param array $attributes The block attributes. + * @param WP_Block_List $inner_blocks The list of inner blocks. + * @return string Returns the html for the inner blocks of the navigation block. + */ + private static function get_inner_blocks_html( $attributes, $inner_blocks ) { + $has_submenus = static::has_submenus( $inner_blocks ); + $is_interactive = static::is_interactive( $attributes, $inner_blocks ); + + $style = static::get_styles( $attributes ); + $class = static::get_classes( $attributes ); + $container_attributes = get_block_wrapper_attributes( + array( + 'class' => 'wp-block-navigation__container ' . $class, + 'style' => $style, + ) + ); + + $inner_blocks_html = ''; + $is_list_open = false; + + foreach ( $inner_blocks as $inner_block ) { + $is_list_item = in_array( $inner_block->name, static::$nav_blocks_wrapped_in_list_item, true ); + + if ( $is_list_item && ! $is_list_open ) { + $is_list_open = true; + $inner_blocks_html .= sprintf( + '
      ', + $container_attributes + ); + } + + if ( ! $is_list_item && $is_list_open ) { + $is_list_open = false; + $inner_blocks_html .= '
    '; + } + + $inner_blocks_html .= static::get_markup_for_inner_block( $inner_block ); + } + + if ( $is_list_open ) { + $inner_blocks_html .= ''; + } + + // Add directives to the submenu if needed. + if ( $has_submenus && $is_interactive ) { + $tags = new WP_HTML_Tag_Processor( $inner_blocks_html ); + $inner_blocks_html = block_core_navigation_add_directives_to_submenu( $tags, $attributes ); + } + + return $inner_blocks_html; + } + + /** + * Gets the inner blocks for the navigation block from the navigation post. + * + * @param array $attributes The block attributes. + * @return WP_Block_List Returns the inner blocks for the navigation block. + */ + private static function get_inner_blocks_from_navigation_post( $attributes ) { + $navigation_post = get_post( $attributes['ref'] ); + if ( ! isset( $navigation_post ) ) { + return new WP_Block_List( array(), $attributes ); + } + + // Only published posts are valid. If this is changed then a corresponding change + // must also be implemented in `use-navigation-menu.js`. + if ( 'publish' === $navigation_post->post_status ) { + $parsed_blocks = parse_blocks( $navigation_post->post_content ); + + // 'parse_blocks' includes a null block with '\n\n' as the content when + // it encounters whitespace. This code strips it. + $blocks = block_core_navigation_filter_out_empty_blocks( $parsed_blocks ); + + if ( function_exists( 'get_hooked_blocks' ) ) { + // Run Block Hooks algorithm to inject hooked blocks. + $markup = block_core_navigation_insert_hooked_blocks( $blocks, $navigation_post ); + $root_nav_block = parse_blocks( $markup )[0]; + + $blocks = isset( $root_nav_block['innerBlocks'] ) ? $root_nav_block['innerBlocks'] : $blocks; + } + + // TODO - this uses the full navigation block attributes for the + // context which could be refined. + return new WP_Block_List( $blocks, $attributes ); + } + } + + /** + * Gets the inner blocks for the navigation block from the fallback. + * + * @param array $attributes The block attributes. + * @return WP_Block_List Returns the inner blocks for the navigation block. + */ + private static function get_inner_blocks_from_fallback( $attributes ) { + $fallback_blocks = block_core_navigation_get_fallback_blocks(); + + // Fallback my have been filtered so do basic test for validity. + if ( empty( $fallback_blocks ) || ! is_array( $fallback_blocks ) ) { + return new WP_Block_List( array(), $attributes ); + } + + return new WP_Block_List( $fallback_blocks, $attributes ); + } + + /** + * Gets the inner blocks for the navigation block. + * + * @param array $attributes The block attributes. + * @param WP_Block $block The parsed block. + * @return WP_Block_List Returns the inner blocks for the navigation block. + */ + private static function get_inner_blocks( $attributes, $block ) { + $inner_blocks = $block->inner_blocks; + + // Ensure that blocks saved with the legacy ref attribute name (navigationMenuId) continue to render. + if ( array_key_exists( 'navigationMenuId', $attributes ) ) { + $attributes['ref'] = $attributes['navigationMenuId']; + } + + // If: + // - the gutenberg plugin is active + // - `__unstableLocation` is defined + // - we have menu items at the defined location + // - we don't have a relationship to a `wp_navigation` Post (via `ref`). + // ...then create inner blocks from the classic menu assigned to that location. + if ( + defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN && + array_key_exists( '__unstableLocation', $attributes ) && + ! array_key_exists( 'ref', $attributes ) && + ! empty( block_core_navigation_get_menu_items_at_location( $attributes['__unstableLocation'] ) ) + ) { + $inner_blocks = block_core_navigation_get_inner_blocks_from_unstable_location( $attributes ); + } + + // Load inner blocks from the navigation post. + if ( array_key_exists( 'ref', $attributes ) ) { + $inner_blocks = static::get_inner_blocks_from_navigation_post( $attributes ); + } + + // If there are no inner blocks then fallback to rendering an appropriate fallback. + if ( empty( $inner_blocks ) ) { + $inner_blocks = static::get_inner_blocks_from_fallback( $attributes ); + } + + /** + * Filter navigation block $inner_blocks. + * Allows modification of a navigation block menu items. + * + * @since 6.1.0 + * + * @param \WP_Block_List $inner_blocks + */ + $inner_blocks = apply_filters( 'block_core_navigation_render_inner_blocks', $inner_blocks ); + + $post_ids = block_core_navigation_get_post_ids( $inner_blocks ); + if ( $post_ids ) { + _prime_post_caches( $post_ids, false, false ); + } + + return $inner_blocks; + } + + /** + * Gets the name of the current navigation, if it has one. + * + * @param array $attributes The block attributes. + * @return string Returns the name of the navigation. + */ + private static function get_navigation_name( $attributes ) { + + $navigation_name = $attributes['ariaLabel'] ?? ''; + + // Load the navigation post. + if ( array_key_exists( 'ref', $attributes ) ) { + $navigation_post = get_post( $attributes['ref'] ); + if ( ! isset( $navigation_post ) ) { + return $navigation_name; + } + + // Only published posts are valid. If this is changed then a corresponding change + // must also be implemented in `use-navigation-menu.js`. + if ( 'publish' === $navigation_post->post_status ) { + $navigation_name = $navigation_post->post_title; + + // This is used to count the number of times a navigation name has been seen, + // so that we can ensure every navigation has a unique id. + if ( isset( static::$seen_menu_names[ $navigation_name ] ) ) { + ++static::$seen_menu_names[ $navigation_name ]; + } else { + static::$seen_menu_names[ $navigation_name ] = 1; + } + } + } + + return $navigation_name; + } + + /** + * Returns the layout class for the navigation block. + * + * @param array $attributes The block attributes. + * @return string Returns the layout class for the navigation block. + */ + private static function get_layout_class( $attributes ) { + $layout_justification = array( + 'left' => 'items-justified-left', + 'right' => 'items-justified-right', + 'center' => 'items-justified-center', + 'space-between' => 'items-justified-space-between', + ); + + $layout_class = ''; + if ( + isset( $attributes['layout']['justifyContent'] ) && + isset( $layout_justification[ $attributes['layout']['justifyContent'] ] ) + ) { + $layout_class .= $layout_justification[ $attributes['layout']['justifyContent'] ]; + } + if ( isset( $attributes['layout']['orientation'] ) && 'vertical' === $attributes['layout']['orientation'] ) { + $layout_class .= ' is-vertical'; + } + + if ( isset( $attributes['layout']['flexWrap'] ) && 'nowrap' === $attributes['layout']['flexWrap'] ) { + $layout_class .= ' no-wrap'; + } + return $layout_class; + } + + /** + * Return classes for the navigation block. + * + * @param array $attributes The block attributes. + * @return string Returns the classes for the navigation block. + */ + private static function get_classes( $attributes ) { + // Restore legacy classnames for submenu positioning. + $layout_class = static::get_layout_class( $attributes ); + $colors = block_core_navigation_build_css_colors( $attributes ); + $font_sizes = block_core_navigation_build_css_font_sizes( $attributes ); + $is_responsive_menu = static::is_responsive( $attributes ); + + // Manually add block support text decoration as CSS class. + $text_decoration = $attributes['style']['typography']['textDecoration'] ?? null; + $text_decoration_class = sprintf( 'has-text-decoration-%s', $text_decoration ); + + // Sets the is-collapsed class when the navigation is set to always use the overlay. + // This saves us from needing to do this check in the view.js file (see the collapseNav function). + $is_collapsed_class = static::is_always_overlay( $attributes ) ? array( 'is-collapsed' ) : array(); + + $classes = array_merge( + $colors['css_classes'], + $font_sizes['css_classes'], + $is_responsive_menu ? array( 'is-responsive' ) : array(), + $layout_class ? array( $layout_class ) : array(), + $text_decoration ? array( $text_decoration_class ) : array(), + $is_collapsed_class + ); + return implode( ' ', $classes ); + } + + private static function is_always_overlay( $attributes ) { + return isset( $attributes['overlayMenu'] ) && 'always' === $attributes['overlayMenu']; + } + + /** + * Get styles for the navigation block. + * + * @param array $attributes The block attributes. + * @return string Returns the styles for the navigation block. + */ + private static function get_styles( $attributes ) { + $colors = block_core_navigation_build_css_colors( $attributes ); + $font_sizes = block_core_navigation_build_css_font_sizes( $attributes ); + $block_styles = isset( $attributes['styles'] ) ? $attributes['styles'] : ''; + return $block_styles . $colors['inline_styles'] . $font_sizes['inline_styles']; + } + + /** + * Get the responsive container markup + * + * @param array $attributes The block attributes. + * @param WP_Block_List $inner_blocks The list of inner blocks. + * @param string $inner_blocks_html The markup for the inner blocks. + * @return string Returns the container markup. + */ + private static function get_responsive_container_markup( $attributes, $inner_blocks, $inner_blocks_html ) { + $is_interactive = static::is_interactive( $attributes, $inner_blocks ); + $colors = block_core_navigation_build_css_colors( $attributes ); + $modal_unique_id = wp_unique_id( 'modal-' ); + + $responsive_container_classes = array( + 'wp-block-navigation__responsive-container', + implode( ' ', $colors['overlay_css_classes'] ), + ); + $open_button_classes = array( + 'wp-block-navigation__responsive-container-open', + ); + + $should_display_icon_label = isset( $attributes['hasIcon'] ) && true === $attributes['hasIcon']; + $toggle_button_icon = ''; + if ( isset( $attributes['icon'] ) ) { + if ( 'menu' === $attributes['icon'] ) { + $toggle_button_icon = ''; + } + } + $toggle_button_content = $should_display_icon_label ? $toggle_button_icon : __( 'Menu' ); + $toggle_close_button_icon = ''; + $toggle_close_button_content = $should_display_icon_label ? $toggle_close_button_icon : __( 'Close' ); + $toggle_aria_label_open = $should_display_icon_label ? 'aria-label="' . __( 'Open menu' ) . '"' : ''; // Open button label. + $toggle_aria_label_close = $should_display_icon_label ? 'aria-label="' . __( 'Close menu' ) . '"' : ''; // Close button label. + + // Add Interactivity API directives to the markup if needed. + $open_button_directives = ''; + $responsive_container_directives = ''; + $responsive_dialog_directives = ''; + $close_button_directives = ''; + if ( $is_interactive ) { + $open_button_directives = ' + data-wp-on--click="actions.openMenuOnClick" + data-wp-on--keydown="actions.handleMenuKeydown" + '; + $responsive_container_directives = ' + data-wp-class--has-modal-open="state.isMenuOpen" + data-wp-class--is-menu-open="state.isMenuOpen" + data-wp-watch="callbacks.initMenu" + data-wp-on--keydown="actions.handleMenuKeydown" + data-wp-on--focusout="actions.handleMenuFocusout" + tabindex="-1" + '; + $responsive_dialog_directives = ' + data-wp-bind--aria-modal="state.ariaModal" + data-wp-bind--aria-label="state.ariaLabel" + data-wp-bind--role="state.roleAttribute" + '; + $close_button_directives = ' + data-wp-on--click="actions.closeMenuOnClick" + '; + $responsive_container_content_directives = ' + data-wp-watch="callbacks.focusFirstElement" + '; + } + + return sprintf( + ' +
    +
    +
    + +
    + %2$s +
    +
    +
    +
    ', + esc_attr( $modal_unique_id ), + $inner_blocks_html, + $toggle_aria_label_open, + $toggle_aria_label_close, + esc_attr( implode( ' ', $responsive_container_classes ) ), + esc_attr( implode( ' ', $open_button_classes ) ), + esc_attr( safecss_filter_attr( $colors['overlay_inline_styles'] ) ), + $toggle_button_content, + $toggle_close_button_content, + $open_button_directives, + $responsive_container_directives, + $responsive_dialog_directives, + $close_button_directives, + $responsive_container_content_directives + ); + } + + /** + * Get the wrapper attributes + * + * @param array $attributes The block attributes. + * @param WP_Block_List $inner_blocks A list of inner blocks. + * @return string Returns the navigation block markup. + */ + private static function get_nav_wrapper_attributes( $attributes, $inner_blocks ) { + $nav_menu_name = static::get_unique_navigation_name( $attributes ); + $is_interactive = static::is_interactive( $attributes, $inner_blocks ); + $is_responsive_menu = static::is_responsive( $attributes ); + $style = static::get_styles( $attributes ); + $class = static::get_classes( $attributes ); + $wrapper_attributes = get_block_wrapper_attributes( + array( + 'class' => $class, + 'style' => $style, + 'aria-label' => $nav_menu_name, + ) + ); + + if ( $is_responsive_menu ) { + $nav_element_directives = static::get_nav_element_directives( $is_interactive, $attributes ); + $wrapper_attributes .= ' ' . $nav_element_directives; + } + + return $wrapper_attributes; + } + + /** + * Gets the nav element directives. + * + * @param bool $is_interactive Whether the block is interactive. + * @param array $attributes The block attributes. + * @return string the directives for the navigation element. + */ + private static function get_nav_element_directives( $is_interactive, $attributes ) { + if ( ! $is_interactive ) { + return ''; + } + // When adding to this array be mindful of security concerns. + $nav_element_context = wp_json_encode( + array( + 'overlayOpenedBy' => array(), + 'type' => 'overlay', + 'roleAttribute' => '', + 'ariaLabel' => __( 'Menu' ), + ), + JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP + ); + $nav_element_directives = ' + data-wp-interactive=\'{"namespace":"core/navigation"}\' + data-wp-context=\'' . $nav_element_context . '\' + '; + + /* + * When the navigation's 'overlayMenu' attribute is set to 'always', JavaScript + * is not needed for collapsing the menu because the class is set manually. + */ + if ( ! static::is_always_overlay( $attributes ) ) { + $nav_element_directives .= 'data-wp-init="callbacks.initNav"'; + $nav_element_directives .= ' '; // space separator + $nav_element_directives .= 'data-wp-class--is-collapsed="context.isCollapsed"'; + } + + return $nav_element_directives; + } + + /** + * Handle view script module loading. + * + * @param array $attributes The block attributes. + * @param WP_Block $block The parsed block. + * @param WP_Block_List $inner_blocks The list of inner blocks. + */ + private static function handle_view_script_module_loading( $attributes, $block, $inner_blocks ) { + if ( static::is_interactive( $attributes, $inner_blocks ) ) { + wp_enqueue_script_module( '@wordpress/block-library/navigation-block' ); + } + } + + /** + * Returns the markup for the navigation block. + * + * @param array $attributes The block attributes. + * @param WP_Block_List $inner_blocks The list of inner blocks. + * @return string Returns the navigation wrapper markup. + */ + private static function get_wrapper_markup( $attributes, $inner_blocks ) { + $inner_blocks_html = static::get_inner_blocks_html( $attributes, $inner_blocks ); + if ( static::is_responsive( $attributes ) ) { + return static::get_responsive_container_markup( $attributes, $inner_blocks, $inner_blocks_html ); + } + return $inner_blocks_html; + } + + /** + * Returns a unique name for the navigation. + * + * @param array $attributes The block attributes. + * @return string Returns a unique name for the navigation. + */ + private static function get_unique_navigation_name( $attributes ) { + $nav_menu_name = static::get_navigation_name( $attributes ); + + // If the menu name has been used previously then append an ID + // to the name to ensure uniqueness across a given post. + if ( isset( static::$seen_menu_names[ $nav_menu_name ] ) && static::$seen_menu_names[ $nav_menu_name ] > 1 ) { + $count = static::$seen_menu_names[ $nav_menu_name ]; + $nav_menu_name = $nav_menu_name . ' ' . ( $count ); + } + + return $nav_menu_name; + } + + /** + * Renders the navigation block. + * + * @param array $attributes The block attributes. + * @param string $content The saved content. + * @param WP_Block $block The parsed block. + * @return string Returns the navigation block markup. + */ + public static function render( $attributes, $content, $block ) { + /** + * Deprecated: + * The rgbTextColor and rgbBackgroundColor attributes + * have been deprecated in favor of + * customTextColor and customBackgroundColor ones. + * Move the values from old attrs to the new ones. + */ + if ( isset( $attributes['rgbTextColor'] ) && empty( $attributes['textColor'] ) ) { + $attributes['customTextColor'] = $attributes['rgbTextColor']; + } + + if ( isset( $attributes['rgbBackgroundColor'] ) && empty( $attributes['backgroundColor'] ) ) { + $attributes['customBackgroundColor'] = $attributes['rgbBackgroundColor']; + } + + unset( $attributes['rgbTextColor'], $attributes['rgbBackgroundColor'] ); + + $inner_blocks = static::get_inner_blocks( $attributes, $block ); + // Prevent navigation blocks referencing themselves from rendering. + if ( block_core_navigation_block_contains_core_navigation( $inner_blocks ) ) { + return ''; + } + + static::handle_view_script_module_loading( $attributes, $block, $inner_blocks ); + + return sprintf( + '', + static::get_nav_wrapper_attributes( $attributes, $inner_blocks ), + static::get_wrapper_markup( $attributes, $inner_blocks ) + ); + } +} + // These functions are used for the __unstableLocation feature and only active // when the gutenberg plugin is active. if ( defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN ) { @@ -349,6 +991,17 @@ function block_core_navigation_get_fallback_blocks() { // Normalizing blocks may result in an empty array of blocks if they were all `null` blocks. // In this case default to the (Page List) fallback. $fallback_blocks = ! empty( $maybe_fallback ) ? $maybe_fallback : $fallback_blocks; + + if ( function_exists( 'get_hooked_blocks' ) ) { + // Run Block Hooks algorithm to inject hooked blocks. + // We have to run it here because we need the post ID of the Navigation block to track ignored hooked blocks. + $markup = block_core_navigation_insert_hooked_blocks( $fallback_blocks, $navigation_post ); + $blocks = parse_blocks( $markup ); + + if ( isset( $blocks[0]['innerBlocks'] ) ) { + $fallback_blocks = $blocks[0]['innerBlocks']; + } + } } /** @@ -360,7 +1013,7 @@ function block_core_navigation_get_fallback_blocks() { * * @since 5.9.0 * - * @param array[] default fallback blocks provided by the default block mechanic. + * @param array[] $fallback_blocks default fallback blocks provided by the default block mechanic. */ return apply_filters( 'block_core_navigation_render_fallback', $fallback_blocks ); } @@ -427,14 +1080,12 @@ function register_block_core_navigation() { ) ); - if ( defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN ) { - gutenberg_register_module( - '@wordpress/block-library/navigation-block', - gutenberg_url( '/build/interactivity/navigation.min.js' ), - array( '@wordpress/interactivity' ), - defined( 'GUTENBERG_VERSION' ) ? GUTENBERG_VERSION : get_bloginfo( 'version' ) - ); - } + wp_register_script_module( + '@wordpress/block-library/navigation-block', + gutenberg_url( '/build/interactivity/navigation.min.js' ), + array( '@wordpress/interactivity' ), + defined( 'GUTENBERG_VERSION' ) ? GUTENBERG_VERSION : get_bloginfo( 'version' ) + ); } add_action( 'init', 'register_block_core_navigation' ); @@ -473,25 +1124,6 @@ function block_core_navigation_typographic_presets_backcompatibility( $parsed_bl add_filter( 'render_block_data', 'block_core_navigation_typographic_presets_backcompatibility' ); -/** - * Ensure that the view script has the `wp-interactivity` dependency. - * - * @since 6.4.0 - * - * @global WP_Scripts $wp_scripts - */ -function block_core_navigation_ensure_interactivity_dependency() { - global $wp_scripts; - if ( - isset( $wp_scripts->registered['wp-block-navigation-view'] ) && - ! in_array( 'wp-interactivity', $wp_scripts->registered['wp-block-navigation-view']->deps, true ) - ) { - $wp_scripts->registered['wp-block-navigation-view']->deps[] = 'wp-interactivity'; - } -} - -add_action( 'wp_print_scripts', 'block_core_navigation_ensure_interactivity_dependency' ); - /** * Turns menu item data into a nested array of parsed blocks * @@ -710,3 +1342,120 @@ function block_core_navigation_get_most_recently_published_navigation() { return null; } + +/** + * Insert hooked blocks into a Navigation block. + * + * Given a Navigation block's inner blocks and its corresponding `wp_navigation` post object, + * this function inserts hooked blocks into it, and returns the serialized inner blocks in a + * mock Navigation block wrapper. + * + * If there are any hooked blocks that need to be inserted as the Navigation block's first or last + * children, the `wp_navigation` post's `_wp_ignored_hooked_blocks` meta is checked to see if any + * of those hooked blocks should be exempted from insertion. + * + * @param array $inner_blocks Parsed inner blocks of a Navigation block. + * @param WP_Post $post `wp_navigation` post object corresponding to the block. + * @return string Serialized inner blocks in mock Navigation block wrapper, with hooked blocks inserted, if any. + */ +function block_core_navigation_insert_hooked_blocks( $inner_blocks, $post = null ) { + $before_block_visitor = null; + $after_block_visitor = null; + $hooked_blocks = get_hooked_blocks(); + $attributes = array(); + + if ( isset( $post->ID ) ) { + $ignored_hooked_blocks = get_post_meta( $post->ID, '_wp_ignored_hooked_blocks', true ); + if ( ! empty( $ignored_hooked_blocks ) ) { + $ignored_hooked_blocks = json_decode( $ignored_hooked_blocks, true ); + $attributes['metadata'] = array( + 'ignoredHookedBlocks' => $ignored_hooked_blocks, + ); + } + } + + $mock_anchor_parent_block = array( + 'blockName' => 'core/navigation', + 'attrs' => $attributes, + 'innerBlocks' => $inner_blocks, + 'innerContent' => array_fill( 0, count( $inner_blocks ), null ), + ); + $before_block_visitor = null; + $after_block_visitor = null; + + if ( ! empty( $hooked_blocks ) || has_filter( 'hooked_block_types' ) ) { + $before_block_visitor = make_before_block_visitor( $hooked_blocks, $post ); + $after_block_visitor = make_after_block_visitor( $hooked_blocks, $post ); + } + + return traverse_and_serialize_block( $mock_anchor_parent_block, $before_block_visitor, $after_block_visitor ); +} + +/** + * Updates the post meta with the list of ignored hooked blocks when the navigation is created or updated via the REST API. + * + * @param WP_Post $post Post object. + */ +function block_core_navigation_update_ignore_hooked_blocks_meta( $post ) { + if ( ! isset( $post->ID ) ) { + return; + } + + // We run the Block Hooks mechanism so it will return the list of ignored hooked blocks + // in the mock root Navigation block's metadata attribute. + // We ignore the rest of the returned `$markup`; `$post->post_content` already has the hooked + // blocks inserted, whereas `$markup` will have them inserted twice. + $blocks = parse_blocks( $post->post_content ); + $markup = block_core_navigation_insert_hooked_blocks( $blocks, $post ); + $root_nav_block = parse_blocks( $markup )[0]; + $ignored_hooked_blocks = isset( $root_nav_block['attrs']['metadata']['ignoredHookedBlocks'] ) + ? $root_nav_block['attrs']['metadata']['ignoredHookedBlocks'] + : array(); + + if ( ! empty( $ignored_hooked_blocks ) ) { + $existing_ignored_hooked_blocks = get_post_meta( $post->ID, '_wp_ignored_hooked_blocks', true ); + if ( ! empty( $existing_ignored_hooked_blocks ) ) { + $existing_ignored_hooked_blocks = json_decode( $existing_ignored_hooked_blocks, true ); + $ignored_hooked_blocks = array_unique( array_merge( $ignored_hooked_blocks, $existing_ignored_hooked_blocks ) ); + } + update_post_meta( $post->ID, '_wp_ignored_hooked_blocks', json_encode( $ignored_hooked_blocks ) ); + } +} + +// Injection of hooked blocks into the Navigation block relies on some functions present in WP >= 6.4 +// that are not present in Gutenberg's WP 6.4 compatibility layer. +if ( function_exists( 'get_hooked_blocks' ) ) { + add_action( 'rest_insert_wp_navigation', 'block_core_navigation_update_ignore_hooked_blocks_meta', 10, 3 ); +} + +/** + * Hooks into the REST API response for the core/navigation block and adds the first and last inner blocks. + * + * @param WP_REST_Response $response The response object. + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response The response object. + */ +function block_core_navigation_insert_hooked_blocks_into_rest_response( $response, $post ) { + if ( ! isset( $response->data['content']['raw'] ) || ! isset( $response->data['content']['rendered'] ) ) { + return $response; + } + $parsed_blocks = parse_blocks( $response->data['content']['raw'] ); + $content = block_core_navigation_insert_hooked_blocks( $parsed_blocks, $post ); + + // Remove mock Navigation block wrapper. + $start = strpos( $content, '-->' ) + strlen( '-->' ); + $end = strrpos( $content, '`. Support these by defaulting an undefined label and @@ -77,39 +77,19 @@ function render_block_core_search( $attributes, $content, $block ) { $input->set_attribute( 'value', get_search_query() ); $input->set_attribute( 'placeholder', $attributes['placeholder'] ); + // If it's interactive, enqueue the script module and add the directives. $is_expandable_searchfield = 'button-only' === $button_position; if ( $is_expandable_searchfield ) { + wp_enqueue_script_module( '@wordpress/block-library/search-block' ); + $input->set_attribute( 'data-wp-bind--aria-hidden', '!context.isSearchInputVisible' ); $input->set_attribute( 'data-wp-bind--tabindex', 'state.tabindex' ); - // Adding these attributes manually is needed until the Interactivity API SSR logic is added to core. + + // Adding these attributes manually is needed until the Interactivity API + // SSR logic is added to core. $input->set_attribute( 'aria-hidden', 'true' ); $input->set_attribute( 'tabindex', '-1' ); } - - $is_gutenberg_plugin = defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN; - $script_handles = $block->block_type->view_script_handles; - $view_js_file = 'wp-block-search-view'; - - if ( $is_gutenberg_plugin ) { - if ( $is_expandable_searchfield ) { - gutenberg_enqueue_module( '@wordpress/block-library/search-block' ); - } - // Remove the view script because we are using the module. - $block->block_type->view_script_handles = array_diff( $script_handles, array( $view_js_file ) ); - } else { - // If the script already exists, there is no point in removing it from viewScript. - if ( ! wp_script_is( $view_js_file ) ) { - - // If the script is not needed, and it is still in the `view_script_handles`, remove it. - if ( ! $is_expandable_searchfield && in_array( $view_js_file, $script_handles, true ) ) { - $block->block_type->view_script_handles = array_diff( $script_handles, array( $view_js_file ) ); - } - // If the script is needed, but it was previously removed, add it again. - if ( $is_expandable_searchfield && ! in_array( $view_js_file, $script_handles, true ) ) { - $block->block_type->view_script_handles = array_merge( $script_handles, array( $view_js_file ) ); - } - } - } } if ( count( $query_params ) > 0 ) { @@ -159,7 +139,9 @@ function render_block_core_search( $attributes, $content, $block ) { $button->set_attribute( 'data-wp-bind--aria-expanded', 'context.isSearchInputVisible' ); $button->set_attribute( 'data-wp-bind--type', 'state.type' ); $button->set_attribute( 'data-wp-on--click', 'actions.openSearchInput' ); - // Adding these attributes manually is needed until the Interactivity API SSR logic is added to core. + + // Adding these attributes manually is needed until the Interactivity + // API SSR logic is added to core. $button->set_attribute( 'aria-label', __( 'Expand search field' ) ); $button->set_attribute( 'aria-controls', 'wp-block-search__input-' . $input_id ); $button->set_attribute( 'aria-expanded', 'false' ); @@ -181,6 +163,8 @@ function render_block_core_search( $attributes, $content, $block ) { array( 'class' => $classnames ) ); $form_directives = ''; + + // If it's interactive, add the directives. if ( $is_expandable_searchfield ) { $aria_label_expanded = __( 'Submit Search' ); $aria_label_collapsed = __( 'Expand search field' ); @@ -213,14 +197,12 @@ function register_block_core_search() { ) ); - if ( defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN ) { - gutenberg_register_module( - '@wordpress/block-library/search-block', - gutenberg_url( '/build/interactivity/search.min.js' ), - array( '@wordpress/interactivity' ), - defined( 'GUTENBERG_VERSION' ) ? GUTENBERG_VERSION : get_bloginfo( 'version' ) - ); - } + wp_register_script_module( + '@wordpress/block-library/search-block', + gutenberg_url( '/build/interactivity/search.min.js' ), + array( '@wordpress/interactivity' ), + defined( 'GUTENBERG_VERSION' ) ? GUTENBERG_VERSION : get_bloginfo( 'version' ) + ); } add_action( 'init', 'register_block_core_search' ); diff --git a/packages/block-library/src/table-of-contents/hooks.js b/packages/block-library/src/table-of-contents/hooks.js index af7b66568123f8..ef24d8167381ad 100644 --- a/packages/block-library/src/table-of-contents/hooks.js +++ b/packages/block-library/src/table-of-contents/hooks.js @@ -17,7 +17,7 @@ function getLatestHeadings( select, clientId ) { getBlockAttributes, getBlockName, getClientIdsWithDescendants, - __experimentalGetGlobalBlocksByName: getGlobalBlocksByName, + getBlocksByName, } = select( blockEditorStore ); // FIXME: @wordpress/block-library should not depend on @wordpress/editor. @@ -28,7 +28,7 @@ function getLatestHeadings( select, clientId ) { // eslint-disable-next-line @wordpress/data-no-store-string-literals const permalink = select( 'core/editor' ).getPermalink() ?? null; - const isPaginated = getGlobalBlocksByName( 'core/nextpage' ).length !== 0; + const isPaginated = getBlocksByName( 'core/nextpage' ).length !== 0; const { onlyIncludeCurrentPage } = getBlockAttributes( clientId ) ?? {}; // Get the client ids of all blocks in the editor. diff --git a/packages/block-library/src/template-part/edit/index.js b/packages/block-library/src/template-part/edit/index.js index ec92996978bd6f..72fc06338e7807 100644 --- a/packages/block-library/src/template-part/edit/index.js +++ b/packages/block-library/src/template-part/edit/index.js @@ -7,8 +7,8 @@ import { useBlockProps, Warning, store as blockEditorStore, - __experimentalRecursionProvider as RecursionProvider, - __experimentalUseHasRecursion as useHasRecursion, + RecursionProvider, + useHasRecursion, InspectorControls, } from '@wordpress/block-editor'; import { Spinner, Modal, MenuItem } from '@wordpress/components'; diff --git a/packages/block-library/tsconfig.json b/packages/block-library/tsconfig.json index a9a30e9804e1f3..a5f01298a8fad4 100644 --- a/packages/block-library/tsconfig.json +++ b/packages/block-library/tsconfig.json @@ -27,6 +27,7 @@ { "path": "../i18n" }, { "path": "../icons" }, { "path": "../interactivity" }, + { "path": "../interactivity-router" }, { "path": "../notices" }, { "path": "../keycodes" }, { "path": "../primitives" }, diff --git a/packages/block-serialization-default-parser/CHANGELOG.md b/packages/block-serialization-default-parser/CHANGELOG.md index b705866a049578..3d22380bb83251 100644 --- a/packages/block-serialization-default-parser/CHANGELOG.md +++ b/packages/block-serialization-default-parser/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.50.0 (2024-01-24) + ## 4.49.0 (2024-01-10) ## 4.48.0 (2023-12-13) diff --git a/packages/block-serialization-default-parser/package.json b/packages/block-serialization-default-parser/package.json index 668fc92eea94b6..a91cfafd099841 100644 --- a/packages/block-serialization-default-parser/package.json +++ b/packages/block-serialization-default-parser/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-serialization-default-parser", - "version": "4.49.0", + "version": "4.50.0", "description": "Block serialization specification parser for WordPress posts.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/block-serialization-spec-parser/CHANGELOG.md b/packages/block-serialization-spec-parser/CHANGELOG.md index c6f586ce6b4805..8c286aeb97510a 100644 --- a/packages/block-serialization-spec-parser/CHANGELOG.md +++ b/packages/block-serialization-spec-parser/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.50.0 (2024-01-24) + ## 4.49.0 (2024-01-10) ## 4.48.0 (2023-12-13) diff --git a/packages/block-serialization-spec-parser/package.json b/packages/block-serialization-spec-parser/package.json index df841c8a506c15..317519d3b6b8b4 100644 --- a/packages/block-serialization-spec-parser/package.json +++ b/packages/block-serialization-spec-parser/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-serialization-spec-parser", - "version": "4.49.0", + "version": "4.50.0", "description": "Block serialization specification parser for WordPress posts.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/blocks/CHANGELOG.md b/packages/blocks/CHANGELOG.md index d932b12a54531c..d019301aaf4c15 100644 --- a/packages/blocks/CHANGELOG.md +++ b/packages/blocks/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 12.27.0 (2024-01-24) + ## 12.26.0 (2024-01-10) ## 12.25.0 (2023-12-13) diff --git a/packages/blocks/package.json b/packages/blocks/package.json index af0e0765745185..e950643892783d 100644 --- a/packages/blocks/package.json +++ b/packages/blocks/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/blocks", - "version": "12.26.0", + "version": "12.27.0", "description": "Block API for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/blocks/src/api/constants.js b/packages/blocks/src/api/constants.js index f08717592cb4b2..8af2d0940c8fcc 100644 --- a/packages/blocks/src/api/constants.js +++ b/packages/blocks/src/api/constants.js @@ -20,6 +20,11 @@ export const __EXPERIMENTAL_STYLE_PROPERTY = { value: [ 'color', 'link' ], support: [ 'color', 'link' ], }, + aspectRatio: { + value: [ 'dimensions', 'aspectRatio' ], + support: [ 'dimensions', 'aspectRatio' ], + useEngine: true, + }, background: { value: [ 'color', 'gradient' ], support: [ 'color', 'gradients' ], diff --git a/packages/browserslist-config/CHANGELOG.md b/packages/browserslist-config/CHANGELOG.md index cf94e1caf70e53..db648ec7f8aca8 100644 --- a/packages/browserslist-config/CHANGELOG.md +++ b/packages/browserslist-config/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 5.33.0 (2024-01-24) + ## 5.32.0 (2024-01-10) ## 5.31.0 (2023-12-13) diff --git a/packages/browserslist-config/package.json b/packages/browserslist-config/package.json index 4b2747e505e162..95544e220ea920 100644 --- a/packages/browserslist-config/package.json +++ b/packages/browserslist-config/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/browserslist-config", - "version": "5.32.0", + "version": "5.33.0", "description": "WordPress Browserslist shared configuration.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/commands/CHANGELOG.md b/packages/commands/CHANGELOG.md index 3b42cc552899f1..5c03351cf93c09 100644 --- a/packages/commands/CHANGELOG.md +++ b/packages/commands/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 0.21.0 (2024-01-24) + ## 0.20.0 (2024-01-10) ## Enhancements diff --git a/packages/commands/package.json b/packages/commands/package.json index 5314cf671b1ba7..b0c53a60d7b585 100644 --- a/packages/commands/package.json +++ b/packages/commands/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/commands", - "version": "0.20.0", + "version": "0.21.0", "description": "Handles the commands menu.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 92ce15dc425f6d..c30aeee656754c 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 25.16.0 (2024-01-24) + ### Enhancements - `ColorPicker`: improve the UX around HSL sliders ([#57555](https://github.com/WordPress/gutenberg/pull/57555)). @@ -9,16 +11,22 @@ - `PaletteEdit`: improve unit tests ([#57645](https://github.com/WordPress/gutenberg/pull/57645)). - `PaletteEdit` and `CircularOptionPicker`: improve unit tests ([#57809](https://github.com/WordPress/gutenberg/pull/57809)). - `Tooltip`: no-op when nested inside other `Tooltip` components ([#57202](https://github.com/WordPress/gutenberg/pull/57202)). +- `Tooltip` and `Button`: tidy up unit tests ([#57975](https://github.com/WordPress/gutenberg/pull/57975)). +- `BorderControl`, `BorderBoxControl`: Replace style picker with ToggleGroupControl ([#57562](https://github.com/WordPress/gutenberg/pull/57562)). +- `SlotFill`: fix typo in use-slot-fills return docs ([#57654](https://github.com/WordPress/gutenberg/pull/57654)) ### Bug Fix - `ToggleGroupControl`: Improve controlled value detection ([#57770](https://github.com/WordPress/gutenberg/pull/57770)). - `Tooltip`: Improve props forwarding to children of nested `Tooltip` components ([#57878](https://github.com/WordPress/gutenberg/pull/57878)). +- `Tooltip`: revert prop types to only accept component-specific props ([#58125](https://github.com/WordPress/gutenberg/pull/58125)). +- `Button`: prevent the component from trashing and re-creating the HTML element ([#56490](https://github.com/WordPress/gutenberg/pull/56490)). ### Experimental - `BoxControl`: Update design ([#56665](https://github.com/WordPress/gutenberg/pull/56665)). - `CustomSelect`: adjust `renderSelectedValue` to fix sizing ([#57865](https://github.com/WordPress/gutenberg/pull/57865)). +- `Theme`: Set `color` on wrapper div ([#58095](https://github.com/WordPress/gutenberg/pull/58095)). ## 25.15.0 (2024-01-10) diff --git a/packages/components/package.json b/packages/components/package.json index a16778a7778b56..071fa44727b1d1 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/components", - "version": "25.15.0", + "version": "25.16.0", "description": "UI components for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/components/src/border-control/border-control-dropdown/component.tsx b/packages/components/src/border-control/border-control-dropdown/component.tsx index 3ee01bcda8f3b3..0a56edd946308b 100644 --- a/packages/components/src/border-control/border-control-dropdown/component.tsx +++ b/packages/components/src/border-control/border-control-dropdown/component.tsx @@ -142,6 +142,7 @@ const BorderControlDropdown = ( enableStyle, indicatorClassName, indicatorWrapperClassName, + isStyleSettable, onReset, onColorChange, onStyleChange, @@ -218,7 +219,7 @@ const BorderControlDropdown = ( clearable={ false } enableAlpha={ enableAlpha } /> - { enableStyle && ( + { enableStyle && isStyleSettable && ( { - const { label, hideLabelFromVision } = props; - - if ( ! label ) { - return null; - } - - return hideLabelFromVision ? ( - { label } - ) : ( - { label } - ); -}; - -const BorderControlStylePicker = ( - props: WordPressComponentProps< StylePickerProps, 'div' >, +function UnconnectedBorderControlStylePicker( + { onChange, ...restProps }: StylePickerProps, forwardedRef: React.ForwardedRef< any > -) => { - const { - buttonClassName, - hideLabelFromVision, - label, - onChange, - value, - ...otherProps - } = useBorderControlStylePicker( props ); - +) { return ( - -