diff --git a/.github/ISSUE_TEMPLATE/Custom.md b/.github/ISSUE_TEMPLATE/Custom.md index 89783e5bbea906..196cdeb63305fa 100644 --- a/.github/ISSUE_TEMPLATE/Custom.md +++ b/.github/ISSUE_TEMPLATE/Custom.md @@ -1,13 +1,17 @@ --- -name: Question -about: Questions or 'how to' about Gutenberg +name: Help Request +about: Please post help requests or ‘how to’ questions in support channels first --- -If you have a question you have a few places that you can ask this: +Search first! Your issue may have already been reported. -- Support Forums: https://wordpress.org/support/plugin/gutenberg -- Handbook: https://wordpress.org/gutenberg/handbook -- https://chat.wordpress.org #core-editor +For general help requests, please post in the support forum at https://wordpress.org/support/forum/how-to-and-troubleshooting/. -If you are unable to ask in those places you can ask here, however you will get faster responses through those recommended places. +Technical help requests have their own section of the support forum at https://wordpress.org/support/forum/wp-advanced/. + +You may also ask for technical support at https://wordpress.stackexchange.com/. + +Please make sure you have checked the Handbook at https://wordpress.org/gutenberg/handbook before asking your question. + +Thank you! diff --git a/.travis.yml b/.travis.yml index 55fd93ba4a1efc..8a464d766d9739 100644 --- a/.travis.yml +++ b/.travis.yml @@ -68,6 +68,7 @@ jobs: - ./bin/setup-local-env.sh script: - $( npm bin )/wp-scripts test-e2e --config=./packages/e2e-tests/jest.config.js --listTests > ~/.jest-e2e-tests + - npm run build - npm run test-e2e -- --ci --cacheDirectory="$HOME/.jest-cache" --runTestsByPath $( awk 'NR % 2 == 0' < ~/.jest-e2e-tests ) - name: E2E tests (Admin with plugins) (2/2) @@ -76,6 +77,7 @@ jobs: - ./bin/setup-local-env.sh script: - $( npm bin )/wp-scripts test-e2e --config=./packages/e2e-tests/jest.config.js --listTests > ~/.jest-e2e-tests + - npm run build - npm run test-e2e -- --ci --cacheDirectory="$HOME/.jest-cache" --runTestsByPath $( awk 'NR % 2 == 1' < ~/.jest-e2e-tests ) - name: E2E tests (Author without plugins) (1/2) @@ -84,6 +86,7 @@ jobs: - ./bin/setup-local-env.sh script: - $( npm bin )/wp-scripts test-e2e --config=./packages/e2e-tests/jest.config.js --listTests > ~/.jest-e2e-tests + - npm run build - npm run test-e2e -- --ci --cacheDirectory="$HOME/.jest-cache" --runTestsByPath $( awk 'NR % 2 == 0' < ~/.jest-e2e-tests ) - name: E2E tests (Author without plugins) (2/2) @@ -92,4 +95,5 @@ jobs: - ./bin/setup-local-env.sh script: - $( npm bin )/wp-scripts test-e2e --config=./packages/e2e-tests/jest.config.js --listTests > ~/.jest-e2e-tests + - npm run build - npm run test-e2e -- --ci --cacheDirectory="$HOME/.jest-cache" --runTestsByPath $( awk 'NR % 2 == 1' < ~/.jest-e2e-tests ) diff --git a/bin/install-wordpress.sh b/bin/install-wordpress.sh index 40eec0810bd90e..a5ba87ec4f4d5a 100755 --- a/bin/install-wordpress.sh +++ b/bin/install-wordpress.sh @@ -65,6 +65,7 @@ fi # Make sure the uploads and upgrade folders exist and we have permissions to add files. echo -e $(status_message "Ensuring that files can be uploaded...") docker-compose $DOCKER_COMPOSE_FILE_OPTIONS run --rm $CONTAINER chmod 767 /var/www/html/wp-content/plugins +docker-compose $DOCKER_COMPOSE_FILE_OPTIONS run --rm $CONTAINER chmod 767 /var/www/html/wp-settings.php docker-compose $DOCKER_COMPOSE_FILE_OPTIONS run --rm $CONTAINER mkdir -p /var/www/html/wp-content/uploads docker-compose $DOCKER_COMPOSE_FILE_OPTIONS run --rm $CONTAINER chmod -v 767 /var/www/html/wp-content/uploads docker-compose $DOCKER_COMPOSE_FILE_OPTIONS run --rm $CONTAINER mkdir -p /var/www/html/wp-content/upgrade diff --git a/docs/contributors/reference.md b/docs/contributors/reference.md index 551c6f82ab8e72..95c0b603323e50 100644 --- a/docs/contributors/reference.md +++ b/docs/contributors/reference.md @@ -1,9 +1,9 @@ # Reference -- [Glossary](../../docs/designers-developers/glossary.md) -- [Coding Guidelines](../../docs/contributors/coding-guidelines.md) -- [Testing Overview](../../docs/contributors/testing-overview.md) -- [Frequently Asked Questions](../../docs/designers-developers/faq.md) +- [Glossary](/docs/designers-developers/glossary.md) +- [Coding Guidelines](/docs/contributors/coding-guidelines.md) +- [Testing Overview](/docs/contributors/testing-overview.md) +- [Frequently Asked Questions](/docs/designers-developers/faq.md) ## Logo Gutenberg Logo @@ -14,4 +14,4 @@ Released under GPL license, made by [Cristel Rossignol](https://twitter.com/cris ## Mockups -Mockup Sketch files are available in the Design section. +Mockup Sketch files are available in [the Design section](/docs/designers-developers/designers/design-resources.md). diff --git a/docs/contributors/repository-management.md b/docs/contributors/repository-management.md index d4e8df0ebf6aa7..08b47e1777f2a2 100644 --- a/docs/contributors/repository-management.md +++ b/docs/contributors/repository-management.md @@ -1,6 +1,6 @@ # Repository Management -The goal is for this to be a living document explaining how we collaboratively manage the Gutenberg repository. If you’d like to suggest a change, please open an issue for discussion or submit a pull request to the document. +This is a living document explaining how we collaboratively manage the Gutenberg repository. If you’d like to suggest a change, please open an issue for discussion or submit a pull request to the document. This document covers: @@ -17,14 +17,24 @@ This document covers: ## Issues -A healthy issue backlog is one where issues are relevant and actionable. *Relevant* in the sense that they relate to the project’s current priorities. *Actionable* in the sense that it’s clear what action(s) need to be taken to resolve the issue. +A healthy issue list is one where issues are relevant and actionable. *Relevant* in the sense that they relate to the project’s current priorities. *Actionable* in the sense that it’s clear what action(s) need to be taken to resolve the issue. -Any issues that are irrelevant or not actionable should be closed, because they get in the way of making progress on the project. Imagine the issue backlog as a desk: the more clutter you have on it, the more difficult it is to use the space to get work done. +Any issues that are irrelevant or not actionable should be closed, because they get in the way of making progress on the project. Imagine the issue list as a desk: the more clutter you have on it, the more difficult it is to use the space to get work done. ### Labels -To better organize the issue backlog, all issues should have [one or more labels](https://github.com/WordPress/gutenberg/labels). Here are some you might commonly see: +All issues should have [one or more labels](https://github.com/WordPress/gutenberg/labels). +Workflow labels start with “Needs” and may be applied as needed. Ideally, each workflow label will have a group that follows it, such as the Accessibility Team for `Needs Accessibility Feedback`, the Testing Team for `Needs Testing`, etc. + +[Priority High](https://github.com/WordPress/gutenberg/labels/Priority%20High) and [Priority OMGWTFBBQ](https://github.com/WordPress/gutenberg/labels/Priority%20OMGWTFBBQ) issues should have an assignee and/or be in an active milestone. + +Help requests or 'how to' questions should be posted in a relevant support forum as a first step. If something might be a bug but it's not clear, the Support Team or a forum volunteer can help troubleshoot the case to help get all the right information needed for an effective bug report. + +Here are some labels you might commonly see: + +- [Good First Issue](https://github.com/WordPress/gutenberg/labels/Good%20First%20Issue) - Issues identified as good for new contributors to work on. Comment to note that you intend to work on the issue and reference the issue number in the pull request you submit. +- [Good First Review](https://github.com/WordPress/gutenberg/labels/Good%20First%20Review) - Pull requests identified as good for new contributors who are interested in doing code reviews. - [Needs Accessibility Feedback](https://github.com/WordPress/gutenberg/labels/Accessibility) - Changes that impact accessibility and need corresponding review (e.g. markup changes). - [Needs Design Feedback](https://github.com/WordPress/gutenberg/labels/Needs%20Design%20Feedback) - Changes that modify the design or user experience in some way and need sign-off. - [[Type] Bug](https://github.com/WordPress/gutenberg/labels/%5BType%5D%20Bug) - An existing feature is broken in some way. @@ -32,53 +42,41 @@ To better organize the issue backlog, all issues should have [one or more labels - [[Type] Plugin / Extension Conflict](https://github.com/WordPress/gutenberg/labels/%5BType%5D%20Plugin%20%2F%20Extension%20Conflict) - Documentation of a conflict between Gutenberg and a plugin or extension. The plugin author should be informed and provided documentation on how to address. - [[Status] Needs More Info](https://github.com/WordPress/gutenberg/labels/%5BStatus%5D%20Needs%20More%20Info) - The issue needs more information in order to be actionable and relevant. Typically this requires follow-up from the original reporter. -Workflow labels may be applied as needed and start with “Needs”. Ideally, each workflow label will have a group that follows it, such as the Accessibility Team for `Needs Accessibility Feedback`, the Testing Team for `Needs Testing`, etc. - -`Priority High` and `Priority OMGWTFBBQ` issues should have an assignee and/or be in an active milestone. - [Check out the label directory](https://github.com/WordPress/gutenberg/labels) for a listing of all labels. ### Milestones -We put issues into [milestones](https://github.com/wordpress/gutenberg/milestones) to better categorize them. Here are some you might see: - -- The next 2 releases we have milestones for (e.g. 2.2, 2.3). -- [Feature Complete](https://github.com/WordPress/gutenberg/milestone/8): This includes big features and is what will be managing the vision of Gutenberg. All of this would be done before even merge proposal is thought about. Examples here include nesting, drag and drop and extensibility API. -- [Merge Proposal: Editor](https://github.com/WordPress/gutenberg/milestone/22): All issues related to merge proposal for the editor. -- [Merge Proposal: Rest API](https://github.com/WordPress/gutenberg/milestone/39): All issues related to merge proposal for the Rest API -- [Merge Proposal: Accessibility](https://github.com/WordPress/gutenberg/milestone/43): All accessibility issues related to merge proposal. -- [Merge Proposal: Media](https://github.com/WordPress/gutenberg/milestone/42): All issues related to merge proposal for the media component. -- [Merge Proposal: Documentation](https://github.com/WordPress/gutenberg/milestone/50): All issues related to documentation for the merge proposal. -- [Merge Proposal: i18n](https://github.com/WordPress/gutenberg/milestone/49): All translation issues for the merge proposal. -- [Merge Proposal: Customization](https://github.com/WordPress/gutenberg/milestone/44): All Customization issues for the merge proposal. -- [Merge Proposal: Plugin](https://github.com/WordPress/gutenberg/milestone/48): All plugin and extensibility issues for the merge proposal. -- [Merge Proposal: Back Compat](https://github.com/WordPress/gutenberg/milestone/47): All back compatibility issues for the merge proposal. -- [Merge Proposal: Themes](https://github.com/WordPress/gutenberg/milestone/48): All theme issues for the merge proposal. -- [Merge Proposal: Core](https://github.com/WordPress/gutenberg/milestone/45): All core issues for the merge proposal that don't fit other merge proposal milestones. -- [Bonus Features](https://github.com/WordPress/gutenberg/milestone/32): Again likely not part of triage and includes nice to haves for the project, if time before merge. A few examples include collaborative editing and footnotes. +We put issues into [milestones](https://github.com/wordpress/gutenberg/milestones) to better categorize them. Issues are added to milestones starting with `WordPress` and pull requests are added to milestones ending in `(Gutenberg)`. + +Here are some milestones you might see: + +- [WordPress X.Y](https://github.com/WordPress/gutenberg/milestone/70): Tasks that should be done for future WordPress releases. +- [X.Y (Gutenberg)](https://github.com/WordPress/gutenberg/milestone/85): PRs targeted for the Gutenberg Plugin X.Y release. - [Future](https://github.com/WordPress/gutenberg/milestone/35): this is something that is confirmed by everyone as a good thing but doesn’t fall into other criteria. ### Triaging Issues -To keep the issue backlog healthy, it needs to be triaged regularly. *Triage* is the practice of reviewing existing issues to make sure they’re relevant, actionable, and have all the information they need. +To keep the issue list healthy, it needs to be triaged regularly. *Triage* is the practice of reviewing existing issues to make sure they’re relevant, actionable, and have all the information they need. -Anyone can help triage the backlog, although you’ll need contributor permission on the Gutenberg repository to modify an issue’s labels or edit its title. +Anyone can help triage, although you’ll need contributor permission on the Gutenberg repository to modify an issue’s labels or edit its title. Here are a couple places you can start: -- [All Gutenberg issues without an assigned label](https://github.com/wordpress/gutenberg/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-asc+no%3Alabel) -- [The least recently updated Gutenberg issues](https://github.com/WordPress/gutenberg/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-asc) +- [All Gutenberg issues without an assigned label](https://github.com/wordpress/gutenberg/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-asc+no%3Alabel). +- [The least recently updated Gutenberg issues](https://github.com/WordPress/gutenberg/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-asc). -When reviewing the issue backlog, here are some steps you can perform: +When reviewing issues, here are some steps you can perform: -- If it’s a bug report, test to confirm the report. If there is not enough information to confirm the report, add the `[Status] Needs More Info` label. -- If the issue is missing labels, add some to better categorize it. -- If the issue is duplicate of another already in the backlog, close the issue by commenting with “Duplicate of #”. Add any relevant new details to the existing issue. +- First search for duplicates. If the issue is duplicate, close it by commenting with “Duplicate of #” and add any relevant new details to the existing issue. +- If the issue is missing labels, add some to better categorize it (requires proper permissions). +- If the title doesn’t communicate the issue, edit it for clarity (requires proper permissions). +- If it’s a bug report, test to confirm the report or add the `Needs Testing` label. If there is not enough information to confirm the report, add the `[Status] Needs More Info` label and ask for the details needed. +- Remove the `[Status] Needs More Info` if the author of the issue has responded with enough details. +- Close the issue with a note if it has a `[Status] Needs More Info` label but the author didn't respond in 2+ weeks. - If there was conversation on the issue but no actionable steps identified, follow up with the participants to see what’s actionable. -- If the title doesn’t communicate the issue, edit it for clarity. - If you feel comfortable triaging the issue further, then you can also: - Check that the bug report is valid by debugging it to see if you can track down the technical specifics. - - Check if the issue is missing some detail and see if you can fill in those details. For instance, if a bug report is missing visual detail, it’s helpful to reproduce the issue locally and upload a GIF. + - Check if the issue is missing some detail and see if you can fill in those details. For instance, if a bug report is missing visual detail, it’s helpful to reproduce the issue locally and upload a screenshot or GIF. ## Pull Requests @@ -158,6 +156,6 @@ We use [GitHub projects](https://github.com/WordPress/gutenberg/projects) to kee Some key projects include: -* [Customization](https://github.com/WordPress/gutenberg/projects/13) - Blocks and tasks needed for customization in Gutenberg. -* [Extensibility](https://github.com/WordPress/gutenberg/projects/14) - Comprises the entirety of extensibility APIs. See [Native Gutenberg Extensibility Overview](https://github.com/WordPress/gutenberg/issues/3330) for more details. -* [Third-Party Compatibility](https://github.com/WordPress/gutenberg/projects/15) - Issue that impact Gutenberg's adoption in the real world. +* [Phase 2](https://github.com/WordPress/gutenberg/projects/13) - Development tasks needed for Phase 2 of Gutenberg. +* [Phase 2 design](https://github.com/WordPress/gutenberg/projects/21) - Tasks for design in Phase 2. Note: specific projects may have their own boards. +* [Ideas](https://github.com/WordPress/gutenberg/projects/8) - Project containing tickets that, while closed for the time being, can be revisited in the future. diff --git a/docs/contributors/scripts.md b/docs/contributors/scripts.md index dd7e1d295d528c..5d5c1f672a4b2a 100644 --- a/docs/contributors/scripts.md +++ b/docs/contributors/scripts.md @@ -8,38 +8,38 @@ The editor includes a number of packages to enable various pieces of functionali | Script Name | Handle | Description | |-------------|--------|-------------| -| [Blob](https://wordpress.org/gutenberg/handbook/packages/packages-blob/) | wp-blob | Blob utilities | -| [Block Library](https://wordpress.org/gutenberg/handbook/packages/packages-block-library/) | wp-block-library | Block library for the editor | -| [Blocks](https://wordpress.org/gutenberg/handbook/packages/packages-blocks/) | wp-blocks | Block creations | -| [Block Serialization Default Parser](https://wordpress.org/gutenberg/handbook/packages/packages-block-serialization-default-parser/) | wp-block-serialization-default-parser | Default block serialization parser implementations for WordPress documents | -| [Block Serialization Spec Parser](https://wordpress.org/gutenberg/handbook/packages/packages-block-serialization-spec-parser/) | wp-block-serialization-spec-parser | Grammar file (grammar.pegjs) for WordPress posts | -| [Components](https://wordpress.org/gutenberg/handbook/packages/packages-components/) | wp-components | Generic components to be used for creating common UI elements | -| [Compose](https://wordpress.org/gutenberg/handbook/packages/packages-compose/) | wp-compose | Collection of handy Higher Order Components (HOCs) | -| [Core Data](https://wordpress.org/gutenberg/handbook/packages/packages-core-data/) | wp-core-data | Simplify access to and manipulation of core WordPress entities | -| [Data](https://wordpress.org/gutenberg/handbook/packages/packages-data/) | wp-data | Data module serves as a hub to manage application state for both plugins and WordPress itself | -| [Date](https://wordpress.org/gutenberg/handbook/packages/packages-date/) | wp-date | Date module for WordPress | -| [Deprecated](https://wordpress.org/gutenberg/handbook/packages/packages-deprecated/) | wp-deprecated | Utility to log a message to notify developers about a deprecated feature | -| [Dom](https://wordpress.org/gutenberg/handbook/packages/packages-dom/) | wp-dom | DOM utilities module for WordPress | -| [Dom Ready](https://wordpress.org/gutenberg/handbook/packages/packages-dom-ready/) | wp-dom-ready | Execute callback after the DOM is loaded | -| [Editor](https://wordpress.org/gutenberg/handbook/packages/packages-editor/) | wp-editor | Building blocks for WordPress editors | -| [Edit Post](https://wordpress.org/gutenberg/handbook/packages/packages-edit-post/) | wp-edit-post | Edit Post Module for WordPress | -| [Element](https://wordpress.org/gutenberg/handbook/packages/packages-element/) | wp-element |Element is, quite simply, an abstraction layer atop [React](https://reactjs.org/) | -| [Escape Html](https://wordpress.org/gutenberg/handbook/packages/packages-escape-html/) | wp-escape-html | Escape HTML utils | -| [Hooks](https://wordpress.org/gutenberg/handbook/packages/packages-hooks/) | wp-hooks | A lightweight and efficient EventManager for JavaScript | -| [Html Entities](https://wordpress.org/gutenberg/handbook/packages/packages-html-entities/) | wp-html-entities | HTML entity utilities for WordPress | -| [I18N](https://wordpress.org/gutenberg/handbook/packages/packages-i18n/) | wp-i18n | Internationalization utilities for client-side localization | -| [Is Shallow Equal](https://wordpress.org/gutenberg/handbook/packages/packages-is-shallow-equal/) | wp-is-shallow-equal | A function for performing a shallow comparison between two objects or arrays | -| [Keycodes](https://wordpress.org/gutenberg/handbook/packages/packages-keycodes/) | wp-keycodes | Keycodes utilities for WordPress, used to check the key pressed in events like `onKeyDown` | -| [List Reusable Bocks](https://wordpress.org/gutenberg/handbook/packages/packages-list-reusable-blocks/) | wp-list-reusable-blocks | Package used to add import/export links to the listing page of the reusable blocks | -| [NUX](https://wordpress.org/gutenberg/handbook/packages/packages-nux/) | wp-nux | Components, and wp.data methods useful for onboarding a new user to the WordPress admin interface | -| [Plugins](https://wordpress.org/gutenberg/handbook/packages/packages-plugins/) | wp-plugins | Plugins module for WordPress | -| [Redux Routine](https://wordpress.org/gutenberg/handbook/packages/packages-redux-routine/) | wp-redux-routine | Redux middleware for generator coroutines | -| [Rich Text](https://wordpress.org/gutenberg/handbook/packages/packages-rich-text/) | wp-rich-text | Helper functions to convert HTML or a DOM tree into a rich text value and back | -| [Shortcode](https://wordpress.org/gutenberg/handbook/packages/packages-shortcode/) | wp-shortcode | Shortcode module for WordPress | -| [Token List](https://wordpress.org/gutenberg/handbook/packages/packages-token-list/) | wp-token-list | Constructable, plain JavaScript [DOMTokenList](https://developer.mozilla.org/en-US/docs/Web/API/DOMTokenList) implementation, supporting non-browser runtimes | -| [URL](https://wordpress.org/gutenberg/handbook/packages/packages-url/) | wp-url | A collection of utilities to manipulate URLs | -| [Viewport](https://wordpress.org/gutenberg/handbook/packages/packages-viewport/) | wp-viewport | Module for responding to changes in the browser viewport size | -| [Wordcount](https://wordpress.org/gutenberg/handbook/packages/packages-wordcount/) | wp-wordcount | WordPress word count utility | +| [Blob](/packages/blob/README.md) | wp-blob | Blob utilities | +| [Block Library](/packages/block-library/README.md) | wp-block-library | Block library for the editor | +| [Blocks](/packages/blocks/README.md) | wp-blocks | Block creations | +| [Block Serialization Default Parser](/packages/block-serialization-default-parser/README.md) | wp-block-serialization-default-parser | Default block serialization parser implementations for WordPress documents | +| [Block Serialization Spec Parser](/packages/block-serialization-spec-parser/README.md) | wp-block-serialization-spec-parser | Grammar file (grammar.pegjs) for WordPress posts | +| [Components](/packages/components/README.md) | wp-components | Generic components to be used for creating common UI elements | +| [Compose](/packages/compose/README.md) | wp-compose | Collection of handy Higher Order Components (HOCs) | +| [Core Data](/packages/core-data/README.md) | wp-core-data | Simplify access to and manipulation of core WordPress entities | +| [Data](/packages/data/README.md) | wp-data | Data module serves as a hub to manage application state for both plugins and WordPress itself | +| [Date](/packages/date/README.md) | wp-date | Date module for WordPress | +| [Deprecated](/packages/deprecated/README.md) | wp-deprecated | Utility to log a message to notify developers about a deprecated feature | +| [Dom](/packages/dom/README.md) | wp-dom | DOM utilities module for WordPress | +| [Dom Ready](/packages/dom-ready/README.md) | wp-dom-ready | Execute callback after the DOM is loaded | +| [Editor](/packages/editor/README.md) | wp-editor | Building blocks for WordPress editors | +| [Edit Post](/packages/edit-post/README.md) | wp-edit-post | Edit Post Module for WordPress | +| [Element](/packages/element/README.md) | wp-element |Element is, quite simply, an abstraction layer atop [React](https://reactjs.org/) | +| [Escape Html](/packages/escape-html/README.md) | wp-escape-html | Escape HTML utils | +| [Hooks](/packages/hooks/README.md) | wp-hooks | A lightweight and efficient EventManager for JavaScript | +| [Html Entities](/packages/html-entities/README.md) | wp-html-entities | HTML entity utilities for WordPress | +| [I18N](/packages/i18n/README.md) | wp-i18n | Internationalization utilities for client-side localization | +| [Is Shallow Equal](/packages/is-shallow-equal/README.md) | wp-is-shallow-equal | A function for performing a shallow comparison between two objects or arrays | +| [Keycodes](/packages/keycodes/README.md) | wp-keycodes | Keycodes utilities for WordPress, used to check the key pressed in events like `onKeyDown` | +| [List Reusable Bocks](/packages/list-reusable-blocks/README.md) | wp-list-reusable-blocks | Package used to add import/export links to the listing page of the reusable blocks | +| [NUX](/packages/nux/README.md) | wp-nux | Components, and wp.data methods useful for onboarding a new user to the WordPress admin interface | +| [Plugins](/packages/plugins/README.md) | wp-plugins | Plugins module for WordPress | +| [Redux Routine](/packages/redux-routine/README.md) | wp-redux-routine | Redux middleware for generator coroutines | +| [Rich Text](/packages/rich-text/README.md) | wp-rich-text | Helper functions to convert HTML or a DOM tree into a rich text value and back | +| [Shortcode](/packages/shortcode/README.md) | wp-shortcode | Shortcode module for WordPress | +| [Token List](/packages/token-list/README.md) | wp-token-list | Constructable, plain JavaScript [DOMTokenList](https://developer.mozilla.org/en-US/docs/Web/API/DOMTokenList) implementation, supporting non-browser runtimes | +| [URL](/packages/url/README.md) | wp-url | A collection of utilities to manipulate URLs | +| [Viewport](/packages/viewport/README.md) | wp-viewport | Module for responding to changes in the browser viewport size | +| [Wordcount](/packages/wordcount/README.md) | wp-wordcount | WordPress word count utility | ## Vendor Scripts diff --git a/docs/contributors/testing-overview.md b/docs/contributors/testing-overview.md index 495109b5f07120..b2a2602491f9d5 100644 --- a/docs/contributors/testing-overview.md +++ b/docs/contributors/testing-overview.md @@ -286,7 +286,7 @@ describe( 'SolarSystem', () => { } ); ``` -Reducer tests are also be a great fit for snapshots. They are often large, complex data structures that shouldn't change unexpectedly, exactly what snapshots excel at! +Reducer tests are also a great fit for snapshots. They are often large, complex data structures that shouldn't change unexpectedly, exactly what snapshots excel at! #### Working with snapshots diff --git a/docs/designers-developers/developers/accessibility.md b/docs/designers-developers/developers/accessibility.md new file mode 100644 index 00000000000000..ab6c0d0066b1dc --- /dev/null +++ b/docs/designers-developers/developers/accessibility.md @@ -0,0 +1,17 @@ +# Accessibility + +Accessibility documentation for developers working on the Gutenberg Project. + +For more information on accessibility and WordPress see the [Make WordPress Accessibility Handbook](https://make.wordpress.org/accessibility/handbook/) and the [Accessibility Team section](https://make.wordpress.org/accessibility/). + +## Landmark Regions + +It is a best practice to include ALL content on the page in landmarks, so that screen reader users who rely on them to navigate from section to section do not lose track of content. + +For setting up navigation between different regions, see the [navigateRegions package](/packages/components/src/higher-order/navigate-regions/README.md) for additional documentation. + +Read more regarding landmark design from W3C: + +- [General Principles of Landmark Design](https://www.w3.org/TR/wai-aria-practices-1.1/#general-principles-of-landmark-design) +- [ARIA Landmarks Example](https://www.w3.org/TR/wai-aria-practices/examples/landmarks/) +- [HTML5 elements that by default define ARIA landmarks](https://www.w3.org/TR/wai-aria-practices/examples/landmarks/HTML5.html) diff --git a/docs/designers-developers/developers/backward-compatibility/deprecations.md b/docs/designers-developers/developers/backward-compatibility/deprecations.md index f2ce4c1ee8d2f2..6ff124b27a34fa 100644 --- a/docs/designers-developers/developers/backward-compatibility/deprecations.md +++ b/docs/designers-developers/developers/backward-compatibility/deprecations.md @@ -56,6 +56,8 @@ The Gutenberg project's deprecation policy is intended to support backward compa - The PHP function `gutenberg_meta_box_post_form_hidden_fields` has been removed. Use [`the_block_editor_meta_box_post_form_hidden_fields`](https://developer.wordpress.org/reference/functions/the_block_editor_meta_box_post_form_hidden_fields/) instead. - The PHP function `gutenberg_toggle_custom_fields` has been removed. - The PHP function `gutenberg_collect_meta_box_data` has been removed. Use [`register_and_do_post_meta_boxes`](https://developer.wordpress.org/reference/functions/register_and_do_post_meta_boxes/) instead. +- `window._wpLoadGutenbergEditor` has been removed. Use `window._wpLoadBlockEditor` instead. Note: This is a private API, not intended for public use. It may be removed in the future. +- The PHP function `gutenberg_get_script_polyfill` has been removed. Use [`wp_get_script_polyfill`](https://developer.wordpress.org/reference/functions/wp_get_script_polyfill/) instead. ## 4.5.0 - `Dropdown.refresh()` has been deprecated as the contained `Popover` is now automatically refreshed. @@ -214,11 +216,11 @@ The Gutenberg project's deprecation policy is intended to support backward compa ## 3.0.0 - `wp.blocks.registerCoreBlocks` function removed. Please use `wp.coreBlocks.registerCoreBlocks` instead. - - Raw TinyMCE event handlers for `RichText` have been deprecated. Please use [documented props](https://wordpress.org/gutenberg/handbook/block-api/rich-text-api/), ancestor event handler, or onSetup access to the internal editor instance event hub instead. + - Raw TinyMCE event handlers for `RichText` have been deprecated. Please use [documented props](/packages/editor/src/components/rich-text/README.md), ancestor event handler, or onSetup access to the internal editor instance event hub instead. ## 2.8.0 - - `Original autocompleter interface in wp.components.Autocomplete` updated. Please use `latest autocompleter interface` instead. See: https://github.com/WordPress/gutenberg/blob/master/components/autocomplete/README.md. + - `Original autocompleter interface in wp.components.Autocomplete` updated. Please use `latest autocompleter interface` instead. See [autocomplete](/packages/components/src/autocomplete/README.md) for more info. - `getInserterItems`: the `allowedBlockTypes` argument is now mandatory. - `getFrecentInserterItems`: the `allowedBlockTypes` argument is now mandatory. @@ -240,6 +242,6 @@ The Gutenberg project's deprecation policy is intended to support backward compa - `wp.blocks.BlockDescription` component removed. Please use the `description` block property instead. - `wp.blocks.InspectorControls.*` components removed. Please use `wp.components.*` components instead. - - `wp.blocks.source.*` matchers removed. Please use the declarative attributes instead. See: https://wordpress.org/gutenberg/handbook/block-api/attributes/. + - `wp.blocks.source.*` matchers removed. Please use the declarative attributes instead. See [block attributes](/docs/designers-developers/developers/block-api/block-attributes.md) for more info. - `wp.data.select( 'selector', ...args )` removed. Please use `wp.data.select( reducerKey' ).*` instead. - `wp.blocks.MediaUploadButton` component removed. Please use `wp.blocks.MediaUpload` component instead. diff --git a/docs/designers-developers/developers/block-api/README.md b/docs/designers-developers/developers/block-api/README.md index af153d59ba38a4..56d159bda6d135 100644 --- a/docs/designers-developers/developers/block-api/README.md +++ b/docs/designers-developers/developers/block-api/README.md @@ -4,8 +4,8 @@ Blocks are the fundamental element of the editor. They are the primary way in wh ## Registering a block -All blocks must be registered before they can be used in the editor. You can learn about block registration, and the available options, in the [block registration](../../../../docs/designers-developers/developers/block-api/block-registration.md) documentation. +All blocks must be registered before they can be used in the editor. You can learn about block registration, and the available options, in the [block registration](/docs/designers-developers/developers/block-api/block-registration.md) documentation. ## Block `edit` and `save` -The `edit` and `save` functions define the editor interface with which a user would interact, and the markup to be serialized back when a post is saved. They are the heart of how a block operates, so they are [covered separately](../../../../docs/designers-developers/developers/block-api/block-edit-save.md). +The `edit` and `save` functions define the editor interface with which a user would interact, and the markup to be serialized back when a post is saved. They are the heart of how a block operates, so they are [covered separately](/docs/designers-developers/developers/block-api/block-edit-save.md). diff --git a/docs/designers-developers/developers/block-api/block-attributes.md b/docs/designers-developers/developers/block-api/block-attributes.md index 48a9b36ef83b51..fd61866f5286c4 100644 --- a/docs/designers-developers/developers/block-api/block-attributes.md +++ b/docs/designers-developers/developers/block-api/block-attributes.md @@ -4,7 +4,7 @@ Attribute sources are used to define the strategy by which block attribute values are extracted from saved post content. They provide a mechanism to map from the saved markup to a JavaScript representation of a block. -If no attribute source is specified, the attribute will be saved to (and read from) the block's [comment delimiter](../../../../docs/designers-developers/key-concepts.md#delimiters-and-parsing-expression-grammar). +If no attribute source is specified, the attribute will be saved to (and read from) the block's [comment delimiter](/docs/designers-developers/key-concepts.md#delimiters-and-parsing-expression-grammar). Each source accepts an optional selector as the first argument. If a selector is specified, the source behavior will be run against the corresponding element(s) contained within the block. Otherwise it will be run against the block's root node. diff --git a/docs/designers-developers/developers/block-api/block-deprecation.md b/docs/designers-developers/developers/block-api/block-deprecation.md index 3fd500c1645417..7e64bfb89a9991 100644 --- a/docs/designers-developers/developers/block-api/block-deprecation.md +++ b/docs/designers-developers/developers/block-api/block-deprecation.md @@ -9,9 +9,9 @@ A block can have several deprecated versions. A deprecation will be tried if a p Deprecations are defined on a block type as its `deprecated` property, an array of deprecation objects where each object takes the form: -- `attributes` (Object): The [attributes definition](../../../../docs/designers-developers/developers/block-api/block-attributes.md) of the deprecated form of the block. -- `support` (Object): The [supports definition](../../../../docs/designers-developers/developers/block-api/block-registration.md) of the deprecated form of the block. -- `save` (Function): The [save implementation](../../../../docs/designers-developers/developers/block-api/block-edit-save.md) of the deprecated form of the block. +- `attributes` (Object): The [attributes definition](/docs/designers-developers/developers/block-api/block-attributes.md) of the deprecated form of the block. +- `support` (Object): The [supports definition](/docs/designers-developers/developers/block-api/block-registration.md) of the deprecated form of the block. +- `save` (Function): The [save implementation](/docs/designers-developers/developers/block-api/block-edit-save.md) of the deprecated form of the block. - `migrate` (Function, Optional): A function which, given the attributes and inner blocks of the parsed block, is expected to return either the attributes compatible with the deprecated block, or a tuple array of `[ attributes, innerBlocks ]`. - `isEligible` (Function, Optional): A function which, given the attributes and inner blocks of the parsed block, returns true if the deprecation can handle the block migration. This is particularly useful in cases where a block is technically valid even once deprecated, and requires updates to its attributes or inner blocks. @@ -275,4 +275,4 @@ registerBlockType( 'gutenberg/block-with-deprecated-version', { 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/master/packages/block-library/src). Core blocks have been updated across releases and contain simple and complex deprecations.* +*Above are example cases of block deprecation. For more, real-world examples, check for deprecations in the [core block library](/packages/block-library/src/README.md). Core blocks have been updated across releases and contain simple and complex deprecations.* diff --git a/docs/designers-developers/developers/block-api/block-edit-save.md b/docs/designers-developers/developers/block-api/block-edit-save.md index f72baac90858d8..1bfaf08f10a22e 100644 --- a/docs/designers-developers/developers/block-api/block-edit-save.md +++ b/docs/designers-developers/developers/block-api/block-edit-save.md @@ -117,11 +117,11 @@ save() { ``` {% end %} -For most blocks, the return value of `save` should be an [instance of WordPress Element](https://github.com/WordPress/gutenberg/blob/master/packages/element/README.md) representing how the block is to appear on the front of the site. +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. _Note:_ While it is possible to return a string value from `save`, it _will be escaped_. If the string includes HTML markup, the markup will be shown on the front of the site verbatim, not as the equivalent HTML node content. If you must return raw HTML from `save`, use `wp.element.RawHTML`. As the name implies, this is prone to [cross-site scripting](https://en.wikipedia.org/wiki/Cross-site_scripting) and therefore is discouraged in favor of a WordPress Element hierarchy whenever possible. -For [dynamic blocks](../../../../docs/designers-developers/developers/tutorials/block-tutorial/creating-dynamic-blocks.md), the return value of `save` could either represent a cached copy of the block's content to be shown only in case the plugin implementing the block is ever disabled. Alternatively, return a `null` (empty) value to save no markup in post content for the dynamic block, instead deferring this to always be calculated when the block is shown on the front of the site. +For [dynamic blocks](/docs/designers-developers/developers/tutorials/block-tutorial/creating-dynamic-blocks.md), the return value of `save` could either represent a cached copy of the block's content to be shown only in case the plugin implementing the block is ever disabled. Alternatively, return a `null` (empty) value to save no markup in post content for the dynamic block, instead deferring this to always be calculated when the block is shown on the front of the site. ### attributes @@ -171,10 +171,10 @@ The two most common sources of block invalidations are: Before starting to debug, be sure to familiarize yourself with the validation step described above documenting the process for detecting whether a block is invalid. A block is invalid if its regenerated markup does not match what is saved in post content, so often this can be caused by the attributes of a block being parsed incorrectly from the saved content. -If you're using [attribute sources](../../../../docs/designers-developers/developers/block-api/block-attributes.md), be sure that attributes sourced from markup are saved exactly as you expect, and in the correct type (usually a `'string'` or `'number'`). +If you're using [attribute sources](/docs/designers-developers/developers/block-api/block-attributes.md), be sure that attributes sourced from markup are saved exactly as you expect, and in the correct type (usually a `'string'` or `'number'`). When a block is detected as invalid, a warning will be logged into your browser's developer tools console. The warning will include specific details about the exact point at which a difference in markup occurred. Be sure to look closely at any differences in the expected and actual markups to see where problems are occurring. **I've changed my block's `save` behavior and old content now includes invalid blocks. How can I fix this?** -Refer to the guide on [Deprecated Blocks](../../../../docs/designers-developers/developers/block-api/block-deprecations.md) to learn more about how to accommodate legacy content in intentional markup changes. +Refer to the guide on [Deprecated Blocks](/docs/designers-developers/developers/block-api/block-deprecations.md) to learn more about how to accommodate legacy content in intentional markup changes. diff --git a/docs/designers-developers/developers/block-api/block-registration.md b/docs/designers-developers/developers/block-api/block-registration.md index 90d0752ff170ea..4bc32fb9f4e0fa 100644 --- a/docs/designers-developers/developers/block-api/block-registration.md +++ b/docs/designers-developers/developers/block-api/block-registration.md @@ -66,7 +66,7 @@ The core provided categories are: category: 'widgets', ``` -Plugins and Themes can also register [custom block categories](../docs/extensibility/extending-blocks/#managing-block-categories). +Plugins and Themes can also register [custom block categories](/docs/designers-developers/developers/filters/block-filters.md#managing-block-categories). #### Icon (optional) @@ -82,7 +82,7 @@ icon: 'book-alt', icon: , ``` -**Note:** Custom SVG icons are automatically wrapped in the [`wp.components.SVG` component](https://github.com/WordPress/gutenberg/tree/master/packages/components/src/primitives/svg/) to add accessibility attributes (`aria-hidden`, `role`, and `focusable`). +**Note:** Custom SVG icons are automatically wrapped in the [`wp.components.SVG` component](/packages/components/src/primitives/svg/) to add accessibility attributes (`aria-hidden`, `role`, and `focusable`). An object can also be passed as icon, in this case, icon, as specified above, should be included in the src property. Besides src the object can contain background and foreground colors, this colors will appear with the icon @@ -138,7 +138,7 @@ styles: [ ], ``` -Plugins and Themes can also register [custom block style](../docs/extensibility/extending-blocks/#block-style-variations) for existing blocks. +Plugins and Themes can also register [custom block style](/docs/designers-developers/developers/filters/block-filters.md#block-style-variations) for existing blocks. #### Attributes (optional) @@ -166,7 +166,7 @@ attributes: { }, ``` -* **See: [Attributes](../docs/block-api/attributes.md).** +* **See: [Attributes](/docs/designers-developers/developers/block-api/block-attributes.md).** #### Transforms (optional) @@ -311,6 +311,39 @@ transforms: { ``` {% end %} +A block with innerBlocks can also be transformed from and to another block with innerBlocks. + +{% codetabs %} +{% ES5 %} +```js +transforms: { + to: [ + { + type: 'block', + blocks: [ 'some/block-with-innerblocks' ], + transform: function( attributes, innerBlocks ) { + return createBlock( 'some/other-block-with-innerblocks', attributes, innerBlocks ); + }, + }, + ], +}, +``` +{% ESNext %} +```js +transforms: { + to: [ + { + type: 'block', + blocks: [ 'some/block-with-innerblocks' ], + transform: ( attributes, innerBlocks ) => { + return createBlock( 'some/other-block-with-innerblocks', attributes, innerBlocks); + }, + }, + ], +}, +``` +{% end %} + An optional `isMatch` function can be specified on a transform object. This provides an opportunity to perform additional checks on whether a transform should be possible. Returning `false` from this function will prevent the transform from being displayed as an option to the user. {% codetabs %} @@ -453,7 +486,7 @@ transforms: { * **Type:** `Array` -Blocks are able to be inserted into blocks that use [`InnerBlocks`](https://github.com/WordPress/gutenberg/blob/master/packages/editor/src/components/inner-blocks/README.md) as nested content. Sometimes it is useful to restrict a block so that it is only available as a nested block. For example, you might want to allow an 'Add to Cart' block to only be available within a 'Product' block. +Blocks are able to be inserted into blocks that use [`InnerBlocks`](/packages/editor/src/components/inner-blocks/README.md) as nested content. Sometimes it is useful to restrict a block so that it is only available as a nested block. For example, you might want to allow an 'Add to Cart' block to only be available within a 'Product' block. Setting `parent` lets a block require that it is only available when nested within the specified blocks. @@ -492,7 +525,7 @@ attributes: { } ``` -- `alignWide` (default `true`): This property allows to enable [wide alignment](../docs/extensibility/theme-support.md#wide-alignment) for your theme. To disable this behavior for a single block, set this flag to `false`. +- `alignWide` (default `true`): This property allows to enable [wide alignment](/docs/designers-developers/developers/themes/theme-support.md#wide-alignment) for your theme. To disable this behavior for a single block, set this flag to `false`. ```js // Remove the support for wide alignment. diff --git a/docs/designers-developers/developers/block-api/block-templates.md b/docs/designers-developers/developers/block-api/block-templates.md index bbc6efe968ebb4..caa96ff20b5bd4 100644 --- a/docs/designers-developers/developers/block-api/block-templates.md +++ b/docs/designers-developers/developers/block-api/block-templates.md @@ -17,18 +17,50 @@ Planned additions: Templates can be declared in JS or in PHP as an array of blockTypes (block name and optional attributes). +The first example in PHP creates a template for posts that includes an image block to start, you can add as many or as few blocks to your template as needed. + +PHP example: + +```php +template = array( + array( 'core/image' ), + ); +} +add_action( 'init', 'myplugin_register_template' ); +``` + +The following example in JavaScript creates a new block using [InnerBlocks](/packages/editor/src/components/inner-blocks) and templates, when inserted creates a set of blocks based off the template. + ```js -const template = [ - [ 'block/name', {} ], // [ blockName, attributes ] +const el = wp.element.createElement; +const { registerBlockType } = wp.blocks; +const { InnerBlocks } = wp.editor; + +const BLOCKS_TEMPLATE = [ + [ 'core/image', {} ], + [ 'core/paragraph', { placeholder: 'Image Details' } ], ]; -``` -```php -'template' => array( - array( 'block/name' ), -), +registerBlockType( 'myplugin/template', { + title: 'My Template Block', + category: 'widgets', + edit: ( props ) => { + return el( InnerBlocks, { + template: BLOCKS_TEMPLATE, + templateLock: false + }); + }, + save: ( props ) => { + return el( InnerBlocks.Content, {} ); + }, +}); ``` +See the [Meta Block Tutorial](/docs/designers-developers/developers/tutorials/metabox/meta-block-5-finishing.md) for a full example of a template in use. + ## Custom Post types A custom post type can register its own template during registration: @@ -61,20 +93,7 @@ add_action( 'init', 'myplugin_register_book_post_type' ); Sometimes the intention might be to lock the template on the UI so that the blocks presented cannot be manipulated. This is achieved with a `template_lock` property. ```php -'template_lock' => 'all', // or 'insert' to allow moving -``` - -*Options:* - -- `all` — prevents all operations. It is not possible to insert new blocks, move existing blocks, or delete blocks. -- `insert` — prevents inserting or removing blocks, but allows moving existing blocks. - -## Existing Post Types - -It is also possible to assign a template to an existing post type like "posts" and "pages": - -```php -function my_add_template_to_posts() { +function myplugin_register_template() { $post_type_object = get_post_type_object( 'post' ); $post_type_object->template = array( array( 'core/paragraph', array( @@ -83,9 +102,14 @@ function my_add_template_to_posts() { ); $post_type_object->template_lock = 'all'; } -add_action( 'init', 'my_add_template_to_posts' ); +add_action( 'init', 'myplugin_register_template' ); ``` +*Options:* + +- `all` — prevents all operations. It is not possible to insert new blocks, move existing blocks, or delete blocks. +- `insert` — prevents inserting or removing blocks, but allows moving existing blocks. + ## Nested Templates Container blocks like the columns blocks also support templates. This is achieved by assigning a nested template to the block. diff --git a/docs/designers-developers/developers/data/README.md b/docs/designers-developers/developers/data/README.md index 5233753c1445e5..7ba7f9264e8bc7 100644 --- a/docs/designers-developers/developers/data/README.md +++ b/docs/designers-developers/developers/data/README.md @@ -1,10 +1,10 @@ # Data Module Reference - - [**core**: WordPress Core Data](../../docs/designers-developers/developers/data/data-core.md) - - [**core/annotations**: Annotations](../../docs/designers-developers/developers/data/data-core-annotations.md) - - [**core/blocks**: Block Types Data](../../docs/designers-developers/developers/data/data-core-blocks.md) - - [**core/editor**: The Editor’s Data](../../docs/designers-developers/developers/data/data-core-editor.md) - - [**core/edit-post**: The Editor’s UI Data](../../docs/designers-developers/developers/data/data-core-edit-post.md) - - [**core/notices**: Notices Data](../../docs/designers-developers/developers/data/data-core-notices.md) - - [**core/nux**: The NUX (New User Experience) Data](../../docs/designers-developers/developers/data/data-core-nux.md) - - [**core/viewport**: The Viewport Data](../../docs/designers-developers/developers/data/data-core-viewport.md) \ No newline at end of file + - [**core**: WordPress Core Data](/docs/designers-developers/developers/data/data-core.md) + - [**core/annotations**: Annotations](/docs/designers-developers/developers/data/data-core-annotations.md) + - [**core/blocks**: Block Types Data](/docs/designers-developers/developers/data/data-core-blocks.md) + - [**core/editor**: The Editor’s Data](/docs/designers-developers/developers/data/data-core-editor.md) + - [**core/edit-post**: The Editor’s UI Data](/docs/designers-developers/developers/data/data-core-edit-post.md) + - [**core/notices**: Notices Data](/docs/designers-developers/developers/data/data-core-notices.md) + - [**core/nux**: The NUX (New User Experience) Data](/docs/designers-developers/developers/data/data-core-nux.md) + - [**core/viewport**: The Viewport Data](/docs/designers-developers/developers/data/data-core-viewport.md) \ No newline at end of file diff --git a/docs/designers-developers/developers/filters/README.md b/docs/designers-developers/developers/filters/README.md index f6da621ac74be6..c110d2ed3a5543 100644 --- a/docs/designers-developers/developers/filters/README.md +++ b/docs/designers-developers/developers/filters/README.md @@ -4,4 +4,4 @@ There are two types of hooks: [Actions](https://developer.wordpress.org/plugins/hooks/actions/) and [Filters](https://developer.wordpress.org/plugins/hooks/filters/). In addition to PHP actions and filters, WordPress also provides a mechanism for registering and executing hooks in JavaScript. This functionality is also available on npm as the [@wordpress/hooks](https://www.npmjs.com/package/@wordpress/hooks) package, for general purpose use. -You can also learn more about both APIs: [PHP](https://codex.wordpress.org/Plugin_API/) and [JavaScript](https://github.com/WordPress/packages/tree/master/packages/hooks). +You can also learn more about both APIs: [PHP](https://codex.wordpress.org/Plugin_API/) and [JavaScript](/packages/tree/master/packages/hooks). diff --git a/docs/designers-developers/developers/filters/block-filters.md b/docs/designers-developers/developers/filters/block-filters.md index cc4f71bb95540e..99ba4cc0a0d49f 100644 --- a/docs/designers-developers/developers/filters/block-filters.md +++ b/docs/designers-developers/developers/filters/block-filters.md @@ -17,7 +17,7 @@ wp.blocks.registerBlockStyle( 'core/quote', { The example above registers a block style variation named `fancy-quote` to the `core/quote` block. When the user selects this block style variation from the styles selector, an `is-style-fancy-quote` className will be added to the block's wrapper. -By adding `isDefault: true`, you can make registered style variation to be active by default when a block is inserted. +By adding `isDefault: true` you can mark the registered style variation as the one that is recognized as active when no custom class name is provided. It also means that there will be no custom class name added to the HTML output for the style that is marked as default. To remove a block style variation use `wp.blocks.unregisterBlockStyle()`. @@ -113,7 +113,7 @@ wp.hooks.addFilter( ); ``` -_Note:_ This filter must always be run on every page load, and not in your browser's developer tools console. Otherwise, a [block validation](../../../../docs/designers-developers/developers/block-api/block-edit-save.md#validation) error will occur the next time the post is edited. This is due to the fact that block validation occurs by verifying that the saved output matches what is stored in the post's content during editor initialization. So, if this filter does not exist when the editor loads, the block will be marked as invalid. +_Note:_ This filter must always be run on every page load, and not in your browser's developer tools console. Otherwise, a [block validation](/docs/designers-developers/developers/block-api/block-edit-save.md#validation) error will occur the next time the post is edited. This is due to the fact that block validation occurs by verifying that the saved output matches what is stored in the post's content during editor initialization. So, if this filter does not exist when the editor loads, the block will be marked as invalid. #### `blocks.getBlockDefaultClassName` diff --git a/docs/designers-developers/developers/themes/theme-support.md b/docs/designers-developers/developers/themes/theme-support.md index 07ab19f68df7d1..765483eadb798f 100644 --- a/docs/designers-developers/developers/themes/theme-support.md +++ b/docs/designers-developers/developers/themes/theme-support.md @@ -266,7 +266,7 @@ To change the main column width of the editor, add the following CSS to `style-e You can use those editor widths to match those in your theme. You can use any CSS width unit, including `%` or `px`. -Further reading: [Applying Styles with Stylesheets](https://wordpress.org/gutenberg/handbook/blocks/applying-styles-with-stylesheets/). +Further reading: [Applying Styles with Stylesheets](/docs/designers-developers/developers/tutorials/block-tutorial/applying-styles-with-stylesheets.md). ## Default block styles diff --git a/docs/designers-developers/developers/tutorials/block-tutorial/creating-dynamic-blocks.md b/docs/designers-developers/developers/tutorials/block-tutorial/creating-dynamic-blocks.md index e905d0b1a9a941..aa785ff85e31f6 100644 --- a/docs/designers-developers/developers/tutorials/block-tutorial/creating-dynamic-blocks.md +++ b/docs/designers-developers/developers/tutorials/block-tutorial/creating-dynamic-blocks.md @@ -124,7 +124,7 @@ There are a few things to notice: ## Live rendering in Gutenberg editor -Gutenberg 2.8 added the [``](https://github.com/WordPress/gutenberg/tree/master/packages/components/src/server-side-render) block which enables rendering to take place on the server using PHP rather than in JavaScript. +Gutenberg 2.8 added the [``](/packages/components/src/server-side-render) block which enables rendering to take place on the server using PHP rather than in JavaScript. *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).* diff --git a/docs/designers-developers/developers/tutorials/block-tutorial/generate-blocks-with-wp-cli.md b/docs/designers-developers/developers/tutorials/block-tutorial/generate-blocks-with-wp-cli.md index 81c3fa0373a82b..b8fc6862dc69dc 100644 --- a/docs/designers-developers/developers/tutorials/block-tutorial/generate-blocks-with-wp-cli.md +++ b/docs/designers-developers/developers/tutorials/block-tutorial/generate-blocks-with-wp-cli.md @@ -5,7 +5,7 @@ It turns out that writing the simplest possible block which contains only static - [zgordon/gutenberg-course](https://github.com/zgordon/gutenberg-course) - a repository for Zac Gordon's Gutenberg Development Course - [ahmadawais/create-guten-block](https://github.com/ahmadawais/create-guten-block) - A zero-configuration developer toolkit for building WordPress Gutenberg block plugins -It might be also a good idea to browse the folder with [all core blocks](https://github.com/WordPress/gutenberg/tree/master/packages/block-library/src) to see how they are implemented. +It might be also a good idea to browse the folder with [all core blocks](/packages/block-library/src) to see how they are implemented. ## WP-CLI @@ -62,7 +62,7 @@ This will generate 4 files inside the `movies` plugin directory. All files conta * Registers all block assets so that they can be enqueued through Gutenberg in * the corresponding context. * - * @see https://wordpress.org/gutenberg/handbook/blocks/writing-your-first-block-type/#enqueuing-block-scripts + * @see https://wordpress.org/gutenberg/handbook/designers-developers/developers/tutorials/block-tutorial/writing-your-first-block-type/ */ function movie_block_init() { $dir = dirname( __FILE__ ); @@ -109,23 +109,23 @@ add_action( 'init', 'movie_block_init' ); ( function( wp ) { /** * Registers a new block provided a unique name and an object defining its behavior. - * @see https://github.com/WordPress/gutenberg/tree/master/blocks#api + * @see https://wordpress.org/gutenberg/handbook/designers-developers/developers/block-api/#registering-a-block */ var registerBlockType = wp.blocks.registerBlockType; /** * Returns a new element of given type. Element is an abstraction layer atop React. - * @see https://github.com/WordPress/gutenberg/tree/master/packages/element#element + * @see https://wordpress.org/gutenberg/handbook/designers-developers/developers/packages/packages-element/ */ var el = wp.element.createElement; /** * Retrieves the translation of text. - * @see https://github.com/WordPress/gutenberg/tree/master/i18n#api + * @see https://wordpress.org/gutenberg/handbook/designers-developers/developers/packages/packages-i18n/ */ var __ = wp.i18n.__; /** * Every block starts by registering a new block type definition. - * @see https://wordpress.org/gutenberg/handbook/block-api/ + * @see https://wordpress.org/gutenberg/handbook/designers-developers/developers/block-api/#registering-a-block */ registerBlockType( 'movies/movie', { /** @@ -151,7 +151,7 @@ add_action( 'init', 'movie_block_init' ); /** * 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. - * @see https://wordpress.org/gutenberg/handbook/block-edit-save/#edit + * @see https://wordpress.org/gutenberg/handbook/designers-developers/developers/block-api/block-edit-save/#edit * * @param {Object} [props] Properties passed from the editor. * @return {Element} Element to render. @@ -167,7 +167,7 @@ add_action( 'init', 'movie_block_init' ); /** * The save function defines the way in which the different attributes should be combined * into the final markup, which is then serialized by Gutenberg into `post_content`. - * @see https://wordpress.org/gutenberg/handbook/block-edit-save/#save + * @see https://wordpress.org/gutenberg/handbook/designers-developers/developers/block-api/block-edit-save/#save * * @return {Element} Element to render. */ diff --git a/docs/designers-developers/developers/tutorials/block-tutorial/introducing-attributes-and-editable-fields.md b/docs/designers-developers/developers/tutorials/block-tutorial/introducing-attributes-and-editable-fields.md index cd2b40e7d561e9..7fa730c73abcee 100644 --- a/docs/designers-developers/developers/tutorials/block-tutorial/introducing-attributes-and-editable-fields.md +++ b/docs/designers-developers/developers/tutorials/block-tutorial/introducing-attributes-and-editable-fields.md @@ -110,7 +110,7 @@ registerBlockType( 'gutenberg-boilerplate-esnext/hello-world-step-03', { ``` {% end %} -When registering a new block type, the `attributes` property describes the shape of the attributes object you'd like to receive in the `edit` and `save` functions. Each value is a [source function](../../../../../docs/designers-developers/developers/block-api/block-attributes.md) to find the desired value from the markup of the block. +When registering a new block type, the `attributes` property describes the shape of the attributes object you'd like to receive in the `edit` and `save` functions. Each value is a [source function](/docs/designers-developers/developers/block-api/block-attributes.md) to find the desired value from the markup of the block. In the code snippet above, when loading the editor, we will extract the `content` value as the HTML of the paragraph element in the saved post's markup. diff --git a/docs/designers-developers/developers/tutorials/block-tutorial/writing-your-first-block-type.md b/docs/designers-developers/developers/tutorials/block-tutorial/writing-your-first-block-type.md index 4a7c8449763bca..ea9a18bbf23684 100644 --- a/docs/designers-developers/developers/tutorials/block-tutorial/writing-your-first-block-type.md +++ b/docs/designers-developers/developers/tutorials/block-tutorial/writing-your-first-block-type.md @@ -28,7 +28,7 @@ add_action( 'init', 'gutenberg_boilerplate_block' ); Note the two script dependencies: - __`wp-blocks`__ includes block type registration and related functions -- __`wp-element`__ includes the [WordPress Element abstraction](https://github.com/WordPress/gutenberg/tree/master/packages/element) for describing the structure of your blocks +- __`wp-element`__ includes the [WordPress Element abstraction](/packages/element/README.md) for describing the structure of your blocks If you were to use a component from the `wp-editor` package, for example the RichText component, you would also need to add `wp-editor` to the dependency list. @@ -82,7 +82,7 @@ registerBlockType( 'gutenberg-boilerplate-esnext/hello-world-step-01', { ``` {% end %} -Once a block is registered, you should immediately see that it becomes available as an option in the editor inserter dialog, using values from `title`, `icon`, and `category` to organize its display. You can choose an icon from any included in the built-in [Dashicons icon set](https://developer.wordpress.org/resource/dashicons/), or provide a [custom svg element](https://wordpress.org/gutenberg/handbook/designers-developers/developers/block-api/block-registration/#icon-optional). +Once a block is registered, you should immediately see that it becomes available as an option in the editor inserter dialog, using values from `title`, `icon`, and `category` to organize its display. You can choose an icon from any included in the built-in [Dashicons icon set](https://developer.wordpress.org/resource/dashicons/), or provide a [custom svg element](/docs/designers-developers/developers/block-api/block-registration.md#icon-optional). 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. diff --git a/docs/designers-developers/developers/tutorials/javascript/extending-the-block-editor.md b/docs/designers-developers/developers/tutorials/javascript/extending-the-block-editor.md index 67d703cb37310f..8d175ef3e6d9b2 100644 --- a/docs/designers-developers/developers/tutorials/javascript/extending-the-block-editor.md +++ b/docs/designers-developers/developers/tutorials/javascript/extending-the-block-editor.md @@ -1,6 +1,6 @@ # Extending the Block Editor -Let's look at using the [Block Style Variation example](../../../../../docs/designers-developers/developers/filters/block-filters.md#block-style-variations) to extend the editor. This example allows you to add your own custom CSS class name to any core block type. +Let's look at using the [Block Style Variation example](/docs/designers-developers/developers/filters/block-filters.md#block-style-variations) to extend the editor. This example allows you to add your own custom CSS class name to any core block type. Replace the existing `console.log()` code in your `myguten.js` file with: @@ -30,7 +30,7 @@ add_action( 'enqueue_block_editor_assets', 'myguten_enqueue' ); The last argument in the `wp_enqueue_script()` function is an array of dependencies. WordPress makes packages available under the `wp` namespace. In the example, you use `wp.blocks` to access the items that the blocks package exports (in this case the `registerBlockStyle()` function). -See [Packages](../../../../../docs/designers-developers/developers/packages.md) for list of available packages and what objects they export. +See [Packages](/docs/designers-developers/developers/packages.md) for list of available packages and what objects they export. After you have updated both JavaScript and PHP files, go to the Block Editor and create a new post. diff --git a/docs/designers-developers/developers/tutorials/javascript/readme.md b/docs/designers-developers/developers/tutorials/javascript/readme.md index 11fecdff4d269e..55628c1cd398e4 100644 --- a/docs/designers-developers/developers/tutorials/javascript/readme.md +++ b/docs/designers-developers/developers/tutorials/javascript/readme.md @@ -11,9 +11,9 @@ The Block Editor introduced in WordPress 5.0 is written entirely in JavaScript, ### Table of Contents -1. [Plugins Background](../../../../../docs/designers-developers/developers/tutorials/javascript/plugins-background.md) -2. [Loading JavaScript](../../../../../docs/designers-developers/developers/tutorials/javascript/loading-javascript.md) -3. [Extending the Block Editor](../../../../../docs/designers-developers/developers/tutorials/javascript/extending-the-block-editor.md) -4. [Troubleshooting](../../../../../docs/designers-developers/developers/tutorials/javascript/troubleshooting.md) -5. [JavaScript Versions and Building](../../../../../docs/designers-developers/developers/tutorials/javascript/versions-and-building.md) -6. [Scope your code](../../../../../docs/designers-developers/developers/tutorials/javascript/scope-your-code.md) \ No newline at end of file +1. [Plugins Background](/docs/designers-developers/developers/tutorials/javascript/plugins-background.md) +2. [Loading JavaScript](/docs/designers-developers/developers/tutorials/javascript/loading-javascript.md) +3. [Extending the Block Editor](/docs/designers-developers/developers/tutorials/javascript/extending-the-block-editor.md) +4. [Troubleshooting](/docs/designers-developers/developers/tutorials/javascript/troubleshooting.md) +5. [JavaScript Versions and Building](/docs/designers-developers/developers/tutorials/javascript/versions-and-building.md) +6. [Scope your code](/docs/designers-developers/developers/tutorials/javascript/scope-your-code.md) diff --git a/docs/designers-developers/developers/tutorials/metabox/meta-block-3-add.md b/docs/designers-developers/developers/tutorials/metabox/meta-block-3-add.md index 3bfc68e09262b6..0ad9966b91a12c 100644 --- a/docs/designers-developers/developers/tutorials/metabox/meta-block-3-add.md +++ b/docs/designers-developers/developers/tutorials/metabox/meta-block-3-add.md @@ -2,7 +2,7 @@ With the meta field registered in the previous step, next you will create a new block used to display the field value to the user. See the [Block Tutorial](/docs/designers-developers/developers/tutorials/block-tutorial/readme.md) for a deeper understanding of creating custom blocks. -For this block, you will use the TextControl component, which is similar to an HTML input text field. For additional components, check out the [components](https://github.com/WordPress/gutenberg/tree/master/packages/components/src) and [editor](https://github.com/WordPress/gutenberg/tree/master/packages/editor/src/components) packages repositories. +For this block, you will use the TextControl component, which is similar to an HTML input text field. For additional components, check out the [components](/packages/components/src) and [editor](/packages/editor/src/components) packages repositories. Attributes are the information displayed in blocks. As shown in the block tutorial, the source of attributes come from the text or HTML a user writes in the editor. For your meta block, the attribute will come from the post meta field. diff --git a/docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-0.md b/docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-0.md index 9e7efca41b793c..3ec8bcba59e930 100644 --- a/docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-0.md +++ b/docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-0.md @@ -1,12 +1,12 @@ # Creating a sidebar for your plugin -This tutorial starts with you having an existing plugin setup and ready to add PHP and JavaScript code. Please, refer to [Getting started with JavaScript](../../../../../docs/designers-developers/developers/tutorials/javascript/) tutorial for an introduction to WordPress plugins and how to use JavaScript to extend the block editor. +This tutorial starts with you having an existing plugin setup and ready to add PHP and JavaScript code. Please, refer to [Getting started with JavaScript](/docs/designers-developers/developers/tutorials/javascript/) tutorial for an introduction to WordPress plugins and how to use JavaScript to extend the block editor. In the next sections, you're going to create a custom sidebar for a plugin that contains a text control so the user can update a value that is stored in the `post_meta` table. -1. [Get a sidebar up and running](../../../../../docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-1-up-and-running.md) -2. [Tweak the sidebar style and add controls](../../../../../docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-2-styles-and-controls.md) -3. [Register a new meta field](../../../../../docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-3-register-meta.md) -4. [Initialize the input control with the meta field value](../../../../../docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-4-initialize-input.md) -5. [Update the meta field value when input's content changes](../../../../../docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-5-update-meta.md) -6. [Finishing touches](../../../../../docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-6-finishing-touches.md) +1. [Get a sidebar up and running](/docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-1-up-and-running.md) +2. [Tweak the sidebar style and add controls](/docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-2-styles-and-controls.md) +3. [Register a new meta field](/docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-3-register-meta.md) +4. [Initialize the input control with the meta field value](/docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-4-initialize-input.md) +5. [Update the meta field value when input's content changes](/docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-5-update-meta.md) +6. [Finishing touches](/docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-6-finishing-touches.md) diff --git a/docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-1-up-and-running.md b/docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-1-up-and-running.md index 54fc46066f2721..6b9c335f335a58 100644 --- a/docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-1-up-and-running.md +++ b/docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-1-up-and-running.md @@ -1,6 +1,6 @@ # Get a sidebar up and running -The first step in the journey is to tell the editor that there is a new plugin that will have its own sidebar. You can do so by using the [registerPlugin](../../../../../docs/designers-developers/developers/packages/packages-plugins/), [PluginSidebar](../../../../../docs/designers-developers/developers/packages/packages-edit-post/#pluginsidebar), and [createElement](../../../../../docs/designers-developers/developers/packages/packages-element/) utilities provided by WordPress, to be found in the `@wordpress/plugins`, `@wordpress/edit-post`, and `@wordpress/element` [packages](../../../../../docs/designers-developers/developers/packages/), respectively. +The first step in the journey is to tell the editor that there is a new plugin that will have its own sidebar. You can do so by using the [registerPlugin](/packages/plugins/REAMDE.md), [PluginSidebar](/packages/edit-post/README.md#pluginsidebar), and [createElement](/packages/element/README.md) utilities provided by WordPress, to be found in the `@wordpress/plugins`, `@wordpress/edit-post`, and `@wordpress/element` [packages](/docs/designers-developers/developers/packages.md), respectively. Add the following code to a JavaScript file called `plugin-sidebar.js` and save it within your plugin's directory: diff --git a/docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-2-styles-and-controls.md b/docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-2-styles-and-controls.md index 27e05ba9b22164..d919d11c657aed 100644 --- a/docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-2-styles-and-controls.md +++ b/docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-2-styles-and-controls.md @@ -2,7 +2,7 @@ After the sidebar is up and running, the next step is to fill it up with the necessary components and basic styling. -To visualize and edit the meta field value you'll use an input component. The `@wordpress/components` package contains many components available for you to reuse, and, specifically, the [TextControl](../../../../../docs/designers-developers/developers/components/text-control/) is aimed at creating an input field: +To visualize and edit the meta field value you'll use an input component. The `@wordpress/components` package contains many components available for you to reuse, and, specifically, the [TextControl](/packages/components/src/text-control/README.md) is aimed at creating an input field: ```js ( function( wp ) { diff --git a/docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-3-register-meta.md b/docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-3-register-meta.md index a4d150401e519a..9434ee462fc8bf 100644 --- a/docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-3-register-meta.md +++ b/docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-3-register-meta.md @@ -12,7 +12,7 @@ register_meta( 'post', 'sidebar_plugin_meta_block_field', array( ) ); ``` -To make sure the field has been loaded, query the block editor [internal data structures](../../../../../docs/designers-developers/developers/data/), also known as _stores_. Open your browser's console, and execute this piece of code: +To make sure the field has been loaded, query the block editor [internal data structures](/docs/designers-developers/developers/data/), also known as _stores_. Open your browser's console, and execute this piece of code: ```js wp.data.select( 'core/editor' ).getCurrentPost().meta; diff --git a/docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-4-initialize-input.md b/docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-4-initialize-input.md index 38e09a2529548c..d826d9df5547cc 100644 --- a/docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-4-initialize-input.md +++ b/docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-4-initialize-input.md @@ -39,7 +39,7 @@ Now that the field is available in the editor store, it can be surfaced to the U Now you can focus solely on the `MetaBlockField` component. The goal is to initialize it with the value of `sidebar_plugin_meta_block_field`, but also to keep it updated when that value changes. -WordPress has [some utilities to work with data](../../../../../docs/designers-developers/developers/packages/packages-data/) from the stores. The first you're going to use is [withSelect](../../../../../docs/designers-developers/developers/packages/packages-data/#withselect-mapselecttoprops-function-function), whose signature is: +WordPress has [some utilities to work with data](/packages/data/README.md) from the stores. The first you're going to use is [withSelect](/packages/data/README.md#withselect-mapselecttoprops-function-function), whose signature is: ```js withSelect( @@ -105,7 +105,7 @@ This is how the code changes from the previous section: * The `MetaBlockField` function has now a `props` argument as input. It contains the data object returned by the `mapSelectToProps` function, which it uses to initialize its value property. * The component rendered within the `div` element was also updated, the plugin now uses `MetaBlockFieldWithData`. This will be updated every time the original data changes. -* [getEditedPostAttribute](../../../../../docs/designers-developers/developers/data/data-core-editor/#geteditedpostattribute) is used to retrieve data instead of [getCurrentPost](../../../../../docs/designers-developers/developers/data/data-core-editor/#getcurrentpost) because it returns the most recent values of the post, including user editions that haven't been yet saved. +* [getEditedPostAttribute](/docs/designers-developers/developers/data/data-core-editor.md#geteditedpostattribute) is used to retrieve data instead of [getCurrentPost](/docs/designers-developers/developers/data/data-core-editor.md#getcurrentpost) because it returns the most recent values of the post, including user editions that haven't been yet saved. Update the code and open the sidebar. The input's content is no longer `Initial value` but a void string. Users can't type values yet, but let's check that the component is updated if the value in the store changes. Open the browser's console, execute diff --git a/docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-5-update-meta.md b/docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-5-update-meta.md index 0cb56a29b7e852..0ccab7e115b18f 100644 --- a/docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-5-update-meta.md +++ b/docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-5-update-meta.md @@ -1,6 +1,6 @@ # Update the meta field when the input's content changes -The last step in the journey is to update the meta field when the input content changes. To do that, you'll use another utility from the `@wordpress/data` package, [withDispatch](../../../../../docs/designers-developers/developers/packages/packages-data/#withdispatch-mapdispatchtoprops-function-function). +The last step in the journey is to update the meta field when the input content changes. To do that, you'll use another utility from the `@wordpress/data` package, [withDispatch](/packages/data/README.md#withdispatch-mapdispatchtoprops-function-function). `withDispatch` works similarly to `withSelect`. It takes two functions, the first returns an object with data, and the second takes that data object as input and returns a new UI component. Let's see how to use it: diff --git a/docs/designers-developers/faq.md b/docs/designers-developers/faq.md index fb2d16dfe68fce..1f54d5d78e7ea8 100644 --- a/docs/designers-developers/faq.md +++ b/docs/designers-developers/faq.md @@ -251,7 +251,7 @@ Our [list of supported browsers can be found in the Make WordPress handbook](htt ## How do I make my own block? -The API for creating blocks is a crucial aspect of the project. We are working on improved documentation and tutorials. Check out the [Creating Block Types](../../docs/designers-developers/developers/tutorials/block-tutorial/readme.md) section to get started. +The API for creating blocks is a crucial aspect of the project. We are working on improved documentation and tutorials. Check out the [Creating Block Types](/docs/designers-developers/developers/tutorials/block-tutorial/readme.md) section to get started. ## Does Gutenberg involve editing posts/pages in the front-end? @@ -295,7 +295,7 @@ Blocks will be able to provide base structural CSS styles, and themes can add st Other features, like the new _wide_ and _full-wide_ alignment options, will simply be CSS classes applied to blocks that offer this alignment. We are looking at how a theme can opt in to this feature, for example using `add_theme_support`. -*See:* [Theme Support](../../docs/designers-developers/developers/themes/theme-support.md) +*See:* [Theme Support](/docs/designers-developers/developers/themes/theme-support.md) ## How will editor styles work? @@ -308,7 +308,7 @@ function gutenbergtheme_editor_styles() { add_action( 'enqueue_block_editor_assets', 'gutenbergtheme_editor_styles' ); ``` -*See:* [Editor Styles](../../docs/designers-developers/developers/themes/theme-support.md#editor-styles) +*See:* [Editor Styles](/docs/designers-developers/developers/themes/theme-support.md#editor-styles) ## Should I be concerned that Gutenberg will make my plugin obsolete? @@ -353,7 +353,7 @@ Our approach—as outlined in [the technical overview introduction](https://make This also [gives us the flexibility](https://github.com/WordPress/gutenberg/issues/1516) to store those blocks that are inherently separate from the content stream (reusable pieces like widgets or small post type elements) elsewhere, and just keep token references for their placement. -We suggest you look at the [Gutenberg key concepts](../../docs/designers-developers/key-concepts.md) to learn more about how this aspect of the project works. +We suggest you look at the [Gutenberg key concepts](/docs/designers-developers/key-concepts.md) to learn more about how this aspect of the project works. ## How can I parse the post content back out into blocks in PHP or JS? In JS: diff --git a/docs/designers-developers/key-concepts.md b/docs/designers-developers/key-concepts.md index ff9007185fe0e7..3252337ba2b339 100644 --- a/docs/designers-developers/key-concepts.md +++ b/docs/designers-developers/key-concepts.md @@ -123,7 +123,7 @@ After running this through the parser we're left with a simple object we can man This has dramatic implications for how simple and performant we can make our parser. These explicit boundaries also protect damage in a single block from bleeding into other blocks or tarnishing the entire document. It also allows the system to identify unrecognized blocks before rendering them. -_N.B.:_ The defining aspect of blocks are their semantics and the isolation mechanism they provide; in other words, their identity. On the other hand, where their data is stored is a more liberal aspect. Blocks support more than just static local data (via JSON literals inside the HTML comment or within the block's HTML), and more mechanisms (_e.g._, global blocks or otherwise resorting to storage in complementary `WP_Post` objects) are expected. See [attributes](../../docs/designers-developers/developers/block-api/block-attributes.md) for details. +_N.B.:_ The defining aspect of blocks are their semantics and the isolation mechanism they provide; in other words, their identity. On the other hand, where their data is stored is a more liberal aspect. Blocks support more than just static local data (via JSON literals inside the HTML comment or within the block's HTML), and more mechanisms (_e.g._, global blocks or otherwise resorting to storage in complementary `WP_Post` objects) are expected. See [attributes](/docs/designers-developers/developers/block-api/block-attributes.md) for details. ## The Anatomy of a Serialized Block diff --git a/docs/manifest.json b/docs/manifest.json index e7d2a88e11cc59..35964c417ab5ec 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -101,6 +101,12 @@ "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/internationalization.md", "parent": "developers" }, + { + "title": "Accessibility", + "slug": "accessibility", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/accessibility.md", + "parent": "developers" + }, { "title": "Data Module Reference", "slug": "data", @@ -899,6 +905,12 @@ "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/packages/components/src/external-link/README.md", "parent": "components" }, + { + "title": "FocalPointPicker", + "slug": "focal-point-picker", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/packages/components/src/focal-point-picker/README.md", + "parent": "components" + }, { "title": "FocusableIframe", "slug": "focusable-iframe", diff --git a/docs/readme.md b/docs/readme.md index 5164003a654a87..eb699e6a2ed08f 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -6,7 +6,7 @@ The Gutenberg project provides three sources of documentation: Learn how to build blocks and extend the editor, best practices for designing block interfaces, and how to create themes that make the most of the new features Gutenberg provides. -[Visit the Designer & Developer Handbook](../docs/designers-developers/readme.md) +[Visit the Designer & Developer Handbook](/docs/designers-developers/readme.md) ## User Handbook @@ -17,4 +17,4 @@ Discover the new features Gutenberg offers, learn how your site will be affected Help make Gutenberg better by contributing ideas, code, testing, and more. -[Visit the Contributor Handbook](../docs/contributors/readme.md) +[Visit the Contributor Handbook](/docs/contributors/readme.md) diff --git a/docs/toc.json b/docs/toc.json index cdb3fa096507b6..4e06b65d552ce7 100644 --- a/docs/toc.json +++ b/docs/toc.json @@ -18,6 +18,7 @@ {"docs/designers-developers/developers/filters/autocomplete-filters.md": []} ]}, {"docs/designers-developers/developers/internationalization.md": []}, + {"docs/designers-developers/developers/accessibility.md": []}, {"docs/designers-developers/developers/data/README.md": "{{data}}"}, {"docs/designers-developers/developers/packages.md": "{{packages}}"}, {"packages/components/README.md": "{{components}}"}, diff --git a/docs/tool/generator.js b/docs/tool/generator.js index a632fb40b52681..f5ee6f584e78f7 100644 --- a/docs/tool/generator.js +++ b/docs/tool/generator.js @@ -17,7 +17,7 @@ function generateTableOfContent( parsedNamespaces ) { '# Data Module Reference', '', Object.values( parsedNamespaces ).map( ( parsedNamespace ) => { - return ` - [**${ parsedNamespace.name }**: ${ parsedNamespace.title }](../../docs/designers-developers/developers/data/data-${ kebabCase( parsedNamespace.name ) }.md)`; + return ` - [**${ parsedNamespace.name }**: ${ parsedNamespace.title }](/docs/designers-developers/developers/data/data-${ kebabCase( parsedNamespace.name ) }.md)`; } ).join( '\n' ), ].join( '\n' ); } diff --git a/gutenberg.php b/gutenberg.php index f91df4b9185978..13f0e148b57de8 100644 --- a/gutenberg.php +++ b/gutenberg.php @@ -201,8 +201,6 @@ function gutenberg_pre_init() { * @return bool Whether Gutenberg was initialized. */ function gutenberg_init( $return, $post ) { - global $title, $post_type; - if ( true === $return && current_filter() === 'replace_editor' ) { return $return; } @@ -224,17 +222,6 @@ function gutenberg_init( $return, $post ) { add_filter( 'screen_options_show_screen', '__return_false' ); add_filter( 'admin_body_class', 'gutenberg_add_admin_body_class' ); - $post_type_object = get_post_type_object( $post_type ); - - /* - * Always force to 'Edit Post' (or equivalent) - * because it needs to be in a generic state for both - * post-new.php and post.php?post=<id>. - */ - if ( ! empty( $post_type_object ) ) { - $title = $post_type_object->labels->edit_item; - } - /* * Remove the emoji script as it is incompatible with both React and any * contenteditable fields. @@ -254,19 +241,6 @@ function gutenberg_init( $return, $post ) { return true; } -/** - * Redirects the demo page to edit a new post. - */ -function gutenberg_redirect_demo() { - global $pagenow; - - if ( 'admin.php' === $pagenow && isset( $_GET['page'] ) && 'gutenberg' === $_GET['page'] ) { - wp_safe_redirect( admin_url( 'post-new.php?gutenberg-demo' ) ); - exit; - } -} -add_action( 'admin_init', 'gutenberg_redirect_demo' ); - /** * Adds the filters to register additional links for the Gutenberg editor in * the post/page screens. diff --git a/lib/client-assets.php b/lib/client-assets.php index c90cb8a05a8ff6..469310edbe118c 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -43,27 +43,10 @@ function gutenberg_url( $path ) { * @return string Conditional polyfill inline script. */ function gutenberg_get_script_polyfill( $tests ) { - global $wp_scripts; - - $polyfill = ''; - foreach ( $tests as $test => $handle ) { - if ( ! array_key_exists( $handle, $wp_scripts->registered ) ) { - continue; - } + _deprecated_function( __FUNCTION__, '5.0.0', 'wp_get_script_polyfill' ); - $polyfill .= ( - // Test presence of feature... - '( ' . $test . ' ) || ' . - // ...appending polyfill on any failures. Cautious viewers may balk - // at the `document.write`. Its caveat of synchronous mid-stream - // blocking write is exactly the behavior we need though. - 'document.write( \'<script src="' . - esc_url( $wp_scripts->registered[ $handle ]->src ) . - '"></scr\' + \'ipt>\' );' - ); - } - - return $polyfill; + global $wp_scripts; + return wp_get_script_polyfill( $wp_scripts, $tests ); } if ( ! function_exists( 'register_tinymce_scripts' ) ) { @@ -153,11 +136,14 @@ function gutenberg_register_packages_scripts() { * @since 0.1.0 */ function gutenberg_register_scripts_and_styles() { + global $wp_scripts; + gutenberg_register_vendor_scripts(); wp_add_inline_script( 'wp-polyfill', - gutenberg_get_script_polyfill( + wp_get_script_polyfill( + $wp_scripts, array( '\'fetch\' in window' => 'wp-polyfill-fetch', 'document.contains' => 'wp-polyfill-node-contains', @@ -599,32 +585,21 @@ function gutenberg_register_vendor_scripts() { * @since 0.1.0 */ function gutenberg_vendor_script_filename( $handle, $src ) { - $match_tinymce_plugin = preg_match( - '@tinymce.*/plugins/([^/]+)/plugin(\.min)?\.js$@', - $src, - $tinymce_plugin_pieces + $filename = basename( $src ); + $match = preg_match( + '/^' + . '(?P<ignore>.*?)' + . '(?P<suffix>\.min)?' + . '(?P<extension>\.js)' + . '(?P<extra>.*)' + . '$/', + $filename, + $filename_pieces ); - if ( $match_tinymce_plugin ) { - $prefix = 'tinymce-plugin-' . $tinymce_plugin_pieces[1]; - $suffix = isset( $tinymce_plugin_pieces[2] ) ? $tinymce_plugin_pieces[2] : ''; - } else { - $filename = basename( $src ); - $match = preg_match( - '/^' - . '(?P<ignore>.*?)' - . '(?P<suffix>\.min)?' - . '(?P<extension>\.js)' - . '(?P<extra>.*)' - . '$/', - $filename, - $filename_pieces - ); - - $prefix = $handle; - $suffix = $match ? $filename_pieces['suffix'] : ''; - } - $hash = substr( md5( $src ), 0, 8 ); + $prefix = $handle; + $suffix = $match ? $filename_pieces['suffix'] : ''; + $hash = substr( md5( $src ), 0, 8 ); return "${prefix}${suffix}.${hash}.js"; } @@ -876,8 +851,6 @@ function gutenberg_get_available_image_sizes() { * @param string $hook Screen name. */ function gutenberg_editor_scripts_and_styles( $hook ) { - $is_demo = isset( $_GET['gutenberg-demo'] ); - global $wp_scripts, $wp_meta_boxes; // Add "wp-hooks" as dependency of "heartbeat". @@ -994,17 +967,7 @@ function gutenberg_editor_scripts_and_styles( $hook ) { // Assign initial edits, if applicable. These are not initially assigned // to the persisted post, but should be included in its save payload. - if ( $is_new_post && $is_demo ) { - // Prepopulate with some test content in demo. - ob_start(); - include gutenberg_dir_path() . 'post-content.php'; - $demo_content = ob_get_clean(); - - $initial_edits = array( - 'title' => __( 'Welcome to the Gutenberg Editor', 'gutenberg' ), - 'content' => $demo_content, - ); - } elseif ( $is_new_post ) { + if ( $is_new_post ) { // Override "(Auto Draft)" new post default title with empty string, // or filtered value. $initial_edits = array( @@ -1231,11 +1194,27 @@ function gutenberg_editor_scripts_and_styles( $hook ) { $init_script = <<<JS ( function() { - window._wpLoadGutenbergEditor = window._wpLoadBlockEditor = new Promise( function( resolve ) { + window._wpLoadBlockEditor = new Promise( function( resolve ) { wp.domReady( function() { resolve( wp.editPost.initializeEditor( 'editor', "%s", %d, %s, %s ) ); } ); } ); + + Object.defineProperty( window, '_wpLoadGutenbergEditor', { + get: function() { + // TODO: Hello future maintainer. In removing this deprecation, + // ensure also to check whether `wp-editor`'s dependencies in + // `package-dependencies.php` still require `wp-deprecated`. + wp.deprecated( '`window._wpLoadGutenbergEditor`', { + plugin: 'Gutenberg', + version: '5.2', + alternative: '`window._wpLoadBlockEditor`', + hint: 'This is a private API, not intended for public use. It may be removed in the future.' + } ); + + return window._wpLoadBlockEditor; + } + } ); } )(); JS; diff --git a/lib/demo.php b/lib/demo.php new file mode 100644 index 00000000000000..11272050bf4c2e --- /dev/null +++ b/lib/demo.php @@ -0,0 +1,64 @@ +<?php +/** + * Supports for populating the Gutenberg demo content new post. + * + * @package gutenberg + */ + +if ( ! defined( 'ABSPATH' ) ) { + die( 'Silence is golden.' ); +} + +/** + * Redirects the demo page to edit a new post. + */ +function gutenberg_redirect_demo() { + global $pagenow; + + if ( 'admin.php' === $pagenow && isset( $_GET['page'] ) && 'gutenberg' === $_GET['page'] ) { + wp_safe_redirect( admin_url( 'post-new.php?gutenberg-demo' ) ); + exit; + } +} +add_action( 'admin_init', 'gutenberg_redirect_demo' ); + +/** + * Assigns the default content for the Gutenberg demo post. + * + * @param string $content Default post content. + * + * @return string Demo content if creating a new Gutenberg demo post, or the + * default content otherwise. + */ +function gutenberg_default_demo_content( $content ) { + $is_demo = isset( $_GET['gutenberg-demo'] ); + + if ( $is_demo ) { + // Prepopulate with some test content in demo. + ob_start(); + include gutenberg_dir_path() . 'post-content.php'; + return ob_get_clean(); + } + + return $content; +} +add_filter( 'default_content', 'gutenberg_default_demo_content' ); + +/** + * Assigns the default title for the Gutenberg demo post. + * + * @param string $title Default post title. + * + * @return string Demo title if creating a new Gutenberg demo post, or the + * default title otherwise. + */ +function gutenberg_default_demo_title( $title ) { + $is_demo = isset( $_GET['gutenberg-demo'] ); + + if ( $is_demo ) { + return __( 'Welcome to the Gutenberg Editor', 'gutenberg' ); + } + + return $title; +} +add_filter( 'default_title', 'gutenberg_default_demo_title' ); diff --git a/lib/load.php b/lib/load.php index ada0850ce4163f..2f8644ccf4636f 100644 --- a/lib/load.php +++ b/lib/load.php @@ -22,7 +22,7 @@ require dirname( __FILE__ ) . '/plugin-compat.php'; require dirname( __FILE__ ) . '/i18n.php'; require dirname( __FILE__ ) . '/register.php'; - +require dirname( __FILE__ ) . '/demo.php'; // Register server-side code for individual blocks. if ( ! function_exists( 'render_block_core_archives' ) ) { diff --git a/lib/rest-api.php b/lib/rest-api.php index 616493a371219f..d12d7df12dfcc2 100644 --- a/lib/rest-api.php +++ b/lib/rest-api.php @@ -21,12 +21,9 @@ function gutenberg_register_rest_routes() { } /** - * Make sure oEmbed REST Requests apply the WP Embed security mechanism for WordPress embeds. + * Handle a failing oEmbed proxy request to try embedding as a shortcode. * - * @see https://core.trac.wordpress.org/ticket/32522 - * - * TODO: This is a temporary solution. Next step would be to edit the WP_oEmbed_Controller, - * once merged into Core. + * @see https://core.trac.wordpress.org/ticket/45447 * * @since 2.3.0 * @@ -36,50 +33,32 @@ function gutenberg_register_rest_routes() { * @return WP_HTTP_Response|object|WP_Error The REST Request response. */ function gutenberg_filter_oembed_result( $response, $handler, $request ) { - if ( 'GET' !== $request->get_method() ) { + if ( ! is_wp_error( $response ) || 'oembed_invalid_url' !== $response->get_error_code() || + '/oembed/1.0/proxy' !== $request->get_route() ) { return $response; } - if ( is_wp_error( $response ) && 'oembed_invalid_url' !== $response->get_error_code() ) { + // Try using a classic embed instead. + global $wp_embed; + $html = $wp_embed->shortcode( array(), $_GET['url'] ); + if ( ! $html ) { return $response; } - // External embeds. - if ( '/oembed/1.0/proxy' === $request->get_route() ) { - if ( is_wp_error( $response ) ) { - // It's possibly a local post, so lets try and retrieve it that way. - $post_id = url_to_postid( $_GET['url'] ); - $data = get_oembed_response_data( $post_id, apply_filters( 'oembed_default_width', 600 ) ); - - if ( $data ) { - // It's a local post! - $response = (object) $data; - } else { - // Try using a classic embed, instead. - global $wp_embed; - $html = $wp_embed->shortcode( array(), $_GET['url'] ); - if ( $html ) { - global $wp_scripts; - // Check if any scripts were enqueued by the shortcode, and - // include them in the response. - $enqueued_scripts = array(); - foreach ( $wp_scripts->queue as $script ) { - $enqueued_scripts[] = $wp_scripts->registered[ $script ]->src; - } - return array( - 'provider_name' => __( 'Embed Handler', 'gutenberg' ), - 'html' => $html, - 'scripts' => $enqueued_scripts, - ); - } - } - } - - // Make sure the HTML is run through the oembed sanitisation routines. - $response->html = wp_oembed_get( $_GET['url'], $_GET ); + global $wp_scripts; + + // Check if any scripts were enqueued by the shortcode, and include them in + // the response. + $enqueued_scripts = array(); + foreach ( $wp_scripts->queue as $script ) { + $enqueued_scripts[] = $wp_scripts->registered[ $script ]->src; } - return $response; + return array( + 'provider_name' => __( 'Embed Handler', 'gutenberg' ), + 'html' => $html, + 'scripts' => $enqueued_scripts, + ); } add_filter( 'rest_request_after_callbacks', 'gutenberg_filter_oembed_result', 10, 3 ); diff --git a/package.json b/package.json index c59a6c200a189d..136d1db76679fb 100644 --- a/package.json +++ b/package.json @@ -177,7 +177,7 @@ "publish:dev": "npm run build:packages && lerna publish --npm-tag next", "publish:prod": "npm run build:packages && lerna publish", "test": "npm run lint && npm run test-unit", - "pretest-e2e": "concurrently \"./bin/reset-e2e-tests.sh\" \"npm run build\"", + "pretest-e2e": "./bin/reset-e2e-tests.sh", "test-e2e": "wp-scripts test-e2e --config packages/e2e-tests/jest.config.js", "test-e2e:watch": "npm run test-e2e -- --watch", "test-php": "npm run lint-php && npm run test-unit-php", diff --git a/packages/annotations/package.json b/packages/annotations/package.json index 6a89aa196b2650..a259d8a4d55ac4 100644 --- a/packages/annotations/package.json +++ b/packages/annotations/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/annotations", - "version": "1.0.6", + "version": "1.0.8", "description": "Annotate content in the Gutenberg editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/babel-plugin-makepot/CHANGELOG.md b/packages/babel-plugin-makepot/CHANGELOG.md index c70831cbada60c..8079f5197a2fde 100644 --- a/packages/babel-plugin-makepot/CHANGELOG.md +++ b/packages/babel-plugin-makepot/CHANGELOG.md @@ -1,3 +1,9 @@ +## v2.1.1 (Unreleased) + +### Bug Fix + +- Fixed Babel plugin for POT file generation to properly handle plural numbers specified in the passed header. ([#13577](https://github.com/WordPress/gutenberg/pull/13577)) + ## 2.1.0 (2018-09-05) ### New Feature diff --git a/packages/babel-plugin-makepot/src/index.js b/packages/babel-plugin-makepot/src/index.js index 1f9524f524ac57..cf66b8bbc80f3c 100644 --- a/packages/babel-plugin-makepot/src/index.js +++ b/packages/babel-plugin-makepot/src/index.js @@ -246,7 +246,7 @@ module.exports = function() { // Attempt to exract nplurals from header const pluralsMatch = ( baseData.headers[ 'plural-forms' ] || '' ).match( /nplurals\s*=\s*(\d+);/ ); if ( pluralsMatch ) { - nplurals = pluralsMatch[ 1 ]; + nplurals = parseInt( pluralsMatch[ 1 ], 10 ); } } diff --git a/packages/block-library/CHANGELOG.md b/packages/block-library/CHANGELOG.md index 398b2af5887d9b..12ab36c1f21047 100644 --- a/packages/block-library/CHANGELOG.md +++ b/packages/block-library/CHANGELOG.md @@ -3,6 +3,7 @@ ### New Feature - Add background color controls for the table block. +- Add new `RSS` block ([#7966](https://github.com/WordPress/gutenberg/pull/7966)). ## 2.2.12 (2019-01-03) diff --git a/packages/block-library/package.json b/packages/block-library/package.json index 03123f6c4aadd3..855f2257ca3f1d 100644 --- a/packages/block-library/package.json +++ b/packages/block-library/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-library", - "version": "2.2.13", + "version": "2.2.15", "description": "Block library for the WordPress editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/block-library/src/categories/edit.js b/packages/block-library/src/categories/edit.js index 5341ad6ff51682..ad1dea3ec55bf4 100644 --- a/packages/block-library/src/categories/edit.js +++ b/packages/block-library/src/categories/edit.js @@ -201,7 +201,7 @@ export default compose( withSelect( ( select ) => { const { getEntityRecords } = select( 'core' ); const { isResolving } = select( 'core/data' ); - const query = { per_page: -1 }; + const query = { per_page: -1, hide_empty: true }; return { categories: getEntityRecords( 'taxonomy', 'category', query ), diff --git a/packages/block-library/src/cover/index.js b/packages/block-library/src/cover/index.js index b1ceb293c336f4..1df4323902ecb1 100644 --- a/packages/block-library/src/cover/index.js +++ b/packages/block-library/src/cover/index.js @@ -7,6 +7,7 @@ import classnames from 'classnames'; * WordPress dependencies */ import { + FocalPointPicker, IconButton, PanelBody, RangeControl, @@ -67,6 +68,9 @@ const blockAttributes = { type: 'string', default: 'image', }, + focalPoint: { + type: 'object', + }, }; export const name = 'core/cover'; @@ -175,6 +179,7 @@ export const settings = { backgroundType, contentAlign, dimRatio, + focalPoint, hasParallax, id, title, @@ -224,6 +229,10 @@ export const settings = { backgroundColor: overlayColor.color, }; + if ( focalPoint ) { + style.backgroundPosition = `${ focalPoint.x * 100 }% ${ focalPoint.y * 100 }%`; + } + const controls = ( <Fragment> <BlockControls> @@ -265,6 +274,14 @@ export const settings = { onChange={ toggleParallax } /> ) } + { IMAGE_BACKGROUND_TYPE === backgroundType && ! hasParallax && ( + <FocalPointPicker + label={ __( 'Focal Point Picker' ) } + url={ url } + value={ focalPoint } + onChange={ ( value ) => setAttributes( { focalPoint: value } ) } + /> + ) } <PanelColorSettings title={ __( 'Overlay' ) } initialOpen={ true } @@ -370,6 +387,7 @@ export const settings = { contentAlign, customOverlayColor, dimRatio, + focalPoint, hasParallax, overlayColor, title, @@ -382,6 +400,9 @@ export const settings = { if ( ! overlayColorClass ) { style.backgroundColor = customOverlayColor; } + if ( focalPoint && ! hasParallax ) { + style.backgroundPosition = `${ focalPoint.x * 100 }% ${ focalPoint.y * 100 }%`; + } const classes = classnames( dimRatioToClass( dimRatio ), diff --git a/packages/block-library/src/embed/core-embeds.js b/packages/block-library/src/embed/core-embeds.js index 9fe6c2b335cd85..7edb114dae9ba2 100644 --- a/packages/block-library/src/embed/core-embeds.js +++ b/packages/block-library/src/embed/core-embeds.js @@ -145,6 +145,25 @@ export const others = [ }, patterns: [ /^https?:\/\/(www\.)?collegehumor\.com\/.+/i ], }, + { + name: 'core-embed/crowdsignal', + settings: { + title: 'Crowdsignal', + icon: embedContentIcon, + keywords: [ 'polldaddy' ], + transform: [ { + type: 'block', + blocks: [ 'core-embed/polldaddy' ], + transform: ( content ) => { + return createBlock( 'core-embed/crowdsignal', { + content, + } ); + }, + } ], + description: __( 'Embed Crowdsignal (formerly Polldaddy) content.' ), + }, + patterns: [ /^https?:\/\/((.+\.)?polldaddy\.com|poll\.fm|.+\.survey\.fm)\/.+/i ], + }, { name: 'core-embed/dailymotion', settings: { @@ -210,13 +229,17 @@ export const others = [ patterns: [ /^https?:\/\/(www\.)?mixcloud\.com\/.+/i ], }, { + // Deprecated in favour of the core-embed/crowdsignal block name: 'core-embed/polldaddy', settings: { title: 'Polldaddy', icon: embedContentIcon, description: __( 'Embed Polldaddy content.' ), + supports: { + inserter: false, + }, }, - patterns: [ /^https?:\/\/(www\.)?polldaddy\.com\/.+/i ], + patterns: [], }, { name: 'core-embed/reddit', diff --git a/packages/block-library/src/embed/util.js b/packages/block-library/src/embed/util.js index 5352fce1fb58e6..5161d13523c6b6 100644 --- a/packages/block-library/src/embed/util.js +++ b/packages/block-library/src/embed/util.js @@ -51,7 +51,7 @@ export const isFromWordPress = ( html ) => { export const getPhotoHtml = ( photo ) => { // 100% width for the preview so it fits nicely into the document, some "thumbnails" are - // acually the full size photo. + // actually the full size photo. const photoPreview = <p><img src={ photo.thumbnail_url } alt={ photo.title } width="100%" /></p>; return renderToString( photoPreview ); }; diff --git a/packages/block-library/src/image/edit.native.js b/packages/block-library/src/image/edit.native.js index eb73ef558542d4..6c5fad9b7b05ec 100644 --- a/packages/block-library/src/image/edit.native.js +++ b/packages/block-library/src/image/edit.native.js @@ -5,10 +5,10 @@ import React from 'react'; import { View, Image, TextInput } from 'react-native'; import { subscribeMediaUpload, - onMediaLibraryPressed, - onUploadMediaPressed, - onCapturePhotoPressed, - onImageQueryReattach, + requestMediaPickFromMediaLibrary, + requestMediaPickFromDeviceLibrary, + requestMediaPickFromDeviceCamera, + mediaUploadSync, } from 'react-native-gutenberg-bridge'; /** @@ -45,7 +45,7 @@ export default class ImageEdit extends React.Component { if ( attributes.id && ! isURL( attributes.url ) ) { this.addMediaUploadListener(); - onImageQueryReattach(); + mediaUploadSync(); } } @@ -108,7 +108,7 @@ export default class ImageEdit extends React.Component { const { url, caption, height, width } = attributes; const onMediaLibraryButtonPressed = () => { - onMediaLibraryPressed( ( mediaId, mediaUrl ) => { + requestMediaPickFromMediaLibrary( ( mediaId, mediaUrl ) => { if ( mediaUrl ) { setAttributes( { id: mediaId, url: mediaUrl } ); } @@ -116,8 +116,8 @@ export default class ImageEdit extends React.Component { }; if ( ! url ) { - const onUploadMediaButtonPressed = () => { - onUploadMediaPressed( ( mediaId, mediaUri ) => { + const onMediaUploadButtonPressed = () => { + requestMediaPickFromDeviceLibrary( ( mediaId, mediaUri ) => { if ( mediaUri ) { this.addMediaUploadListener( ); setAttributes( { url: mediaUri, id: mediaId } ); @@ -125,8 +125,8 @@ export default class ImageEdit extends React.Component { } ); }; - const onCapturePhotoButtonPressed = () => { - onCapturePhotoPressed( ( mediaId, mediaUri ) => { + const onMediaCaptureButtonPressed = () => { + requestMediaPickFromDeviceCamera( ( mediaId, mediaUri ) => { if ( mediaUri ) { this.addMediaUploadListener( ); setAttributes( { url: mediaUri, id: mediaId } ); @@ -136,9 +136,9 @@ export default class ImageEdit extends React.Component { return ( <MediaPlaceholder - onUploadMediaPressed={ onUploadMediaButtonPressed } + onUploadMediaPressed={ onMediaUploadButtonPressed } onMediaLibraryPressed={ onMediaLibraryButtonPressed } - onCapturePhotoPressed={ onCapturePhotoButtonPressed } + onCapturePhotoPressed={ onMediaCaptureButtonPressed } /> ); } diff --git a/packages/blocks/CHANGELOG.md b/packages/blocks/CHANGELOG.md index 1f40a53235f4cd..e4a1b5977bcc01 100644 --- a/packages/blocks/CHANGELOG.md +++ b/packages/blocks/CHANGELOG.md @@ -1,3 +1,13 @@ +## 6.1.0 (Unreleased) + +### New Feature + +- Blocks' `transforms` will receive `innerBlocks` as the second argument (or an array of each block's respective `innerBlocks` for a multi-transform). + +### Bug Fixes + +- Block validation will now correctly validate character references, resolving some issues where a standalone ampersand `&` followed later in markup by a character reference (e.g. `&`) could wrongly mark a block as being invalid. ([#13512](https://github.com/WordPress/gutenberg/pull/13512)) + ## 6.0.5 (2019-01-03) ## 6.0.4 (2018-12-12) diff --git a/packages/blocks/src/api/factory.js b/packages/blocks/src/api/factory.js index 9068a8b3d8cd3b..c7bd01f2c699c4 100644 --- a/packages/blocks/src/api/factory.js +++ b/packages/blocks/src/api/factory.js @@ -346,9 +346,12 @@ export function switchToBlockType( blocks, name ) { let transformationResults; if ( transformation.isMultiBlock ) { - transformationResults = transformation.transform( blocksArray.map( ( currentBlock ) => currentBlock.attributes ) ); + transformationResults = transformation.transform( + blocksArray.map( ( currentBlock ) => currentBlock.attributes ), + blocksArray.map( ( currentBlock ) => currentBlock.innerBlocks ) + ); } else { - transformationResults = transformation.transform( firstBlock.attributes ); + transformationResults = transformation.transform( firstBlock.attributes, firstBlock.innerBlocks ); } // Ensure that the transformation function returned an object or an array diff --git a/packages/blocks/src/api/test/factory.js b/packages/blocks/src/api/test/factory.js index eaf222b8b5c866..11249b08d36ced 100644 --- a/packages/blocks/src/api/test/factory.js +++ b/packages/blocks/src/api/test/factory.js @@ -1122,6 +1122,106 @@ describe( 'block factory', () => { value: 'smoked ribs', } ); } ); + + it( 'should pass through inner blocks to transform', () => { + registerBlockType( 'core/updated-columns-block', { + attributes: { + value: { + type: 'string', + }, + }, + transforms: { + from: [ { + type: 'block', + blocks: [ 'core/columns-block' ], + transform( attributes, innerBlocks ) { + return createBlock( + 'core/updated-columns-block', + attributes, + innerBlocks.map( ( innerBlock ) => { + return cloneBlock( innerBlock, { + value: 'after', + } ); + } ), + ); + }, + } ], + }, + save: noop, + category: 'common', + title: 'updated columns block', + } ); + registerBlockType( 'core/columns-block', defaultBlockSettings ); + registerBlockType( 'core/column-block', defaultBlockSettings ); + + const block = createBlock( + 'core/columns-block', + {}, + [ createBlock( 'core/column-block', { value: 'before' } ) ] + ); + + const transformedBlocks = switchToBlockType( block, 'core/updated-columns-block' ); + + expect( transformedBlocks ).toHaveLength( 1 ); + expect( transformedBlocks[ 0 ].innerBlocks ).toHaveLength( 1 ); + expect( transformedBlocks[ 0 ].innerBlocks[ 0 ].attributes.value ).toBe( 'after' ); + } ); + + it( 'should pass through inner blocks to transform (multi)', () => { + registerBlockType( 'core/updated-columns-block', { + attributes: { + value: { + type: 'string', + }, + }, + transforms: { + from: [ { + type: 'block', + blocks: [ 'core/columns-block' ], + isMultiBlock: true, + transform( blocksAttributes, blocksInnerBlocks ) { + return blocksAttributes.map( ( attributes, i ) => { + return createBlock( + 'core/updated-columns-block', + attributes, + blocksInnerBlocks[ i ].map( ( innerBlock ) => { + return cloneBlock( innerBlock, { + value: 'after' + i, + } ); + } ), + ); + } ); + }, + } ], + }, + save: noop, + category: 'common', + title: 'updated columns block', + } ); + registerBlockType( 'core/columns-block', defaultBlockSettings ); + registerBlockType( 'core/column-block', defaultBlockSettings ); + + const blocks = [ + createBlock( + 'core/columns-block', + {}, + [ createBlock( 'core/column-block', { value: 'before' } ) ] + ), + createBlock( + 'core/columns-block', + {}, + [ createBlock( 'core/column-block', { value: 'before' } ) ] + ), + ]; + + const transformedBlocks = switchToBlockType( blocks, 'core/updated-columns-block' ); + + expect( transformedBlocks ).toHaveLength( 2 ); + expect( transformedBlocks[ 0 ].innerBlocks ).toHaveLength( 1 ); + expect( transformedBlocks[ 0 ].innerBlocks[ 0 ].attributes.value ).toBe( 'after0' ); + expect( transformedBlocks[ 1 ].innerBlocks ).toHaveLength( 1 ); + expect( transformedBlocks[ 1 ].innerBlocks[ 0 ].attributes.value ).toBe( 'after1' ); + } ); } ); describe( 'getBlockTransforms', () => { diff --git a/packages/blocks/src/api/test/validation.js b/packages/blocks/src/api/test/validation.js index babcad7081ae0e..fcfb61bff6ae2c 100644 --- a/packages/blocks/src/api/test/validation.js +++ b/packages/blocks/src/api/test/validation.js @@ -2,7 +2,8 @@ * Internal dependencies */ import { - IdentityEntityParser, + isValidCharacterReference, + DecodeEntityParser, getTextPiecesSplitOnWhitespace, getTextWithCollapsedWhitespace, getMeaningfulAttributePairs, @@ -41,13 +42,39 @@ describe( 'validation', () => { } ); } ); - describe( 'IdentityEntityParser', () => { + describe( 'isValidCharacterReference', () => { + it( 'returns true for a named character reference', () => { + const result = isValidCharacterReference( 'blk12' ); + + expect( result ).toBe( true ); + } ); + + it( 'returns true for a decimal character reference', () => { + const result = isValidCharacterReference( '#33' ); + + expect( result ).toBe( true ); + } ); + + it( 'returns true for a hexadecimal character reference', () => { + const result = isValidCharacterReference( '#xC6' ); + + expect( result ).toBe( true ); + } ); + + it( 'returns false for an invalid character reference', () => { + const result = isValidCharacterReference( ' Test</h2><h2>Test &' ); + + expect( result ).toBe( false ); + } ); + } ); + + describe( 'DecodeEntityParser', () => { it( 'can be constructed', () => { - expect( new IdentityEntityParser() instanceof IdentityEntityParser ).toBe( true ); + expect( new DecodeEntityParser() instanceof DecodeEntityParser ).toBe( true ); } ); it( 'returns parse as decoded value', () => { - expect( new IdentityEntityParser().parse( 'quot' ) ).toBe( '"' ); + expect( new DecodeEntityParser().parse( 'quot' ) ).toBe( '"' ); } ); } ); @@ -397,6 +424,20 @@ describe( 'validation', () => { expect( isEquivalent ).toBe( true ); } ); + it( 'should account for character reference validity', () => { + // Regression: Previously the validator would wrongly evaluate the + // segment of text ` Test</h2><h2>Test &` as a character + // reference, as it's between an opening `&` and terminating `;`. + // + // See: https://github.com/WordPress/gutenberg/issues/12448 + const isEquivalent = isEquivalentHTML( + '<h2>Test & Test</h2><h2>Test & Test</h2>', + '<h2>Test & Test</h2><h2>Test & Test</h2>' + ); + + expect( isEquivalent ).toBe( true ); + } ); + it( 'should return false when more tokens in first', () => { const isEquivalent = isEquivalentHTML( '<div>Hello</div>', diff --git a/packages/blocks/src/api/validation.js b/packages/blocks/src/api/validation.js index 4ccf580b8ffdd9..452c8f832a43fc 100644 --- a/packages/blocks/src/api/validation.js +++ b/packages/blocks/src/api/validation.js @@ -154,24 +154,91 @@ const TEXT_NORMALIZATIONS = [ ]; /** - * Subsitute EntityParser class for `simple-html-tokenizer` which bypasses - * entity substitution in favor of validator's internal normalization. + * Regular expression matching a named character reference. In lieu of bundling + * a full set of references, the pattern covers the minimal necessary to test + * positively against the full set. + * + * "The ampersand must be followed by one of the names given in the named + * character references section, using the same case." + * + * Tested aginst "12.5 Named character references": + * + * ``` + * const references = [ ...document.querySelectorAll( + * '#named-character-references-table tr[id^=entity-] td:first-child' + * ) ].map( ( code ) => code.textContent ) + * references.every( ( reference ) => /^[\da-z]+$/i.test( reference ) ) + * ``` + * + * @link https://html.spec.whatwg.org/multipage/syntax.html#character-references + * @link https://html.spec.whatwg.org/multipage/named-characters.html#named-character-references + * + * @type {RegExp} + */ +const REGEXP_NAMED_CHARACTER_REFERENCE = /^[\da-z]+$/i; + +/** + * Regular expression matching a decimal character reference. + * + * "The ampersand must be followed by a U+0023 NUMBER SIGN character (#), + * followed by one or more ASCII digits, representing a base-ten integer" + * + * @link https://html.spec.whatwg.org/multipage/syntax.html#character-references + * + * @type {RegExp} + */ +const REGEXP_DECIMAL_CHARACTER_REFERENCE = /^#\d+$/; + +/** + * Regular expression matching a hexadecimal character reference. + * + * "The ampersand must be followed by a U+0023 NUMBER SIGN character (#), which + * must be followed by either a U+0078 LATIN SMALL LETTER X character (x) or a + * U+0058 LATIN CAPITAL LETTER X character (X), which must then be followed by + * one or more ASCII hex digits, representing a hexadecimal integer" + * + * @link https://html.spec.whatwg.org/multipage/syntax.html#character-references + * + * @type {RegExp} + */ +const REGEXP_HEXADECIMAL_CHARACTER_REFERENCE = /^#x[\da-f]+$/i; + +/** + * Returns true if the given string is a valid character reference segment, or + * false otherwise. The text should be stripped of `&` and `;` demarcations. + * + * @param {string} text Text to test. + * + * @return {boolean} Whether text is valid character reference. + */ +export function isValidCharacterReference( text ) { + return ( + REGEXP_NAMED_CHARACTER_REFERENCE.test( text ) || + REGEXP_DECIMAL_CHARACTER_REFERENCE.test( text ) || + REGEXP_HEXADECIMAL_CHARACTER_REFERENCE.test( text ) + ); +} + +/** + * Subsitute EntityParser class for `simple-html-tokenizer` which uses the + * implementation of `decodeEntities` from `html-entities`, in order to avoid + * bundling a massive named character reference. * * @see https://github.com/tildeio/simple-html-tokenizer/tree/master/src/entity-parser.ts */ -export class IdentityEntityParser { +export class DecodeEntityParser { /** * Returns a substitute string for an entity string sequence between `&` * and `;`, or undefined if no substitution should occur. * - * In this implementation, undefined is always returned. - * * @param {string} entity Entity fragment discovered in HTML. * * @return {?string} Entity substitute value. */ parse( entity ) { - return decodeEntities( '&' + entity + ';' ); + if ( isValidCharacterReference( entity ) ) { + return decodeEntities( '&' + entity + ';' ); + } } } @@ -451,7 +518,7 @@ export function getNextNonWhitespaceToken( tokens ) { */ function getHTMLTokens( html ) { try { - return new Tokenizer( new IdentityEntityParser() ).tokenize( html ); + return new Tokenizer( new DecodeEntityParser() ).tokenize( html ); } catch ( e ) { log.warning( 'Malformed HTML detected: %s', html ); } diff --git a/packages/components/CONTRIBUTING.md b/packages/components/CONTRIBUTING.md index cebec43084b1ee..18ef8d72b238ac 100644 --- a/packages/components/CONTRIBUTING.md +++ b/packages/components/CONTRIBUTING.md @@ -2,7 +2,7 @@ Thank you for taking the time to contribute. -The following is a set of guidelines for contributing to the `@wordpress/components` package to be considered in addition to the general ones described in our [Contributing Policy](../../CONTRIBUTING.md). +The following is a set of guidelines for contributing to the `@wordpress/components` package to be considered in addition to the general ones described in our [Contributing Policy](/CONTRIBUTING.md). ## Examples diff --git a/packages/components/package.json b/packages/components/package.json index 2b97622f3da9ce..7cfe766f406176 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/components", - "version": "7.0.6", + "version": "7.0.8", "description": "UI components for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/components/src/focal-point-picker/README.md b/packages/components/src/focal-point-picker/README.md new file mode 100644 index 00000000000000..7afb26f2781522 --- /dev/null +++ b/packages/components/src/focal-point-picker/README.md @@ -0,0 +1,68 @@ +# Focal Point Picker + +Focal Point Picker is a component which creates a UI for identifying the most important visual point of an image. This component addresses a specific problem: with large background images it is common to see undesirable crops, especially when viewing on smaller viewports such as mobile phones. This component allows the selection of the point with the most important visual information and returns it as a pair of numbers between 0 and 1. This value can be easily converted into the CSS `background-position` attribute, and will ensure that the focal point is never cropped out, regardless of viewport. + +Example focal point picker value: `{ x: 0.5, y: 0.1 }` +Corresponding CSS: `background-position: 50% 10%;` + +## Usage + +```jsx +import { FocalPointPicker } from '@wordpress/components'; + +const MyFocalPointPicker = withState( { + focalPoint: { + x: 0.5, + y: 0.5 + }, +} )( ( { focalPoint, setState } ) => { + const url = '/path/to/image'; + const dimensions = { + width: 400, + height: 100 + }; + return ( + <FocalPointPicker + url={ url } + dimensions={ dimensions } + value={ focalPoint } + onChange={ ( focalPoint ) => setState( { focalPoint } ) } + /> + ) +} ); + +/* Example function to render the CSS styles based on Focal Point Picker value */ +const renderImageContainerWithFocalPoint = ( url, focalPoint ) => { + const style = { + backgroundImage: `url(${ url })` , + backgroundPosition: `${ focalPoint.x * 100 }% ${ focalPoint.y * 100 }%` + } + return <div style={ style } />; +}; +``` + +## Props + +### `url` + +- Type: `Text` +- Required: Yes +- Description: URL of the image to be displayed + +### `dimensions` + +- Type: `Object` +- Required: Yes +- Description: An object describing the height and width of the image. Requires two paramaters: `height`, `width`. + +### `value` + +- Type: `Object` +- Required: Yes +- Description: The focal point. Should be an object containing `x` and `y` params. + +### `onChange` + +- Type: `Function` +- Required: Yes +- Description: Callback which is called when the focal point changes. diff --git a/packages/components/src/focal-point-picker/index.js b/packages/components/src/focal-point-picker/index.js new file mode 100644 index 00000000000000..315c28f7303b7b --- /dev/null +++ b/packages/components/src/focal-point-picker/index.js @@ -0,0 +1,251 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { Component, createRef } from '@wordpress/element'; +import { withInstanceId, compose } from '@wordpress/compose'; + +/** + * Internal dependencies + */ +import BaseControl from '../base-control'; +import withFocusOutside from '../higher-order/with-focus-outside'; +import { Path, SVG } from '../primitives'; + +const TEXTCONTROL_MIN = 0; +const TEXTCONTROL_MAX = 100; + +export class FocalPointPicker extends Component { + constructor() { + super( ...arguments ); + this.onMouseMove = this.onMouseMove.bind( this ); + this.state = { + isDragging: false, + bounds: {}, + percentages: {}, + }; + this.containerRef = createRef(); + this.imageRef = createRef(); + this.horizontalPositionChanged = this.horizontalPositionChanged.bind( this ); + this.verticalPositionChanged = this.verticalPositionChanged.bind( this ); + this.onLoad = this.onLoad.bind( this ); + } + componentDidMount() { + this.setState( { + percentages: this.props.value, + } ); + } + componentDidUpdate( prevProps ) { + if ( prevProps.url !== this.props.url ) { + this.setState( { + isDragging: false, + } ); + } + } + calculateBounds() { + const bounds = { + top: 0, + left: 0, + bottom: 0, + right: 0, + width: 0, + height: 0, + }; + if ( ! this.imageRef.current ) { + return bounds; + } + const dimensions = { + width: this.imageRef.current.clientWidth, + height: this.imageRef.current.clientHeight, + }; + const pickerDimensions = this.pickerDimensions(); + const widthRatio = pickerDimensions.width / dimensions.width; + const heightRatio = pickerDimensions.height / dimensions.height; + if ( heightRatio >= widthRatio ) { + bounds.width = bounds.right = pickerDimensions.width; + bounds.height = dimensions.height * widthRatio; + bounds.top = ( pickerDimensions.height - bounds.height ) / 2; + bounds.bottom = bounds.top + bounds.height; + } else { + bounds.height = bounds.bottom = pickerDimensions.height; + bounds.width = dimensions.width * heightRatio; + bounds.left = ( pickerDimensions.width - bounds.width ) / 2; + bounds.right = bounds.left + bounds.width; + } + return bounds; + } + onLoad() { + this.setState( { + bounds: this.calculateBounds(), + } ); + } + onMouseMove( event ) { + const { isDragging, bounds } = this.state; + const { onChange } = this.props; + + if ( isDragging ) { + const pickerDimensions = this.pickerDimensions(); + const cursorPosition = { + left: event.pageX - pickerDimensions.left, + top: event.pageY - pickerDimensions.top, + }; + const left = Math.max( + bounds.left, + Math.min( + cursorPosition.left, bounds.right + ) + ); + const top = Math.max( + bounds.top, + Math.min( + cursorPosition.top, bounds.bottom + ) + ); + const percentages = { + x: ( left - bounds.left ) / ( pickerDimensions.width - ( bounds.left * 2 ) ), + y: ( top - bounds.top ) / ( pickerDimensions.height - ( bounds.top * 2 ) ), + }; + this.setState( { percentages }, function() { + onChange( { + x: this.state.percentages.x, + y: this.state.percentages.y, + } ); + } ); + } + } + fractionToPercentage( fraction ) { + return Math.round( fraction * 100 ); + } + horizontalPositionChanged( event ) { + this.positionChangeFromTextControl( 'x', event.target.value ); + } + verticalPositionChanged( event ) { + this.positionChangeFromTextControl( 'y', event.target.value ); + } + positionChangeFromTextControl( axis, value ) { + const { onChange } = this.props; + const { percentages } = this.state; + const cleanValue = Math.max( Math.min( parseInt( value ), 100 ), 0 ); + percentages[ axis ] = cleanValue ? cleanValue / 100 : 0; + this.setState( { percentages }, function() { + onChange( { + x: this.state.percentages.x, + y: this.state.percentages.y, + } ); + } ); + } + pickerDimensions() { + if ( this.containerRef.current ) { + return { + width: this.containerRef.current.clientWidth, + height: this.containerRef.current.clientHeight, + top: this.containerRef.current.getBoundingClientRect().top + document.body.scrollTop, + left: this.containerRef.current.getBoundingClientRect().left, + }; + } + return { + width: 0, + height: 0, + left: 0, + top: 0, + }; + } + handleFocusOutside() { + this.setState( { + isDragging: false, + } ); + } + render() { + const { instanceId, url, value, label, help, className } = this.props; + const { bounds, isDragging, percentages } = this.state; + const pickerDimensions = this.pickerDimensions(); + const iconCoordinates = { + left: ( value.x * ( pickerDimensions.width - ( bounds.left * 2 ) ) ) + bounds.left, + top: ( value.y * ( pickerDimensions.height - ( bounds.top * 2 ) ) ) + bounds.top, + }; + const iconContainerStyle = { + left: `${ iconCoordinates.left }px`, + top: `${ iconCoordinates.top }px`, + }; + const iconContainerClasses = classnames( + 'components-focal-point-picker__icon_container', + isDragging ? 'is-dragging' : null + ); + const id = `inspector-focal-point-picker-control-${ instanceId }`; + const horizontalPositionId = `inspector-focal-point-picker-control-horizontal-position-${ instanceId }`; + const verticalPositionId = `inspector-focal-point-picker-control-horizontal-position-${ instanceId }`; + return ( + <BaseControl label={ label } id={ id } help={ help } className={ className }> + <div className="components-focal-point-picker-wrapper"> + <div + className="components-focal-point-picker" + onMouseDown={ () => this.setState( { isDragging: true } ) } + onDragStart={ () => this.setState( { isDragging: true } ) } + onMouseUp={ () => this.setState( { isDragging: false } ) } + onDrop={ () => this.setState( { isDragging: false } ) } + onMouseMove={ this.onMouseMove } + ref={ this.containerRef } + role="button" + tabIndex="-1" + > + <img + alt="Dimensions helper" + onLoad={ this.onLoad } + ref={ this.imageRef } + src={ url } + draggable="false" + /> + <div className={ iconContainerClasses } style={ iconContainerStyle }> + <SVG className="components-focal-point-picker__icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30"> + <Path className="components-focal-point-picker__icon-outline" d="M15 1C7.3 1 1 7.3 1 15s6.3 14 14 14 14-6.3 14-14S22.7 1 15 1zm0 22c-4.4 0-8-3.6-8-8s3.6-8 8-8 8 3.6 8 8-3.6 8-8 8z" /> + <Path className="components-focal-point-picker__icon-fill" d="M15 3C8.4 3 3 8.4 3 15s5.4 12 12 12 12-5.4 12-12S21.6 3 15 3zm0 22C9.5 25 5 20.5 5 15S9.5 5 15 5s10 4.5 10 10-4.5 10-10 10z" /> + </SVG> + </div> + </div> + </div> + <div className="components-focal-point-picker_position-display-container"> + <BaseControl label={ __( 'Horizontal Pos.' ) }> + <input + className="components-text-control__input" + id={ horizontalPositionId } + max={ TEXTCONTROL_MAX } + min={ TEXTCONTROL_MIN } + onChange={ this.horizontalPositionChanged } + type="number" + value={ this.fractionToPercentage( percentages.x ) } + /> + <span>%</span> + </BaseControl> + <BaseControl label={ __( 'Vertical Pos.' ) }> + <input + className="components-text-control__input" + id={ verticalPositionId } + max={ TEXTCONTROL_MAX } + min={ TEXTCONTROL_MIN } + onChange={ this.verticalPositionChanged } + type="number" + value={ this.fractionToPercentage( percentages.y ) } + /> + <span>%</span> + </BaseControl> + </div> + </BaseControl> + ); + } +} + +FocalPointPicker.defaultProps = { + url: null, + value: { + x: 0.5, + y: 0.5, + }, + onChange: () => {}, +}; + +export default compose( [ withInstanceId, withFocusOutside ] )( FocalPointPicker ); diff --git a/packages/components/src/focal-point-picker/style.scss b/packages/components/src/focal-point-picker/style.scss new file mode 100644 index 00000000000000..f93f2e57ad6886 --- /dev/null +++ b/packages/components/src/focal-point-picker/style.scss @@ -0,0 +1,75 @@ +.components-focal-point-picker-wrapper { + background-color: transparent; + border: 1px solid $light-gray-500; + height: 200px; + width: 100%; + padding: 14px; +} + +.components-focal-point-picker { + align-items: center; + cursor: pointer; + display: flex; + height: 100%; + justify-content: center; + position: relative; + width: 100%; + + img { + height: auto; + max-height: 100%; + max-width: 100%; + width: auto; + user-select: none; + } +} + +.components-focal-point-picker__icon_container { + background-color: transparent; + cursor: grab; + height: 30px; + opacity: 0.8; + position: absolute; + will-change: transform; + width: 30px; + z-index: 10000; + + &.is-dragging { + cursor: grabbing; + } +} + +.components-focal-point-picker__icon { + display: block; + height: 100%; + left: -15px; + position: absolute; + top: -15px; + width: 100%; + + .components-focal-point-picker__icon-outline { + fill: $white; + } + + .components-focal-point-picker__icon-fill { + fill: theme(primary); + } +} + +.components-focal-point-picker_position-display-container { + margin: 1em 0; + display: flex; + + .components-base-control__field { + margin: 0 1em 0 0; + } + + input[type="number"].components-text-control__input { // Needs specificity to override padding. + max-width: 4em; + padding: 6px 4px; + } + + span { + margin: 0 0 0 0.2em; + } +} diff --git a/packages/components/src/index.js b/packages/components/src/index.js index 415cd5a1be5dc4..f892867c7a4005 100644 --- a/packages/components/src/index.js +++ b/packages/components/src/index.js @@ -19,6 +19,7 @@ export { default as DropZoneProvider } from './drop-zone/provider'; export { default as Dropdown } from './dropdown'; export { default as DropdownMenu } from './dropdown-menu'; export { default as ExternalLink } from './external-link'; +export { default as FocalPointPicker } from './focal-point-picker'; export { default as FocusableIframe } from './focusable-iframe'; export { default as FontSizePicker } from './font-size-picker'; export { default as FormFileUpload } from './form-file-upload'; diff --git a/packages/components/src/notice/README.md b/packages/components/src/notice/README.md index c32bf02a3de100..9b0b62937b7f84 100644 --- a/packages/components/src/notice/README.md +++ b/packages/components/src/notice/README.md @@ -31,4 +31,4 @@ The following props are used to control the display of the component. * `status`: (string) can be `warning` (yellow), `success` (green), `error` (red). * `onRemove`: function called when dismissing the notice * `isDismissible`: (boolean) defaults to true, whether the notice should be dismissible or not -* `actions`: (array) an array of action objects. Each member object should contain a `label` and either a `url` link string or `onClick` callback function. +* `actions`: (array) an array of action objects. Each member object should contain a `label` and either a `url` link string or `onClick` callback function. A `className` property can be used to add custom classes to the button styles. By default, some classes are used (e.g: is-link or is-default) the default classes can be removed by setting property `noDefaultClasses` to `false`. diff --git a/packages/components/src/notice/index.js b/packages/components/src/notice/index.js index 90b2d908deee92..99d5f9968f1111 100644 --- a/packages/components/src/notice/index.js +++ b/packages/components/src/notice/index.js @@ -36,17 +36,35 @@ function Notice( { <div className={ classes }> <div className="components-notice__content"> { children } - { actions.map( ( { label, url, onClick }, index ) => ( - <Button - key={ index } - href={ url } - isLink={ !! url } - onClick={ onClick } - className="components-notice__action" - > - { label } - </Button> - ) ) } + { actions.map( + ( + { + className: buttonCustomClasses, + label, + noDefaultClasses = false, + onClick, + url, + }, + index + ) => { + return ( + <Button + key={ index } + href={ url } + isDefault={ ! noDefaultClasses && ! url } + isLink={ ! noDefaultClasses && !! url } + onClick={ url ? undefined : onClick } + className={ classnames( + 'components-notice__action', + buttonCustomClasses + ) } + > + { label } + </Button> + ); + } + + ) } </div> { isDismissible && ( <IconButton diff --git a/packages/components/src/notice/style.scss b/packages/components/src/notice/style.scss index 31b364c7e21a6c..8003d33384a3c6 100644 --- a/packages/components/src/notice/style.scss +++ b/packages/components/src/notice/style.scss @@ -34,6 +34,9 @@ &.is-link { margin-left: 4px; } + &.is-default { + vertical-align: initial; + } } .components-notice__dismiss { diff --git a/packages/components/src/notice/test/__snapshots__/index.js.snap b/packages/components/src/notice/test/__snapshots__/index.js.snap index 7be3c43c0a1b5d..28b35679ceec66 100644 --- a/packages/components/src/notice/test/__snapshots__/index.js.snap +++ b/packages/components/src/notice/test/__snapshots__/index.js.snap @@ -11,6 +11,7 @@ exports[`Notice should match snapshot 1`] = ` <ForwardRef(Button) className="components-notice__action" href="https://example.com" + isDefault={false} isLink={true} > View diff --git a/packages/components/src/style.scss b/packages/components/src/style.scss index c3e8b38e7a7efd..046c7b6264efdc 100644 --- a/packages/components/src/style.scss +++ b/packages/components/src/style.scss @@ -13,6 +13,7 @@ @import "./drop-zone/style.scss"; @import "./dropdown-menu/style.scss"; @import "./external-link/style.scss"; +@import "./focal-point-picker/style.scss"; @import "./font-size-picker/style.scss"; @import "./form-file-upload/style.scss"; @import "./form-toggle/style.scss"; diff --git a/packages/components/src/tab-panel/index.js b/packages/components/src/tab-panel/index.js index 2e94bd2b05e14d..5558272f2ffc4a 100644 --- a/packages/components/src/tab-panel/index.js +++ b/packages/components/src/tab-panel/index.js @@ -13,9 +13,10 @@ import { withInstanceId } from '@wordpress/compose'; * Internal dependencies */ import { NavigableMenu } from '../navigable-container'; +import Button from '../button'; const TabButton = ( { tabId, onClick, children, selected, ...rest } ) => ( - <button role="tab" + <Button role="tab" tabIndex={ selected ? null : -1 } aria-selected={ selected } id={ tabId } @@ -23,7 +24,7 @@ const TabButton = ( { tabId, onClick, children, selected, ...rest } ) => ( { ...rest } > { children } - </button> + </Button> ); class TabPanel extends Component { diff --git a/packages/e2e-tests/plugins/inner-blocks-templates/index.js b/packages/e2e-tests/plugins/inner-blocks-templates/index.js index 0922fe106cecf2..6c0b62c51fea4e 100644 --- a/packages/e2e-tests/plugins/inner-blocks-templates/index.js +++ b/packages/e2e-tests/plugins/inner-blocks-templates/index.js @@ -1,5 +1,6 @@ ( function() { var registerBlockType = wp.blocks.registerBlockType; + var createBlock = wp.blocks.createBlock; var el = wp.element.createElement; var InnerBlocks = wp.editor.InnerBlocks; var __ = wp.i18n.__; @@ -28,10 +29,10 @@ edit: function( props ) { return el( - InnerBlocks, - { - template: TEMPLATE, - } + InnerBlocks, + { + template: TEMPLATE, + } ); }, @@ -45,11 +46,11 @@ edit: function( props ) { return el( - InnerBlocks, - { - template: TEMPLATE, - templateLock: 'all', - } + InnerBlocks, + { + template: TEMPLATE, + templateLock: 'all', + } ); }, @@ -63,13 +64,57 @@ edit: function( props ) { return el( - InnerBlocks, - { - template: TEMPLATE_PARAGRAPH_PLACEHOLDER, - } + InnerBlocks, + { + template: TEMPLATE_PARAGRAPH_PLACEHOLDER, + } ); }, save, } ); + + registerBlockType( 'test/test-inner-blocks-transformer-target', { + title: 'Test Inner Blocks transformer target', + icon: 'cart', + category: 'common', + + transforms: { + from: [ + { + type: 'block', + blocks: [ + 'test/i-dont-exist', + 'test/test-inner-blocks-no-locking', + 'test/test-inner-blocks-locking-all', + 'test/test-inner-blocks-paragraph-placeholder' + ], + transform: function( attributes, innerBlocks ) { + return createBlock( 'test/test-inner-blocks-transformer-target', attributes, innerBlocks ); + }, + }, + ], + to: [ + { + type: 'block', + blocks: [ 'test/i-dont-exist' ], + transform: function( attributes, innerBlocks ) { + return createBlock( 'test/test-inner-blocks-transformer-target', attributes, innerBlocks ); + }, + } + ] + }, + + edit: function( props ) { + return el( + InnerBlocks, + { + template: TEMPLATE, + } + ); + }, + + save, + } ); + } )(); diff --git a/packages/e2e-tests/specs/blocks/__snapshots__/list.test.js.snap b/packages/e2e-tests/specs/blocks/__snapshots__/list.test.js.snap index a4cb6cdb75be57..71118e8d65118c 100644 --- a/packages/e2e-tests/specs/blocks/__snapshots__/list.test.js.snap +++ b/packages/e2e-tests/specs/blocks/__snapshots__/list.test.js.snap @@ -144,6 +144,18 @@ exports[`List should indent and outdent level 2 3`] = ` <!-- /wp:list -->" `; +exports[`List should outdent with children 1`] = ` +"<!-- wp:list --> +<ul><li>a<ul><li>b<ul><li>c</li></ul></li></ul></li></ul> +<!-- /wp:list -->" +`; + +exports[`List should outdent with children 2`] = ` +"<!-- wp:list --> +<ul><li>a</li><li>b<ul><li>c</li></ul></li></ul> +<!-- /wp:list -->" +`; + exports[`List should split indented list item 1`] = ` "<!-- wp:list --> <ul><li>one<ul><li>two</li><li>three</li></ul></li></ul> diff --git a/packages/e2e-tests/specs/blocks/list.test.js b/packages/e2e-tests/specs/blocks/list.test.js index 022c0b88b13362..98b7f74bd25362 100644 --- a/packages/e2e-tests/specs/blocks/list.test.js +++ b/packages/e2e-tests/specs/blocks/list.test.js @@ -262,4 +262,22 @@ describe( 'List', () => { expect( await getEditedPostContent() ).toMatchSnapshot(); } ); + + it( 'should outdent with children', async () => { + await insertBlock( 'List' ); + await page.keyboard.type( 'a' ); + await page.keyboard.press( 'Enter' ); + await pressKeyWithModifier( 'primary', 'm' ); + await page.keyboard.type( 'b' ); + await page.keyboard.press( 'Enter' ); + await pressKeyWithModifier( 'primary', 'm' ); + await page.keyboard.type( 'c' ); + + expect( await getEditedPostContent() ).toMatchSnapshot(); + + await page.keyboard.press( 'ArrowUp' ); + await pressKeyWithModifier( 'primaryShift', 'm' ); + + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); } ); diff --git a/packages/edit-post/package.json b/packages/edit-post/package.json index fc44436ec47922..d6ec1e5c769a42 100644 --- a/packages/edit-post/package.json +++ b/packages/edit-post/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/edit-post", - "version": "3.1.8", + "version": "3.1.10", "description": "Edit Post module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/editor/package.json b/packages/editor/package.json index 79684c95217c3f..f27cff81ed68db 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/editor", - "version": "9.0.8", + "version": "9.0.10", "description": "Building blocks for WordPress editors.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/editor/src/components/block-list/block.js b/packages/editor/src/components/block-list/block.js index 87d9d117957670..82e7154b76995b 100644 --- a/packages/editor/src/components/block-list/block.js +++ b/packages/editor/src/components/block-list/block.js @@ -99,8 +99,9 @@ export class BlockListBlock extends Component { this.focusTabbable( true ); } - // When triggering a multi-selection, - // move the focus to the wrapper of the first selected block. + // When triggering a multi-selection, move the focus to the wrapper of the first selected block. + // This ensures that it is not possible to continue editing the initially selected block + // when a multi-selection is triggered. if ( this.props.isFirstMultiSelected && ! prevProps.isFirstMultiSelected ) { this.wrapperNode.focus(); } @@ -301,6 +302,8 @@ export class BlockListBlock extends Component { deleteOrInsertAfterWrapper( event ) { const { keyCode, target } = event; + // These block shortcuts should only trigger if the wrapper of the block is selected + // And when it's not a multi-selection to avoid conflicting with RichText/Inputs and multiselection. if ( ! this.props.isSelected || target !== this.wrapperNode || diff --git a/packages/editor/src/components/block-styles/index.js b/packages/editor/src/components/block-styles/index.js index 6892df08c6d06a..c44f8f686e191d 100644 --- a/packages/editor/src/components/block-styles/index.js +++ b/packages/editor/src/components/block-styles/index.js @@ -11,6 +11,8 @@ import { compose } from '@wordpress/compose'; import { withSelect, withDispatch } from '@wordpress/data'; import TokenList from '@wordpress/token-list'; import { ENTER, SPACE } from '@wordpress/keycodes'; +import { _x } from '@wordpress/i18n'; +import { getBlockType } from '@wordpress/blocks'; /** * Internal dependencies @@ -68,6 +70,7 @@ function BlockStyles( { onChangeClassName, name, attributes, + type, onSwitch = noop, onHoverClassName = noop, } ) { @@ -75,10 +78,22 @@ function BlockStyles( { return null; } + if ( ! type.styles && ! find( styles, 'isDefault' ) ) { + styles = [ + { + name: 'default', + label: _x( 'Default', 'block style' ), + isDefault: true, + }, + ...styles, + ]; + } + const activeStyle = getActiveStyle( styles, className ); function updateClassName( style ) { const updatedClassName = replaceActiveStyle( className, activeStyle, style ); onChangeClassName( updatedClassName ); + onHoverClassName( null ); onSwitch(); } @@ -131,12 +146,14 @@ export default compose( [ const { getBlock } = select( 'core/editor' ); const { getBlockStyles } = select( 'core/blocks' ); const block = getBlock( clientId ); + const blockType = getBlockType( block.name ); return { name: block.name, attributes: block.attributes, className: block.attributes.className || '', styles: getBlockStyles( block.name ), + type: blockType, }; } ), withDispatch( ( dispatch, { clientId } ) => { diff --git a/packages/editor/src/hooks/custom-class-name.js b/packages/editor/src/hooks/custom-class-name.js index 9779dfd1a2bb5a..3d4af472276701 100644 --- a/packages/editor/src/hooks/custom-class-name.js +++ b/packages/editor/src/hooks/custom-class-name.js @@ -65,7 +65,7 @@ export const withInspectorControl = createHigherOrderComponent( ( BlockEdit ) => value={ props.attributes.className || '' } onChange={ ( nextValue ) => { props.setAttributes( { - className: nextValue, + className: nextValue !== '' ? nextValue : undefined, } ); } } /> diff --git a/packages/format-library/package.json b/packages/format-library/package.json index a18456013c62c4..cb77cba8519cdf 100644 --- a/packages/format-library/package.json +++ b/packages/format-library/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/format-library", - "version": "1.2.11", + "version": "1.2.13", "description": "Format library for the WordPress editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/is-shallow-equal/README.md b/packages/is-shallow-equal/README.md index ac68c2997ac73c..3c53ed3261e445 100644 --- a/packages/is-shallow-equal/README.md +++ b/packages/is-shallow-equal/README.md @@ -29,6 +29,24 @@ import { isShallowEqualArrays } from '@wordpress/is-shallow-equal'; import { isShallowEqualObjects } from '@wordpress/is-shallow-equal'; ``` +Shallow comparison differs from deep comparison by the fact that it compares members from each as being strictly equal to the other, meaning that arrays and objects will be compared by their _references_, not by their values (see also [_Object Equality in JavaScript_.](http://adripofjavascript.com/blog/drips/object-equality-in-javascript.html)) In situations where nested objects must be compared by value, consider using [Lodash's `isEqual`](https://lodash.com/docs/4.17.11#isEqual) instead. + +```js +import isShallowEqual from '@wordpress/is-shallow-equal'; +import { isEqual } from 'lodash'; // deep comparison + +let object = { a: 1 }; + +isShallowEqual( [ { a: 1 } ], [ { a: 1 } ] ); +// ⇒ false + +isEqual( [ { a: 1 } ], [ { a: 1 } ] ); +// ⇒ true + +isShallowEqual( [ object ], [ object ] ); +// ⇒ true +``` + ## Rationale Shallow equality utilities are already a dime a dozen. Since these operations are often at the core of critical hot code paths, the WordPress contributors had specific requirements that were found to only be partially satisfied by existing solutions. diff --git a/packages/is-shallow-equal/test/index.js b/packages/is-shallow-equal/test/index.js index ee918194cdad14..49f2526dbdf279 100644 --- a/packages/is-shallow-equal/test/index.js +++ b/packages/is-shallow-equal/test/index.js @@ -84,6 +84,14 @@ describe( 'isShallowEqual', () => { expect( isShallowEqual( b, a ) ).toBe( true ); } ); + it( 'returns false on object deep-but-referentially-unequal values', () => { + const a = { foo: {} }; + const b = { foo: {} }; + + expect( isShallowEqual( a, b ) ).toBe( false ); + expect( isShallowEqual( b, a ) ).toBe( false ); + } ); + it( 'returns false if a array has more keys than b', () => { const a = [ 1, 2 ]; const b = [ 1 ]; diff --git a/packages/list-reusable-blocks/package.json b/packages/list-reusable-blocks/package.json index 9a51dc4dd0957f..06ac0200076863 100644 --- a/packages/list-reusable-blocks/package.json +++ b/packages/list-reusable-blocks/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/list-reusable-blocks", - "version": "1.1.19", + "version": "1.1.21", "description": "Adding Export/Import support to the reusable blocks listing.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/nux/package.json b/packages/nux/package.json index b0015ff5c201f9..aadfc11b17e135 100644 --- a/packages/nux/package.json +++ b/packages/nux/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/nux", - "version": "3.0.7", + "version": "3.0.9", "description": "NUX (New User eXperience) module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/rich-text/package.json b/packages/rich-text/package.json index 7c475baec9a525..541cccc79cc560 100644 --- a/packages/rich-text/package.json +++ b/packages/rich-text/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/rich-text", - "version": "3.0.5", + "version": "3.0.7", "description": "Rich text value and manipulation API.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/rich-text/src/change-list-type.js b/packages/rich-text/src/change-list-type.js index 2b92dda0ab92ce..1dfc0406573636 100644 --- a/packages/rich-text/src/change-list-type.js +++ b/packages/rich-text/src/change-list-type.js @@ -21,9 +21,10 @@ import { getParentLineIndex } from './get-parent-line-index'; */ export function changeListType( value, newFormat ) { const { text, formats, start, end } = value; - const startLineFormats = formats[ getLineIndex( value, start ) ] || []; + const startingLineIndex = getLineIndex( value, start ); + const startLineFormats = formats[ startingLineIndex ] || []; const endLineFormats = formats[ getLineIndex( value, end ) ] || []; - const startIndex = getParentLineIndex( value, start ); + const startIndex = getParentLineIndex( value, startingLineIndex ); const newFormats = formats.slice( 0 ); const startCount = startLineFormats.length - 1; const endCount = endLineFormats.length - 1; diff --git a/packages/rich-text/src/get-last-child-index.js b/packages/rich-text/src/get-last-child-index.js new file mode 100644 index 00000000000000..976051b10c0a3e --- /dev/null +++ b/packages/rich-text/src/get-last-child-index.js @@ -0,0 +1,40 @@ +/** + * Internal dependencies + */ + +import { LINE_SEPARATOR } from './special-characters'; + +/** + * Gets the line index of the last child in the list. + * + * @param {Object} value Value to search. + * @param {number} lineIndex Line index of a list item in the list. + * + * @return {Array} The index of the last child. + */ +export function getLastChildIndex( { text, formats }, lineIndex ) { + const lineFormats = formats[ lineIndex ] || []; + // Use the given line index in case there are no next children. + let childIndex = lineIndex; + + // `lineIndex` could be `undefined` if it's the first line. + for ( let index = lineIndex || 0; index < text.length; index++ ) { + // We're only interested in line indices. + if ( text[ index ] !== LINE_SEPARATOR ) { + continue; + } + + const formatsAtIndex = formats[ index ] || []; + + // If the amout of formats is equal or more, store it, then return the + // last one if the amount of formats is less. + if ( formatsAtIndex.length >= lineFormats.length ) { + childIndex = index; + } else { + return childIndex; + } + } + + // If the end of the text is reached, return the last child index. + return childIndex; +} diff --git a/packages/rich-text/src/get-parent-line-index.js b/packages/rich-text/src/get-parent-line-index.js index 332764dc45bd30..bd3f72de965196 100644 --- a/packages/rich-text/src/get-parent-line-index.js +++ b/packages/rich-text/src/get-parent-line-index.js @@ -9,27 +9,23 @@ import { LINE_SEPARATOR } from './special-characters'; * go through every list item until we find one with exactly one format type * less. * - * @param {Object} value Value to search. - * @param {number} startIndex Index to start search at. + * @param {Object} value Value to search. + * @param {number} lineIndex Line index of a child list item. * * @return {Array} The parent list line index. */ -export function getParentLineIndex( { text, formats }, startIndex ) { - let index = startIndex; - let startFormats; +export function getParentLineIndex( { text, formats }, lineIndex ) { + const startFormats = formats[ lineIndex ] || []; - while ( index-- ) { + let index = lineIndex; + + while ( index-- >= 0 ) { if ( text[ index ] !== LINE_SEPARATOR ) { continue; } const formatsAtIndex = formats[ index ] || []; - if ( ! startFormats ) { - startFormats = formatsAtIndex; - continue; - } - if ( formatsAtIndex.length === startFormats.length - 1 ) { return index; } diff --git a/packages/rich-text/src/indent-list-items.js b/packages/rich-text/src/indent-list-items.js index 39226de4722dc4..516b3c85f96ea2 100644 --- a/packages/rich-text/src/indent-list-items.js +++ b/packages/rich-text/src/indent-list-items.js @@ -6,6 +6,36 @@ import { LINE_SEPARATOR } from './special-characters'; import { normaliseFormats } from './normalise-formats'; import { getLineIndex } from './get-line-index'; +/** + * Gets the line index of the first previous list item with higher indentation. + * + * @param {Object} value Value to search. + * @param {number} lineIndex Line index of the list item to compare with. + * + * @return {boolean} The line index. + */ +function getTargetLevelLineIndex( { text, formats }, lineIndex ) { + const startFormats = formats[ lineIndex ] || []; + + let index = lineIndex; + + while ( index-- >= 0 ) { + if ( text[ index ] !== LINE_SEPARATOR ) { + continue; + } + + const formatsAtIndex = formats[ index ] || []; + + // Return the first line index that is one level higher. If the level is + // lower or equal, there is no result. + if ( formatsAtIndex.length === startFormats.length + 1 ) { + return index; + } else if ( formatsAtIndex.length <= startFormats.length ) { + return; + } + } +} + /** * Indents any selected list items if possible. * @@ -23,62 +53,39 @@ export function indentListItems( value, rootFormat ) { } const { text, formats, start, end } = value; + const previousLineIndex = getLineIndex( value, lineIndex ); const formatsAtLineIndex = formats[ lineIndex ] || []; - const targetFormats = formats[ getLineIndex( value, lineIndex ) ] || []; + const formatsAtPreviousLineIndex = formats[ previousLineIndex ] || []; // The the indentation of the current line is greater than previous line, // then the line cannot be furter indented. - if ( formatsAtLineIndex.length > targetFormats.length ) { + if ( formatsAtLineIndex.length > formatsAtPreviousLineIndex.length ) { return value; } const newFormats = formats.slice(); + const targetLevelLineIndex = getTargetLevelLineIndex( value, lineIndex ); for ( let index = lineIndex; index < end; index++ ) { if ( text[ index ] !== LINE_SEPARATOR ) { continue; } - // If the indentation of the previous line is the same as the current - // line, then duplicate the type and append all current types. E.g. - // - // 1. one - // 2. two <= Selected - // * three <= Selected - // - // should become: - // - // 1. one - // 1. two <= Selected - // * three <= Selected - // - // ^ Inserted list - // - // Otherwise take the target formats and append traling lists. E.g. - // - // 1. one - // * target - // 2. two <= Selected - // * three <= Selected - // - // should become: - // - // 1. one - // * target - // * two <= Selected - // * three <= Selected - // - if ( targetFormats.length === formatsAtLineIndex.length ) { + // Get the previous list, and if there's a child list, take over the + // formats. If not, duplicate the last level and create a new level. + if ( targetLevelLineIndex ) { + const targetFormats = formats[ targetLevelLineIndex ] || []; + newFormats[ index ] = targetFormats.concat( + ( newFormats[ index ] || [] ).slice( targetFormats.length - 1 ) + ); + } else { + const targetFormats = formats[ previousLineIndex ] || []; const lastformat = targetFormats[ targetFormats.length - 1 ] || rootFormat; newFormats[ index ] = targetFormats.concat( [ lastformat ], ( newFormats[ index ] || [] ).slice( targetFormats.length ) ); - } else { - newFormats[ index ] = targetFormats.concat( - ( newFormats[ index ] || [] ).slice( targetFormats.length - 1 ) - ); } } diff --git a/packages/rich-text/src/outdent-list-items.js b/packages/rich-text/src/outdent-list-items.js index 78036aae165e1b..3a493caa9b03a8 100644 --- a/packages/rich-text/src/outdent-list-items.js +++ b/packages/rich-text/src/outdent-list-items.js @@ -6,6 +6,7 @@ import { LINE_SEPARATOR } from './special-characters'; import { normaliseFormats } from './normalise-formats'; import { getLineIndex } from './get-line-index'; import { getParentLineIndex } from './get-parent-line-index'; +import { getLastChildIndex } from './get-last-child-index'; /** * Outdents any selected list items if possible. @@ -16,28 +17,37 @@ import { getParentLineIndex } from './get-parent-line-index'; */ export function outdentListItems( value ) { const { text, formats, start, end } = value; - const lineIndex = getLineIndex( value ); - const lineFormats = formats[ lineIndex ]; + const startingLineIndex = getLineIndex( value, start ); - if ( lineFormats === undefined ) { + // Return early if the starting line index cannot be further outdented. + if ( formats[ startingLineIndex ] === undefined ) { return value; } const newFormats = formats.slice( 0 ); - const parentFormats = formats[ getParentLineIndex( value, lineIndex ) ] || []; - - for ( let index = lineIndex; index < end; index++ ) { + const parentFormats = formats[ getParentLineIndex( value, startingLineIndex ) ] || []; + const endingLineIndex = getLineIndex( value, end ); + const lastChildIndex = getLastChildIndex( value, endingLineIndex ); + + // Outdent all list items from the starting line index until the last child + // index of the ending list. All children of the ending list need to be + // outdented, otherwise they'll be orphaned. + for ( let index = startingLineIndex; index <= lastChildIndex; index++ ) { + // Skip indices that are not line separators. if ( text[ index ] !== LINE_SEPARATOR ) { continue; } + // In the case of level 0, the formats at the index are undefined. + const currentFormats = newFormats[ index ] || []; + // Omit the indentation level where the selection starts. newFormats[ index ] = parentFormats.concat( - newFormats[ index ].slice( parentFormats.length + 1 ) + currentFormats.slice( parentFormats.length + 1 ) ); if ( newFormats[ index ].length === 0 ) { - delete newFormats[ lineIndex ]; + delete newFormats[ index ]; } } diff --git a/packages/rich-text/src/test/get-last-child-index.js b/packages/rich-text/src/test/get-last-child-index.js new file mode 100644 index 00000000000000..55c881d356555c --- /dev/null +++ b/packages/rich-text/src/test/get-last-child-index.js @@ -0,0 +1,50 @@ +/** + * External dependencies + */ +import deepFreeze from 'deep-freeze'; + +/** + * Internal dependencies + */ + +import { getLastChildIndex } from '../get-last-child-index'; +import { LINE_SEPARATOR } from '../special-characters'; + +describe( 'outdentListItems', () => { + const ul = { type: 'ul' }; + + it( 'should return undefined if there is only one line', () => { + expect( getLastChildIndex( deepFreeze( { + formats: [ , ], + text: '1', + } ), undefined ) ).toBe( undefined ); + } ); + + it( 'should return the last line if no line is indented', () => { + expect( getLastChildIndex( deepFreeze( { + formats: [ , ], + text: `1${ LINE_SEPARATOR }`, + } ), undefined ) ).toBe( 1 ); + } ); + + it( 'should return the last child index', () => { + expect( getLastChildIndex( deepFreeze( { + formats: [ , [ ul ], , [ ul ], , ], + text: `1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }3`, + } ), undefined ) ).toBe( 3 ); + } ); + + it( 'should return the last child index by sibling', () => { + expect( getLastChildIndex( deepFreeze( { + formats: [ , [ ul ], , [ ul ], , ], + text: `1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }3`, + } ), 1 ) ).toBe( 3 ); + } ); + + it( 'should return the last child index (with further lower indented items)', () => { + expect( getLastChildIndex( deepFreeze( { + formats: [ , [ ul ], , , , ], + text: `1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }3`, + } ), 1 ) ).toBe( 1 ); + } ); +} ); diff --git a/packages/rich-text/src/test/get-parent-line-index.js b/packages/rich-text/src/test/get-parent-line-index.js new file mode 100644 index 00000000000000..4e6a75ffd0a6e6 --- /dev/null +++ b/packages/rich-text/src/test/get-parent-line-index.js @@ -0,0 +1,43 @@ +/** + * External dependencies + */ +import deepFreeze from 'deep-freeze'; + +/** + * Internal dependencies + */ + +import { getParentLineIndex } from '../get-parent-line-index'; +import { LINE_SEPARATOR } from '../special-characters'; + +describe( 'getParentLineIndex', () => { + const ul = { type: 'ul' }; + + it( 'should return undefined if there is only one line', () => { + expect( getParentLineIndex( deepFreeze( { + formats: [ , ], + text: '1', + } ), undefined ) ).toBe( undefined ); + } ); + + it( 'should return undefined if the list is part of the first root list child', () => { + expect( getParentLineIndex( deepFreeze( { + formats: [ , ], + text: `1${ LINE_SEPARATOR }2`, + } ), 2 ) ).toBe( undefined ); + } ); + + it( 'should return the line index of the parent list (1)', () => { + expect( getParentLineIndex( deepFreeze( { + formats: [ , , , [ ul ], , ], + text: `1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }3`, + } ), 3 ) ).toBe( 1 ); + } ); + + it( 'should return the line index of the parent list (2)', () => { + expect( getParentLineIndex( deepFreeze( { + formats: [ , [ ul ], , [ ul, ul ], , [ ul ], , ], + text: `1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }3${ LINE_SEPARATOR }4`, + } ), 5 ) ).toBe( undefined ); + } ); +} ); diff --git a/packages/rich-text/src/test/indent-list-items.js b/packages/rich-text/src/test/indent-list-items.js index c55d5063d21a8b..e7f631e5fa8f94 100644 --- a/packages/rich-text/src/test/indent-list-items.js +++ b/packages/rich-text/src/test/indent-list-items.js @@ -130,4 +130,48 @@ describe( 'indentListItems', () => { expect( result ).not.toBe( record ); expect( getSparseArrayLength( result.formats ) ).toBe( 2 ); } ); + + it( 'should indent one level at a time', () => { + // As we're testing list formats, the text should remain the same. + const text = `1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }3${ LINE_SEPARATOR }4`; + const record = { + formats: [ , [ ul ], , [ ul, ul ], , , , ], + text, + start: 6, + end: 6, + }; + + const result1 = indentListItems( deepFreeze( record ), ul ); + + expect( result1 ).not.toBe( record ); + expect( getSparseArrayLength( result1.formats ) ).toBe( 3 ); + expect( result1 ).toEqual( { + formats: [ , [ ul ], , [ ul, ul ], , [ ul ], , ], + text, + start: 6, + end: 6, + } ); + + const result2 = indentListItems( deepFreeze( result1 ), ul ); + + expect( result2 ).not.toBe( result1 ); + expect( getSparseArrayLength( result2.formats ) ).toBe( 3 ); + expect( result2 ).toEqual( { + formats: [ , [ ul ], , [ ul, ul ], , [ ul, ul ], , ], + text, + start: 6, + end: 6, + } ); + + const result3 = indentListItems( deepFreeze( result2 ), ul ); + + expect( result3 ).not.toBe( result2 ); + expect( getSparseArrayLength( result3.formats ) ).toBe( 3 ); + expect( result3 ).toEqual( { + formats: [ , [ ul ], , [ ul, ul ], , [ ul, ul, ul ], , ], + text, + start: 6, + end: 6, + } ); + } ); } ); diff --git a/packages/rich-text/src/test/outdent-list-items.js b/packages/rich-text/src/test/outdent-list-items.js index d321a4c02ffe83..c2bf8b30e4766b 100644 --- a/packages/rich-text/src/test/outdent-list-items.js +++ b/packages/rich-text/src/test/outdent-list-items.js @@ -21,7 +21,7 @@ describe( 'outdentListItems', () => { start: 1, end: 1, }; - const result = outdentListItems( deepFreeze( record ), ul ); + const result = outdentListItems( deepFreeze( record ) ); expect( result ).toEqual( record ); expect( result ).toBe( record ); @@ -43,7 +43,7 @@ describe( 'outdentListItems', () => { start: 2, end: 2, }; - const result = outdentListItems( deepFreeze( record ), ul ); + const result = outdentListItems( deepFreeze( record ) ); expect( result ).toEqual( expected ); expect( result ).not.toBe( record ); @@ -65,7 +65,7 @@ describe( 'outdentListItems', () => { start: 5, end: 5, }; - const result = outdentListItems( deepFreeze( record ), ul ); + const result = outdentListItems( deepFreeze( record ) ); expect( result ).toEqual( expected ); expect( result ).not.toBe( record ); @@ -87,10 +87,76 @@ describe( 'outdentListItems', () => { start: 2, end: 5, }; - const result = outdentListItems( deepFreeze( record ), ul ); + const result = outdentListItems( deepFreeze( record ) ); expect( result ).toEqual( expected ); expect( result ).not.toBe( record ); expect( getSparseArrayLength( result.formats ) ).toBe( 1 ); } ); + + it( 'should outdent list item with children', () => { + // As we're testing list formats, the text should remain the same. + const text = `1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }3${ LINE_SEPARATOR }4`; + const record = { + formats: [ , [ ul ], , [ ul, ul ], , [ ul, ul ], , ], + text, + start: 2, + end: 2, + }; + const expected = { + formats: [ , , , [ ul ], , [ ul ], , ], + text, + start: 2, + end: 2, + }; + const result = outdentListItems( deepFreeze( record ) ); + + expect( result ).toEqual( expected ); + expect( result ).not.toBe( record ); + expect( getSparseArrayLength( result.formats ) ).toBe( 2 ); + } ); + + it( 'should outdent list based on parent list', () => { + // As we're testing list formats, the text should remain the same. + const text = `1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }3${ LINE_SEPARATOR }4`; + const record = { + formats: [ , [ ul ], , [ ul, ul ], , [ ul ], , ], + text, + start: 6, + end: 6, + }; + const expected = { + formats: [ , [ ul ], , [ ul, ul ], , , , ], + text, + start: 6, + end: 6, + }; + const result = outdentListItems( deepFreeze( record ) ); + + expect( result ).toEqual( expected ); + expect( result ).not.toBe( record ); + expect( getSparseArrayLength( result.formats ) ).toBe( 2 ); + } ); + + it( 'should outdent when a selected item is at level 0', () => { + // As we're testing list formats, the text should remain the same. + const text = `1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }3`; + const record = { + formats: [ , [ ul ], , , , ], + text, + start: 2, + end: 5, + }; + const expected = { + formats: [ , , , , , ], + text, + start: 2, + end: 5, + }; + const result = outdentListItems( deepFreeze( record ) ); + + expect( result ).toEqual( expected ); + expect( result ).not.toBe( record ); + expect( getSparseArrayLength( result.formats ) ).toBe( 0 ); + } ); } ); diff --git a/phpunit/class-polyfill-test.php b/phpunit/class-polyfill-test.php deleted file mode 100644 index c64878bd13fb24..00000000000000 --- a/phpunit/class-polyfill-test.php +++ /dev/null @@ -1,54 +0,0 @@ -<?php -/** - * Test gutenberg_get_script_polyfill() - * - * @package Gutenberg - */ - -class Polyfill_Test extends WP_UnitTestCase { - var $old_wp_scripts; - - function setUp() { - parent::setUp(); - $this->old_wp_scripts = isset( $GLOBALS['wp_scripts'] ) ? $GLOBALS['wp_scripts'] : null; - remove_action( 'wp_default_scripts', 'wp_default_scripts' ); - - $GLOBALS['wp_scripts'] = new WP_Scripts(); - - $GLOBALS['wp_scripts']->default_version = get_bloginfo( 'version' ); - - gutenberg_register_scripts_and_styles(); - } - - public function tearDown() { - $GLOBALS['wp_scripts'] = $this->old_wp_scripts; - add_action( 'wp_default_scripts', 'wp_default_scripts' ); - parent::tearDown(); - } - - function test_gutenberg_get_script_polyfill_ignores_missing_handle() { - $polyfill = gutenberg_get_script_polyfill( - array( - '\'Promise\' in window' => 'promise', - ) - ); - - $this->assertEquals( '', $polyfill ); - } - - function test_gutenberg_get_script_polyfill_returns_inline_script() { - wp_register_script( 'promise', 'https://unpkg.com/promise-polyfill/promise.js' ); - - $polyfill = gutenberg_get_script_polyfill( - array( - '\'Promise\' in window' => 'promise', - ) - ); - - $this->assertEquals( - '( \'Promise\' in window ) || document.write( \'<script src="https://unpkg.com/promise-polyfill/promise.js"></scr\' + \'ipt>\' );', - $polyfill - ); - } - -} diff --git a/phpunit/class-vendor-script-filename-test.php b/phpunit/class-vendor-script-filename-test.php index f053df2011159b..0ee7a7b78b83d4 100644 --- a/phpunit/class-vendor-script-filename-test.php +++ b/phpunit/class-vendor-script-filename-test.php @@ -19,16 +19,6 @@ function vendor_script_filename_cases() { 'https://unpkg.com/react-dom@16.6.3/umd/react-dom.development.js', 'react-dom-handle.HASH.js', ), - array( - 'tinymce-handle', - 'https://fiddle.azurewebsites.net/tinymce/nightly/tinymce.js', - 'tinymce-handle.HASH.js', - ), - array( - 'tinymce-plugin-handle', - 'https://fiddle.azurewebsites.net/tinymce/nightly/plugins/lists/plugin.js', - 'tinymce-plugin-lists.HASH.js', - ), // Production mode scripts. array( 'react-handle', @@ -40,16 +30,6 @@ function vendor_script_filename_cases() { 'https://unpkg.com/react-dom@16.6.3/umd/react-dom.production.min.js', 'react-dom-handle.min.HASH.js', ), - array( - 'tinymce-handle', - 'https://fiddle.azurewebsites.net/tinymce/nightly/tinymce.min.js', - 'tinymce-handle.min.HASH.js', - ), - array( - 'tinymce-plugin-handle', - 'https://fiddle.azurewebsites.net/tinymce/nightly/plugins/lists/plugin.min.js', - 'tinymce-plugin-lists.min.HASH.js', - ), // Other cases. array( 'something-handle',