From 66250c085f96c4356710e7339337d4e7f9b10278 Mon Sep 17 00:00:00 2001 From: Gary Pendergast Date: Tue, 30 Oct 2018 12:33:14 +1100 Subject: [PATCH 01/98] chore(release): update changelog files --- packages/block-library/CHANGELOG.md | 9 +++++++++ packages/edit-post/CHANGELOG.md | 2 ++ packages/editor/CHANGELOG.md | 7 +++++++ packages/format-library/CHANGELOG.md | 5 +++++ packages/rich-text/CHANGELOG.md | 6 ++++++ 5 files changed, 29 insertions(+) create mode 100644 packages/format-library/CHANGELOG.md diff --git a/packages/block-library/CHANGELOG.md b/packages/block-library/CHANGELOG.md index a28fed6c42a210..f3a7e80e7e4651 100644 --- a/packages/block-library/CHANGELOG.md +++ b/packages/block-library/CHANGELOG.md @@ -1,3 +1,12 @@ +## 2.1.6 (2018-10-30) + +### Bug Fixes + +- Classic Block: Prevent theme styles from italicising the italicise button. +- Gallery Block: Fix the "Remove Image" button appearing blank when an image is focussed. + +## 2.1.5 (2018-10-29) + ## 2.1.4 (2018-10-22) ### Bug Fixes diff --git a/packages/edit-post/CHANGELOG.md b/packages/edit-post/CHANGELOG.md index 74580362f45c53..105e2b38412604 100644 --- a/packages/edit-post/CHANGELOG.md +++ b/packages/edit-post/CHANGELOG.md @@ -1,3 +1,5 @@ +## 2.0.1 (2018-10-30) + ## 2.0.0 (2018-10-29) ### Breaking Changes diff --git a/packages/editor/CHANGELOG.md b/packages/editor/CHANGELOG.md index 80d4585e69ccd2..70cb95e06f342a 100644 --- a/packages/editor/CHANGELOG.md +++ b/packages/editor/CHANGELOG.md @@ -1,3 +1,10 @@ +## 6.0.1 (2018-10-30) + +### Bug Fixes + +- Tweak the vanilla style sheet for consistency. +- Fix the "Copy Post Text" button not copying the post text. + ## 6.0.0 (2018-10-29) ### Breaking Changes diff --git a/packages/format-library/CHANGELOG.md b/packages/format-library/CHANGELOG.md new file mode 100644 index 00000000000000..ab080e1495e9b6 --- /dev/null +++ b/packages/format-library/CHANGELOG.md @@ -0,0 +1,5 @@ +## 1.0.1 (2018-10-30) + +## 1.0.0 (2018-10-29) + +Initial release. diff --git a/packages/rich-text/CHANGELOG.md b/packages/rich-text/CHANGELOG.md index e780b87752277f..3efab18e80d667 100644 --- a/packages/rich-text/CHANGELOG.md +++ b/packages/rich-text/CHANGELOG.md @@ -1,3 +1,9 @@ +## 2.0.0 (2018-10-30) + +- Remove `@wordpress/blocks` as a dependency. + +## 1.0.2 (2018-10-29) + ## 1.0.1 (2018-10-19) ## 1.0.0 (2018-10-18) From 43dacc7d826cdc4ffdb6ee8189e56911a167f8bf Mon Sep 17 00:00:00 2001 From: Gary Pendergast Date: Tue, 30 Oct 2018 12:35:48 +1100 Subject: [PATCH 02/98] chore(release): update additional changelog file --- packages/blocks/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/blocks/CHANGELOG.md b/packages/blocks/CHANGELOG.md index 38dfaa12859d98..7db53884fe79e3 100644 --- a/packages/blocks/CHANGELOG.md +++ b/packages/blocks/CHANGELOG.md @@ -1,4 +1,4 @@ -## 5.1.0 (Unreleased) +## 5.1.0 (2018-10-30) ### New feature From 40fcc82c9bb4fa27ff296e4ae87ea5f95e00642d Mon Sep 17 00:00:00 2001 From: Gary Pendergast Date: Tue, 30 Oct 2018 12:53:16 +1100 Subject: [PATCH 03/98] chore(release): publish - @wordpress/block-library@2.1.6 - @wordpress/blocks@5.1.0 - @wordpress/edit-post@2.0.1 - @wordpress/editor@6.0.1 - @wordpress/format-library@1.0.1 - @wordpress/rich-text@2.0.0 --- packages/block-library/package.json | 2 +- packages/blocks/package.json | 2 +- packages/edit-post/package.json | 2 +- packages/editor/package.json | 2 +- packages/format-library/package.json | 2 +- packages/rich-text/package.json | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/block-library/package.json b/packages/block-library/package.json index dd63da8bb551bc..99378cfd77f3fb 100644 --- a/packages/block-library/package.json +++ b/packages/block-library/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-library", - "version": "2.1.5", + "version": "2.1.6", "description": "Block library for the WordPress editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/blocks/package.json b/packages/blocks/package.json index 53cc985faeb720..395a73310bada4 100644 --- a/packages/blocks/package.json +++ b/packages/blocks/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/blocks", - "version": "5.0.0", + "version": "5.1.0", "description": "Block API for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/edit-post/package.json b/packages/edit-post/package.json index b50049a7ca5dc4..3ae06ca6baa103 100644 --- a/packages/edit-post/package.json +++ b/packages/edit-post/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/edit-post", - "version": "2.0.0", + "version": "2.0.1", "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 f14dcc20cb9536..2d2253cd3d3607 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/editor", - "version": "6.0.0", + "version": "6.0.1", "description": "Building blocks for WordPress editors.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/format-library/package.json b/packages/format-library/package.json index 214fd4cafb79e4..2bbf6563d7de38 100644 --- a/packages/format-library/package.json +++ b/packages/format-library/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/format-library", - "version": "1.0.0", + "version": "1.0.1", "description": "Format library for the WordPress editor.", "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 d2633515d3f00a..f4940b12f88c42 100644 --- a/packages/rich-text/package.json +++ b/packages/rich-text/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/rich-text", - "version": "1.0.2", + "version": "2.0.0", "description": "Rich text value and manipulation API.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", From c6a1b18490603d5609ce1780ea13cc82fbbc0f06 Mon Sep 17 00:00:00 2001 From: Andrea Fercia Date: Tue, 30 Oct 2018 08:50:05 +0100 Subject: [PATCH 04/98] Fix blocks navigation menu SVG icon size. (#11153) --- packages/components/src/icon-button/index.js | 6 +-- .../components/src/icon-button/test/index.js | 2 +- .../test/__snapshots__/index.js.snap | 45 ++++++++++--------- .../components/block-navigation/dropdown.js | 9 ++-- 4 files changed, 34 insertions(+), 28 deletions(-) diff --git a/packages/components/src/icon-button/index.js b/packages/components/src/icon-button/index.js index 745152409ccd46..eec8ecd913a51f 100644 --- a/packages/components/src/icon-button/index.js +++ b/packages/components/src/icon-button/index.js @@ -2,7 +2,7 @@ * External dependencies */ import classnames from 'classnames'; -import { isString, isArray } from 'lodash'; +import { isArray } from 'lodash'; /** * WordPress dependencies @@ -14,7 +14,7 @@ import { Component } from '@wordpress/element'; */ import Tooltip from '../tooltip'; import Button from '../button'; -import Dashicon from '../dashicon'; +import Icon from '../icon'; // This is intentionally a Component class, not a function component because it // is common to apply a ref to the button element (only supported in class) @@ -42,7 +42,7 @@ class IconButton extends Component { let element = ( ); diff --git a/packages/components/src/icon-button/test/index.js b/packages/components/src/icon-button/test/index.js index d5b67eec7930f6..b93f46a3c5bf66 100644 --- a/packages/components/src/icon-button/test/index.js +++ b/packages/components/src/icon-button/test/index.js @@ -20,7 +20,7 @@ describe( 'IconButton', () => { it( 'should render a Dashicon component matching the wordpress icon', () => { const iconButton = shallow( ); - expect( iconButton.find( 'Dashicon' ).shallow().hasClass( 'dashicons-wordpress' ) ).toBe( true ); + expect( iconButton.find( 'Icon' ).prop( 'icon' ) ).toBe( 'wordpress' ); } ); it( 'should render child elements when passed as children', () => { diff --git a/packages/edit-post/src/components/header/more-menu/test/__snapshots__/index.js.snap b/packages/edit-post/src/components/header/more-menu/test/__snapshots__/index.js.snap index 244a185376638c..425ee6a645971d 100644 --- a/packages/edit-post/src/components/header/more-menu/test/__snapshots__/index.js.snap +++ b/packages/edit-post/src/components/header/more-menu/test/__snapshots__/index.js.snap @@ -42,22 +42,16 @@ exports[`MoreMenu should match snapshot 1`] = ` onMouseLeave={[Function]} type="button" > - - - - - + > + + + + + + diff --git a/packages/editor/src/components/block-navigation/dropdown.js b/packages/editor/src/components/block-navigation/dropdown.js index 3fded1f9591efa..20910e4eebb042 100644 --- a/packages/editor/src/components/block-navigation/dropdown.js +++ b/packages/editor/src/components/block-navigation/dropdown.js @@ -4,15 +4,15 @@ import { Fragment } from '@wordpress/element'; import { IconButton, Dropdown, SVG, Path, KeyboardShortcuts } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; -import { rawShortcut } from '@wordpress/keycodes'; +import { rawShortcut, displayShortcut } from '@wordpress/keycodes'; /** * Internal dependencies */ import BlockNavigation from './'; -const menuIcon = ( - +const MenuIcon = ( + ); @@ -29,11 +29,12 @@ function BlockNavigationDropdown() { } } /> ) } From 01b4860a7730dab8db545e933622df3fa4b8d37e Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Tue, 30 Oct 2018 18:54:33 +1100 Subject: [PATCH 05/98] Fix converting a reusable block with nested blocks into a static block (#11188) * Fix converting a reusable block with nested blocks into a static block The CONVERT_BLOCK_TO_STATIC effect needs to handle the case where the block being converted has inner blocks. * Clone referenced block when converting a reusable block to static By using cloneBlock(), we correctly make a copy of the innerBlocks which means that updates to one block don't affect another. --- .../src/store/effects/reusable-blocks.js | 2 +- .../src/store/effects/test/reusable-blocks.js | 40 +++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/packages/editor/src/store/effects/reusable-blocks.js b/packages/editor/src/store/effects/reusable-blocks.js index 861c1821498ce9..7d9795d64d34d6 100644 --- a/packages/editor/src/store/effects/reusable-blocks.js +++ b/packages/editor/src/store/effects/reusable-blocks.js @@ -237,7 +237,7 @@ export const convertBlockToStatic = ( action, store ) => { if ( referencedBlock.name === 'core/template' ) { newBlocks = referencedBlock.innerBlocks.map( ( innerBlock ) => cloneBlock( innerBlock ) ); } else { - newBlocks = [ createBlock( referencedBlock.name, referencedBlock.attributes ) ]; + newBlocks = [ cloneBlock( referencedBlock ) ]; } store.dispatch( replaceBlocks( oldBlock.clientId, newBlocks ) ); }; diff --git a/packages/editor/src/store/effects/test/reusable-blocks.js b/packages/editor/src/store/effects/test/reusable-blocks.js index 3d6c8cb3692b20..4aca3995d165a9 100644 --- a/packages/editor/src/store/effects/test/reusable-blocks.js +++ b/packages/editor/src/store/effects/test/reusable-blocks.js @@ -404,6 +404,46 @@ describe( 'reusable blocks effects', () => { time: expect.any( Number ), } ); } ); + + it( 'should convert a reusable block with nested blocks into a static block', () => { + const associatedBlock = createBlock( 'core/block', { ref: 123 } ); + const reusableBlock = { id: 123, title: 'My cool block' }; + const parsedBlock = createBlock( 'core/test-block', { name: 'Big Bird' }, [ + createBlock( 'core/test-block', { name: 'Oscar the Grouch' } ), + createBlock( 'core/test-block', { name: 'Cookie Monster' } ), + ] ); + + const state = reduce( [ + resetBlocks( [ associatedBlock ] ), + receiveReusableBlocksAction( [ { reusableBlock, parsedBlock } ] ), + receiveBlocks( [ parsedBlock ] ), + ], reducer, undefined ); + + const dispatch = jest.fn(); + const store = { getState: () => state, dispatch }; + + convertBlockToStatic( convertBlockToStaticAction( associatedBlock.clientId ), store ); + + expect( dispatch ).toHaveBeenCalledWith( { + type: 'REPLACE_BLOCKS', + clientIds: [ associatedBlock.clientId ], + blocks: [ + expect.objectContaining( { + name: 'core/test-block', + attributes: { name: 'Big Bird' }, + innerBlocks: [ + expect.objectContaining( { + attributes: { name: 'Oscar the Grouch' }, + } ), + expect.objectContaining( { + attributes: { name: 'Cookie Monster' }, + } ), + ], + } ), + ], + time: expect.any( Number ), + } ); + } ); } ); describe( 'convertBlockToReusable', () => { From d44a1bf5dc99a201a1e5c1a8988327ca91c66ac7 Mon Sep 17 00:00:00 2001 From: Dominik Schilling Date: Tue, 30 Oct 2018 09:02:48 +0100 Subject: [PATCH 06/98] Add aria-label to describe action of featured image update button (#10869) * Add aria-label to describe action of featured image update button * Drop the 'Click to' prefix --- packages/editor/src/components/post-featured-image/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/editor/src/components/post-featured-image/index.js b/packages/editor/src/components/post-featured-image/index.js index b09c432a1bda04..67f02c9b7a1d15 100644 --- a/packages/editor/src/components/post-featured-image/index.js +++ b/packages/editor/src/components/post-featured-image/index.js @@ -52,13 +52,13 @@ function PostFeaturedImage( { currentPostId, featuredImageId, onUpdateImage, onR allowedTypes={ ALLOWED_MEDIA_TYPES } modalClass="editor-post-featured-image__media-modal" render={ ( { open } ) => ( - { + } { ! media && } From ca4e9fdea00fa282efab9f04ff756c767f62e469 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Tue, 30 Oct 2018 16:14:13 +0800 Subject: [PATCH 07/98] Add Alt + F10 (navigate to the nearest toolbar) to the shortcut docs and modal (#11096) --- docs/reference/faq.md | 5 +++++ .../src/components/keyboard-shortcut-help-modal/config.js | 5 +++++ .../test/__snapshots__/index.js.snap | 8 ++++++++ packages/keycodes/src/index.js | 1 + 4 files changed, 19 insertions(+) diff --git a/docs/reference/faq.md b/docs/reference/faq.md index bfd1228243d2d9..449b028a87d86a 100644 --- a/docs/reference/faq.md +++ b/docs/reference/faq.md @@ -110,6 +110,11 @@ This is the canonical list of keyboard shortcuts: Shift+Alt+P P + + Navigate to the nearest toolbar. + Alt+F10 + F10 + Switch between Visual Editor and Code Editor. Ctrl+Shift+Alt+M diff --git a/packages/edit-post/src/components/keyboard-shortcut-help-modal/config.js b/packages/edit-post/src/components/keyboard-shortcut-help-modal/config.js index 5fa9db21324231..f18c1b06293931 100644 --- a/packages/edit-post/src/components/keyboard-shortcut-help-modal/config.js +++ b/packages/edit-post/src/components/keyboard-shortcut-help-modal/config.js @@ -16,6 +16,7 @@ const { // Ctrl+Alt+ on a mac, Shift+Alt+ elsewhere. access, ctrl, + alt, ctrlShift, shiftAlt, } = displayShortcutList; @@ -66,6 +67,10 @@ const globalShortcuts = { keyCombination: shiftAlt( 'p' ), description: __( 'Navigate to the previous part of the editor (alternative).' ), }, + { + keyCombination: alt( 'F10' ), + description: __( 'Navigate to the nearest toolbar.' ), + }, { keyCombination: secondary( 'm' ), description: __( 'Switch between Visual Editor and Code Editor.' ), diff --git a/packages/edit-post/src/components/keyboard-shortcut-help-modal/test/__snapshots__/index.js.snap b/packages/edit-post/src/components/keyboard-shortcut-help-modal/test/__snapshots__/index.js.snap index 085e19e99465e7..80b31b22c6ae04 100644 --- a/packages/edit-post/src/components/keyboard-shortcut-help-modal/test/__snapshots__/index.js.snap +++ b/packages/edit-post/src/components/keyboard-shortcut-help-modal/test/__snapshots__/index.js.snap @@ -123,6 +123,14 @@ exports[`KeyboardShortcutHelpModal should match snapshot when the modal is activ "P", ], }, + Object { + "description": "Navigate to the nearest toolbar.", + "keyCombination": Array [ + "Alt", + "+", + "F10", + ], + }, Object { "description": "Switch between Visual Editor and Code Editor.", "keyCombination": Array [ diff --git a/packages/keycodes/src/index.js b/packages/keycodes/src/index.js index b9479cb7ff6781..611fc58c62279e 100644 --- a/packages/keycodes/src/index.js +++ b/packages/keycodes/src/index.js @@ -50,6 +50,7 @@ const modifiers = { secondary: ( _isApple ) => _isApple() ? [ SHIFT, ALT, COMMAND ] : [ CTRL, SHIFT, ALT ], access: ( _isApple ) => _isApple() ? [ CTRL, ALT ] : [ SHIFT, ALT ], ctrl: () => [ CTRL ], + alt: () => [ ALT ], ctrlShift: () => [ CTRL, SHIFT ], shift: () => [ SHIFT ], shiftAlt: () => [ SHIFT, ALT ], From afe5b456ce1c8407f95454c6b1c4c4a00f8e2d6a Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Tue, 30 Oct 2018 18:06:44 +0800 Subject: [PATCH 08/98] Reduce noisiness of ENTER_FORMATTED_TEXT and EXIT_FORMATTED_TEXT by only triggering them when they cause a change in state (#11235) --- .../editor/src/components/rich-text/index.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/editor/src/components/rich-text/index.js b/packages/editor/src/components/rich-text/index.js index 49574450ccb8c4..6b774dc0e1f925 100644 --- a/packages/editor/src/components/rich-text/index.js +++ b/packages/editor/src/components/rich-text/index.js @@ -426,13 +426,14 @@ export class RichText extends Component { const { start, end, formats } = this.createRecord(); - if ( formats[ start ] ) { - this.props.onEnterFormattedText(); - } else { - this.props.onExitFormattedText(); - } - if ( start !== this.state.start || end !== this.state.end ) { + const isCaretWithinFormattedText = this.props.isCaretWithinFormattedText; + if ( ! isCaretWithinFormattedText && formats[ start ] ) { + this.props.onEnterFormattedText(); + } else if ( isCaretWithinFormattedText && ! formats[ start ] ) { + this.props.onExitFormattedText(); + } + this.setState( { start, end } ); } } @@ -956,11 +957,12 @@ const RichTextContainer = compose( [ } ), withSelect( ( select ) => { const { isViewportMatch } = select( 'core/viewport' ); - const { canUserUseUnfilteredHTML } = select( 'core/editor' ); + const { canUserUseUnfilteredHTML, isCaretWithinFormattedText } = select( 'core/editor' ); return { isViewportSmall: isViewportMatch( '< small' ), canUserUseUnfilteredHTML: canUserUseUnfilteredHTML(), + isCaretWithinFormattedText: isCaretWithinFormattedText(), }; } ), withDispatch( ( dispatch ) => { From 844f0ea2a333249cd367dd22570b5815c8aa74c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20Van=C2=A0Dorpe?= Date: Tue, 30 Oct 2018 11:14:56 +0100 Subject: [PATCH 09/98] Fix rich text value for nested lists (#10799) * Fix rich text value for nested lists * Add inline comments * Ignore formats on line separator * Add e2e test * Fix list placeholder when it has sublist * Address feedback * Fix failing e2e test * Handle delete and backspace key inside multiline instances * Fix selection extraction if container is element node * Move removeNextLineSeparator, removePreviousLineSeparator * Simplify multiline format to only have multiline specific formats on the separator * Add unit test for multiline manipulations * Change toHTMLString signature * Add comment for e2e mousemove * Remove list wrapper references to tag from rich text value * Adjust list transforms to handle nested items (also broken in master) * Fix after rebase * Fix selection accumulation after ba37dca7d05767e0b89e4dac15b31f3e4c60cb97 * Address feedback * Add inline comment for padding lines * Update deprecated uses of toHTMLString --- lib/client-assets.php | 1 + packages/block-library/src/list/index.js | 33 +++- packages/block-library/src/quote/index.js | 18 +- .../editor/src/components/rich-text/index.js | 107 +++++++--- .../src/components/rich-text/index.native.js | 5 +- .../src/components/rich-text/tinymce.js | 7 +- packages/rich-text/README.md | 35 +++- packages/rich-text/package.json | 1 + packages/rich-text/src/char-at.js | 12 ++ packages/rich-text/src/create.js | 183 ++++++++++++------ packages/rich-text/src/get-selection-end.js | 12 ++ packages/rich-text/src/get-selection-start.js | 12 ++ packages/rich-text/src/index.js | 5 + .../rich-text/src/insert-line-separator.js | 39 ++++ packages/rich-text/src/is-empty.js | 8 +- packages/rich-text/src/special-characters.js | 2 + .../src/test/__snapshots__/to-dom.js.snap | 99 +++++++--- packages/rich-text/src/test/create.js | 18 +- packages/rich-text/src/test/helpers/index.js | 115 ++++++++++- .../src/test/insert-line-separator.js | 76 ++++++++ packages/rich-text/src/test/to-dom.js | 16 +- packages/rich-text/src/test/to-html-string.js | 19 +- packages/rich-text/src/to-dom.js | 82 +++++--- packages/rich-text/src/to-html-string.js | 34 +++- packages/rich-text/src/to-tree.js | 139 ++++++++----- .../deprecated-node-matcher.test.js.snap | 2 +- .../blocks/__snapshots__/list.test.js.snap | 16 ++ test/e2e/specs/blocks/list.test.js | 31 +++ 28 files changed, 901 insertions(+), 226 deletions(-) create mode 100644 packages/rich-text/src/char-at.js create mode 100644 packages/rich-text/src/get-selection-end.js create mode 100644 packages/rich-text/src/get-selection-start.js create mode 100644 packages/rich-text/src/insert-line-separator.js create mode 100644 packages/rich-text/src/test/insert-line-separator.js diff --git a/lib/client-assets.php b/lib/client-assets.php index 3c65afc08586bb..c34e03edc7d5f1 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -428,6 +428,7 @@ function gutenberg_register_scripts_and_styles() { 'lodash', 'wp-polyfill', 'wp-data', + 'wp-deprecated', 'wp-escape-html', ), filemtime( gutenberg_dir_path() . 'build/rich-text/index.js' ), diff --git a/packages/block-library/src/list/index.js b/packages/block-library/src/list/index.js index 21b40b4201392f..c5683dc5fe82db 100644 --- a/packages/block-library/src/list/index.js +++ b/packages/block-library/src/list/index.js @@ -18,7 +18,7 @@ import { BlockControls, RichText, } from '@wordpress/editor'; -import { replace, join, split, create, toHTMLString } from '@wordpress/rich-text'; +import { replace, join, split, create, toHTMLString, LINE_SEPARATOR } from '@wordpress/rich-text'; import { G, Path, SVG } from '@wordpress/components'; const listContentSchema = { @@ -77,9 +77,12 @@ export const settings = { blocks: [ 'core/paragraph' ], transform: ( blockAttributes ) => { return createBlock( 'core/list', { - values: toHTMLString( join( blockAttributes.map( ( { content } ) => - replace( create( { html: content } ), /\n/g, '\u2028' ) - ), '\u2028' ), 'li' ), + values: toHTMLString( { + value: join( blockAttributes.map( ( { content } ) => + replace( create( { html: content } ), /\n/g, LINE_SEPARATOR ) + ), LINE_SEPARATOR ), + multilineTag: 'li', + } ), } ); }, }, @@ -88,7 +91,10 @@ export const settings = { blocks: [ 'core/quote' ], transform: ( { value } ) => { return createBlock( 'core/list', { - values: toHTMLString( create( { html: value, multilineTag: 'p' } ), 'li' ), + values: toHTMLString( { + value: create( { html: value, multilineTag: 'p' } ), + multilineTag: 'li', + } ), } ); }, }, @@ -134,10 +140,14 @@ export const settings = { type: 'block', blocks: [ 'core/paragraph' ], transform: ( { values } ) => - split( create( { html: values, multilineTag: 'li' } ), '\u2028' ) + split( create( { + html: values, + multilineTag: 'li', + multilineWrapperTags: [ 'ul', 'ol' ], + } ), LINE_SEPARATOR ) .map( ( piece ) => createBlock( 'core/paragraph', { - content: toHTMLString( piece ), + content: toHTMLString( { value: piece } ), } ) ), }, @@ -146,7 +156,14 @@ export const settings = { blocks: [ 'core/quote' ], transform: ( { values } ) => { return createBlock( 'core/quote', { - value: toHTMLString( create( { html: values, multilineTag: 'li' } ), 'p' ), + value: toHTMLString( { + value: create( { + html: values, + multilineTag: 'li', + multilineWrapperTags: [ 'ul', 'ol' ], + } ), + multilineTag: 'p', + } ), } ); }, }, diff --git a/packages/block-library/src/quote/index.js b/packages/block-library/src/quote/index.js index c04cd2bcef5b27..bd226c2bc9a7f3 100644 --- a/packages/block-library/src/quote/index.js +++ b/packages/block-library/src/quote/index.js @@ -60,9 +60,12 @@ export const settings = { blocks: [ 'core/paragraph' ], transform: ( attributes ) => { return createBlock( 'core/quote', { - value: toHTMLString( join( attributes.map( ( { content } ) => - create( { html: content } ) - ), '\u2028' ), 'p' ), + value: toHTMLString( { + value: join( attributes.map( ( { content } ) => + create( { html: content } ) + ), '\u2028' ), + multilineTag: 'p', + } ), } ); }, }, @@ -117,7 +120,7 @@ export const settings = { ...split( create( { html: value, multilineTag: 'p' } ), '\u2028' ) .map( ( piece ) => createBlock( 'core/paragraph', { - content: toHTMLString( piece ), + content: toHTMLString( { value: piece } ), } ) ) ); @@ -157,12 +160,15 @@ export const settings = { return [ createBlock( 'core/heading', { - content: toHTMLString( pieces[ 0 ] ), + content: toHTMLString( { value: pieces[ 0 ] } ), } ), createBlock( 'core/quote', { ...attrs, citation, - value: toHTMLString( quotePieces.length ? join( pieces.slice( 1 ), '\u2028' ) : create(), 'p' ), + value: toHTMLString( { + value: quotePieces.length ? join( pieces.slice( 1 ), '\u2028' ) : create(), + multilineTag: 'p', + } ), } ), ]; }, diff --git a/packages/editor/src/components/rich-text/index.js b/packages/editor/src/components/rich-text/index.js index 6b774dc0e1f925..2548240a6671f9 100644 --- a/packages/editor/src/components/rich-text/index.js +++ b/packages/editor/src/components/rich-text/index.js @@ -36,8 +36,15 @@ import { toHTMLString, getTextContent, insert, + insertLineSeparator, isEmptyLine, unstableToDom, + getSelectionStart, + getSelectionEnd, + charAt, + LINE_SEPARATOR, + remove, + isCollapsed, } from '@wordpress/rich-text'; import { decodeEntities } from '@wordpress/html-entities'; @@ -77,6 +84,10 @@ export class RichText extends Component { this.multilineTag = multiline === true ? 'p' : multiline; } + if ( this.multilineTag === 'li' ) { + this.multilineWrapperTags = [ 'ul', 'ol' ]; + } + this.onInit = this.onInit.bind( this ); this.getSettings = this.getSettings.bind( this ); this.onSetup = this.onSetup.bind( this ); @@ -240,6 +251,7 @@ export class RichText extends Component { element: this.editableRef, range, multilineTag: this.multilineTag, + multilineWrapperTags: this.multilineWrapperTags, removeNode: ( node ) => node.getAttribute( 'data-mce-bogus' ) === 'all', unwrapNode: ( node ) => !! node.getAttribute( 'data-mce-bogus' ), removeAttribute: ( attribute ) => attribute.indexOf( 'data-mce-' ) === 0, @@ -248,7 +260,17 @@ export class RichText extends Component { } applyRecord( record ) { - apply( record, this.editableRef, this.multilineTag ); + apply( { + value: record, + current: this.editableRef, + multilineTag: this.multilineTag, + multilineWrapperTags: this.multilineWrapperTags, + createLinePadding( doc ) { + const element = doc.createElement( 'br' ); + element.setAttribute( 'data-mce-bogus', '1' ); + return element; + }, + } ); } isEmpty() { @@ -498,10 +520,8 @@ export class RichText extends Component { const { keyCode } = event; const isReverse = keyCode === BACKSPACE; - const { isCollapsed } = getSelection(); - // Only process delete if the key press occurs at uncollapsed edge. - if ( ! isCollapsed ) { + if ( ! getSelection().isCollapsed ) { return; } @@ -589,32 +609,68 @@ export class RichText extends Component { onKeyDown( event ) { const { keyCode } = event; - const isDelete = keyCode === DELETE || keyCode === BACKSPACE; - if ( isDelete ) { - this.onDeleteKeyDown( event ); - } - const isHorizontalNavigation = keyCode === LEFT || keyCode === RIGHT; if ( isHorizontalNavigation ) { this.onHorizontalNavigationKeyDown( event ); } + if ( keyCode === DELETE || keyCode === BACKSPACE ) { + if ( this.multilineTag ) { + const value = this.createRecord(); + const start = getSelectionStart( value ); + const end = getSelectionEnd( value ); + + let newValue; + + if ( keyCode === BACKSPACE ) { + if ( charAt( value, start - 1 ) === LINE_SEPARATOR ) { + newValue = remove( + value, + // Only remove the line if the selection is + // collapsed. + isCollapsed( value ) ? start - 1 : start, + end + ); + } + } else if ( charAt( value, end ) === LINE_SEPARATOR ) { + newValue = remove( + value, + start, + // Only remove the line if the selection is collapsed. + isCollapsed( value ) ? end + 1 : end, + ); + } + + if ( newValue ) { + this.onChange( newValue ); + + event.preventDefault(); + // It's important that we stop other handlers (e.g. ones + // registered by TinyMCE) from also handling this event. + event.stopImmediatePropagation(); + } + } + + this.onDeleteKeyDown( event ); + } + // If we click shift+Enter on inline RichTexts, we avoid creating two contenteditables // We also split the content and call the onSplit prop if provided. if ( keyCode === ENTER ) { event.preventDefault(); + // It's important that we stop other handlers (e.g. ones registered + // by TinyMCE) from also handling this event. + event.stopImmediatePropagation(); + + const record = this.createRecord(); if ( this.props.onReplace ) { - const text = getTextContent( this.getRecord() ); + const text = getTextContent( record ); const transformation = findTransform( this.enterPatterns, ( item ) => { return item.regExp.test( text ); } ); if ( transformation ) { - // Calling onReplace() will destroy the editor, so it's - // important that we stop other handlers (e.g. ones - // registered by TinyMCE) from also handling this event. - event.stopImmediatePropagation(); this.props.onReplace( [ transformation.transform( { content: text } ), ] ); @@ -623,16 +679,12 @@ export class RichText extends Component { } if ( this.multilineTag ) { - const record = this.getRecord(); - if ( this.props.onSplit && isEmptyLine( record ) ) { this.props.onSplit( ...split( record ).map( this.valueToFormat ) ); } else { - // Character is used to separate lines in multiline values. - this.onChange( insert( record, '\u2028' ) ); + this.onChange( insertLineSeparator( record ) ); } } else if ( event.shiftKey || ! this.props.onSplit ) { - const record = this.getRecord(); const text = getTextContent( record ); const length = text.length; let toInsert = '\n'; @@ -647,7 +699,7 @@ export class RichText extends Component { toInsert = '\n\n'; } - this.onChange( insert( this.getRecord(), toInsert ) ); + this.onChange( insert( record, toInsert ) ); } else { this.splitContent(); } @@ -811,6 +863,7 @@ export class RichText extends Component { return create( { html: children.toHTML( value ), multilineTag: this.multilineTag, + multilineWrapperTags: this.multilineWrapperTags, } ); } @@ -818,6 +871,7 @@ export class RichText extends Component { return create( { html: value, multilineTag: this.multilineTag, + multilineWrapperTags: this.multilineWrapperTags, } ); } @@ -833,11 +887,19 @@ export class RichText extends Component { valueToFormat( { formats, text } ) { // Handle deprecated `children` and `node` sources. if ( this.usedDeprecatedChildrenSource ) { - return children.fromDOM( unstableToDom( { formats, text }, this.multilineTag ).body.childNodes ); + return children.fromDOM( unstableToDom( { + value: { formats, text }, + multilineTag: this.multilineTag, + multilineWrapperTags: this.multilineWrapperTags, + } ).body.childNodes ); } if ( this.props.format === 'string' ) { - return toHTMLString( { formats, text }, this.multilineTag ); + return toHTMLString( { + value: { formats, text }, + multilineTag: this.multilineTag, + multilineWrapperTags: this.multilineWrapperTags, + } ); } return { formats, text }; @@ -910,6 +972,7 @@ export class RichText extends Component { onPaste={ this.onPaste } onInput={ this.onInput } multilineTag={ this.multilineTag } + multilineWrapperTags={ this.multilineWrapperTags } setRef={ this.setRef } /> { isPlaceholderVisible && diff --git a/packages/editor/src/components/rich-text/index.native.js b/packages/editor/src/components/rich-text/index.native.js index 3514a85436ae39..7d44b942d47d8e 100644 --- a/packages/editor/src/components/rich-text/index.native.js +++ b/packages/editor/src/components/rich-text/index.native.js @@ -137,7 +137,10 @@ export class RichText extends Component { } valueToFormat( { formats, text } ) { - const value = toHTMLString( { formats, text }, this.multilineTag ); + const value = toHTMLString( { + value: { formats, text }, + multilineTag: this.multilineTag, + } ); // remove the outer root tags return this.removeRootTagsProduceByAztec( value ); } diff --git a/packages/editor/src/components/rich-text/tinymce.js b/packages/editor/src/components/rich-text/tinymce.js index c1d752a1d3c7ad..25b3a2ac860326 100644 --- a/packages/editor/src/components/rich-text/tinymce.js +++ b/packages/editor/src/components/rich-text/tinymce.js @@ -214,6 +214,7 @@ export default class TinyMCE extends Component { onPaste, onInput, multilineTag, + multilineWrapperTags, } = this.props; /* @@ -239,7 +240,11 @@ export default class TinyMCE extends Component { } else if ( Array.isArray( defaultValue ) ) { initialHTML = children.toHTML( defaultValue ); } else if ( typeof defaultValue !== 'string' ) { - initialHTML = toHTMLString( defaultValue, multilineTag ); + initialHTML = toHTMLString( { + value: defaultValue, + multilineTag, + multilineWrapperTags, + } ); } return createElement( tagName, { diff --git a/packages/rich-text/README.md b/packages/rich-text/README.md index 7655b43c4cc4f7..1b8f714627415d 100644 --- a/packages/rich-text/README.md +++ b/packages/rich-text/README.md @@ -17,23 +17,44 @@ _This package assumes that your code will run in an **ES2015+** environment. If ### create ```js -create( ?input: Element | string, ?range: Range, ?multilineTag: string, ?settings: Object ): Object -``` - -Create a RichText value from an `Element` tree (DOM), an HTML string or a plain text string, with optionally a `Range` object to set the selection. If called without a given `input`, an empty value will be created. If `multilineTag` is provided, any content of direct children whose type matches `multilineTag` will be separated by two newlines. The `settings` object can be used to filter out content. +create( { + ?element: Element, + ?text: string, + ?html: string, + ?range: Range, + ?multilineTag: string, + ?multilineWrapperTags: Array, + ?removeNode: Function, + ?unwrapNode: Function, + ?filterString: Function, + ?removeAttribute: Function, +} ): Object +``` + +Create a RichText value from an `Element` tree (DOM), an HTML string or a plain text string, with optionally a `Range` object to set the selection. If called without any arguments, an empty value will be created. If `multilineTag` is provided, any content of direct children whose type matches `multilineTag` will be separated by a line separator. The remaining parameters can be used to filter out content. ### toHTMLString ```js -toHTMLString( value: Object, ?multilineTag: string ): string +toHTMLString( { + value: Object, + ?multilineTag: string, + ?multilineWrapperTags: Array, +} ): string ``` -Create an HTML string from a Rich Text value. If a `multilineTag` is provided, text separated by two new lines will be wrapped in it. +Create an HTML string from a Rich Text value. If a `multilineTag` is provided, text separated by a line separator will be wrapped in it. ### apply ```js -apply( value: Object, current: Element, ?multilineTag ): void +apply( { + value: Object, + current: Element, + ?multilineTag: string + ?multilineWrapperTags: Array, + ?createLinePadding: Function, +} ): void ``` Create an `Element` tree from a Rich Text value and applies the difference to the `Element` tree contained by `current`. If a `multilineTag` is provided, text separated by two new lines will be wrapped in an `Element` of that type. diff --git a/packages/rich-text/package.json b/packages/rich-text/package.json index f4940b12f88c42..6c2474383df7da 100644 --- a/packages/rich-text/package.json +++ b/packages/rich-text/package.json @@ -22,6 +22,7 @@ "dependencies": { "@babel/runtime": "^7.0.0", "@wordpress/data": "file:../data", + "@wordpress/deprecated": "file:../deprecated", "@wordpress/escape-html": "file:../escape-html", "lodash": "^4.17.10", "rememo": "^3.0.0" diff --git a/packages/rich-text/src/char-at.js b/packages/rich-text/src/char-at.js new file mode 100644 index 00000000000000..6f04c2e2ac1aa4 --- /dev/null +++ b/packages/rich-text/src/char-at.js @@ -0,0 +1,12 @@ +/** + * Gets the character at the specified index, or returns `undefined` if no + * character was found. + * + * @param {Object} value Value to get the character from. + * @param {string} index Index to use. + * + * @return {?string} A one character long string, or undefined. + */ +export function charAt( { text }, index ) { + return text[ index ]; +} diff --git a/packages/rich-text/src/create.js b/packages/rich-text/src/create.js index 1c050735919fa9..39028ddc2ff8d8 100644 --- a/packages/rich-text/src/create.js +++ b/packages/rich-text/src/create.js @@ -12,6 +12,10 @@ import { isEmpty } from './is-empty'; import { isFormatEqual } from './is-format-equal'; import { createElement } from './create-element'; import { getFormatTypes } from './get-format-types'; +import { + LINE_SEPARATOR, + OBJECT_REPLACEMENT_CHARACTER, +} from './special-characters'; /** * Browser dependencies @@ -72,20 +76,23 @@ function toFormat( { type, attributes } ) { * `multilineTag` will be separated by two newlines. The optional functions can * be used to filter out content. * - * @param {?Object} $1 Optional named argements. - * @param {?Element} $1.element Element to create value from. - * @param {?string} $1.text Text to create value from. - * @param {?string} $1.html HTML to create value from. - * @param {?Range} $1.range Range to create value from. - * @param {?string} $1.multilineTag Multiline tag if the structure is - * multiline. - * @param {?Function} $1.removeNode Function to declare whether the given - * node should be removed. - * @param {?Function} $1.unwrapNode Function to declare whether the given - * node should be unwrapped. - * @param {?Function} $1.filterString Function to filter the given string. - * @param {?Function} $1.removeAttribute Wether to remove an attribute based on - * the name. + * @param {?Object} $1 Optional named argements. + * @param {?Element} $1.element Element to create value from. + * @param {?string} $1.text Text to create value from. + * @param {?string} $1.html HTML to create value from. + * @param {?Range} $1.range Range to create value from. + * @param {?string} $1.multilineTag Multiline tag if the structure is + * multiline. + * @param {?Array} $1.multilineWrapperTags Tags where lines can be found if + * nesting is possible. + * @param {?Function} $1.removeNode Function to declare whether the + * given node should be removed. + * @param {?Function} $1.unwrapNode Function to declare whether the + * given node should be unwrapped. + * @param {?Function} $1.filterString Function to filter the given + * string. + * @param {?Function} $1.removeAttribute Wether to remove an attribute + * based on the name. * * @return {Object} A rich text value. */ @@ -95,6 +102,7 @@ export function create( { html, range, multilineTag, + multilineWrapperTags, removeNode, unwrapNode, filterString, @@ -130,6 +138,7 @@ export function create( { element, range, multilineTag, + multilineWrapperTags, removeNode, unwrapNode, filterString, @@ -159,7 +168,7 @@ function accumulateSelection( accumulator, node, range, value ) { if ( value.start !== undefined ) { accumulator.start = currentLength + value.start; // Range indicates that the current node has selection. - } else if ( node === startContainer ) { + } else if ( node === startContainer && node.nodeType === TEXT_NODE ) { accumulator.start = currentLength + startOffset; // Range indicates that the current node is selected. } else if ( @@ -167,13 +176,22 @@ function accumulateSelection( accumulator, node, range, value ) { node === startContainer.childNodes[ startOffset ] ) { accumulator.start = currentLength; + // Range indicates that the selection is after the current node. + } else if ( + parentNode === startContainer && + node === startContainer.childNodes[ startOffset - 1 ] + ) { + accumulator.start = currentLength + value.text.length; + // Fallback if no child inside handled the selection. + } else if ( node === startContainer ) { + accumulator.start = currentLength; } // Selection can be extracted from value. if ( value.end !== undefined ) { accumulator.end = currentLength + value.end; // Range indicates that the current node has selection. - } else if ( node === endContainer ) { + } else if ( node === endContainer && node.nodeType === TEXT_NODE ) { accumulator.end = currentLength + endOffset; // Range indicates that the current node is selected. } else if ( @@ -187,6 +205,9 @@ function accumulateSelection( accumulator, node, range, value ) { node === endContainer.childNodes[ endOffset ] ) { accumulator.end = currentLength; + // Fallback if no child inside handled the selection. + } else if ( node === endContainer ) { + accumulator.end = currentLength + endOffset; } } @@ -221,22 +242,30 @@ function filterRange( node, range, filter ) { /** * Creates a Rich Text value from a DOM element and range. * - * @param {Object} $1 Named argements. - * @param {?Element} $1.element Element to create value from. - * @param {?Range} $1.range Range to create value from. - * @param {?Function} $1.removeNode Function to declare whether the given - * node should be removed. - * @param {?Function} $1.unwrapNode Function to declare whether the given - * node should be unwrapped. - * @param {?Function} $1.filterString Function to filter the given string. - * @param {?Function} $1.removeAttribute Wether to remove an attribute based on - * the name. + * @param {Object} $1 Named argements. + * @param {?Element} $1.element Element to create value from. + * @param {?Range} $1.range Range to create value from. + * @param {?string} $1.multilineTag Multiline tag if the structure is + * multiline. + * @param {?Array} $1.multilineWrapperTags Tags where lines can be found if + * nesting is possible. + * @param {?Function} $1.removeNode Function to declare whether the + * given node should be removed. + * @param {?Function} $1.unwrapNode Function to declare whether the + * given node should be unwrapped. + * @param {?Function} $1.filterString Function to filter the given + * string. + * @param {?Function} $1.removeAttribute Wether to remove an attribute + * based on the name. * * @return {Object} A rich text value. */ function createFromElement( { element, range, + multilineTag, + multilineWrapperTags, + currentWrapperTags = [], removeNode, unwrapNode, filterString, @@ -271,6 +300,7 @@ function createFromElement( { // Optimise for speed. for ( let index = 0; index < length; index++ ) { const node = element.childNodes[ index ]; + const type = node.nodeName.toLowerCase(); if ( node.nodeType === TEXT_NODE ) { const text = filterStringComplete( node.nodeValue ); @@ -295,7 +325,7 @@ function createFromElement( { continue; } - if ( node.nodeName === 'BR' ) { + if ( type === 'br' ) { accumulateSelection( accumulator, node, range, createEmptyValue() ); accumulator.text += '\n'; accumulator.formats.length += 1; @@ -305,10 +335,11 @@ function createFromElement( { const lastFormats = accumulator.formats[ accumulator.formats.length - 1 ]; const lastFormat = lastFormats && lastFormats[ lastFormats.length - 1 ]; let format; + let value; if ( ! unwrapNode || ! unwrapNode( node ) ) { const newFormat = toFormat( { - type: node.nodeName.toLowerCase(), + type, attributes: getAttributes( { element: node, removeAttribute, @@ -323,14 +354,31 @@ function createFromElement( { } } - const value = createFromElement( { - element: node, - range, - removeNode, - unwrapNode, - filterString, - removeAttribute, - } ); + if ( multilineWrapperTags && multilineWrapperTags.indexOf( type ) !== -1 ) { + value = createFromMultilineElement( { + element: node, + range, + multilineTag, + multilineWrapperTags, + removeNode, + unwrapNode, + filterString, + removeAttribute, + currentWrapperTags: [ ...currentWrapperTags, format ], + } ); + format = undefined; + } else { + value = createFromElement( { + element: node, + range, + multilineTag, + multilineWrapperTags, + removeNode, + unwrapNode, + filterString, + removeAttribute, + } ); + } const text = value.text; const start = accumulator.text.length; @@ -346,8 +394,7 @@ function createFromElement( { if ( format && format.attributes && text.length === 0 ) { format.object = true; - // Object replacement character. - accumulator.text += '\ufffc'; + accumulator.text += OBJECT_REPLACEMENT_CHARACTER; if ( formats[ start ] ) { formats[ start ].unshift( format ); @@ -356,6 +403,7 @@ function createFromElement( { } } else { accumulator.text += text; + accumulator.formats.length += text.length; let i = value.formats.length; @@ -389,18 +437,23 @@ function createFromElement( { * Creates a rich text value from a DOM element and range that should be * multiline. * - * @param {Object} $1 Named argements. - * @param {?Element} $1.element Element to create value from. - * @param {?Range} $1.range Range to create value from. - * @param {?string} $1.multilineTag Multiline tag if the structure is - * multiline. - * @param {?Function} $1.removeNode Function to declare whether the given - * node should be removed. - * @param {?Function} $1.unwrapNode Function to declare whether the given - * node should be unwrapped. - * @param {?Function} $1.filterString Function to filter the given string. - * @param {?Function} $1.removeAttribute Wether to remove an attribute based on - * the name. + * @param {Object} $1 Named argements. + * @param {?Element} $1.element Element to create value from. + * @param {?Range} $1.range Range to create value from. + * @param {?string} $1.multilineTag Multiline tag if the structure is + * multiline. + * @param {?Array} $1.multilineWrapperTags Tags where lines can be found if + * nesting is possible. + * @param {?Function} $1.removeNode Function to declare whether the + * given node should be removed. + * @param {?Function} $1.unwrapNode Function to declare whether the + * given node should be unwrapped. + * @param {?Function} $1.filterString Function to filter the given + * string. + * @param {?Function} $1.removeAttribute Wether to remove an attribute + * based on the name. + * @param {boolean} $1.currentWrapperTags Whether to prepend a line + * separator. * * @return {Object} A rich text value. */ @@ -408,10 +461,12 @@ function createFromMultilineElement( { element, range, multilineTag, + multilineWrapperTags, removeNode, unwrapNode, filterString, removeAttribute, + currentWrapperTags = [], } ) { const accumulator = createEmptyValue(); @@ -429,20 +484,40 @@ function createFromMultilineElement( { continue; } - const value = createFromElement( { + let value = createFromElement( { element: node, range, multilineTag, + multilineWrapperTags, + currentWrapperTags, removeNode, unwrapNode, filterString, removeAttribute, } ); + // If a line consists of one single line break (invisible), consider the + // line empty, wether this is the browser's doing or not. + if ( value.text === '\n' ) { + const start = value.start; + const end = value.end; + + value = createEmptyValue(); + + if ( start !== undefined ) { + value.start = 0; + } + + if ( end !== undefined ) { + value.end = 0; + } + } + // Multiline value text should be separated by a double line break. - if ( index !== 0 ) { - accumulator.formats = accumulator.formats.concat( [ , ] ); - accumulator.text += '\u2028'; + if ( index !== 0 || currentWrapperTags.length > 0 ) { + const formats = currentWrapperTags.length > 0 ? [ currentWrapperTags ] : [ , ]; + accumulator.formats = accumulator.formats.concat( formats ); + accumulator.text += LINE_SEPARATOR; } accumulateSelection( accumulator, node, range, value ); diff --git a/packages/rich-text/src/get-selection-end.js b/packages/rich-text/src/get-selection-end.js new file mode 100644 index 00000000000000..90dee341391c18 --- /dev/null +++ b/packages/rich-text/src/get-selection-end.js @@ -0,0 +1,12 @@ +/** + * Gets the end index of the current selection, or returns `undefined` if no + * selection exists. The selection ends right before the character at this + * index. + * + * @param {Object} value Value to get the selection from. + * + * @return {?number} Index where the selection ends. + */ +export function getSelectionEnd( { end } ) { + return end; +} diff --git a/packages/rich-text/src/get-selection-start.js b/packages/rich-text/src/get-selection-start.js new file mode 100644 index 00000000000000..948d3800523149 --- /dev/null +++ b/packages/rich-text/src/get-selection-start.js @@ -0,0 +1,12 @@ +/** + * Gets the start index of the current selection, or returns `undefined` if no + * selection exists. The selection starts right before the character at this + * index. + * + * @param {Object} value Value to get the selection from. + * + * @return {?number} Index where the selection starts. + */ +export function getSelectionStart( { start } ) { + return start; +} diff --git a/packages/rich-text/src/index.js b/packages/rich-text/src/index.js index a52b095499cea8..05c1340d5b3459 100644 --- a/packages/rich-text/src/index.js +++ b/packages/rich-text/src/index.js @@ -1,11 +1,14 @@ import './store'; export { applyFormat } from './apply-format'; +export { charAt } from './char-at'; export { concat } from './concat'; export { create } from './create'; export { getActiveFormat } from './get-active-format'; export { getFormatType } from './get-format-type'; export { getFormatTypes } from './get-format-types'; +export { getSelectionEnd } from './get-selection-end'; +export { getSelectionStart } from './get-selection-start'; export { getTextContent } from './get-text-content'; export { isCollapsed } from './is-collapsed'; export { isEmpty, isEmptyLine } from './is-empty'; @@ -15,10 +18,12 @@ export { removeFormat } from './remove-format'; export { remove } from './remove'; export { replace } from './replace'; export { insert } from './insert'; +export { insertLineSeparator } from './insert-line-separator'; export { insertObject } from './insert-object'; export { slice } from './slice'; export { split } from './split'; export { apply, toDom as unstableToDom } from './to-dom'; export { toHTMLString } from './to-html-string'; export { toggleFormat } from './toggle-format'; +export { LINE_SEPARATOR } from './special-characters'; export { unregisterFormatType } from './unregister-format-type'; diff --git a/packages/rich-text/src/insert-line-separator.js b/packages/rich-text/src/insert-line-separator.js new file mode 100644 index 00000000000000..17ab2957ed7298 --- /dev/null +++ b/packages/rich-text/src/insert-line-separator.js @@ -0,0 +1,39 @@ +/** + * Internal dependencies + */ + +import { getTextContent } from './get-text-content'; +import { insert } from './insert'; +import { LINE_SEPARATOR } from './special-characters'; + +/** + * Insert a line break character into a Rich Text value at the given + * `startIndex`. Any content between `startIndex` and `endIndex` will be + * removed. Indices are retrieved from the selection if none are provided. + * + * @param {Object} value Value to modify. + * @param {number} startIndex Start index. + * @param {number} endIndex End index. + * + * @return {Object} A new value with the value inserted. + */ +export function insertLineSeparator( + value, + startIndex = value.start, + endIndex = value.end, +) { + const beforeText = getTextContent( value ).slice( 0, startIndex ); + const previousLineSeparatorIndex = beforeText.lastIndexOf( LINE_SEPARATOR ); + let formats = [ , ]; + + if ( previousLineSeparatorIndex !== -1 ) { + formats = [ value.formats[ previousLineSeparatorIndex ] ]; + } + + const valueToInsert = { + formats, + text: LINE_SEPARATOR, + }; + + return insert( value, valueToInsert, startIndex, endIndex ); +} diff --git a/packages/rich-text/src/is-empty.js b/packages/rich-text/src/is-empty.js index a669314ee45032..f7078eb3440aad 100644 --- a/packages/rich-text/src/is-empty.js +++ b/packages/rich-text/src/is-empty.js @@ -1,3 +1,5 @@ +import { LINE_SEPARATOR } from './special-characters'; + /** * Check if a Rich Text value is Empty, meaning it contains no text or any * objects (such as images). @@ -27,13 +29,13 @@ export function isEmptyLine( { text, start, end } ) { return true; } - if ( start === 0 && text.slice( 0, 1 ) === '\u2028' ) { + if ( start === 0 && text.slice( 0, 1 ) === LINE_SEPARATOR ) { return true; } - if ( start === text.length && text.slice( -1 ) === '\u2028' ) { + if ( start === text.length && text.slice( -1 ) === LINE_SEPARATOR ) { return true; } - return text.slice( start - 1, end + 1 ) === '\u2028\u2028'; + return text.slice( start - 1, end + 1 ) === `${ LINE_SEPARATOR }${ LINE_SEPARATOR }`; } diff --git a/packages/rich-text/src/special-characters.js b/packages/rich-text/src/special-characters.js index a607ab03fb7541..04ffc18ce88625 100644 --- a/packages/rich-text/src/special-characters.js +++ b/packages/rich-text/src/special-characters.js @@ -1 +1,3 @@ +export const LINE_SEPARATOR = '\u2028'; +export const OBJECT_REPLACEMENT_CHARACTER = '\ufffc'; export const ZERO_WIDTH_NO_BREAK_SPACE = '\uFEFF'; diff --git a/packages/rich-text/src/test/__snapshots__/to-dom.js.snap b/packages/rich-text/src/test/__snapshots__/to-dom.js.snap index 77cfe39a7be885..0b3d69d139c950 100644 --- a/packages/rich-text/src/test/__snapshots__/to-dom.js.snap +++ b/packages/rich-text/src/test/__snapshots__/to-dom.js.snap @@ -96,18 +96,14 @@ exports[`recordToDom should create a value without formatting 1`] = ` exports[`recordToDom should create an empty value 1`] = ` -
+
`; exports[`recordToDom should create an empty value from empty tags 1`] = ` -
+
`; @@ -147,9 +143,7 @@ exports[`recordToDom should filter text outside format with settings 1`] = ` exports[`recordToDom should filter text with settings 1`] = ` -
+
`; @@ -190,16 +184,61 @@ exports[`recordToDom should handle double br 1`] = ` `; +exports[`recordToDom should handle empty list value 1`] = ` + +
  • + +
    +
  • + +`; + +exports[`recordToDom should handle empty multiline value 1`] = ` + +

    + +
    +

    + +`; + +exports[`recordToDom should handle middle empty list value 1`] = ` + +
  • + +
    +
  • +
  • + +
    +
  • +
  • + +
    +
  • + +`; + exports[`recordToDom should handle multiline list value 1`] = `
  • one
    • - two + a +
    • +
    • + b +
        +
      1. + 1 +
      2. +
      3. + 2 +
      4. +
    -
  • three @@ -218,6 +257,14 @@ exports[`recordToDom should handle multiline value 1`] = ` `; +exports[`recordToDom should handle multiline value with element selection 1`] = ` + +
  • + one +
  • + +`; + exports[`recordToDom should handle multiline value with empty 1`] = `

    @@ -225,13 +272,25 @@ exports[`recordToDom should handle multiline value with empty 1`] = `

    -
    +

    `; +exports[`recordToDom should handle nested empty list value 1`] = ` + +
  • +
    +
      +
    • + +
      +
    • +
    +
  • + +`; + exports[`recordToDom should handle selection before br 1`] = ` a @@ -245,9 +304,7 @@ exports[`recordToDom should handle selection before br 1`] = ` exports[`recordToDom should ignore line breaks to format HTML 1`] = ` -
    +
    `; @@ -269,9 +326,7 @@ exports[`recordToDom should preserve emoji in formatting 1`] = ` exports[`recordToDom should remove br with settings 1`] = ` -
    +
    `; @@ -284,9 +339,7 @@ exports[`recordToDom should remove with children with settings 1`] = ` exports[`recordToDom should remove with settings 1`] = ` -
    +
    `; diff --git a/packages/rich-text/src/test/create.js b/packages/rich-text/src/test/create.js index 2ce9b605817845..f4bbbad889b229 100644 --- a/packages/rich-text/src/test/create.js +++ b/packages/rich-text/src/test/create.js @@ -23,11 +23,25 @@ describe( 'create', () => { require( '../store' ); } ); - spec.forEach( ( { description, multilineTag, settings, html, createRange, record } ) => { + spec.forEach( ( { + description, + multilineTag, + multilineWrapperTags, + settings, + html, + createRange, + record, + } ) => { it( description, () => { const element = createElement( document, html ); const range = createRange( element ); - const createdRecord = create( { element, range, multilineTag, ...settings } ); + const createdRecord = create( { + element, + range, + multilineTag, + multilineWrapperTags, + ...settings, + } ); const formatsLength = getSparseArrayLength( record.formats ); const createdFormatsLength = getSparseArrayLength( createdRecord.formats ); diff --git a/packages/rich-text/src/test/helpers/index.js b/packages/rich-text/src/test/helpers/index.js index f1d5b021a34863..cb8ad42d9997b4 100644 --- a/packages/rich-text/src/test/helpers/index.js +++ b/packages/rich-text/src/test/helpers/index.js @@ -6,7 +6,8 @@ const em = { type: 'em' }; const strong = { type: 'strong' }; const img = { type: 'img', attributes: { src: '' }, object: true }; const a = { type: 'a', attributes: { href: '#' } }; -const list = [ { type: 'ul' }, { type: 'li' } ]; +const ul = { type: 'ul' }; +const ol = { type: 'ol' }; export const spec = [ { @@ -351,6 +352,25 @@ export const spec = [ end: 2, }, }, + { + description: 'should handle empty multiline value', + multilineTag: 'p', + html: '

    ', + createRange: ( element ) => ( { + startOffset: 0, + startContainer: element.firstChild, + endOffset: 0, + endContainer: element.firstChild, + } ), + startPath: [ 0, 0, 0 ], + endPath: [ 0, 0, 0 ], + record: { + start: 0, + end: 0, + formats: [], + text: '', + }, + }, { description: 'should handle multiline value', multilineTag: 'p', @@ -373,20 +393,81 @@ export const spec = [ { description: 'should handle multiline list value', multilineTag: 'li', - html: '
  • one
    • two
  • three
  • ', + multilineWrapperTags: [ 'ul', 'ol' ], + html: '
  • one
    • a
    • b
      1. 1
      2. 2
  • three
  • ', createRange: ( element ) => ( { startOffset: 0, startContainer: element, endOffset: 1, - endContainer: element, + endContainer: element.querySelector( 'ol > li' ).firstChild, } ), startPath: [ 0, 0, 0 ], - endPath: [ 1, 0, 0 ], + endPath: [ 0, 1, 1, 1, 0, 0, 1 ], record: { start: 0, - end: 7, - formats: [ , , , list, list, list, , , , , , , ], - text: 'onetwo\u2028three', + end: 9, + formats: [ , , , [ ul ], , [ ul ], , [ ul, ol ], , [ ul, ol ], , , , , , , , ], + text: 'one\u2028a\u2028b\u20281\u20282\u2028three', + }, + }, + { + description: 'should handle empty list value', + multilineTag: 'li', + multilineWrapperTags: [ 'ul', 'ol' ], + html: '
  • ', + createRange: ( element ) => ( { + startOffset: 0, + startContainer: element.firstChild, + endOffset: 0, + endContainer: element.firstChild, + } ), + startPath: [ 0, 0, 0 ], + endPath: [ 0, 0, 0 ], + record: { + start: 0, + end: 0, + formats: [], + text: '', + }, + }, + { + description: 'should handle nested empty list value', + multilineTag: 'li', + multilineWrapperTags: [ 'ul', 'ol' ], + html: '
  • ', + createRange: ( element ) => ( { + startOffset: 0, + startContainer: element.querySelector( 'ul > li' ), + endOffset: 0, + endContainer: element.querySelector( 'ul > li' ), + } ), + startPath: [ 0, 0, 0, 0, 0 ], + endPath: [ 0, 0, 0, 0, 0 ], + record: { + start: 1, + end: 1, + formats: [ [ ul ] ], + text: '\u2028', + }, + }, + { + description: 'should handle middle empty list value', + multilineTag: 'li', + multilineWrapperTags: [ 'ul', 'ol' ], + html: '
  • ', + createRange: ( element ) => ( { + startOffset: 0, + startContainer: element.firstChild.nextSibling, + endOffset: 0, + endContainer: element.firstChild.nextSibling, + } ), + startPath: [ 1, 0, 0 ], + endPath: [ 1, 0, 0 ], + record: { + start: 1, + end: 1, + formats: [ , , ], + text: '\u2028\u2028', }, }, { @@ -408,6 +489,26 @@ export const spec = [ text: 'one\u2028', }, }, + { + description: 'should handle multiline value with element selection', + multilineTag: 'li', + multilineWrapperTags: [ 'ul', 'ol' ], + html: '
  • one
  • ', + createRange: ( element ) => ( { + startOffset: 1, + startContainer: element.firstChild, + endOffset: 1, + endContainer: element.firstChild, + } ), + startPath: [ 0, 0, 3 ], + endPath: [ 0, 0, 3 ], + record: { + start: 3, + end: 3, + formats: [ , , , ], + text: 'one', + }, + }, { description: 'should remove with settings', settings: { diff --git a/packages/rich-text/src/test/insert-line-separator.js b/packages/rich-text/src/test/insert-line-separator.js new file mode 100644 index 00000000000000..3dd252278ebeb8 --- /dev/null +++ b/packages/rich-text/src/test/insert-line-separator.js @@ -0,0 +1,76 @@ +/** + * External dependencies + */ +import deepFreeze from 'deep-freeze'; + +/** + * Internal dependencies + */ + +import { insertLineSeparator } from '../insert-line-separator'; +import { LINE_SEPARATOR } from '../special-characters'; +import { getSparseArrayLength } from './helpers'; + +describe( 'insertLineSeparator', () => { + const ol = { type: 'ol' }; + + it( 'should insert line separator at end', () => { + const value = { + formats: [ , ], + text: '1', + start: 1, + end: 1, + }; + const expected = { + formats: [ , , ], + text: `1${ LINE_SEPARATOR }`, + start: 2, + end: 2, + }; + const result = insertLineSeparator( deepFreeze( value ) ); + + expect( result ).not.toBe( value ); + expect( result ).toEqual( expected ); + expect( getSparseArrayLength( result.formats ) ).toBe( 0 ); + } ); + + it( 'should insert line separator at start', () => { + const value = { + formats: [ , ], + text: '1', + start: 0, + end: 0, + }; + const expected = { + formats: [ , , ], + text: `${ LINE_SEPARATOR }1`, + start: 1, + end: 1, + }; + const result = insertLineSeparator( deepFreeze( value ) ); + + expect( result ).not.toBe( value ); + expect( result ).toEqual( expected ); + expect( getSparseArrayLength( result.formats ) ).toBe( 0 ); + } ); + + it( 'should insert line separator in nested item', () => { + const value = { + formats: [ , , , [ ol ], , ], + text: `1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }a`, + start: 5, + end: 5, + }; + const expected = { + formats: [ , , , [ ol ], , [ ol ] ], + text: `1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }a${ LINE_SEPARATOR }`, + start: 6, + end: 6, + }; + const result = insertLineSeparator( deepFreeze( value ) ); + + expect( result ).not.toBe( value ); + expect( result ).toEqual( expected ); + expect( getSparseArrayLength( result.formats ) ).toBe( 2 ); + } ); +} ); diff --git a/packages/rich-text/src/test/to-dom.js b/packages/rich-text/src/test/to-dom.js index 7be38e49fa80d0..92cc47aa28846e 100644 --- a/packages/rich-text/src/test/to-dom.js +++ b/packages/rich-text/src/test/to-dom.js @@ -21,9 +21,21 @@ describe( 'recordToDom', () => { require( '../store' ); } ); - spec.forEach( ( { description, multilineTag, record, startPath, endPath } ) => { + spec.forEach( ( { + description, + multilineTag, + multilineWrapperTags, + record, + startPath, + endPath, + } ) => { it( description, () => { - const { body, selection } = toDom( record, multilineTag ); + const { body, selection } = toDom( { + value: record, + multilineTag, + multilineWrapperTags, + createLinePadding: ( doc ) => doc.createElement( 'br' ), + } ); expect( body ).toMatchSnapshot(); expect( selection ).toEqual( { startPath, endPath } ); } ); diff --git a/packages/rich-text/src/test/to-html-string.js b/packages/rich-text/src/test/to-html-string.js index daadd733fd9a21..975663acdadbb2 100644 --- a/packages/rich-text/src/test/to-html-string.js +++ b/packages/rich-text/src/test/to-html-string.js @@ -30,50 +30,53 @@ describe( 'toHTMLString', () => { const HTML = 'one two 🍒 three'; const element = createNode( `

    ${ HTML }

    ` ); - expect( toHTMLString( create( { element } ) ) ).toEqual( HTML ); + expect( toHTMLString( { value: create( { element } ) } ) ).toEqual( HTML ); } ); it( 'should extract recreate HTML 2', () => { const HTML = 'one two 🍒 test three'; const element = createNode( `

    ${ HTML }

    ` ); - expect( toHTMLString( create( { element } ) ) ).toEqual( HTML ); + expect( toHTMLString( { value: create( { element } ) } ) ).toEqual( HTML ); } ); it( 'should extract recreate HTML 3', () => { const HTML = ''; const element = createNode( `

    ${ HTML }

    ` ); - expect( toHTMLString( create( { element } ) ) ).toEqual( HTML ); + expect( toHTMLString( { value: create( { element } ) } ) ).toEqual( HTML ); } ); it( 'should extract recreate HTML 4', () => { const HTML = 'two 🍒'; const element = createNode( `

    ${ HTML }

    ` ); - expect( toHTMLString( create( { element } ) ) ).toEqual( HTML ); + expect( toHTMLString( { value: create( { element } ) } ) ).toEqual( HTML ); } ); it( 'should extract recreate HTML 5', () => { const HTML = 'If you want to learn more about how to build additional blocks, or if you are interested in helping with the project, head over to the GitHub repository.'; const element = createNode( `

    ${ HTML }

    ` ); - expect( toHTMLString( create( { element } ) ) ).toEqual( HTML ); + expect( toHTMLString( { value: create( { element } ) } ) ).toEqual( HTML ); } ); it( 'should extract recreate HTML 6', () => { const HTML = '
  • one
    • two
  • three
  • '; const element = createNode( `
      ${ HTML }
    ` ); const multilineTag = 'li'; + const multilineWrapperTags = [ 'ul', 'ol' ]; + const value = create( { element, multilineTag, multilineWrapperTags } ); + const result = toHTMLString( { value, multilineTag, multilineWrapperTags } ); - expect( toHTMLString( create( { element, multilineTag } ), 'li' ) ).toEqual( HTML ); + expect( result ).toEqual( HTML ); } ); it( 'should serialize neighbouring formats of same type', () => { const HTML = 'aa'; const element = createNode( `

    ${ HTML }

    ` ); - expect( toHTMLString( create( { element } ) ) ).toEqual( HTML ); + expect( toHTMLString( { value: create( { element } ) } ) ).toEqual( HTML ); } ); it( 'should serialize neighbouring same formats', () => { @@ -81,6 +84,6 @@ describe( 'toHTMLString', () => { const element = createNode( `

    ${ HTML }

    ` ); const expectedHTML = 'aa'; - expect( toHTMLString( create( { element } ) ) ).toEqual( expectedHTML ); + expect( toHTMLString( { value: create( { element } ) } ) ).toEqual( expectedHTML ); } ); } ); diff --git a/packages/rich-text/src/to-dom.js b/packages/rich-text/src/to-dom.js index 018869337c533c..319e8b61ee1377 100644 --- a/packages/rich-text/src/to-dom.js +++ b/packages/rich-text/src/to-dom.js @@ -58,13 +58,8 @@ function getNodeByPath( node, path ) { }; } -function createEmpty( type ) { +function createEmpty() { const { body } = document.implementation.createHTMLDocument( '' ); - - if ( type ) { - return body.appendChild( body.ownerDocument.createElement( type ) ); - } - return body; } @@ -110,11 +105,46 @@ function remove( node ) { return node.parentNode.removeChild( node ); } -export function toDom( value, multilineTag ) { +function padEmptyLines( { element, createLinePadding, multilineWrapperTags } ) { + const length = element.childNodes.length; + const doc = element.ownerDocument; + + for ( let index = 0; index < length; index++ ) { + const child = element.childNodes[ index ]; + + if ( child.nodeType === TEXT_NODE ) { + if ( length === 1 && ! child.nodeValue ) { + // Pad if the only child is an empty text node. + element.appendChild( createLinePadding( doc ) ); + } + } else { + if ( + multilineWrapperTags && + ! child.previousSibling && + multilineWrapperTags.indexOf( child.nodeName.toLowerCase() ) !== -1 + ) { + // Pad the line if there is no content before a nested wrapper. + element.insertBefore( createLinePadding( doc ), child ); + } + + padEmptyLines( { element: child, createLinePadding, multilineWrapperTags } ); + } + } +} + +export function toDom( { + value, + multilineTag, + multilineWrapperTags, + createLinePadding, +} ) { let startPath = []; let endPath = []; - const tree = toTree( value, multilineTag, { + const tree = toTree( { + value, + multilineTag, + multilineWrapperTags, createEmpty, append, getLastChild, @@ -123,27 +153,18 @@ export function toDom( value, multilineTag ) { getText, remove, appendText, - onStartIndex( body, pointer, multilineIndex ) { + onStartIndex( body, pointer ) { startPath = createPathToNode( pointer, body, [ pointer.nodeValue.length ] ); - - if ( multilineIndex !== undefined ) { - startPath = [ multilineIndex, ...startPath ]; - } }, - onEndIndex( body, pointer, multilineIndex ) { + onEndIndex( body, pointer ) { endPath = createPathToNode( pointer, body, [ pointer.nodeValue.length ] ); - - if ( multilineIndex !== undefined ) { - endPath = [ multilineIndex, ...endPath ]; - } - }, - onEmpty( body ) { - const br = body.ownerDocument.createElement( 'br' ); - br.setAttribute( 'data-mce-bogus', '1' ); - body.appendChild( br ); }, } ); + if ( createLinePadding ) { + padEmptyLines( { element: tree, createLinePadding, multilineWrapperTags } ); + } + return { body: tree, selection: { startPath, endPath }, @@ -160,9 +181,20 @@ export function toDom( value, multilineTag ) { * tree to. * @param {string} multilineTag Multiline tag. */ -export function apply( value, current, multilineTag ) { +export function apply( { + value, + current, + multilineTag, + multilineWrapperTags, + createLinePadding, +} ) { // Construct a new element tree in memory. - const { body, selection } = toDom( value, multilineTag ); + const { body, selection } = toDom( { + value, + multilineTag, + multilineWrapperTags, + createLinePadding, + } ); applyValue( body, current ); diff --git a/packages/rich-text/src/to-html-string.js b/packages/rich-text/src/to-html-string.js index 6cb87705e189b9..1965419bdd552e 100644 --- a/packages/rich-text/src/to-html-string.js +++ b/packages/rich-text/src/to-html-string.js @@ -7,6 +7,7 @@ import { escapeAttribute, isValidAttributeName, } from '@wordpress/escape-html'; +import deprecated from '@wordpress/deprecated'; /** * Internal dependencies @@ -16,15 +17,34 @@ import { toTree } from './to-tree'; /** * Create an HTML string from a Rich Text value. If a `multilineTag` is - * provided, text separated by two new lines will be wrapped in it. + * provided, text separated by a line separator will be wrapped in it. * - * @param {Object} value Rich text value. - * @param {string} multilineTag Multiline tag. + * @param {Object} $1 Named argements. + * @param {Object} $1.value Rich text value. + * @param {string} $1.multilineTag Multiline tag. + * @param {Array} $1.multilineWrapperTags Tags where lines can be found if + * nesting is possible. * * @return {string} HTML string. */ -export function toHTMLString( value, multilineTag ) { - const tree = toTree( value, multilineTag, { +export function toHTMLString( { value, multilineTag, multilineWrapperTags } ) { + // Check other arguments for backward compatibility. + if ( value === undefined ) { + deprecated( 'wp.richText.toHTMLString positional parameters', { + version: '4.4', + alternative: 'named parameters', + plugin: 'Gutenberg', + } ); + + value = arguments[ 0 ]; + multilineTag = arguments[ 1 ]; + multilineWrapperTags = arguments[ 2 ]; + } + + const tree = toTree( { + value, + multilineTag, + multilineWrapperTags, createEmpty, append, getLastChild, @@ -38,8 +58,8 @@ export function toHTMLString( value, multilineTag ) { return createChildrenHTML( tree.children ); } -function createEmpty( type ) { - return { type }; +function createEmpty() { + return {}; } function getLastChild( { children } ) { diff --git a/packages/rich-text/src/to-tree.js b/packages/rich-text/src/to-tree.js index b40c04846b8b1c..d599db049ff9aa 100644 --- a/packages/rich-text/src/to-tree.js +++ b/packages/rich-text/src/to-tree.js @@ -2,8 +2,11 @@ * Internal dependencies */ -import { split } from './split'; import { getFormatType } from './get-format-type'; +import { + LINE_SEPARATOR, + OBJECT_REPLACEMENT_CHARACTER, +} from './special-characters'; function fromFormat( { type, attributes, object } ) { const formatType = getFormatType( type ); @@ -38,56 +41,87 @@ function fromFormat( { type, attributes, object } ) { }; } -export function toTree( value, multilineTag, settings ) { - if ( multilineTag ) { - const { createEmpty, append } = settings; - const tree = createEmpty(); - - split( value, '\u2028' ).forEach( ( piece, index ) => { - append( tree, toTree( piece, null, { - ...settings, - tag: multilineTag, - multilineIndex: index, - } ) ); - } ); - - return tree; - } - - const { - tag, - multilineIndex, - createEmpty, - append, - getLastChild, - getParent, - isText, - getText, - remove, - appendText, - onStartIndex, - onEndIndex, - onEmpty, - } = settings; +export function toTree( { + value, + multilineTag, + multilineWrapperTags, + createEmpty, + append, + getLastChild, + getParent, + isText, + getText, + remove, + appendText, + onStartIndex, + onEndIndex, +} ) { const { formats, text, start, end } = value; const formatsLength = formats.length + 1; - const tree = createEmpty( tag ); + const tree = createEmpty(); + const multilineFormat = { type: multilineTag }; - append( tree, '' ); + let lastSeparatorFormats; + let lastCharacterFormats; + let lastCharacter; + + // If we're building a multiline tree, start off with a multiline element. + if ( multilineTag ) { + append( append( tree, { type: multilineTag } ), '' ); + lastCharacterFormats = lastSeparatorFormats = [ multilineFormat ]; + } else { + append( tree, '' ); + } for ( let i = 0; i < formatsLength; i++ ) { const character = text.charAt( i ); - const characterFormats = formats[ i ]; - const lastCharacterFormats = formats[ i - 1 ]; + let characterFormats = formats[ i ]; + + // Set multiline tags in queue for building the tree. + if ( multilineTag ) { + if ( character === LINE_SEPARATOR ) { + characterFormats = lastSeparatorFormats = ( characterFormats || [] ).reduce( ( accumulator, format ) => { + if ( character === LINE_SEPARATOR && multilineWrapperTags.indexOf( format.type ) !== -1 ) { + accumulator.push( format ); + accumulator.push( multilineFormat ); + } + + return accumulator; + }, [ multilineFormat ] ); + } else { + characterFormats = [ ...lastSeparatorFormats, ...( characterFormats || [] ) ]; + } + } let pointer = getLastChild( tree ); + // Set selection for the start of line. + if ( lastCharacter === LINE_SEPARATOR ) { + let node = pointer; + + while ( ! isText( node ) ) { + node = getLastChild( node ); + } + + if ( onStartIndex && start === i ) { + onStartIndex( tree, node ); + } + + if ( onEndIndex && end === i ) { + onEndIndex( tree, node ); + } + } + if ( characterFormats ) { characterFormats.forEach( ( format, formatIndex ) => { if ( pointer && lastCharacterFormats && - format === lastCharacterFormats[ formatIndex ] + format === lastCharacterFormats[ formatIndex ] && + // Do not reuse the last element if the character is a + // line separator. + ( character !== LINE_SEPARATOR || + characterFormats.length - 1 !== formatIndex ) ) { pointer = getLastChild( pointer ); return; @@ -105,17 +139,25 @@ export function toTree( value, multilineTag, settings ) { } ); } - // If there is selection at 0, handle it before characters are inserted. - - if ( onStartIndex && start === 0 && i === 0 ) { - onStartIndex( tree, pointer, multilineIndex ); + // No need for further processing if the character is a line separator. + if ( character === LINE_SEPARATOR ) { + lastCharacterFormats = characterFormats; + lastCharacter = character; + continue; } - if ( onEndIndex && end === 0 && i === 0 ) { - onEndIndex( tree, pointer, multilineIndex ); + // If there is selection at 0, handle it before characters are inserted. + if ( i === 0 ) { + if ( onStartIndex && start === 0 ) { + onStartIndex( tree, pointer ); + } + + if ( onEndIndex && end === 0 ) { + onEndIndex( tree, pointer ); + } } - if ( character !== '\ufffc' ) { + if ( character !== OBJECT_REPLACEMENT_CHARACTER ) { if ( character === '\n' ) { pointer = append( getParent( pointer ), { type: 'br', object: true } ); // Ensure pointer is text node. @@ -128,16 +170,15 @@ export function toTree( value, multilineTag, settings ) { } if ( onStartIndex && start === i + 1 ) { - onStartIndex( tree, pointer, multilineIndex ); + onStartIndex( tree, pointer ); } if ( onEndIndex && end === i + 1 ) { - onEndIndex( tree, pointer, multilineIndex ); + onEndIndex( tree, pointer ); } - } - if ( onEmpty && text.length === 0 ) { - onEmpty( tree ); + lastCharacterFormats = characterFormats; + lastCharacter = character; } return tree; diff --git a/test/e2e/specs/__snapshots__/deprecated-node-matcher.test.js.snap b/test/e2e/specs/__snapshots__/deprecated-node-matcher.test.js.snap index a0cc53a6c18113..d36c990c04c37d 100644 --- a/test/e2e/specs/__snapshots__/deprecated-node-matcher.test.js.snap +++ b/test/e2e/specs/__snapshots__/deprecated-node-matcher.test.js.snap @@ -8,6 +8,6 @@ exports[`Deprecated Node Matcher should insert block with children source 1`] = exports[`Deprecated Node Matcher should insert block with node source 1`] = ` " -

    test


    +

    test

    " `; diff --git a/test/e2e/specs/blocks/__snapshots__/list.test.js.snap b/test/e2e/specs/blocks/__snapshots__/list.test.js.snap index 9f2a208d343965..dbc0bee94158fd 100644 --- a/test/e2e/specs/blocks/__snapshots__/list.test.js.snap +++ b/test/e2e/specs/blocks/__snapshots__/list.test.js.snap @@ -16,6 +16,16 @@ exports[`List can be converted to paragraphs 1`] = ` " `; +exports[`List can be converted when nested to paragraphs 1`] = ` +" +

    one

    + + + +

    two

    +" +`; + exports[`List can be created by converting a paragraph 1`] = ` "
    • test
    @@ -80,6 +90,12 @@ exports[`List should create paragraph on split at end and merge back with conten " `; +exports[`List should split indented list item 1`] = ` +" +
    • one
      • two
      • three
    +" +`; + exports[`List should split into two with paragraph and merge lists 1`] = ` "
    • one
    • two
    diff --git a/test/e2e/specs/blocks/list.test.js b/test/e2e/specs/blocks/list.test.js index 5c112b83454ee5..f28196cfbdffab 100644 --- a/test/e2e/specs/blocks/list.test.js +++ b/test/e2e/specs/blocks/list.test.js @@ -97,6 +97,21 @@ describe( 'List', () => { expect( await getEditedPostContent() ).toMatchSnapshot(); } ); + it( 'can be converted when nested to paragraphs', async () => { + await insertBlock( 'List' ); + await page.keyboard.type( 'one' ); + await page.keyboard.press( 'Enter' ); + // Pointer device is needed. Shift+Tab won't focus the toolbar. + // To do: fix so Shift+Tab works. + await page.mouse.move( 200, 300, { steps: 10 } ); + await page.mouse.move( 250, 350, { steps: 10 } ); + await page.click( 'button[aria-label="Indent list item"]' ); + await page.keyboard.type( 'two' ); + await convertBlock( 'Paragraph' ); + + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); + it( 'can be created by converting a quote', async () => { await insertBlock( 'Quote' ); await page.keyboard.type( 'one' ); @@ -155,4 +170,20 @@ describe( 'List', () => { expect( await getEditedPostContent() ).toMatchSnapshot(); } ); + + it( 'should split indented list item', async () => { + await insertBlock( 'List' ); + await page.keyboard.type( 'one' ); + await page.keyboard.press( 'Enter' ); + // Pointer device is needed. Shift+Tab won't focus the toolbar. + // To do: fix so Shift+Tab works. + await page.mouse.move( 200, 300, { steps: 10 } ); + await page.mouse.move( 250, 350, { steps: 10 } ); + await page.click( 'button[aria-label="Indent list item"]' ); + await page.keyboard.type( 'two' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( 'three' ); + + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); } ); From 489fe169dad7df1700dea26a6a9dce3fbde14936 Mon Sep 17 00:00:00 2001 From: Ryan Welcher Date: Tue, 30 Oct 2018 07:26:38 -0400 Subject: [PATCH 10/98] Adds aria-label to links that open in new windows (#11063) --- packages/format-library/src/link/inline.js | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/packages/format-library/src/link/inline.js b/packages/format-library/src/link/inline.js index 6ab3198c25a0d8..95fb1eb4045252 100644 --- a/packages/format-library/src/link/inline.js +++ b/packages/format-library/src/link/inline.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { __ } from '@wordpress/i18n'; +import { sprintf, __ } from '@wordpress/i18n'; import { Component, createRef } from '@wordpress/element'; import { ExternalLink, @@ -26,7 +26,16 @@ import PositionedAtSelection from './positioned-at-selection'; const stopKeyPropagation = ( event ) => event.stopPropagation(); -function createLinkFormat( { url, opensInNewWindow } ) { +/** + * Generates the format object that will be applied to the link text. + * + * @param {string} url The href of the link. + * @param {boolean} opensInNewWindow Whether this link will open in a new window. + * @param {Object} text The text that is being hyperlinked. + * + * @return {Object} The final format object. + */ +function createLinkFormat( { url, opensInNewWindow, text } ) { const format = { type: 'core/link', attributes: { @@ -35,8 +44,12 @@ function createLinkFormat( { url, opensInNewWindow } ) { }; if ( opensInNewWindow ) { + // translators: accessibility label for external links, where the argument is the link text + const label = sprintf( __( '%s (opens in a new tab)' ), text ).trim(); + format.attributes.target = '_blank'; format.attributes.rel = 'noreferrer noopener'; + format.attributes[ 'aria-label' ] = label; } return format; @@ -139,7 +152,7 @@ class InlineLinkUI extends Component { // Apply now if URL is not being edited. if ( ! isShowingInput( this.props, this.state ) ) { - onChange( applyFormat( value, createLinkFormat( { url, opensInNewWindow } ) ) ); + onChange( applyFormat( value, createLinkFormat( { url, opensInNewWindow, text: value.text } ) ) ); } } @@ -152,7 +165,7 @@ class InlineLinkUI extends Component { const { isActive, value, onChange, speak } = this.props; const { inputValue, opensInNewWindow } = this.state; const url = prependHTTP( inputValue ); - const format = createLinkFormat( { url, opensInNewWindow } ); + const format = createLinkFormat( { url, opensInNewWindow, text: value.text } ); event.preventDefault(); From 0e83c98b817b4fd1c65a62eb9b993394cc9b54fa Mon Sep 17 00:00:00 2001 From: Timothy Jacobs Date: Tue, 30 Oct 2018 08:12:19 -0400 Subject: [PATCH 11/98] API Fetch: Handle 204 Response Code (#11208) Fixes #11179. Previously, API Fetch would try to always get the response body as JSON, even if the Response Status Code indicated that an empty response body was expected. --- packages/api-fetch/src/index.js | 4 ++++ packages/api-fetch/src/test/index.js | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/packages/api-fetch/src/index.js b/packages/api-fetch/src/index.js index 50ba344be9d6d7..c8e12a5a8ecc91 100644 --- a/packages/api-fetch/src/index.js +++ b/packages/api-fetch/src/index.js @@ -55,6 +55,10 @@ function apiFetch( options ) { const parseResponse = ( response ) => { if ( parse ) { + if ( response.status === 204 ) { + return null; + } + return response.json ? response.json() : Promise.reject( response ); } diff --git a/packages/api-fetch/src/test/index.js b/packages/api-fetch/src/test/index.js index 47aa4d2303e14f..220a618f17a3db 100644 --- a/packages/api-fetch/src/test/index.js +++ b/packages/api-fetch/src/test/index.js @@ -74,6 +74,16 @@ describe( 'apiFetch', () => { } ); } ); + it( 'should return null if response has no content status code', () => { + window.fetch.mockReturnValue( Promise.resolve( { + status: 204, + } ) ); + + return apiFetch( { path: '/random' } ).catch( ( body ) => { + expect( body ).toEqual( null ); + } ); + } ); + it( 'should not try to parse the response', () => { window.fetch.mockReturnValue( Promise.resolve( { status: 200, From db728b7f37272c6e1e9c17eb848db179f8183c89 Mon Sep 17 00:00:00 2001 From: Tammie Lister Date: Tue, 30 Oct 2018 12:35:53 +0000 Subject: [PATCH 12/98] Try different block descriptions (#10971) --- package-lock.json | 1 + packages/block-library/src/archives/index.js | 2 +- packages/block-library/src/audio/index.js | 2 +- packages/block-library/src/button/index.js | 2 +- .../block-library/src/categories/index.js | 2 +- packages/block-library/src/classic/index.js | 2 +- packages/block-library/src/code/index.js | 2 +- packages/block-library/src/cover/index.js | 2 +- .../block-library/src/embed/core-embeds.js | 33 +++++++++++++++++++ packages/block-library/src/embed/index.js | 2 +- packages/block-library/src/file/index.js | 2 +- packages/block-library/src/gallery/index.js | 2 +- packages/block-library/src/heading/index.js | 2 +- packages/block-library/src/html/index.js | 2 +- packages/block-library/src/image/index.js | 2 +- .../src/latest-comments/index.js | 2 +- packages/block-library/src/list/index.js | 2 +- .../block-library/src/media-text/index.js | 2 ++ packages/block-library/src/more/index.js | 2 +- packages/block-library/src/nextpage/index.js | 2 +- packages/block-library/src/paragraph/index.js | 2 +- packages/block-library/src/pullquote/index.js | 2 +- packages/block-library/src/quote/index.js | 2 +- packages/block-library/src/separator/index.js | 2 +- packages/block-library/src/shortcode/index.js | 2 +- packages/block-library/src/spacer/index.js | 2 +- packages/block-library/src/table/index.js | 2 +- packages/block-library/src/verse/index.js | 2 +- packages/block-library/src/video/index.js | 2 +- 29 files changed, 62 insertions(+), 26 deletions(-) diff --git a/package-lock.json b/package-lock.json index 06884a98691237..ed71b4aeeb4313 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2443,6 +2443,7 @@ "requires": { "@babel/runtime": "^7.0.0", "@wordpress/data": "file:packages/data", + "@wordpress/deprecated": "file:packages/deprecated", "@wordpress/escape-html": "file:packages/escape-html", "lodash": "^4.17.10", "rememo": "^3.0.0" diff --git a/packages/block-library/src/archives/index.js b/packages/block-library/src/archives/index.js index e978f7d6403415..8dfca64a8823cd 100644 --- a/packages/block-library/src/archives/index.js +++ b/packages/block-library/src/archives/index.js @@ -14,7 +14,7 @@ export const name = 'core/archives'; export const settings = { title: __( 'Archives' ), - description: __( 'Display a monthly archive of your site’s Posts.' ), + description: __( 'Display a monthly archive of your posts.' ), icon: , diff --git a/packages/block-library/src/audio/index.js b/packages/block-library/src/audio/index.js index 3161bb22e61c08..51a307102397d9 100644 --- a/packages/block-library/src/audio/index.js +++ b/packages/block-library/src/audio/index.js @@ -17,7 +17,7 @@ export const name = 'core/audio'; export const settings = { title: __( 'Audio' ), - description: __( 'Embed an audio file and a simple audio player.' ), + description: __( 'Embed a simple audio player.' ), icon: , diff --git a/packages/block-library/src/button/index.js b/packages/block-library/src/button/index.js index 24531e39eb42ae..5dddccfa760ca6 100644 --- a/packages/block-library/src/button/index.js +++ b/packages/block-library/src/button/index.js @@ -64,7 +64,7 @@ const colorsMigration = ( attributes ) => { export const settings = { title: __( 'Button' ), - description: __( 'Want visitors to click to subscribe, buy, or read more? Get their attention with a button.' ), + description: __( 'Prompt visitors to take action with a custom button.' ), icon: , diff --git a/packages/block-library/src/categories/index.js b/packages/block-library/src/categories/index.js index a51d734c33072c..ab9c198eff4615 100644 --- a/packages/block-library/src/categories/index.js +++ b/packages/block-library/src/categories/index.js @@ -14,7 +14,7 @@ export const name = 'core/categories'; export const settings = { title: __( 'Categories' ), - description: __( 'Display a list of all your site’s categories.' ), + description: __( 'Display a list of all categories.' ), icon: , diff --git a/packages/block-library/src/classic/index.js b/packages/block-library/src/classic/index.js index a8fd64e08daa0e..e30d7fa75eada5 100644 --- a/packages/block-library/src/classic/index.js +++ b/packages/block-library/src/classic/index.js @@ -15,7 +15,7 @@ export const name = 'core/freeform'; export const settings = { title: _x( 'Classic', 'block title' ), - description: __( 'It’s the classic WordPress editor and it’s a block! Drop the editor right in.' ), + description: __( 'Use the classic WordPress editor.' ), icon: , diff --git a/packages/block-library/src/code/index.js b/packages/block-library/src/code/index.js index badc677610aaeb..cb304079e1e2c7 100644 --- a/packages/block-library/src/code/index.js +++ b/packages/block-library/src/code/index.js @@ -18,7 +18,7 @@ export const name = 'core/code'; export const settings = { title: __( 'Code' ), - description: __( 'Add text that respects your spacing and tabs -- perfect for displaying code.' ), + description: __( 'Display code snippets that respect your spacing and tabs.' ), icon: , diff --git a/packages/block-library/src/cover/index.js b/packages/block-library/src/cover/index.js index 4004dfa17bdb84..b282c5c7aeb8ee 100644 --- a/packages/block-library/src/cover/index.js +++ b/packages/block-library/src/cover/index.js @@ -83,7 +83,7 @@ const VIDEO_BACKGROUND_TYPE = 'video'; export const settings = { title: __( 'Cover' ), - description: __( 'Add a full-width image or video, and layer text over it — great for headers.' ), + description: __( 'Add an image or video with a text overlay — great for headers.' ), icon: , diff --git a/packages/block-library/src/embed/core-embeds.js b/packages/block-library/src/embed/core-embeds.js index 004166d227c2fa..a38c47d5be21bd 100644 --- a/packages/block-library/src/embed/core-embeds.js +++ b/packages/block-library/src/embed/core-embeds.js @@ -31,6 +31,7 @@ export const common = [ title: 'Twitter', icon: embedTwitterIcon, keywords: [ 'tweet' ], + description: __( 'Embed a tweet' ), }, patterns: [ /^https?:\/\/(www\.)?twitter\.com\/.+/i ], }, @@ -40,6 +41,7 @@ export const common = [ title: 'YouTube', icon: embedYouTubeIcon, keywords: [ __( 'music' ), __( 'video' ) ], + description: __( 'Embed a YouTube video' ), }, patterns: [ /^https?:\/\/((m|www)\.)?youtube\.com\/.+/i, /^https?:\/\/youtu\.be\/.+/i ], }, @@ -48,6 +50,7 @@ export const common = [ settings: { title: 'Facebook', icon: embedFacebookIcon, + description: __( 'Embed a Facebook post' ), }, patterns: [ /^https?:\/\/www\.facebook.com\/.+/i ], }, @@ -57,6 +60,7 @@ export const common = [ title: 'Instagram', icon: embedInstagramIcon, keywords: [ __( 'image' ) ], + description: __( 'Embed an Instagram post' ), }, patterns: [ /^https?:\/\/(www\.)?instagr(\.am|am\.com)\/.+/i ], }, @@ -67,6 +71,7 @@ export const common = [ icon: embedWordPressIcon, keywords: [ __( 'post' ), __( 'blog' ) ], responsive: false, + description: __( 'Embed a WordPress post' ), }, }, { @@ -75,6 +80,7 @@ export const common = [ title: 'SoundCloud', icon: embedAudioIcon, keywords: [ __( 'music' ), __( 'audio' ) ], + description: __( 'Embed SoundCloud content' ), }, patterns: [ /^https?:\/\/(www\.)?soundcloud\.com\/.+/i ], }, @@ -84,6 +90,7 @@ export const common = [ title: 'Spotify', icon: embedSpotifyIcon, keywords: [ __( 'music' ), __( 'audio' ) ], + description: __( 'Embed Spotify content' ), }, patterns: [ /^https?:\/\/(open|play)\.spotify\.com\/.+/i ], }, @@ -93,6 +100,7 @@ export const common = [ title: 'Flickr', icon: embedFlickrIcon, keywords: [ __( 'image' ) ], + description: __( 'Embed Flickr content' ), }, patterns: [ /^https?:\/\/(www\.)?flickr\.com\/.+/i, /^https?:\/\/flic\.kr\/.+/i ], }, @@ -102,6 +110,7 @@ export const common = [ title: 'Vimeo', icon: embedVimeoIcon, keywords: [ __( 'video' ) ], + description: __( 'Embed a Vimeo video' ), }, patterns: [ /^https?:\/\/(www\.)?vimeo\.com\/.+/i ], }, @@ -113,6 +122,7 @@ export const others = [ settings: { title: 'Animoto', icon: embedVideoIcon, + description: __( 'Embed an Animoto video' ), }, patterns: [ /^https?:\/\/(www\.)?(animoto|video214)\.com\/.+/i ], }, @@ -121,6 +131,7 @@ export const others = [ settings: { title: 'Cloudup', icon: embedContentIcon, + description: __( 'Embed Cloudup content' ), }, patterns: [ /^https?:\/\/cloudup\.com\/.+/i ], }, @@ -129,6 +140,7 @@ export const others = [ settings: { title: 'CollegeHumor', icon: embedVideoIcon, + description: __( 'Embed CollegeHumor content' ), }, patterns: [ /^https?:\/\/(www\.)?collegehumor\.com\/.+/i ], }, @@ -137,6 +149,7 @@ export const others = [ settings: { title: 'Dailymotion', icon: embedVideoIcon, + description: __( 'Embed a Dailymotion video' ), }, patterns: [ /^https?:\/\/(www\.)?dailymotion\.com\/.+/i ], }, @@ -145,6 +158,7 @@ export const others = [ settings: { title: 'Funny or Die', icon: embedVideoIcon, + description: __( 'Embed Funny or Die content' ), }, patterns: [ /^https?:\/\/(www\.)?funnyordie\.com\/.+/i ], }, @@ -153,6 +167,7 @@ export const others = [ settings: { title: 'Hulu', icon: embedVideoIcon, + description: __( 'Embed Hulu content' ), }, patterns: [ /^https?:\/\/(www\.)?hulu\.com\/.+/i ], }, @@ -161,6 +176,7 @@ export const others = [ settings: { title: 'Imgur', icon: embedPhotoIcon, + description: __( 'Embed Imgur content' ), }, patterns: [ /^https?:\/\/(.+\.)?imgur\.com\/.+/i ], }, @@ -169,6 +185,7 @@ export const others = [ settings: { title: 'Issuu', icon: embedContentIcon, + description: __( 'Embed Issuu content' ), }, patterns: [ /^https?:\/\/(www\.)?issuu\.com\/.+/i ], }, @@ -177,6 +194,7 @@ export const others = [ settings: { title: 'Kickstarter', icon: embedContentIcon, + description: __( 'Embed Kickstarter content' ), }, patterns: [ /^https?:\/\/(www\.)?kickstarter\.com\/.+/i, /^https?:\/\/kck\.st\/.+/i ], }, @@ -185,6 +203,7 @@ export const others = [ settings: { title: 'Meetup.com', icon: embedContentIcon, + description: __( 'Embed Meetup.com content' ), }, patterns: [ /^https?:\/\/(www\.)?meetu(\.ps|p\.com)\/.+/i ], }, @@ -194,6 +213,7 @@ export const others = [ title: 'Mixcloud', icon: embedAudioIcon, keywords: [ __( 'music' ), __( 'audio' ) ], + description: __( 'Embed Mixcloud content' ), }, patterns: [ /^https?:\/\/(www\.)?mixcloud\.com\/.+/i ], }, @@ -202,6 +222,7 @@ export const others = [ settings: { title: 'Photobucket', icon: embedPhotoIcon, + description: __( 'Embed a Photobucket image' ), }, patterns: [ /^http:\/\/g?i*\.photobucket\.com\/.+/i ], }, @@ -210,6 +231,7 @@ export const others = [ settings: { title: 'Polldaddy', icon: embedContentIcon, + description: __( 'Embed Polldaddy content' ), }, patterns: [ /^https?:\/\/(www\.)?polldaddy\.com\/.+/i ], }, @@ -218,6 +240,7 @@ export const others = [ settings: { title: 'Reddit', icon: embedRedditIcon, + description: __( 'Embed a Reddit thread' ), }, patterns: [ /^https?:\/\/(www\.)?reddit\.com\/.+/i ], }, @@ -226,6 +249,7 @@ export const others = [ settings: { title: 'ReverbNation', icon: embedAudioIcon, + description: __( 'Embed ReverbNation content' ), }, patterns: [ /^https?:\/\/(www\.)?reverbnation\.com\/.+/i ], }, @@ -234,6 +258,7 @@ export const others = [ settings: { title: 'Screencast', icon: embedVideoIcon, + description: __( 'Embed Screencast content' ), }, patterns: [ /^https?:\/\/(www\.)?screencast\.com\/.+/i ], }, @@ -242,6 +267,7 @@ export const others = [ settings: { title: 'Scribd', icon: embedContentIcon, + description: __( 'Embed Scribd content' ), }, patterns: [ /^https?:\/\/(www\.)?scribd\.com\/.+/i ], }, @@ -250,6 +276,7 @@ export const others = [ settings: { title: 'Slideshare', icon: embedContentIcon, + description: __( 'Embed Slideshare content' ), }, patterns: [ /^https?:\/\/(.+?\.)?slideshare\.net\/.+/i ], }, @@ -258,6 +285,7 @@ export const others = [ settings: { title: 'SmugMug', icon: embedPhotoIcon, + description: __( 'Embed SmugMug content' ), }, patterns: [ /^https?:\/\/(www\.)?smugmug\.com\/.+/i ], }, @@ -287,6 +315,7 @@ export const others = [ } ); }, } ], + description: __( 'Embed Speaker Deck content' ), }, patterns: [ /^https?:\/\/(www\.)?speakerdeck\.com\/.+/i ], }, @@ -295,6 +324,7 @@ export const others = [ settings: { title: 'TED', icon: embedVideoIcon, + description: __( 'Embed a TED video' ), }, patterns: [ /^https?:\/\/(www\.|embed\.)?ted\.com\/.+/i ], }, @@ -303,6 +333,7 @@ export const others = [ settings: { title: 'Tumblr', icon: embedTumbrIcon, + description: __( 'Embed a Tumblr post' ), }, patterns: [ /^https?:\/\/(www\.)?tumblr\.com\/.+/i ], }, @@ -312,6 +343,7 @@ export const others = [ title: 'VideoPress', icon: embedVideoIcon, keywords: [ __( 'video' ) ], + description: __( 'Embed a VideoPress video' ), }, patterns: [ /^https?:\/\/videopress\.com\/.+/i ], }, @@ -320,6 +352,7 @@ export const others = [ settings: { title: 'WordPress.tv', icon: embedVideoIcon, + description: __( 'Embed a WordPress.tv video' ), }, patterns: [ /^https?:\/\/wordpress\.tv\/.+/i ], }, diff --git a/packages/block-library/src/embed/index.js b/packages/block-library/src/embed/index.js index 4331e331c555dd..7bf859a002373d 100644 --- a/packages/block-library/src/embed/index.js +++ b/packages/block-library/src/embed/index.js @@ -15,7 +15,7 @@ export const name = 'core/embed'; export const settings = getEmbedBlockSettings( { title: _x( 'Embed', 'block title' ), - description: __( 'The Embed block allows you to easily add videos, images, tweets, audio, and other content to your post or page.' ), + description: __( 'Embed videos, images, tweets, audio, and other content from external sources.' ), icon: embedContentIcon, // Unknown embeds should not be responsive by default. responsive: false, diff --git a/packages/block-library/src/file/index.js b/packages/block-library/src/file/index.js index 51252abf205803..7f26a4abc025e4 100644 --- a/packages/block-library/src/file/index.js +++ b/packages/block-library/src/file/index.js @@ -23,7 +23,7 @@ export const name = 'core/file'; export const settings = { title: __( 'File' ), - description: __( 'Add a link to a file that visitors can download.' ), + description: __( 'Add a link to a downloadable file.' ), icon: , diff --git a/packages/block-library/src/gallery/index.js b/packages/block-library/src/gallery/index.js index 1b24c82461ab66..e347c806d2af37 100644 --- a/packages/block-library/src/gallery/index.js +++ b/packages/block-library/src/gallery/index.js @@ -69,7 +69,7 @@ export const name = 'core/gallery'; export const settings = { title: __( 'Gallery' ), - description: __( 'Display multiple images in an elegantly organized tiled layout.' ), + description: __( 'Display multiple images in a rich gallery.' ), icon: , category: 'common', keywords: [ __( 'images' ), __( 'photos' ) ], diff --git a/packages/block-library/src/heading/index.js b/packages/block-library/src/heading/index.js index 4772818bd238d4..6f98add18effe0 100644 --- a/packages/block-library/src/heading/index.js +++ b/packages/block-library/src/heading/index.js @@ -64,7 +64,7 @@ export const name = 'core/heading'; export const settings = { title: __( 'Heading' ), - description: __( 'Introduce topics and help visitors (and search engines!) understand how your content is organized.' ), + description: __( 'Introduce new sections and organize content to help visitors (and search engines) understand the structure of your content.' ), icon: , diff --git a/packages/block-library/src/html/index.js b/packages/block-library/src/html/index.js index 5243df8962f49c..c7307aa53e9b49 100644 --- a/packages/block-library/src/html/index.js +++ b/packages/block-library/src/html/index.js @@ -13,7 +13,7 @@ export const name = 'core/html'; export const settings = { title: __( 'Custom HTML' ), - description: __( 'Add your own HTML (and view it right here as you edit!).' ), + description: __( 'Add custom HTML code and preview it as you edit.' ), icon: , diff --git a/packages/block-library/src/image/index.js b/packages/block-library/src/image/index.js index 00ea17c772cfe8..83a022388d35dc 100644 --- a/packages/block-library/src/image/index.js +++ b/packages/block-library/src/image/index.js @@ -103,7 +103,7 @@ const schema = { export const settings = { title: __( 'Image' ), - description: __( 'They’re worth 1,000 words! Insert a single image.' ), + description: __( 'Insert an image to make a visual statement.' ), icon: , diff --git a/packages/block-library/src/latest-comments/index.js b/packages/block-library/src/latest-comments/index.js index 61310f4ebda144..d7420744d0011d 100644 --- a/packages/block-library/src/latest-comments/index.js +++ b/packages/block-library/src/latest-comments/index.js @@ -14,7 +14,7 @@ export const name = 'core/latest-comments'; export const settings = { title: __( 'Latest Comments' ), - description: __( 'Show a list of your site’s most recent comments.' ), + description: __( 'Display a list of your most recent comments.' ), icon: , diff --git a/packages/block-library/src/list/index.js b/packages/block-library/src/list/index.js index c5683dc5fe82db..e7c38a543f56b3 100644 --- a/packages/block-library/src/list/index.js +++ b/packages/block-library/src/list/index.js @@ -60,7 +60,7 @@ export const name = 'core/list'; export const settings = { title: __( 'List' ), - description: __( 'Numbers, bullets, up to you. Add a list of items.' ), + description: __( 'Create a bulleted or numbered list.' ), icon: , category: 'common', keywords: [ __( 'bullet list' ), __( 'ordered list' ), __( 'numbered list' ) ], diff --git a/packages/block-library/src/media-text/index.js b/packages/block-library/src/media-text/index.js index 14a3a3281f0936..d82d86228091e2 100644 --- a/packages/block-library/src/media-text/index.js +++ b/packages/block-library/src/media-text/index.js @@ -26,6 +26,8 @@ export const name = 'core/media-text'; export const settings = { title: __( 'Media & Text' ), + description: __( 'Set media and words side-by-side media for a richer layout.' ), + icon: , category: 'layout', diff --git a/packages/block-library/src/more/index.js b/packages/block-library/src/more/index.js index a42f1025723c0d..9334ed2e0941c2 100644 --- a/packages/block-library/src/more/index.js +++ b/packages/block-library/src/more/index.js @@ -25,7 +25,7 @@ export const name = 'core/more'; export const settings = { title: _x( 'More', 'block name' ), - description: __( 'Want to show only part of this post on your blog’s home page? Insert a "More" block where you want the split.' ), + description: __( 'Want to show only an excerpt of this post on your home page? Use this block to define where you want the separation.' ), icon: , diff --git a/packages/block-library/src/nextpage/index.js b/packages/block-library/src/nextpage/index.js index f5d35e4eb3073c..02114bd72c0788 100644 --- a/packages/block-library/src/nextpage/index.js +++ b/packages/block-library/src/nextpage/index.js @@ -11,7 +11,7 @@ export const name = 'core/nextpage'; export const settings = { title: __( 'Page break' ), - description: __( 'This block allows you to set break points on your post. Visitors of your blog are then presented with content split into multiple pages.' ), + description: __( 'Separate your content into a multi-page experience.' ), icon: , diff --git a/packages/block-library/src/paragraph/index.js b/packages/block-library/src/paragraph/index.js index 6465c43e48391c..e315f551cc9f3f 100644 --- a/packages/block-library/src/paragraph/index.js +++ b/packages/block-library/src/paragraph/index.js @@ -77,7 +77,7 @@ export const name = 'core/paragraph'; export const settings = { title: __( 'Paragraph' ), - description: __( 'Add some basic text.' ), + description: __( 'Start with the building block of all narrative.' ), icon: , diff --git a/packages/block-library/src/pullquote/index.js b/packages/block-library/src/pullquote/index.js index 3693fc2db2190e..1dc6126394678a 100644 --- a/packages/block-library/src/pullquote/index.js +++ b/packages/block-library/src/pullquote/index.js @@ -57,7 +57,7 @@ export const settings = { title: __( 'Pullquote' ), - description: __( 'Highlight a quote from your post or page by displaying it as a graphic element.' ), + description: __( 'Give special visual emphasis to a quote from your text.' ), icon: , diff --git a/packages/block-library/src/quote/index.js b/packages/block-library/src/quote/index.js index bd226c2bc9a7f3..a87b0dba274534 100644 --- a/packages/block-library/src/quote/index.js +++ b/packages/block-library/src/quote/index.js @@ -40,7 +40,7 @@ export const name = 'core/quote'; export const settings = { title: __( 'Quote' ), - description: __( 'Maybe someone else said it better -- add some quoted text.' ), + description: __( 'Give quoted text visual emphasis. "In quoting others, we cite ourselves." — Julio Cortázar' ), icon: , category: 'common', keywords: [ __( 'blockquote' ) ], diff --git a/packages/block-library/src/separator/index.js b/packages/block-library/src/separator/index.js index a85576d97b3ad1..f077d6533413de 100644 --- a/packages/block-library/src/separator/index.js +++ b/packages/block-library/src/separator/index.js @@ -10,7 +10,7 @@ export const name = 'core/separator'; export const settings = { title: __( 'Separator' ), - description: __( 'Insert a horizontal line where you want to create a break between ideas.' ), + description: __( 'Create a break between ideas or sections with a horizontal separator.' ), icon: , diff --git a/packages/block-library/src/shortcode/index.js b/packages/block-library/src/shortcode/index.js index e5fd517798d6cc..06464c52e7082e 100644 --- a/packages/block-library/src/shortcode/index.js +++ b/packages/block-library/src/shortcode/index.js @@ -13,7 +13,7 @@ export const name = 'core/shortcode'; export const settings = { title: __( 'Shortcode' ), - description: __( 'Add a shortcode -- a WordPress-specific snippet of code written between square brackets.' ), + description: __( 'Insert additional custom elements with a WordPress shortcode.' ), icon: , diff --git a/packages/block-library/src/spacer/index.js b/packages/block-library/src/spacer/index.js index 6b66cf4eadd533..12f362b40f623e 100644 --- a/packages/block-library/src/spacer/index.js +++ b/packages/block-library/src/spacer/index.js @@ -17,7 +17,7 @@ export const name = 'core/spacer'; export const settings = { title: __( 'Spacer' ), - description: __( 'Add an element with empty space and custom height.' ), + description: __( 'Add white space between blocks and customize its height.' ), icon: , diff --git a/packages/block-library/src/table/index.js b/packages/block-library/src/table/index.js index a3fb8d91590f8b..aff5c7ca5be8cb 100644 --- a/packages/block-library/src/table/index.js +++ b/packages/block-library/src/table/index.js @@ -77,7 +77,7 @@ export const name = 'core/table'; export const settings = { title: __( 'Table' ), - description: __( 'Insert a table -- perfect for sharing charts and data.' ), + description: __( 'Insert a table — perfect for sharing charts and data.' ), icon: , category: 'formatting', diff --git a/packages/block-library/src/verse/index.js b/packages/block-library/src/verse/index.js index c5072386553c80..e70bc44550735c 100644 --- a/packages/block-library/src/verse/index.js +++ b/packages/block-library/src/verse/index.js @@ -16,7 +16,7 @@ export const name = 'core/verse'; export const settings = { title: __( 'Verse' ), - description: __( 'A block for haiku? Why not? Blocks for all the things! (See what we did here?)' ), + description: __( 'Insert poetry. Use special spacing formats. Or quote song lyrics.' ), icon: , diff --git a/packages/block-library/src/video/index.js b/packages/block-library/src/video/index.js index 8947158c467814..ac20d15dcaa336 100644 --- a/packages/block-library/src/video/index.js +++ b/packages/block-library/src/video/index.js @@ -17,7 +17,7 @@ export const name = 'core/video'; export const settings = { title: __( 'Video' ), - description: __( 'Embed a video file and a simple video player.' ), + description: __( 'Embed a video from your media library or upload a new one.' ), icon: , From 646c531dda07411ad204c523c45431daba03d9af Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Tue, 30 Oct 2018 13:46:31 +0100 Subject: [PATCH 13/98] Fix multiselecting blocks using shift + arrow (#11237) * Fix multiselecting blocks using shift + arrow * Test multiple shift + arrows selections * Fix end2end test failures --- .../editor/src/components/block-list/block.js | 12 +++- .../src/components/block-toolbar/index.js | 70 +++++-------------- test/e2e/specs/multi-block-selection.test.js | 42 ++++++++++- 3 files changed, 71 insertions(+), 53 deletions(-) diff --git a/packages/editor/src/components/block-list/block.js b/packages/editor/src/components/block-list/block.js index 30b3806aad5bd0..1213d1c424453e 100644 --- a/packages/editor/src/components/block-list/block.js +++ b/packages/editor/src/components/block-list/block.js @@ -93,6 +93,12 @@ export class BlockListBlock extends Component { if ( this.props.isSelected && ! prevProps.isSelected ) { this.focusTabbable( true ); } + + // When triggering a multi-selection, + // move the focus to the wrapper of the first selected block. + if ( this.props.isFirstMultiSelected && ! prevProps.isFirstMultiSelected ) { + this.wrapperNode.focus(); + } } setBlockListRef( node ) { @@ -314,7 +320,11 @@ export class BlockListBlock extends Component { deleteOrInsertAfterWrapper( event ) { const { keyCode, target } = event; - if ( target !== this.wrapperNode || this.props.isLocked ) { + if ( + ! this.props.isSelected || + target !== this.wrapperNode || + this.props.isLocked + ) { return; } diff --git a/packages/editor/src/components/block-toolbar/index.js b/packages/editor/src/components/block-toolbar/index.js index de34bf2747ec25..511ed1d7da0536 100644 --- a/packages/editor/src/components/block-toolbar/index.js +++ b/packages/editor/src/components/block-toolbar/index.js @@ -2,8 +2,7 @@ * WordPress Dependencies */ import { withSelect } from '@wordpress/data'; -import { Component, createRef, Fragment } from '@wordpress/element'; -import { focus } from '@wordpress/dom'; +import { Fragment } from '@wordpress/element'; /** * Internal Dependencies @@ -14,63 +13,32 @@ import BlockControls from '../block-controls'; import BlockFormatControls from '../block-format-controls'; import BlockSettingsMenu from '../block-settings-menu'; -class BlockToolbar extends Component { - constructor() { - super( ...arguments ); - this.container = createRef(); +function BlockToolbar( { blockClientIds, isValid, mode } ) { + if ( blockClientIds.length === 0 ) { + return null; } - componentDidMount() { - if ( this.props.blockClientIds.length > 1 ) { - this.focusContainer(); - } - } - - componentDidUpdate( prevProps ) { - if ( - prevProps.blockClientIds.length <= 1 && - this.props.blockClientIds.length > 1 - ) { - this.focusContainer(); - } - } - - focusContainer() { - const tabbables = focus.tabbable.find( this.container.current ); - if ( tabbables.length ) { - tabbables[ 0 ].focus(); - } - } - - render() { - const { blockClientIds, isValid, mode } = this.props; - - if ( blockClientIds.length === 0 ) { - return null; - } - - if ( blockClientIds.length > 1 ) { - return ( -
    - - -
    - ); - } - + if ( blockClientIds.length > 1 ) { return (
    - { mode === 'visual' && isValid && ( - - - - - - ) } +
    ); } + + return ( +
    + { mode === 'visual' && isValid && ( + + + + + + ) } + +
    + ); } export default withSelect( ( select ) => { diff --git a/test/e2e/specs/multi-block-selection.test.js b/test/e2e/specs/multi-block-selection.test.js index d341f099bf3b1e..866b9465834a85 100644 --- a/test/e2e/specs/multi-block-selection.test.js +++ b/test/e2e/specs/multi-block-selection.test.js @@ -10,7 +10,7 @@ import { } from '../support/utils'; describe( 'Multi-block selection', () => { - beforeAll( async () => { + beforeEach( async () => { await newPost(); } ); @@ -77,4 +77,44 @@ describe( 'Multi-block selection', () => { await pressWithModifier( META_KEY, 'a' ); await expectMultiSelected( blocks, true ); } ); + + it( 'Should select/unselect multiple blocks using Shift + Arrows', async () => { + const firstBlockSelector = '[data-type="core/paragraph"]'; + const secondBlockSelector = '[data-type="core/image"]'; + const thirdBlockSelector = '[data-type="core/quote"]'; + const multiSelectedCssClass = 'is-multi-selected'; + + // Creating test blocks + await clickBlockAppender(); + await page.keyboard.type( 'First Paragraph' ); + await insertBlock( 'Image' ); + await insertBlock( 'Quote' ); + await page.keyboard.type( 'Quote Block' ); + + const blocks = [ firstBlockSelector, secondBlockSelector, thirdBlockSelector ]; + const expectMultiSelected = async ( selectors, areMultiSelected ) => { + for ( const selector of selectors ) { + const className = await page.$eval( selector, ( element ) => element.className ); + if ( areMultiSelected ) { + expect( className ).toEqual( expect.stringContaining( multiSelectedCssClass ) ); + } else { + expect( className ).not.toEqual( expect.stringContaining( multiSelectedCssClass ) ); + } + } + }; + + // Default: No selection + await expectMultiSelected( blocks, false ); + + // Multiselect via Shift + click + await page.mouse.move( 200, 300 ); + await page.click( firstBlockSelector ); + await page.keyboard.down( 'Shift' ); + await page.keyboard.press( 'ArrowDown' ); // Two blocks selected + await page.keyboard.press( 'ArrowDown' ); // Three blocks selected + await page.keyboard.up( 'Shift' ); + + // Verify selection + await expectMultiSelected( blocks, true ); + } ); } ); From cc53d7c48e08370120527e8d7afe58f54a8a6c20 Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Tue, 30 Oct 2018 11:06:25 -0300 Subject: [PATCH 14/98] RNMobile: Properly refresh blocks when merging them under iOS. (#11220) * Call onMerge and try to merge blocks for Para blocks * Wire `onMerge` on Heading block * Revert update on GB-mobile hash * Resolves most Xcode 10 warnings. --- packages/editor/src/components/rich-text/index.native.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/editor/src/components/rich-text/index.native.js b/packages/editor/src/components/rich-text/index.native.js index 7d44b942d47d8e..71cd3b85d24228 100644 --- a/packages/editor/src/components/rich-text/index.native.js +++ b/packages/editor/src/components/rich-text/index.native.js @@ -227,6 +227,12 @@ export class RichText extends Component { const empty = this.isEmpty(); if ( onMerge ) { + // The onMerge event can cause a content update event for this block. Such event should + // definitely be processed by our native components, since they have no knowledge of + // how the split works. Setting lastEventCount to undefined forces the native component to + // always update when provided with new content. + this.lastEventCount = undefined; + onMerge( ! isReverse ); } From fe51bd08684462690988de7ec61a3cdda083710a Mon Sep 17 00:00:00 2001 From: Mike Selander Date: Tue, 30 Oct 2018 08:54:13 -0600 Subject: [PATCH 15/98] Filter DropZone and MediaPlaceholder components (#11184) * Export Media Placeholder withFilters * Export Block Drop Zone withFilters * Update test snapshots * Appropriately name DropZone filter * Add super-simple README files * Move withFilters call into the compose function * Update README.md * Update README.md --- .../src/components/block-drop-zone/README.md | 30 +++++++++++++++++++ .../src/components/block-drop-zone/index.js | 8 +++-- .../test/__snapshots__/index.js.snap | 6 ++-- .../components/media-placeholder/README.md | 30 +++++++++++++++++++ .../src/components/media-placeholder/index.js | 3 +- 5 files changed, 71 insertions(+), 6 deletions(-) create mode 100644 packages/editor/src/components/block-drop-zone/README.md create mode 100644 packages/editor/src/components/media-placeholder/README.md diff --git a/packages/editor/src/components/block-drop-zone/README.md b/packages/editor/src/components/block-drop-zone/README.md new file mode 100644 index 00000000000000..ed979c19c184a7 --- /dev/null +++ b/packages/editor/src/components/block-drop-zone/README.md @@ -0,0 +1,30 @@ +BlockDropZone +=========== + +`BlockDropZone` is a React component that renders a container which allows a user to drag media into the editor and immediately place it. + +## Setup + +It includes a `wp.hooks` filter `editor.BlockDropZone` that enables developers to replace or extend it. + +_Example:_ + +Replace implementation of the drop zone: + +```js +function replaceBlockDropZone() { + return function() { + return wp.element.createElement( + 'div', + {}, + 'The replacement contents or components.' + ); + } +} + +wp.hooks.addFilter( + 'editor.BlockDropZone', + 'my-plugin/replace-block-drop-zone', + replaceBlockDropZone +); +``` diff --git a/packages/editor/src/components/block-drop-zone/index.js b/packages/editor/src/components/block-drop-zone/index.js index 5c4df43bb4ad51..885a54fe5a4d73 100644 --- a/packages/editor/src/components/block-drop-zone/index.js +++ b/packages/editor/src/components/block-drop-zone/index.js @@ -6,7 +6,10 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { DropZone } from '@wordpress/components'; +import { + DropZone, + withFilters, +} from '@wordpress/components'; import { rawHandler, getBlockTransforms, @@ -149,5 +152,6 @@ export default compose( isLocked: !! getTemplateLock( rootClientId ), getClientIdsOfDescendants, }; - } ) + } ), + withFilters( 'editor.BlockDropZone' ) )( BlockDropZone ); diff --git a/packages/editor/src/components/default-block-appender/test/__snapshots__/index.js.snap b/packages/editor/src/components/default-block-appender/test/__snapshots__/index.js.snap index a3b6b410ce3eef..46c017374b5149 100644 --- a/packages/editor/src/components/default-block-appender/test/__snapshots__/index.js.snap +++ b/packages/editor/src/components/default-block-appender/test/__snapshots__/index.js.snap @@ -5,7 +5,7 @@ exports[`DefaultBlockAppender should append a default block when input focused 1 className="wp-block editor-default-block-appender" data-root-client-id="" > - + - + - + Date: Wed, 31 Oct 2018 02:11:25 +1100 Subject: [PATCH 16/98] Reusable blocks: Mark API as experimental (#11230) * Reusable blocks: Mark API as experimental The reusable block selectors and actions will need to change in the future. Give ourselves some breathing room to make these changes after Gutenberg 4.2 by marking them as experimental. * Add notes about deprecations added for Reusable Blocks Data API --- docs/data/data-core-editor.md | 26 +++--- docs/reference/deprecated.md | 1 + packages/block-library/src/block/edit.js | 12 +-- packages/editor/CHANGELOG.md | 6 ++ .../reusable-block-convert-button.js | 10 +- .../reusable-block-delete-button.js | 7 +- .../editor/src/components/inserter/menu.js | 2 +- .../src/hooks/default-autocompleters.js | 2 +- packages/editor/src/store/actions.js | 91 +++++++++++++++++-- .../src/store/effects/reusable-blocks.js | 6 +- .../src/store/effects/test/reusable-blocks.js | 12 +-- packages/editor/src/store/selectors.js | 56 ++++++++++-- packages/editor/src/store/test/actions.js | 10 +- packages/editor/src/store/test/selectors.js | 8 +- 14 files changed, 193 insertions(+), 56 deletions(-) diff --git a/docs/data/data-core-editor.md b/docs/data/data-core-editor.md index a4fe2f576da271..4ce76d80ea62a8 100644 --- a/docs/data/data-core-editor.md +++ b/docs/data/data-core-editor.md @@ -1120,7 +1120,7 @@ Items are returned ordered descendingly by their 'utility' and 'frecency'. Items that appear in inserter. -### getReusableBlock +### __experimentalGetReusableBlock Returns the reusable block with the given ID. @@ -1133,7 +1133,7 @@ Returns the reusable block with the given ID. The reusable block, or null if none exists. -### isSavingReusableBlock +### __experimentalIsSavingReusableBlock Returns whether or not the reusable block with the given ID is being saved. @@ -1146,7 +1146,7 @@ Returns whether or not the reusable block with the given ID is being saved. Whether or not the reusable block is being saved. -### isFetchingReusableBlock +### __experimentalIsFetchingReusableBlock Returns true if the reusable block with the given ID is being fetched, or false otherwise. @@ -1160,7 +1160,7 @@ false otherwise. Whether the reusable block is being fetched. -### getReusableBlocks +### __experimentalGetReusableBlocks Returns an array of all reusable blocks. @@ -1649,7 +1649,7 @@ Returns an action object used to lock the editor. * lock: Details about the post lock status, user, and nonce. -### fetchReusableBlocks +### __experimentalFetchReusableBlocks Returns an action object used to fetch a single reusable block or all reusable blocks from the REST API into the store. @@ -1659,7 +1659,7 @@ reusable blocks from the REST API into the store. * id: If given, only a single reusable block with this ID will be fetched. -### receiveReusableBlocks +### __experimentalReceiveReusableBlocks Returns an action object used in signalling that reusable blocks have been received. `results` is an array of objects containing: @@ -1670,7 +1670,7 @@ received. `results` is an array of objects containing: * results: Reusable blocks received. -### saveReusableBlock +### __experimentalSaveReusableBlock Returns an action object used to save a reusable block that's in the store to the REST API. @@ -1679,7 +1679,7 @@ the REST API. * id: The ID of the reusable block to save. -### deleteReusableBlock +### __experimentalDeleteReusableBlock Returns an action object used to delete a reusable block via the REST API. @@ -1687,7 +1687,7 @@ Returns an action object used to delete a reusable block via the REST API. * id: The ID of the reusable block to delete. -### updateReusableBlockTitle +### __experimentalUpdateReusableBlockTitle Returns an action object used in signalling that a reusable block's title is to be updated. @@ -1697,7 +1697,7 @@ to be updated. * id: The ID of the reusable block to update. * title: The new title. -### convertBlockToStatic +### __experimentalConvertBlockToStatic Returns an action object used to convert a reusable block into a static block. @@ -1705,7 +1705,7 @@ Returns an action object used to convert a reusable block into a static block. * clientId: The client ID of the block to attach. -### convertBlockToReusable +### __experimentalConvertBlockToReusable Returns an action object used to convert a static block into a reusable block. @@ -1776,4 +1776,6 @@ Returns an action object signaling that a new term is added to the edited post. * slug: Taxonomy slug. * term: Term object. -### createNotice \ No newline at end of file +### createNotice + +### fetchReusableBlocks \ No newline at end of file diff --git a/docs/reference/deprecated.md b/docs/reference/deprecated.md index bae63e3d84877e..252b56c2ef809d 100644 --- a/docs/reference/deprecated.md +++ b/docs/reference/deprecated.md @@ -31,6 +31,7 @@ Gutenberg's deprecation policy is intended to support backwards-compatibility fo - `wp.components.CodeEditor` has been removed. Used `wp.codeEditor` directly instead. - `wp.blocks.setUnknownTypeHandlerName` has been removed. Please use `setFreeformContentHandlerName` and `setUnregisteredTypeHandlerName` instead. - `wp.blocks.getUnknownTypeHandlerName` has been removed. Please use `getFreeformContentHandlerName` and `getUnregisteredTypeHandlerName` instead. +- The Reusable Blocks Data API was marked as experimental as it's subject to change in the future. ## 4.1.0 diff --git a/packages/block-library/src/block/edit.js b/packages/block-library/src/block/edit.js index fb523cd5c95373..d95b07aedb9150 100644 --- a/packages/block-library/src/block/edit.js +++ b/packages/block-library/src/block/edit.js @@ -146,9 +146,9 @@ class ReusableBlockEdit extends Component { export default compose( [ withSelect( ( select, ownProps ) => { const { - getReusableBlock, - isFetchingReusableBlock, - isSavingReusableBlock, + __experimentalGetReusableBlock: getReusableBlock, + __experimentalIsFetchingReusableBlock: isFetchingReusableBlock, + __experimentalIsSavingReusableBlock: isSavingReusableBlock, getBlock, } = select( 'core/editor' ); const { ref } = ownProps.attributes; @@ -163,10 +163,10 @@ export default compose( [ } ), withDispatch( ( dispatch, ownProps ) => { const { - fetchReusableBlocks, + __experimentalFetchReusableBlocks: fetchReusableBlocks, updateBlockAttributes, - updateReusableBlockTitle, - saveReusableBlock, + __experimentalUpdateReusableBlockTitle: updateReusableBlockTitle, + __experimentalSaveReusableBlock: saveReusableBlock, } = dispatch( 'core/editor' ); const { ref } = ownProps.attributes; diff --git a/packages/editor/CHANGELOG.md b/packages/editor/CHANGELOG.md index 70cb95e06f342a..8ef9c5cce31eaf 100644 --- a/packages/editor/CHANGELOG.md +++ b/packages/editor/CHANGELOG.md @@ -1,3 +1,9 @@ +## 6.1.0 (Unreleased) + +### Deprecations + +- The Reusable Blocks Data API is marked as experimental as it's subject to change in the future ([#11230](https://github.com/WordPress/gutenberg/pull/11230)). + ## 6.0.1 (2018-10-30) ### Bug Fixes diff --git a/packages/editor/src/components/block-settings-menu/reusable-block-convert-button.js b/packages/editor/src/components/block-settings-menu/reusable-block-convert-button.js index b30569720edfb4..84530fb22bbef3 100644 --- a/packages/editor/src/components/block-settings-menu/reusable-block-convert-button.js +++ b/packages/editor/src/components/block-settings-menu/reusable-block-convert-button.js @@ -49,7 +49,11 @@ export function ReusableBlockConvertButton( { export default compose( [ withSelect( ( select, { clientIds } ) => { - const { getBlock, canInsertBlockType, getReusableBlock } = select( 'core/editor' ); + const { + getBlock, + canInsertBlockType, + __experimentalGetReusableBlock: getReusableBlock, + } = select( 'core/editor' ); const { getFreeformFallbackBlockName, getUnregisteredFallbackBlockName, @@ -85,8 +89,8 @@ export default compose( [ } ), withDispatch( ( dispatch, { clientIds, onToggle = noop } ) => { const { - convertBlockToReusable, - convertBlockToStatic, + __experimentalConvertBlockToReusable: convertBlockToReusable, + __experimentalConvertBlockToStatic: convertBlockToStatic, } = dispatch( 'core/editor' ); return { diff --git a/packages/editor/src/components/block-settings-menu/reusable-block-delete-button.js b/packages/editor/src/components/block-settings-menu/reusable-block-delete-button.js index 537af329a9d3aa..52f2ad08108f93 100644 --- a/packages/editor/src/components/block-settings-menu/reusable-block-delete-button.js +++ b/packages/editor/src/components/block-settings-menu/reusable-block-delete-button.js @@ -31,7 +31,10 @@ export function ReusableBlockDeleteButton( { reusableBlock, onDelete } ) { export default compose( [ withSelect( ( select, { clientId } ) => { - const { getBlock, getReusableBlock } = select( 'core/editor' ); + const { + getBlock, + __experimentalGetReusableBlock: getReusableBlock, + } = select( 'core/editor' ); const block = getBlock( clientId ); return { reusableBlock: block && isReusableBlock( block ) ? getReusableBlock( block.attributes.ref ) : null, @@ -39,7 +42,7 @@ export default compose( [ } ), withDispatch( ( dispatch, { onToggle = noop } ) => { const { - deleteReusableBlock, + __experimentalDeleteReusableBlock: deleteReusableBlock, } = dispatch( 'core/editor' ); return { diff --git a/packages/editor/src/components/inserter/menu.js b/packages/editor/src/components/inserter/menu.js index 7af57b6af6adda..4bb2362377c390 100644 --- a/packages/editor/src/components/inserter/menu.js +++ b/packages/editor/src/components/inserter/menu.js @@ -351,7 +351,7 @@ export default compose( }; } ), withDispatch( ( dispatch ) => ( { - fetchReusableBlocks: dispatch( 'core/editor' ).fetchReusableBlocks, + fetchReusableBlocks: dispatch( 'core/editor' ).__experimentalFetchReusableBlocks, showInsertionPoint: dispatch( 'core/editor' ).showInsertionPoint, hideInsertionPoint: dispatch( 'core/editor' ).hideInsertionPoint, } ) ), diff --git a/packages/editor/src/hooks/default-autocompleters.js b/packages/editor/src/hooks/default-autocompleters.js index e1615b41334668..6ab43ed47d6abc 100644 --- a/packages/editor/src/hooks/default-autocompleters.js +++ b/packages/editor/src/hooks/default-autocompleters.js @@ -17,7 +17,7 @@ import { blockAutocompleter, userAutocompleter } from '../components'; const defaultAutocompleters = [ userAutocompleter ]; -const fetchReusableBlocks = once( () => dispatch( 'core/editor' ).fetchReusableBlocks() ); +const fetchReusableBlocks = once( () => dispatch( 'core/editor' ).__experimentalFetchReusableBlocks() ); function setDefaultCompleters( completers, blockName ) { if ( ! completers ) { diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index bf2fd1f7a73408..2935b833270f0e 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -578,7 +578,7 @@ export function updatePostLock( lock ) { * * @return {Object} Action object. */ -export function fetchReusableBlocks( id ) { +export function __experimentalFetchReusableBlocks( id ) { return { type: 'FETCH_REUSABLE_BLOCKS', id, @@ -595,7 +595,7 @@ export function fetchReusableBlocks( id ) { * * @return {Object} Action object. */ -export function receiveReusableBlocks( results ) { +export function __experimentalReceiveReusableBlocks( results ) { return { type: 'RECEIVE_REUSABLE_BLOCKS', results, @@ -610,7 +610,7 @@ export function receiveReusableBlocks( results ) { * * @return {Object} Action object. */ -export function saveReusableBlock( id ) { +export function __experimentalSaveReusableBlock( id ) { return { type: 'SAVE_REUSABLE_BLOCK', id, @@ -624,7 +624,7 @@ export function saveReusableBlock( id ) { * * @return {Object} Action object. */ -export function deleteReusableBlock( id ) { +export function __experimentalDeleteReusableBlock( id ) { return { type: 'DELETE_REUSABLE_BLOCK', id, @@ -640,7 +640,7 @@ export function deleteReusableBlock( id ) { * * @return {Object} Action object. */ -export function updateReusableBlockTitle( id, title ) { +export function __experimentalUpdateReusableBlockTitle( id, title ) { return { type: 'UPDATE_REUSABLE_BLOCK_TITLE', id, @@ -655,7 +655,7 @@ export function updateReusableBlockTitle( id, title ) { * * @return {Object} Action object. */ -export function convertBlockToStatic( clientId ) { +export function __experimentalConvertBlockToStatic( clientId ) { return { type: 'CONVERT_BLOCK_TO_STATIC', clientId, @@ -669,7 +669,7 @@ export function convertBlockToStatic( clientId ) { * * @return {Object} Action object. */ -export function convertBlockToReusable( clientIds ) { +export function __experimentalConvertBlockToReusable( clientIds ) { return { type: 'CONVERT_BLOCK_TO_REUSABLE', clientIds: castArray( clientIds ), @@ -821,3 +821,80 @@ export const createSuccessNotice = partial( createNotice, 'success' ); export const createInfoNotice = partial( createNotice, 'info' ); export const createErrorNotice = partial( createNotice, 'error' ); export const createWarningNotice = partial( createNotice, 'warning' ); + +// +// Deprecated +// + +export function fetchReusableBlocks( id ) { + deprecated( "wp.data.dispatch( 'core/editor' ).fetchReusableBlocks( id )", { + alternative: "wp.data.select( 'core' ).getEntityRecords( 'postType', 'wp_block' )", + plugin: 'Gutenberg', + version: '4.4.0', + } ); + + return __experimentalFetchReusableBlocks( id ); +} + +export function receiveReusableBlocks( results ) { + deprecated( "wp.data.dispatch( 'core/editor' ).receiveReusableBlocks( results )", { + alternative: "wp.data.select( 'core' ).getEntityRecords( 'postType', 'wp_block' )", + plugin: 'Gutenberg', + version: '4.4.0', + } ); + + return __experimentalReceiveReusableBlocks( results ); +} + +export function saveReusableBlock( id ) { + deprecated( "wp.data.dispatch( 'core/editor' ).saveReusableBlock( id )", { + alternative: "wp.data.dispatch( 'core' ).saveEntityRecord( 'postType', 'wp_block', reusableBlock )", + plugin: 'Gutenberg', + version: '4.4.0', + } ); + + return __experimentalSaveReusableBlock( id ); +} + +export function deleteReusableBlock( id ) { + deprecated( 'deleteReusableBlock action (`core/editor` store)', { + alternative: '__experimentalDeleteReusableBlock action (`core/edtior` store)', + plugin: 'Gutenberg', + version: '4.4.0', + hint: 'Using experimental APIs is strongly discouraged as they are subject to removal without notice.', + } ); + + return __experimentalDeleteReusableBlock( id ); +} + +export function updateReusableBlockTitle( id, title ) { + deprecated( "wp.data.dispatch( 'core/editor' ).updateReusableBlockTitle( id, title )", { + alternative: "wp.data.dispatch( 'core' ).saveEntityRecord( 'postType', 'wp_block', reusableBlock )", + plugin: 'Gutenberg', + version: '4.4.0', + } ); + + return __experimentalUpdateReusableBlockTitle( id, title ); +} + +export function convertBlockToStatic( id ) { + deprecated( 'convertBlockToStatic action (`core/editor` store)', { + alternative: '__experimentalConvertBlockToStatic action (`core/edtior` store)', + plugin: 'Gutenberg', + version: '4.4.0', + hint: 'Using experimental APIs is strongly discouraged as they are subject to removal without notice.', + } ); + + return __experimentalConvertBlockToStatic( id ); +} + +export function convertBlockToReusable( id ) { + deprecated( 'convertBlockToReusable action (`core/editor` store)', { + alternative: '__experimentalConvertBlockToReusable action (`core/edtior` store)', + plugin: 'Gutenberg', + version: '4.4.0', + hint: 'Using experimental APIs is strongly discouraged as they are subject to removal without notice.', + } ); + + return __experimentalConvertBlockToReusable( id ); +} diff --git a/packages/editor/src/store/effects/reusable-blocks.js b/packages/editor/src/store/effects/reusable-blocks.js index 7d9795d64d34d6..0c569728b6a39d 100644 --- a/packages/editor/src/store/effects/reusable-blocks.js +++ b/packages/editor/src/store/effects/reusable-blocks.js @@ -26,14 +26,14 @@ import { dispatch as dataDispatch } from '@wordpress/data'; */ import { resolveSelector } from './utils'; import { - receiveReusableBlocks as receiveReusableBlocksAction, + __experimentalReceiveReusableBlocks as receiveReusableBlocksAction, removeBlocks, replaceBlocks, receiveBlocks, - saveReusableBlock, + __experimentalSaveReusableBlock as saveReusableBlock, } from '../actions'; import { - getReusableBlock, + __experimentalGetReusableBlock as getReusableBlock, getBlock, getBlocks, getBlocksByClientId, diff --git a/packages/editor/src/store/effects/test/reusable-blocks.js b/packages/editor/src/store/effects/test/reusable-blocks.js index 4aca3995d165a9..2cc3b83e4f36f7 100644 --- a/packages/editor/src/store/effects/test/reusable-blocks.js +++ b/packages/editor/src/store/effects/test/reusable-blocks.js @@ -27,13 +27,13 @@ import { import { resetBlocks, receiveBlocks, - saveReusableBlock, - deleteReusableBlock, + __experimentalSaveReusableBlock as saveReusableBlock, + __experimentalDeleteReusableBlock as deleteReusableBlock, removeBlocks, - convertBlockToReusable as convertBlockToReusableAction, - convertBlockToStatic as convertBlockToStaticAction, - receiveReusableBlocks as receiveReusableBlocksAction, - fetchReusableBlocks as fetchReusableBlocksAction, + __experimentalConvertBlockToReusable as convertBlockToReusableAction, + __experimentalConvertBlockToStatic as convertBlockToStaticAction, + __experimentalReceiveReusableBlocks as receiveReusableBlocksAction, + __experimentalFetchReusableBlocks as fetchReusableBlocksAction, } from '../../actions'; import reducer from '../../reducer'; import '../../..'; // Ensure store dependencies are imported via root. diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index 1c5036e3d73ccb..ed734f7e5dcbfd 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -1738,7 +1738,7 @@ export const getInserterItems = createSelector( .filter( shouldIncludeBlockType ) .map( buildBlockTypeInserterItem ); - const reusableBlockInserterItems = getReusableBlocks( state ) + const reusableBlockInserterItems = __experimentalGetReusableBlocks( state ) .filter( shouldIncludeReusableBlock ) .map( buildReusableBlockInserterItem ); @@ -1768,7 +1768,7 @@ export const getInserterItems = createSelector( * * @return {Object} The reusable block, or null if none exists. */ -export const getReusableBlock = createSelector( +export const __experimentalGetReusableBlock = createSelector( ( state, ref ) => { const block = state.reusableBlocks.data[ ref ]; if ( ! block ) { @@ -1796,7 +1796,7 @@ export const getReusableBlock = createSelector( * * @return {boolean} Whether or not the reusable block is being saved. */ -export function isSavingReusableBlock( state, ref ) { +export function __experimentalIsSavingReusableBlock( state, ref ) { return state.reusableBlocks.isSaving[ ref ] || false; } @@ -1809,7 +1809,7 @@ export function isSavingReusableBlock( state, ref ) { * * @return {boolean} Whether the reusable block is being fetched. */ -export function isFetchingReusableBlock( state, ref ) { +export function __experimentalIsFetchingReusableBlock( state, ref ) { return !! state.reusableBlocks.isFetching[ ref ]; } @@ -1820,8 +1820,11 @@ export function isFetchingReusableBlock( state, ref ) { * * @return {Array} An array of all reusable blocks. */ -export function getReusableBlocks( state ) { - return map( state.reusableBlocks.data, ( value, ref ) => getReusableBlock( state, ref ) ); +export function __experimentalGetReusableBlocks( state ) { + return map( + state.reusableBlocks.data, + ( value, ref ) => __experimentalGetReusableBlock( state, ref ) + ); } /** @@ -2077,3 +2080,44 @@ export function getNotices() { return select( 'core/notices' ).getNotices(); } + +export function getReusableBlock( state, ref ) { + deprecated( "wp.data.select( 'core/editor' ).getReusableBlock( ref )", { + alternative: "wp.data.select( 'core' ).getEntityRecord( 'postType', 'wp_block', ref )", + plugin: 'Gutenberg', + version: '4.4.0', + } ); + + return __experimentalGetReusableBlock( state, ref ); +} + +export function isSavingReusableBlock( state, ref ) { + deprecated( 'isSavingReusableBlock selector (`core/editor` store)', { + alternative: '__experimentalIsSavingReusableBlock selector (`core/edtior` store)', + plugin: 'Gutenberg', + version: '4.4.0', + hint: 'Using experimental APIs is strongly discouraged as they are subject to removal without notice.', + } ); + + return __experimentalIsSavingReusableBlock( state, ref ); +} + +export function isFetchingReusableBlock( state, ref ) { + deprecated( "wp.data.select( 'core/editor' ).isFetchingReusableBlock( ref )", { + alternative: "wp.data.select( 'core' ).isResolving( 'getEntityRecord', 'wp_block', ref )", + plugin: 'Gutenberg', + version: '4.4.0', + } ); + + return __experimentalIsFetchingReusableBlock( state, ref ); +} + +export function getReusableBlocks( state ) { + deprecated( "wp.data.select( 'core/editor' ).getReusableBlocks( ref )", { + alternative: "wp.data.select( 'core' ).getEntityRecords( 'postType', 'wp_block' )", + plugin: 'Gutenberg', + version: '4.4.0', + } ); + + return __experimentalGetReusableBlocks( state ); +} diff --git a/packages/editor/src/store/test/actions.js b/packages/editor/src/store/test/actions.js index 9c776f0cc47e80..4358054f8573b4 100644 --- a/packages/editor/src/store/test/actions.js +++ b/packages/editor/src/store/test/actions.js @@ -7,11 +7,11 @@ import { stopTyping, enterFormattedText, exitFormattedText, - fetchReusableBlocks, - saveReusableBlock, - deleteReusableBlock, - convertBlockToStatic, - convertBlockToReusable, + __experimentalFetchReusableBlocks as fetchReusableBlocks, + __experimentalSaveReusableBlock as saveReusableBlock, + __experimentalDeleteReusableBlock as deleteReusableBlock, + __experimentalConvertBlockToStatic as convertBlockToStatic, + __experimentalConvertBlockToReusable as convertBlockToReusable, toggleSelection, setupEditor, resetPost, diff --git a/packages/editor/src/store/test/selectors.js b/packages/editor/src/store/test/selectors.js index f94b4c407e3454..dcfb2016d369ba 100644 --- a/packages/editor/src/store/test/selectors.js +++ b/packages/editor/src/store/test/selectors.js @@ -90,11 +90,11 @@ const { didPostSaveRequestFail, getSuggestedPostFormat, getEditedPostContent, - getReusableBlock, - isSavingReusableBlock, - isFetchingReusableBlock, + __experimentalGetReusableBlock: getReusableBlock, + __experimentalIsSavingReusableBlock: isSavingReusableBlock, + __experimentalIsFetchingReusableBlock: isFetchingReusableBlock, isSelectionEnabled, - getReusableBlocks, + __experimentalGetReusableBlocks: getReusableBlocks, getStateBeforeOptimisticTransaction, isPublishingPost, isPublishSidebarEnabled, From 9e80b0cf3dd5e3a2e52ed0f2e7096c2dba709831 Mon Sep 17 00:00:00 2001 From: Joen Asmussen Date: Tue, 30 Oct 2018 16:26:03 +0100 Subject: [PATCH 17/98] Docs: Add do's and don'ts to block design documentation (#11095) * Docs: Add do's and don'ts to block design documentation This PR adds a bunch of examples of what you should do, and what you _shouldn't_ do, when designing a block. It leverages existing principles, but expands with screenshots. * docs: Improve block guide * Address small text tweaks. * Split into two sections * Update screenshots and do/don't format. * Fix typos. * Address feedback. --- docs/design/advanced-settings-do.png | Bin 0 -> 41882 bytes docs/design/block-controls-do.png | Bin 0 -> 95426 bytes docs/design/block-controls-dont.png | Bin 0 -> 89017 bytes docs/design/block-descriptions-do.png | Bin 0 -> 8119 bytes docs/design/block-descriptions-dont.png | Bin 0 -> 15738 bytes docs/design/block-design.md | 108 +++++++++++++++++++----- docs/design/blocks-do.png | Bin 0 -> 5340 bytes docs/design/blocks-dont.png | Bin 0 -> 9619 bytes docs/design/placeholder-do.png | Bin 0 -> 14310 bytes docs/design/placeholder-dont.png | Bin 0 -> 11863 bytes 10 files changed, 89 insertions(+), 19 deletions(-) create mode 100644 docs/design/advanced-settings-do.png create mode 100644 docs/design/block-controls-do.png create mode 100644 docs/design/block-controls-dont.png create mode 100644 docs/design/block-descriptions-do.png create mode 100644 docs/design/block-descriptions-dont.png create mode 100644 docs/design/blocks-do.png create mode 100644 docs/design/blocks-dont.png create mode 100644 docs/design/placeholder-do.png create mode 100644 docs/design/placeholder-dont.png diff --git a/docs/design/advanced-settings-do.png b/docs/design/advanced-settings-do.png new file mode 100644 index 0000000000000000000000000000000000000000..c589ace4c13481651d4f2c4f010bd16646953d69 GIT binary patch literal 41882 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

    tO}k_T>uSSBS1X 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 *$N@=(9u8R zEB;Qzv~tJZfBkozZuR3|9E@PE8pDr{j&#@wLPJlSeN0z6SDI~mF555So`x1@wXLmx z>U-U`yX!m+OxE^iJryz-bS;IUXb(PxpZ=)Z?r^`m6{p0gKQZtf9*Nf-)i-SNjGX@K z8OKKwJzQ~B0C(W}J?L^*1k!#m@?xWg&%qHJTLq*+(EIDgcyR6*{^>9Vx4NrqN_+9! z&g(Pqnak30HRoF$Qj;T>T|Dm@(vLp}N-dt_3*OOm;FwYC$bCY5xsgXaG;n_yyU~HyD@mdENtN%4DZHJpHt#!#G<7fxrKOs4XGrkho$a4L zLNzTxz)HKW-=R{ymPbSX|9vSHAUh z?US>n1p&~$6c_QBB=XRGcG$AJ7Wnqfqmg_ff@)9o^2l4xFcwSxN;Hvmv>?_`l z1%jVVRa3HZvSb+z)l$z6k2tU7>;C^wduJLCW&i#0q>x=i(jbb;mSkVcRuNJ5WwLKs zhOuP}x$WE~R18v4*$svnOZJ$|*k?2tOLk*tY{TzTciq2d|7ZV4zj-hZ<~4J!?>X0X zeZTMXIp>!>Cynu$Y^s zHMw593l;2ERo@aAx!{{^h2+djap9{R5>UzOAg1^((dt&?LeXmy4>3xc0X&M1FGDCK zvfma_qB1k{ak}ibrcGLSIm>rX{e{7z0(Ns*^^bLHyB159*Cfb@!x0X6y>Dpy1uwP*igj0dIu6hafB?i8LF_PANhOh@3A^ zb_Qf&Xzy%gzcP^jE7t@?#hD27KFLEzhquU`8xj-7G#pNW`y?qh8LXM&! zT@DXj>b>?76m{@D#htSvw-&#z=EK~5R1SM%6|3}rQZjIw6zGBV52E<0 zxdUckzHcn%`Frm12*Clmx;-2LOmZH10nGPLzahjfNzd<65C%I_@#TN=~@)$8znKEr&SGQ!|ISCh$f7VQwJHTp~s zVQXblza|rk0Qqh=8o}4A9s3SPLS=2=I@(NIMOlQ+$EY(GdKbV#dnHTxM`oTqbaWJI zX{Yh~kOtb@04Q z3$wCU_WOxAvcY)@uW=!_mx~^({DZ`2SF{f_9P?y`I%9n(y$a`{SR+^ZnGw%Tqcb9u zBCJ4DQ4IM+AL^RVo~+We^z2wLrO4ls@!Z060=k)HNcB^ydpjx2se;Cv^OBSJuEyK8 zWPOf@_6T5CLK*Pl0OF>_KOo>tAO7`yd|p4OE5q;>b)c$TkR!Fcp?~1${z+E4Nb?WJ zk*8duCJY&3PZt>_+|1n*H<<8dhQbUkZ)=G3C#!ZLiew09n;!q&m2n8ax3_Q-V-?V` z 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 literal 0 HcmV?d00001 diff --git a/docs/design/block-controls-dont.png b/docs/design/block-controls-dont.png new file mode 100644 index 0000000000000000000000000000000000000000..301a5211546415f047149ccaad1c4ce5e4a4a7e4 GIT binary patch literal 89017 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

    Su*47uU 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 DYd8$L literal 0 HcmV?d00001 diff --git a/docs/design/block-descriptions-dont.png b/docs/design/block-descriptions-dont.png new file mode 100644 index 0000000000000000000000000000000000000000..9225cb5d8575a345d4d1b70496d1e07b96246442 GIT binary patch literal 15738 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 literal 0 HcmV?d00001 diff --git a/docs/design/block-design.md b/docs/design/block-design.md index babca85260e16e..5163f177447ffc 100644 --- a/docs/design/block-design.md +++ b/docs/design/block-design.md @@ -1,17 +1,89 @@ # Block Design -The following is a light guide to designing a new block with recommendations and detailed descriptions of existing blocks to illustrate good practices. +The following are best practices for designing a new block, with recommendations and detailed descriptions of existing blocks to illustrate our approach to creating blocks. ## Best Practices -- Blocks should have a simple label for the Inserter. Keep it as short as possible. -- Blocks should have an identifying icon, ideally using a single color. Try to avoid using the same icon used by an existing block. The core block icons are based on [Material Design Icons](https://material.io/tools/icons/). Look to that icon set, or to [Dashicons](https://developer.wordpress.org/resource/dashicons/) for style inspiration. -- Blocks should have a instructive placeholder state when they’re first inserted. If the block includes a text input, provide placeholder text. If your block holds media, include buttons for uploading files and accessing media libraries, as well as drop-zones for drag-and-drop. -- When unselected, your block should preview its content as closely to the front-end output as possible. -- When selected, your block may surface additional options like input fields or buttons to configure the block directly, if those are necessary for basic operation. -- Every block should include a description in the “Block” tab of the Settings sidebar. The description should explain as clearly as possible what your block does. Keep it to a single sentence. -- The “Block” tab of the Settings Sidebar can contain additional block options and configuration, but keep in mind that a user might dismiss the sidebar and never use it. Do not put critical options there. -- Check how your block looks, feels, and works on all sorts of devices and screen sizes. +### The primary interface for a block is the content area of the block + +Since the block itself represents what will actually appear on the site, interaction here hews closest to the principle of direct manipulation and will be most intuitive to the user. This should be thought of as the primary interface for adding and manipulating content and adjusting how it is displayed. There are two ways of interacting here: + +1. The placeholder content in the content area of the block can be thought of as a guide or interface for users to follow a set of instructions or “fill in the blanks”. For example, a block that embeds content from a 3rd-party service might contain controls for signing in to that service in the placeholder. +2. After the user has added content, selecting the block can reveal additional controls to adjust or edit that content. For example, a signup block might reveal a control for hiding/showing subscriber count. However, this should be done in minimal ways, so as to avoid dramatically changing the size and display of a block when a user selects it (this could be disorienting or annoying). + +### The block toolbar is a secondary place for required options & controls + +Basic block settings won’t always make sense in the context of the placeholder/content UI. As a secondary option, options that are critical to the functionality of a block can live in the block toolbar. The block toolbar is still highly contextual and visible on all screen sizes. One notable constraint with the block toolbar is that it is icon-based UI, so any controls that live in the block toolbar need to be ones that can effectively be communicated via an icon or icon group. + +### The block sidebar should only be used for advanced, tertiary controls + +The sidebar is not visible by default on a small / mobile screen, and may also be collapsed in a desktop view. Therefore, it should not be relied on for anything that is necessary for the basic operation of the block. Pick good defaults, make important actions available in the block toolbar, and think of the sidebar as something that only power users may discover. In addition, use sections and headers in the block sidebar if there are more than a handful of options, in order to allow users to easily scan and understand the options available. + +## Do's and Don'ts + +### Blocks + +A block should have a straightforward, short name so users can easily find it in the Block Library. A block named "YouTube" is easy to find and understand. The same block, named "Embedded Video (YouTube)", would be less clear and harder to find in the Block Library. + +Blocks should have an identifying icon, ideally using a single color. Try to avoid using the same icon used by an existing block. The core block icons are based on [Material Design Icons](https://material.io/tools/icons/). Look to that icon set, or to [Dashicons](https://developer.wordpress.org/resource/dashicons/) for style inspiration. + +![A screenshot of the Block Library with concise block names](./blocks-do.png) +**Do:** +Use conise block names. + +![A screenshot of the Block Library with long, multi-line block names](./blocks-dont.png) +**Don't:** +Avoid long, multi-line block names. + +### Block Description + +Every block should include a description in the “Block” tab of the Settings sidebar. This description should explain your block's function clearly. Keep it to a single sentence. + +![A screenshot of a short block description](./block-descriptions-do.png) +**Do:** +Use a short, simple, block description. + +![A screenshot of a long block description that includes branding](./block-descriptions-dont.png) +**Don't:** +Avoid long descriptions and branding. + +### Placeholders + +If your block requires a user to configure some options before you can display it, you should provide an instructive placeholder state. + +![A screenshot of the Gallery block's placeholder](./placeholder-do.png) +**Do:** +Provide an instructive placeholder state. + +![An example Gallery block placeholder but with intense, distracting colors and no instructions](./placeholder-dont.png) +**Don't:** +Avoid branding and relying on the title alone to convey instructions. + +### Selected and Unselected States + +When unselected, your block should preview its content as closely to the front-end output as possible. + +When selected, your block may surface additional options like input fields or buttons to configure the block directly, especially when they are necessary for basic operation. + +![A Google Maps block with inline, always-accessible controls required for the block to function](./block-controls-do.png) +**Do:** +For controls that are essential the the operation of the block, provide them directly in inside the block edit view. + +![A Google Maps block with essential controls moved to the sidebar where they can be contextually hidden](./block-controls-dont.png) +**Don't:** +Do not put controls that are essential to the block in the sidebar, or the block will appear non-functional to mobile users, or desktop users who have dismissed the sidebar. + +### Advanced Block Settings + +The “Block” tab of the Settings Sidebar can contain additional block options and configuration. Keep in mind that a user can dismiss the sidebar and never use it. You should not put critical options in the Sidebar. + +![A screenshot of the paragraph block's advanced settings in the sidebar](./advanced-settings-do.png) +**Do:** +Because the Drop Cap feature is not necessary for the basic operation of the block, you can put it ub the Block tab as optional configuration. + +### Consider mobile + +Check how your block looks, feels, and works on as many devices and screen sizes as you can. ## Examples @@ -23,11 +95,11 @@ The most basic unit of the editor. The paragraph block is a simple input field. ![Paragraph Block](https://cldup.com/HVJe5bGZ8H-3000x3000.png) -**Placeholder:** +### Placeholder: - Simple placeholder text that says “Add text or type / to add content.” The placeholder disappears when the block is selected. -**Selected state:** +### Selected state: - Block Toolbar: Has a switcher to perform transformations to headings, etc. - Block Toolbar: Has basic text alignments @@ -39,11 +111,11 @@ Basic image block. ![Image Block Placeholder](https://cldup.com/w6FNywNsj1-3000x3000.png) -**Placeholder:** +### Placeholder: - A generic gray placeholder block with options to upload an image, drag and drop an image directly on it, or pick an image from the media library. -**Selected state:** +### Selected state: - Block Toolbar: Alignments, including wide and full-width if the theme supports it. - Block Toolbar: Edit Image, to open the Media Library @@ -52,7 +124,7 @@ Basic image block. ![Image Block](https://cldup.com/6YYXstl_xX-3000x3000.png) -**Block settings:** +### Block settings: - Has description: “They're worth 1,000 words! Insert a single image.” - Has options for changing or adding alt text and adding additional custom CSS classes. @@ -63,22 +135,20 @@ _Future improvements to the Image block could include getting rid of the media m ![Latest Post Block](https://cldup.com/8lyAByDpy_-3000x3000.png) -**Placeholder:** +### Placeholder: Has no placeholder, as it works immediately upon insertion. The default inserted state shows the last 5 posts. -**Selected state:** +### Selected state: - Block Toolbar: Alignments - Block Toolbar: Options for picking list view or grid view _Note that the Block Toolbar does not include the Block Chip in this case, since there are no similar blocks to switch to._ -**Block settings:** +### Block settings: - Has description: “Display a list of your most recent posts.” - Has options for post order, narrowing the list by category, changing the default number of posts to show, and showing the post date. _Latest Posts is fully functional as soon as it’s inserted, because it comes with good defaults._ - - diff --git a/docs/design/blocks-do.png b/docs/design/blocks-do.png new file mode 100644 index 0000000000000000000000000000000000000000..bd79797dfea12944d16efb252878d928fe11e22b GIT binary patch literal 5340 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 F{{c{Ny>9>j literal 0 HcmV?d00001 diff --git a/docs/design/blocks-dont.png b/docs/design/blocks-dont.png new file mode 100644 index 0000000000000000000000000000000000000000..2c69f1ada09213bbd665a3dfdfdc46d20da39841 GIT binary patch literal 9619 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 literal 0 HcmV?d00001 From 9d465966aec3c55fb316b079486d9a1cce934af7 Mon Sep 17 00:00:00 2001 From: Pinar Olguc Date: Tue, 30 Oct 2018 19:09:01 +0300 Subject: [PATCH 18/98] Port nextpage block to the ReactNative mobile app (#11192) * Port next page block to native app * Edit function is extracted from index.js and put into separate files for mobile and web * New scss is added for mobile * Fix lint issues * Fix failing unittest * Add react-native path to notices package.json * Make some code review fixes --- packages/block-library/src/index.native.js | 2 ++ packages/block-library/src/nextpage/edit.js | 12 ++++++++++ .../block-library/src/nextpage/edit.native.js | 24 +++++++++++++++++++ .../src/nextpage/editor.native.scss | 6 +++++ packages/block-library/src/nextpage/index.js | 9 ++----- packages/notices/package.json | 1 + 6 files changed, 47 insertions(+), 7 deletions(-) create mode 100644 packages/block-library/src/nextpage/edit.js create mode 100644 packages/block-library/src/nextpage/edit.native.js create mode 100644 packages/block-library/src/nextpage/editor.native.scss diff --git a/packages/block-library/src/index.native.js b/packages/block-library/src/index.native.js index 0acf3ee3f49714..3da15242c7cfbd 100644 --- a/packages/block-library/src/index.native.js +++ b/packages/block-library/src/index.native.js @@ -13,6 +13,7 @@ import * as heading from './heading'; import * as more from './more'; import * as paragraph from './paragraph'; import * as image from './image'; +import * as nextpage from './nextpage'; export const registerCoreBlocks = () => { [ @@ -21,6 +22,7 @@ export const registerCoreBlocks = () => { code, more, image, + nextpage, ].forEach( ( { name, settings } ) => { registerBlockType( name, settings ); } ); diff --git a/packages/block-library/src/nextpage/edit.js b/packages/block-library/src/nextpage/edit.js new file mode 100644 index 00000000000000..fb4d16ee52cb17 --- /dev/null +++ b/packages/block-library/src/nextpage/edit.js @@ -0,0 +1,12 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +export default function NextPageEdit() { + return ( +

    + ); +} diff --git a/packages/block-library/src/nextpage/edit.native.js b/packages/block-library/src/nextpage/edit.native.js new file mode 100644 index 00000000000000..e486337e1c47b6 --- /dev/null +++ b/packages/block-library/src/nextpage/edit.native.js @@ -0,0 +1,24 @@ +/** + * External dependencies + */ +import { View, Text } from 'react-native'; + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import styles from './editor.scss'; + +export default function NextPageEdit( { attributes } ) { + const { customText = __( 'Page break' ) } = attributes; + + return ( + + { customText } + + ); +} diff --git a/packages/block-library/src/nextpage/editor.native.scss b/packages/block-library/src/nextpage/editor.native.scss new file mode 100644 index 00000000000000..7101c63e82962a --- /dev/null +++ b/packages/block-library/src/nextpage/editor.native.scss @@ -0,0 +1,6 @@ +// @format + +.block-library-nextpage__container { + align-items: center; + padding: 4px 4px 4px 4px; +} diff --git a/packages/block-library/src/nextpage/index.js b/packages/block-library/src/nextpage/index.js index 02114bd72c0788..ee557a81bb5620 100644 --- a/packages/block-library/src/nextpage/index.js +++ b/packages/block-library/src/nextpage/index.js @@ -5,6 +5,7 @@ import { __ } from '@wordpress/i18n'; import { RawHTML } from '@wordpress/element'; import { createBlock } from '@wordpress/blocks'; import { G, Path, SVG } from '@wordpress/components'; +import edit from './edit'; export const name = 'core/nextpage'; @@ -42,13 +43,7 @@ export const settings = { ], }, - edit() { - return ( -
    - { __( 'Page break' ) } -
    - ); - }, + edit, save() { return ( diff --git a/packages/notices/package.json b/packages/notices/package.json index 46c86828a70ebc..93e9ac42d9900e 100644 --- a/packages/notices/package.json +++ b/packages/notices/package.json @@ -21,6 +21,7 @@ "build-module" ], "main": "build/index.js", + "react-native": "src/index", "dependencies": { "@babel/runtime": "^7.0.0", "@wordpress/a11y": "file:../a11y", From 35c02bc5c672a14d0ef3722b4153f4769fdd6edc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20Van=C2=A0Dorpe?= Date: Tue, 30 Oct 2018 17:50:50 +0100 Subject: [PATCH 19/98] Delay TinyMCE initialisation to focus (#10723) --- .../button/test/__snapshots__/index.js.snap | 6 +++- .../heading/test/__snapshots__/index.js.snap | 6 +++- .../src/list/test/__snapshots__/index.js.snap | 6 +++- .../test/__snapshots__/index.js.snap | 6 +++- .../test/__snapshots__/index.js.snap | 6 +++- .../test/__snapshots__/index.js.snap | 6 +++- packages/block-library/src/quote/index.js | 5 ++-- .../quote/test/__snapshots__/index.js.snap | 6 +++- .../test/__snapshots__/index.js.snap | 12 ++++++-- .../verse/test/__snapshots__/index.js.snap | 6 +++- .../src/components/rich-text/tinymce.js | 28 ++++++++++++++++++- test/e2e/specs/block-deletion.test.js | 4 +++ .../blocks/__snapshots__/quote.test.js.snap | 6 +++- test/e2e/specs/splitting-merging.test.js | 6 ++++ test/e2e/support/utils.js | 2 +- 15 files changed, 95 insertions(+), 16 deletions(-) diff --git a/packages/block-library/src/button/test/__snapshots__/index.js.snap b/packages/block-library/src/button/test/__snapshots__/index.js.snap index fa8009e4cbc03e..9d96ec60114b22 100644 --- a/packages/block-library/src/button/test/__snapshots__/index.js.snap +++ b/packages/block-library/src/button/test/__snapshots__/index.js.snap @@ -23,7 +23,11 @@ exports[`core/button block edit matches snapshot 1`] = ` contenteditable="true" data-is-placeholder-visible="true" role="textbox" - /> + > +
    +
    diff --git a/packages/block-library/src/quote/index.js b/packages/block-library/src/quote/index.js index a87b0dba274534..49eef577f907ad 100644 --- a/packages/block-library/src/quote/index.js +++ b/packages/block-library/src/quote/index.js @@ -115,7 +115,7 @@ export const settings = { blocks: [ 'core/paragraph' ], transform: ( { value, citation } ) => { const paragraphs = []; - if ( value ) { + if ( value && value !== '

    ' ) { paragraphs.push( ...split( create( { html: value, multilineTag: 'p' } ), '\u2028' ) .map( ( piece ) => @@ -125,7 +125,7 @@ export const settings = { ) ); } - if ( citation ) { + if ( citation && citation !== '

    ' ) { paragraphs.push( createBlock( 'core/paragraph', { content: citation, @@ -189,7 +189,6 @@ export const settings = { edit( { attributes, setAttributes, isSelected, mergeBlocks, onReplace, className } ) { const { align, value, citation } = attributes; - return ( diff --git a/packages/block-library/src/quote/test/__snapshots__/index.js.snap b/packages/block-library/src/quote/test/__snapshots__/index.js.snap index 813b6d8417cb66..54f0293d643a5d 100644 --- a/packages/block-library/src/quote/test/__snapshots__/index.js.snap +++ b/packages/block-library/src/quote/test/__snapshots__/index.js.snap @@ -21,7 +21,11 @@ exports[`core/quote block edit matches snapshot 1`] = ` contenteditable="true" data-is-placeholder-visible="true" role="textbox" - /> + > +
    +
    diff --git a/packages/block-library/src/text-columns/test/__snapshots__/index.js.snap b/packages/block-library/src/text-columns/test/__snapshots__/index.js.snap index b618bdb565e07f..19e26bedd4e4f5 100644 --- a/packages/block-library/src/text-columns/test/__snapshots__/index.js.snap +++ b/packages/block-library/src/text-columns/test/__snapshots__/index.js.snap @@ -24,7 +24,11 @@ exports[`core/text-columns block edit matches snapshot 1`] = ` contenteditable="true" data-is-placeholder-visible="true" role="textbox" - /> + > +
    +

    @@ -55,7 +59,11 @@ exports[`core/text-columns block edit matches snapshot 1`] = ` contenteditable="true" data-is-placeholder-visible="true" role="textbox" - /> + > +
    +

    diff --git a/packages/block-library/src/verse/test/__snapshots__/index.js.snap b/packages/block-library/src/verse/test/__snapshots__/index.js.snap index dadd75177de95d..4141acd82063f3 100644 --- a/packages/block-library/src/verse/test/__snapshots__/index.js.snap +++ b/packages/block-library/src/verse/test/__snapshots__/index.js.snap @@ -18,7 +18,11 @@ exports[`core/verse block edit matches snapshot 1`] = ` contenteditable="true" data-is-placeholder-visible="true" role="textbox" - /> + > +
    +

    diff --git a/packages/editor/src/components/rich-text/tinymce.js b/packages/editor/src/components/rich-text/tinymce.js
    index 25b3a2ac860326..c589dde327075d 100644
    --- a/packages/editor/src/components/rich-text/tinymce.js
    +++ b/packages/editor/src/components/rich-text/tinymce.js
    @@ -101,9 +101,10 @@ export default class TinyMCE extends Component {
     	constructor() {
     		super();
     		this.bindEditorNode = this.bindEditorNode.bind( this );
    +		this.onFocus = this.onFocus.bind( this );
     	}
     
    -	componentDidMount() {
    +	onFocus() {
     		this.initialize();
     	}
     
    @@ -158,6 +159,11 @@ export default class TinyMCE extends Component {
     			browser_spellcheck: true,
     			entity_encoding: 'raw',
     			convert_urls: false,
    +			// Disables TinyMCE's parsing to verify HTML. It makes
    +			// initialisation a bit faster. Since we're setting raw HTML
    +			// already with dangerouslySetInnerHTML, we don't need this to be
    +			// verified.
    +			verify_html: false,
     			inline_boundaries_selector: 'a[href],code,b,i,strong,em,del,ins,sup,sub',
     			plugins: [],
     		} );
    @@ -169,6 +175,18 @@ export default class TinyMCE extends Component {
     				this.editor = editor;
     				this.props.onSetup( editor );
     
    +				// TinyMCE resets the element content on initialization, even
    +				// when it's already identical to what exists currently. This
    +				// behavior clobbers a selection which exists at the time of
    +				// initialization, thus breaking writing flow navigation. The
    +				// hack here neutralizes setHTML during initialization.
    +				let setHTML;
    +
    +				editor.on( 'preinit', () => {
    +					setHTML = editor.dom.setHTML;
    +					editor.dom.setHTML = () => {};
    +				} );
    +
     				editor.on( 'init', () => {
     					// See https://github.com/tinymce/tinymce/blob/master/src/core/main/ts/keyboard/FormatShortcuts.ts
     					[ 'b', 'i', 'u' ].forEach( ( character ) => {
    @@ -177,6 +195,8 @@ export default class TinyMCE extends Component {
     					[ 1, 2, 3, 4, 5, 6, 7, 8, 9 ].forEach( ( number ) => {
     						editor.shortcuts.remove( `access+${ number }` );
     					} );
    +
    +					editor.dom.setHTML = setHTML;
     				} );
     			},
     		} );
    @@ -247,6 +267,11 @@ export default class TinyMCE extends Component {
     			} );
     		}
     
    +		if ( initialHTML === '' ) {
    +			// Ensure the field is ready to receive focus by TinyMCE.
    +			initialHTML = '
    '; + } + return createElement( tagName, { ...ariaProps, className: classnames( className, 'editor-rich-text__tinymce' ), @@ -258,6 +283,7 @@ export default class TinyMCE extends Component { dangerouslySetInnerHTML: { __html: initialHTML }, onPaste, onInput, + onFocus: this.onFocus, } ); } } diff --git a/test/e2e/specs/block-deletion.test.js b/test/e2e/specs/block-deletion.test.js index 7da0695f1ab436..fb6dd2b40abd5b 100644 --- a/test/e2e/specs/block-deletion.test.js +++ b/test/e2e/specs/block-deletion.test.js @@ -7,6 +7,7 @@ import { newPost, pressWithModifier, ACCESS_MODIFIER_KEYS, + waitForRichTextInitialization, } from '../support/utils'; const addThreeParagraphsToNewPost = async () => { @@ -16,8 +17,10 @@ const addThreeParagraphsToNewPost = async () => { await clickBlockAppender(); await page.keyboard.type( 'First paragraph' ); await page.keyboard.press( 'Enter' ); + await waitForRichTextInitialization(); await page.keyboard.type( 'Second paragraph' ); await page.keyboard.press( 'Enter' ); + await waitForRichTextInitialization(); }; const clickOnBlockSettingsMenuItem = async ( buttonLabel ) => { @@ -96,6 +99,7 @@ describe( 'block deletion -', () => { // Add a third paragraph for this test. await page.keyboard.type( 'Third paragraph' ); await page.keyboard.press( 'Enter' ); + await waitForRichTextInitialization(); // Press the up arrow once to select the third and fourth blocks. await pressWithModifier( 'Shift', 'ArrowUp' ); diff --git a/test/e2e/specs/blocks/__snapshots__/quote.test.js.snap b/test/e2e/specs/blocks/__snapshots__/quote.test.js.snap index 6a46b767390efc..5257716d67d00c 100644 --- a/test/e2e/specs/blocks/__snapshots__/quote.test.js.snap +++ b/test/e2e/specs/blocks/__snapshots__/quote.test.js.snap @@ -58,7 +58,11 @@ exports[`Quote can be converted to paragraphs and renders a paragraph for the ci " `; -exports[`Quote can be converted to paragraphs and renders a void paragraph if both the cite and quote are void 1`] = `""`; +exports[`Quote can be converted to paragraphs and renders a void paragraph if both the cite and quote are void 1`] = ` +" +

    +" +`; exports[`Quote can be converted to paragraphs and renders one paragraph block per

    within quote 1`] = ` " diff --git a/test/e2e/specs/splitting-merging.test.js b/test/e2e/specs/splitting-merging.test.js index d059fbf42ea63d..fca2a8820a0135 100644 --- a/test/e2e/specs/splitting-merging.test.js +++ b/test/e2e/specs/splitting-merging.test.js @@ -8,6 +8,7 @@ import { pressTimes, pressWithModifier, META_KEY, + waitForRichTextInitialization, } from '../support/utils'; describe( 'splitting and merging blocks', () => { @@ -132,7 +133,9 @@ describe( 'splitting and merging blocks', () => { await pressWithModifier( META_KEY, 'b' ); await page.keyboard.press( 'ArrowRight' ); await page.keyboard.press( 'Enter' ); + await waitForRichTextInitialization(); await page.keyboard.press( 'Enter' ); + await waitForRichTextInitialization(); await page.keyboard.press( 'Backspace' ); @@ -172,8 +175,11 @@ describe( 'splitting and merging blocks', () => { await insertBlock( 'Paragraph' ); await page.keyboard.type( 'First' ); await page.keyboard.press( 'Enter' ); + await waitForRichTextInitialization(); await page.keyboard.press( 'Enter' ); + await waitForRichTextInitialization(); await page.keyboard.press( 'Enter' ); + await waitForRichTextInitialization(); await page.keyboard.type( 'Second' ); await page.keyboard.press( 'ArrowUp' ); await page.keyboard.press( 'ArrowUp' ); diff --git a/test/e2e/support/utils.js b/test/e2e/support/utils.js index 4ac62bf7372991..7275da4d1d2fcf 100644 --- a/test/e2e/support/utils.js +++ b/test/e2e/support/utils.js @@ -100,7 +100,7 @@ async function login() { * @return {Promise} Promise resolving once RichText is initialized, or is * determined to not be a container of the active element. */ -async function waitForRichTextInitialization() { +export async function waitForRichTextInitialization() { const isInRichText = await page.evaluate( () => { return !! document.activeElement.closest( '.editor-rich-text__tinymce' ); } ); From 393f5baa1cb640ea142c70458fdd22b8d8d2334e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20Van=C2=A0Dorpe?= Date: Tue, 30 Oct 2018 17:51:04 +0100 Subject: [PATCH 20/98] RichText: fix format placeholder (#11102) * Fix format placeholder * Unduplicate logic --- packages/rich-text/src/apply-format.js | 18 ++++++++------ packages/rich-text/src/test/apply-format.js | 12 +++++----- packages/rich-text/src/to-dom.js | 1 + packages/rich-text/src/to-tree.js | 24 ++++++++++++++++++- .../__snapshots__/rich-text.test.js.snap | 6 +++++ test/e2e/specs/rich-text.test.js | 13 ++++++++++ 6 files changed, 60 insertions(+), 14 deletions(-) diff --git a/packages/rich-text/src/apply-format.js b/packages/rich-text/src/apply-format.js index ebdf6ecaecfea3..7580ebaee2086a 100644 --- a/packages/rich-text/src/apply-format.js +++ b/packages/rich-text/src/apply-format.js @@ -2,15 +2,13 @@ * External dependencies */ -import { find, reject } from 'lodash'; +import { find } from 'lodash'; /** * Internal dependencies */ import { normaliseFormats } from './normalise-formats'; -import { insert } from './insert'; -import { ZERO_WIDTH_NO_BREAK_SPACE } from './special-characters'; /** * Apply a format object to a Rich Text value from the given `startIndex` to the @@ -56,10 +54,16 @@ export function applyFormat( const previousFormat = newFormats[ startIndex - 1 ] || []; const hasType = find( previousFormat, { type: format.type } ); - return insert( { formats, text, start, end }, { - formats: hasType ? [ reject( previousFormat, { type: format.type } ) ] : [ [ ...previousFormat, format ] ], - text: ZERO_WIDTH_NO_BREAK_SPACE, - } ); + return { + formats, + text, + start, + end, + formatPlaceholder: { + index: startIndex, + format: hasType ? undefined : format, + }, + }; } } else { for ( let index = startIndex; index < endIndex; index++ ) { diff --git a/packages/rich-text/src/test/apply-format.js b/packages/rich-text/src/test/apply-format.js index 73416cdb5cfacc..3aa51c927e71c5 100644 --- a/packages/rich-text/src/test/apply-format.js +++ b/packages/rich-text/src/test/apply-format.js @@ -8,7 +8,6 @@ import deepFreeze from 'deep-freeze'; */ import { applyFormat } from '../apply-format'; -import { ZERO_WIDTH_NO_BREAK_SPACE } from '../special-characters'; import { getSparseArrayLength } from './helpers'; describe( 'applyFormat', () => { @@ -61,16 +60,17 @@ describe( 'applyFormat', () => { end: 0, }; const expected = { - formats: [ [ a2 ], , , , , [ a ], [ a ], [ a ], , , , , , , ], - text: `${ ZERO_WIDTH_NO_BREAK_SPACE }one two three`, - start: 1, - end: 1, + ...record, + formatPlaceholder: { + format: a2, + index: 0, + }, }; const result = applyFormat( deepFreeze( record ), a2 ); expect( result ).toEqual( expected ); expect( result ).not.toBe( record ); - expect( getSparseArrayLength( result.formats ) ).toBe( 4 ); + expect( getSparseArrayLength( result.formats ) ).toBe( 3 ); } ); it( 'should apply format on existing format if selection is collapsed', () => { diff --git a/packages/rich-text/src/to-dom.js b/packages/rich-text/src/to-dom.js index 319e8b61ee1377..b5851817b5db7f 100644 --- a/packages/rich-text/src/to-dom.js +++ b/packages/rich-text/src/to-dom.js @@ -159,6 +159,7 @@ export function toDom( { onEndIndex( body, pointer ) { endPath = createPathToNode( pointer, body, [ pointer.nodeValue.length ] ); }, + isEditableTree: true, } ); if ( createLinePadding ) { diff --git a/packages/rich-text/src/to-tree.js b/packages/rich-text/src/to-tree.js index d599db049ff9aa..893b331c44a37e 100644 --- a/packages/rich-text/src/to-tree.js +++ b/packages/rich-text/src/to-tree.js @@ -6,6 +6,7 @@ import { getFormatType } from './get-format-type'; import { LINE_SEPARATOR, OBJECT_REPLACEMENT_CHARACTER, + ZERO_WIDTH_NO_BREAK_SPACE, } from './special-characters'; function fromFormat( { type, attributes, object } ) { @@ -55,8 +56,9 @@ export function toTree( { appendText, onStartIndex, onEndIndex, + isEditableTree, } ) { - const { formats, text, start, end } = value; + const { formats, text, start, end, formatPlaceholder } = value; const formatsLength = formats.length + 1; const tree = createEmpty(); const multilineFormat = { type: multilineTag }; @@ -73,6 +75,22 @@ export function toTree( { append( tree, '' ); } + function setFormatPlaceholder( pointer, index ) { + if ( isEditableTree && formatPlaceholder && formatPlaceholder.index === index ) { + const parent = getParent( pointer ); + + if ( formatPlaceholder.format === undefined ) { + pointer = getParent( parent ); + } else { + pointer = append( parent, fromFormat( formatPlaceholder.format ) ); + } + + pointer = append( pointer, ZERO_WIDTH_NO_BREAK_SPACE ); + } + + return pointer; + } + for ( let i = 0; i < formatsLength; i++ ) { const character = text.charAt( i ); let characterFormats = formats[ i ]; @@ -146,6 +164,8 @@ export function toTree( { continue; } + pointer = setFormatPlaceholder( pointer, 0 ); + // If there is selection at 0, handle it before characters are inserted. if ( i === 0 ) { if ( onStartIndex && start === 0 ) { @@ -169,6 +189,8 @@ export function toTree( { } } + pointer = setFormatPlaceholder( pointer, i + 1 ); + if ( onStartIndex && start === i + 1 ) { onStartIndex( tree, pointer ); } diff --git a/test/e2e/specs/__snapshots__/rich-text.test.js.snap b/test/e2e/specs/__snapshots__/rich-text.test.js.snap index 2ddafbae544f22..fc96d2d6f1fab5 100644 --- a/test/e2e/specs/__snapshots__/rich-text.test.js.snap +++ b/test/e2e/specs/__snapshots__/rich-text.test.js.snap @@ -1,5 +1,11 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`RichText should apply formatting when selection is collapsed 1`] = ` +" +

    Some bold.

    +" +`; + exports[`RichText should apply formatting with access shortcut 1`] = ` "

    test

    diff --git a/test/e2e/specs/rich-text.test.js b/test/e2e/specs/rich-text.test.js index 398196d0a37db7..9203ec32c303dd 100644 --- a/test/e2e/specs/rich-text.test.js +++ b/test/e2e/specs/rich-text.test.js @@ -45,4 +45,17 @@ describe( 'RichText', () => { expect( await getEditedPostContent() ).toMatchSnapshot(); } ); + + it( 'should apply formatting when selection is collapsed', async () => { + await clickBlockAppender(); + await page.keyboard.type( 'Some ' ); + // All following characters should now be bold. + await pressWithModifier( META_KEY, 'b' ); + await page.keyboard.type( 'bold' ); + // All following characters should no longer be bold. + await pressWithModifier( META_KEY, 'b' ); + await page.keyboard.type( '.' ); + + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); } ); From 6e6c8d5f47d7d17dc808cdede8eb026031fabc1b Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Tue, 30 Oct 2018 13:28:28 -0400 Subject: [PATCH 21/98] Editor: Optimize Inserter props generation and reconciliation (#11243) --- .../test/__snapshots__/index.js.snap | 6 +- .../editor/src/components/inserter/index.js | 87 ++++++++++--------- .../editor/src/components/inserter/menu.js | 57 ++++++++++-- 3 files changed, 95 insertions(+), 55 deletions(-) diff --git a/packages/editor/src/components/default-block-appender/test/__snapshots__/index.js.snap b/packages/editor/src/components/default-block-appender/test/__snapshots__/index.js.snap index 46c017374b5149..71484219b7d98e 100644 --- a/packages/editor/src/components/default-block-appender/test/__snapshots__/index.js.snap +++ b/packages/editor/src/components/default-block-appender/test/__snapshots__/index.js.snap @@ -23,7 +23,7 @@ exports[`DefaultBlockAppender should append a default block when input focused 1 value="Write your story" /> -
    @@ -45,7 +45,7 @@ exports[`DefaultBlockAppender should match snapshot 1`] = ` value="Write your story" /> - @@ -67,7 +67,7 @@ exports[`DefaultBlockAppender should optionally show without prompt 1`] = ` value="" /> - diff --git a/packages/editor/src/components/inserter/index.js b/packages/editor/src/components/inserter/index.js index d10f70e315af2c..1879683b3c072a 100644 --- a/packages/editor/src/components/inserter/index.js +++ b/packages/editor/src/components/inserter/index.js @@ -3,10 +3,9 @@ */ import { __ } from '@wordpress/i18n'; import { Dropdown, IconButton } from '@wordpress/components'; -import { createBlock, isUnmodifiedDefaultBlock } from '@wordpress/blocks'; import { Component } from '@wordpress/element'; -import { withSelect, withDispatch } from '@wordpress/data'; -import { compose } from '@wordpress/compose'; +import { withSelect } from '@wordpress/data'; +import { compose, ifCondition } from '@wordpress/compose'; /** * Internal dependencies @@ -30,6 +29,8 @@ class Inserter extends Component { super( ...arguments ); this.onToggle = this.onToggle.bind( this ); + this.renderToggle = this.renderToggle.bind( this ); + this.renderContent = this.renderContent.bind( this ); } onToggle( isOpen ) { @@ -41,20 +42,46 @@ class Inserter extends Component { } } - render() { + /** + * Render callback to display Dropdown toggle element. + * + * @param {Function} options.onToggle Callback to invoke when toggle is + * pressed. + * @param {boolean} options.isOpen Whether dropdown is currently open. + * + * @return {WPElement} Dropdown toggle element. + */ + renderToggle( { onToggle, isOpen } ) { const { - items, - position, - title, - onInsertBlock, - rootClientId, disabled, renderToggle = defaultRenderToggle, } = this.props; - if ( items.length === 0 ) { - return null; - } + return renderToggle( { onToggle, isOpen, disabled } ); + } + + /** + * Render callback to display Dropdown content element. + * + * @param {Function} options.onClose Callback to invoke when dropdown is + * closed. + * + * @return {WPElement} Dropdown content element. + */ + renderContent( { onClose } ) { + const { rootClientId, index } = this.props; + + return ( + + ); + } + + render() { + const { position, title } = this.props; return ( renderToggle( { onToggle, isOpen, disabled } ) } - renderContent={ ( { onClose } ) => { - const onSelect = ( item ) => { - onInsertBlock( item ); - - onClose(); - }; - - return ( - - ); - } } + renderToggle={ this.renderToggle } + renderContent={ this.renderContent } /> ); } @@ -90,7 +103,6 @@ export default compose( [ const { getEditedPostAttribute, getBlockInsertionPoint, - getSelectedBlock, getInserterItems, } = select( 'core/editor' ); @@ -106,21 +118,10 @@ export default compose( [ return { title: getEditedPostAttribute( 'title' ), - selectedBlock: getSelectedBlock(), - items: getInserterItems( rootClientId ), - index, + hasItems: getInserterItems( rootClientId ).length > 0, rootClientId, + index, }; } ), - withDispatch( ( dispatch, ownProps ) => ( { - onInsertBlock: ( item ) => { - const { selectedBlock, index, rootClientId } = ownProps; - const { name, initialAttributes } = item; - const insertedBlock = createBlock( name, initialAttributes ); - if ( selectedBlock && isUnmodifiedDefaultBlock( selectedBlock ) ) { - return dispatch( 'core/editor' ).replaceBlocks( selectedBlock.clientId, insertedBlock ); - } - return dispatch( 'core/editor' ).insertBlock( insertedBlock, index, rootClientId ); - }, - } ) ), + ifCondition( ( { hasItems } ) => hasItems ), ] )( Inserter ); diff --git a/packages/editor/src/components/inserter/menu.js b/packages/editor/src/components/inserter/menu.js index 4bb2362377c390..9780661f6442f9 100644 --- a/packages/editor/src/components/inserter/menu.js +++ b/packages/editor/src/components/inserter/menu.js @@ -23,7 +23,12 @@ import scrollIntoView from 'dom-scroll-into-view'; import { __, _n, _x, sprintf } from '@wordpress/i18n'; import { Component, findDOMNode, createRef } from '@wordpress/element'; import { withSpokenMessages, PanelBody } from '@wordpress/components'; -import { getCategories, isReusableBlock } from '@wordpress/blocks'; +import { + getCategories, + isReusableBlock, + createBlock, + isUnmodifiedDefaultBlock, +} from '@wordpress/blocks'; import { withDispatch, withSelect } from '@wordpress/data'; import { withInstanceId, compose, withSafeTimeout } from '@wordpress/compose'; import { LEFT, RIGHT, UP, DOWN, BACKSPACE, ENTER } from '@wordpress/keycodes'; @@ -340,21 +345,55 @@ export class InserterMenu extends Component { export default compose( withSelect( ( select, { rootClientId } ) => { const { - getChildBlockNames, - } = select( 'core/blocks' ); - const { + getEditedPostAttribute, + getSelectedBlock, + getInserterItems, getBlockName, } = select( 'core/editor' ); + const { + getChildBlockNames, + } = select( 'core/blocks' ); + const rootBlockName = getBlockName( rootClientId ); + return { + selectedBlock: getSelectedBlock(), rootChildBlocks: getChildBlockNames( rootBlockName ), + title: getEditedPostAttribute( 'title' ), + items: getInserterItems( rootClientId ), + rootClientId, + }; + } ), + withDispatch( ( dispatch, ownProps ) => { + const { + __experimentalFetchReusableBlocks: fetchReusableBlocks, + showInsertionPoint, + hideInsertionPoint, + } = dispatch( 'core/editor' ); + + return { + fetchReusableBlocks, + showInsertionPoint, + hideInsertionPoint, + onSelect( item ) { + const { + replaceBlocks, + insertBlock, + } = dispatch( 'core/editor' ); + const { selectedBlock, index, rootClientId } = ownProps; + const { name, initialAttributes } = item; + + const insertedBlock = createBlock( name, initialAttributes ); + if ( selectedBlock && isUnmodifiedDefaultBlock( selectedBlock ) ) { + replaceBlocks( selectedBlock.clientId, insertedBlock ); + } else { + insertBlock( insertedBlock, index, rootClientId ); + } + + ownProps.onSelect(); + }, }; } ), - withDispatch( ( dispatch ) => ( { - fetchReusableBlocks: dispatch( 'core/editor' ).__experimentalFetchReusableBlocks, - showInsertionPoint: dispatch( 'core/editor' ).showInsertionPoint, - hideInsertionPoint: dispatch( 'core/editor' ).hideInsertionPoint, - } ) ), withSpokenMessages, withInstanceId, withSafeTimeout From 3add7c19e5a922a153a14721810ff681c986409c Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Tue, 30 Oct 2018 13:50:17 -0400 Subject: [PATCH 22/98] Block List: Use default Inserter for sibling insertion (#11018) --- docs/data/data-core-editor.md | 10 +- .../editor/src/components/block-list/block.js | 8 +- .../components/block-list/insertion-point.js | 118 ++++++++---------- .../src/components/block-list/style.scss | 4 +- .../editor/src/components/inserter/index.js | 5 +- .../editor/src/components/inserter/menu.js | 6 +- packages/editor/src/store/actions.js | 10 +- packages/editor/src/store/reducer.js | 14 ++- packages/editor/src/store/selectors.js | 15 ++- packages/editor/src/store/test/reducer.js | 29 +++-- packages/editor/src/store/test/selectors.js | 57 ++++++++- test/e2e/specs/adding-blocks.test.js | 15 ++- 12 files changed, 177 insertions(+), 114 deletions(-) diff --git a/docs/data/data-core-editor.md b/docs/data/data-core-editor.md index 4ce76d80ea62a8..78c2371a6e3e21 100644 --- a/docs/data/data-core-editor.md +++ b/docs/data/data-core-editor.md @@ -707,7 +707,7 @@ otherwise. *Returns* -Whether block is first in mult-selection. +Whether block is first in multi-selection. ### isBlockMultiSelected @@ -1534,7 +1534,7 @@ be inserted, optionally at a specific index respective a root block list. * blocks: Block objects to insert. * index: Index at which block should be inserted. - * rootClientId: Optional root cliente ID of block list on + * rootClientId: Optional root client ID of block list on which to insert. ### showInsertionPoint @@ -1542,6 +1542,12 @@ be inserted, optionally at a specific index respective a root block list. Returns an action object used in signalling that the insertion point should be shown. +*Parameters* + + * rootClientId: Optional root client ID of block list on + which to insert. + * index: Index at which block should be inserted. + ### hideInsertionPoint Returns an action object hiding the insertion point. diff --git a/packages/editor/src/components/block-list/block.js b/packages/editor/src/components/block-list/block.js index 1213d1c424453e..5858f8731fb7ae 100644 --- a/packages/editor/src/components/block-list/block.js +++ b/packages/editor/src/components/block-list/block.js @@ -412,10 +412,9 @@ export class BlockListBlock extends Component { const shouldShowMobileToolbar = shouldAppearSelected; const { error, dragging } = this.state; - // Insertion point can only be made visible when the side inserter is - // not present, and either the block is at the extent of a selection or - // is the first block in the top-level list rendering. - const shouldShowInsertionPoint = ( isPartOfMultiSelection && isFirst ) || ! isPartOfMultiSelection; + // Insertion point can only be made visible if the block is at the + // the extent of a multi-selection, or not in a multi-selection. + const shouldShowInsertionPoint = ( isPartOfMultiSelection && isFirstMultiSelected ) || ! isPartOfMultiSelection; const canShowInBetweenInserter = ! isEmptyDefaultBlock && ! isPreviousBlockADefaultEmptyBlock; // The wp-block className is important for editor styles. @@ -501,7 +500,6 @@ export class BlockListBlock extends Component { clientId={ clientId } rootClientId={ rootClientId } canShowInserter={ canShowInBetweenInserter } - onInsert={ this.hideHoverEffects } /> ) } - { showInsertionPoint &&
    } - { showInserter && ( -
    - + ) } + { canShowInserter && ( +
    +
    ) } @@ -74,44 +77,23 @@ class BlockInsertionPoint extends Component { ); } } -export default compose( - withSelect( ( select, { clientId, rootClientId, canShowInserter } ) => { - const { - canInsertBlockType, - getBlockIndex, - getBlockInsertionPoint, - getBlock, - isBlockInsertionPointVisible, - isTyping, - } = select( 'core/editor' ); - const { - getDefaultBlockName, - } = select( 'core/blocks' ); - const blockIndex = clientId ? getBlockIndex( clientId, rootClientId ) : -1; - const insertIndex = blockIndex; - const insertionPoint = getBlockInsertionPoint(); - const block = clientId ? getBlock( clientId ) : null; - const showInsertionPoint = ( - isBlockInsertionPointVisible() && - insertionPoint.index === insertIndex && - insertionPoint.rootClientId === rootClientId && - ( ! block || ! isUnmodifiedDefaultBlock( block ) ) - ); +export default withSelect( ( select, { clientId, rootClientId } ) => { + const { + getBlockIndex, + getBlockInsertionPoint, + getBlock, + isBlockInsertionPointVisible, + } = select( 'core/editor' ); + const blockIndex = getBlockIndex( clientId, rootClientId ); + const insertIndex = blockIndex; + const insertionPoint = getBlockInsertionPoint(); + const block = getBlock( clientId ); + const showInsertionPoint = ( + isBlockInsertionPointVisible() && + insertionPoint.index === insertIndex && + insertionPoint.rootClientId === rootClientId && + ! isUnmodifiedDefaultBlock( block ) + ); - const defaultBlockName = getDefaultBlockName(); - return { - canInsertDefaultBlock: canInsertBlockType( defaultBlockName, rootClientId ), - showInserter: ! isTyping() && canShowInserter, - index: insertIndex, - showInsertionPoint, - }; - } ), - ifCondition( ( { canInsertDefaultBlock } ) => canInsertDefaultBlock ), - withDispatch( ( dispatch ) => { - const { insertDefaultBlock, startTyping } = dispatch( 'core/editor' ); - return { - insertDefaultBlock, - startTyping, - }; - } ) -)( BlockInsertionPoint ); + return { showInsertionPoint, insertIndex }; +} )( BlockInsertionPoint ); diff --git a/packages/editor/src/components/block-list/style.scss b/packages/editor/src/components/block-list/style.scss index 5ae32d9bdc7edf..67f1aaa00a61d1 100644 --- a/packages/editor/src/components/block-list/style.scss +++ b/packages/editor/src/components/block-list/style.scss @@ -640,7 +640,7 @@ justify-content: center; // Show a clickable plus. - .editor-block-list__insertion-point-button { + .editor-inserter__toggle { margin-top: -4px; border-radius: 50%; color: $blue-medium-focus; @@ -666,7 +666,7 @@ // Don't show the sibling inserter before the selected block. .edit-post-layout:not(.has-fixed-toolbar) { // The child selector is necessary for this to work properly in nested contexts. - .is-selected > .editor-block-list__insertion-point > .editor-block-list__insertion-point-inserter { + .is-selected > .editor-block-list__insertion-point .editor-inserter__toggle { opacity: 0; pointer-events: none; diff --git a/packages/editor/src/components/inserter/index.js b/packages/editor/src/components/inserter/index.js index 1879683b3c072a..8017fa4b65c1a2 100644 --- a/packages/editor/src/components/inserter/index.js +++ b/packages/editor/src/components/inserter/index.js @@ -99,15 +99,14 @@ class Inserter extends Component { } export default compose( [ - withSelect( ( select, { rootClientId } ) => { + withSelect( ( select, { rootClientId, index } ) => { const { getEditedPostAttribute, getBlockInsertionPoint, getInserterItems, } = select( 'core/editor' ); - let index; - if ( rootClientId === undefined ) { + if ( rootClientId === undefined && index === undefined ) { // Unless explicitly provided, the default insertion point provided // by the store occurs immediately following the selected block. // Otherwise, the default behavior for an undefined index is to diff --git a/packages/editor/src/components/inserter/menu.js b/packages/editor/src/components/inserter/menu.js index 9780661f6442f9..be7623a6a50aaf 100644 --- a/packages/editor/src/components/inserter/menu.js +++ b/packages/editor/src/components/inserter/menu.js @@ -130,10 +130,12 @@ export class InserterMenu extends Component { hoveredItem: item, } ); + const { showInsertionPoint, hideInsertionPoint } = this.props; if ( item ) { - this.props.showInsertionPoint(); + const { rootClientId, index } = this.props; + showInsertionPoint( rootClientId, index ); } else { - this.props.hideInsertionPoint(); + hideInsertionPoint(); } } diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index 2935b833270f0e..d0946c4f0e3d0b 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -312,7 +312,7 @@ export function insertBlock( block, index, rootClientId ) { * * @param {Object[]} blocks Block objects to insert. * @param {?number} index Index at which block should be inserted. - * @param {?string} rootClientId Optional root cliente ID of block list on + * @param {?string} rootClientId Optional root client ID of block list on * which to insert. * * @return {Object} Action object. @@ -331,11 +331,17 @@ export function insertBlocks( blocks, index, rootClientId ) { * Returns an action object used in signalling that the insertion point should * be shown. * + * @param {?string} rootClientId Optional root client ID of block list on + * which to insert. + * @param {?number} index Index at which block should be inserted. + * * @return {Object} Action object. */ -export function showInsertionPoint() { +export function showInsertionPoint( rootClientId, index ) { return { type: 'SHOW_INSERTION_POINT', + rootClientId, + index, }; } diff --git a/packages/editor/src/store/reducer.js b/packages/editor/src/store/reducer.js index 306451865409eb..fc6a1e387d9315 100644 --- a/packages/editor/src/store/reducer.js +++ b/packages/editor/src/store/reducer.js @@ -710,21 +710,23 @@ export function blocksMode( state = {}, action ) { } /** - * Reducer returning the block insertion point visibility, a boolean value - * reflecting whether the insertion point should be shown. + * Reducer returning the block insertion point visibility, either null if there + * is not an explicit insertion point assigned, or an object of its `index` and + * `rootClientId`. * * @param {Object} state Current state. * @param {Object} action Dispatched action. * * @return {Object} Updated state. */ -export function isInsertionPointVisible( state = false, action ) { +export function insertionPoint( state = null, action ) { switch ( action.type ) { case 'SHOW_INSERTION_POINT': - return true; + const { rootClientId, index } = action; + return { rootClientId, index }; case 'HIDE_INSERTION_POINT': - return false; + return null; } return state; @@ -1100,7 +1102,7 @@ export default optimist( combineReducers( { blockSelection, blocksMode, blockListSettings, - isInsertionPointVisible, + insertionPoint, preferences, saving, postLock, diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index ed734f7e5dcbfd..79cd4ca0fc8a05 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -1016,10 +1016,10 @@ export function getLastMultiSelectedBlockClientId( state ) { * specified client ID is the first block of the multi-selection set, or false * otherwise. * - * @param {Object} state Editor state. - * @param {string} clientId Block client ID. + * @param {Object} state Editor state. + * @param {string} clientId Block client ID. * - * @return {boolean} Whether block is first in mult-selection. + * @return {boolean} Whether block is first in multi-selection. */ export function isFirstMultiSelectedBlock( state, clientId ) { return getFirstMultiSelectedBlockClientId( state ) === clientId; @@ -1277,7 +1277,12 @@ export function isCaretWithinFormattedText( state ) { export function getBlockInsertionPoint( state ) { let rootClientId, index; - const { end } = state.blockSelection; + const { insertionPoint, blockSelection } = state; + if ( insertionPoint !== null ) { + return insertionPoint; + } + + const { end } = blockSelection; if ( end ) { rootClientId = getBlockRootClientId( state, end ) || undefined; index = getBlockIndex( state, end, rootClientId ) + 1; @@ -1296,7 +1301,7 @@ export function getBlockInsertionPoint( state ) { * @return {?boolean} Whether the insertion point is visible or not. */ export function isBlockInsertionPointVisible( state ) { - return state.isInsertionPointVisible; + return state.insertionPoint !== null; } /** diff --git a/packages/editor/src/store/test/reducer.js b/packages/editor/src/store/test/reducer.js index c0462df96f4112..8ab789e987077a 100644 --- a/packages/editor/src/store/test/reducer.js +++ b/packages/editor/src/store/test/reducer.js @@ -30,7 +30,7 @@ import { preferences, saving, blocksMode, - isInsertionPointVisible, + insertionPoint, reusableBlocks, template, blockListSettings, @@ -1274,27 +1274,36 @@ describe( 'state', () => { } ); } ); - describe( 'isInsertionPointVisible', () => { - it( 'should default to false', () => { - const state = isInsertionPointVisible( undefined, {} ); + describe( 'insertionPoint', () => { + it( 'should default to null', () => { + const state = insertionPoint( undefined, {} ); - expect( state ).toBe( false ); + expect( state ).toBe( null ); } ); - it( 'should set insertion point visible', () => { - const state = isInsertionPointVisible( false, { + it( 'should set insertion point', () => { + const state = insertionPoint( null, { type: 'SHOW_INSERTION_POINT', + rootClientId: 'clientId1', + index: 0, } ); - expect( state ).toBe( true ); + expect( state ).toEqual( { + rootClientId: 'clientId1', + index: 0, + } ); } ); it( 'should clear the insertion point', () => { - const state = isInsertionPointVisible( true, { + const original = deepFreeze( { + rootClientId: 'clientId1', + index: 0, + } ); + const state = insertionPoint( original, { type: 'HIDE_INSERTION_POINT', } ); - expect( state ).toBe( false ); + expect( state ).toBe( null ); } ); } ); diff --git a/packages/editor/src/store/test/selectors.js b/packages/editor/src/store/test/selectors.js index dcfb2016d369ba..157bdb62a5926a 100644 --- a/packages/editor/src/store/test/selectors.js +++ b/packages/editor/src/store/test/selectors.js @@ -2761,6 +2761,40 @@ describe( 'selectors', () => { } ); describe( 'getBlockInsertionPoint', () => { + it( 'should return the explicitly assigned insertion point', () => { + const state = { + currentPost: {}, + preferences: { mode: 'visual' }, + blockSelection: { + start: 'clientId2', + end: 'clientId2', + }, + editor: { + present: { + blocksByClientId: { + clientId1: { clientId: 'clientId1' }, + clientId2: { clientId: 'clientId2' }, + }, + blockOrder: { + '': [ 'clientId1' ], + clientId1: [ 'clientId2' ], + clientId2: [], + }, + edits: {}, + }, + }, + insertionPoint: { + rootClientId: undefined, + index: 0, + }, + }; + + expect( getBlockInsertionPoint( state ) ).toEqual( { + rootClientId: undefined, + index: 0, + } ); + } ); + it( 'should return an object for the selected block', () => { const state = { currentPost: {}, @@ -2781,7 +2815,7 @@ describe( 'selectors', () => { edits: {}, }, }, - isInsertionPointVisible: false, + insertionPoint: null, }; expect( getBlockInsertionPoint( state ) ).toEqual( { @@ -2812,7 +2846,7 @@ describe( 'selectors', () => { edits: {}, }, }, - isInsertionPointVisible: false, + insertionPoint: null, }; expect( getBlockInsertionPoint( state ) ).toEqual( { @@ -2843,7 +2877,7 @@ describe( 'selectors', () => { edits: {}, }, }, - isInsertionPointVisible: false, + insertionPoint: null, }; expect( getBlockInsertionPoint( state ) ).toEqual( { @@ -2874,7 +2908,7 @@ describe( 'selectors', () => { edits: {}, }, }, - isInsertionPointVisible: false, + insertionPoint: null, }; expect( getBlockInsertionPoint( state ) ).toEqual( { @@ -2885,9 +2919,20 @@ describe( 'selectors', () => { } ); describe( 'isBlockInsertionPointVisible', () => { - it( 'should return the value in state', () => { + it( 'should return false if no assigned insertion point', () => { + const state = { + insertionPoint: null, + }; + + expect( isBlockInsertionPointVisible( state ) ).toBe( false ); + } ); + + it( 'should return true if assigned insertion point', () => { const state = { - isInsertionPointVisible: true, + insertionPoint: { + rootClientId: undefined, + index: 5, + }, }; expect( isBlockInsertionPointVisible( state ) ).toBe( true ); diff --git a/test/e2e/specs/adding-blocks.test.js b/test/e2e/specs/adding-blocks.test.js index d4eaf27ad8e45d..0c38f03fdbfa13 100644 --- a/test/e2e/specs/adding-blocks.test.js +++ b/test/e2e/specs/adding-blocks.test.js @@ -67,11 +67,20 @@ describe( 'adding blocks', () => { await page.click( '.editor-post-title__input' ); // Using the between inserter - const insertionPoint = await page.$( '[data-type="core/quote"] .editor-block-list__insertion-point-button' ); + const insertionPoint = await page.$( '[data-type="core/quote"] .editor-inserter__toggle' ); const rect = await insertionPoint.boundingBox(); await page.mouse.move( rect.x + ( rect.width / 2 ), rect.y + ( rect.height / 2 ), { steps: 10 } ); - await page.waitForSelector( '[data-type="core/quote"] .editor-block-list__insertion-point-button' ); - await page.click( '[data-type="core/quote"] .editor-block-list__insertion-point-button' ); + await page.waitForSelector( '[data-type="core/quote"] .editor-inserter__toggle' ); + await page.click( '[data-type="core/quote"] .editor-inserter__toggle' ); + // [TODO]: Search input should be focused immediately. It shouldn't be + // necessary to have `waitForFunction`. + await page.waitForFunction( () => ( + document.activeElement && + document.activeElement.classList.contains( 'editor-inserter__search' ) + ) ); + await page.keyboard.type( 'para' ); + await pressTimes( 'Tab', 3 ); + await page.keyboard.press( 'Enter' ); await page.keyboard.type( 'Second paragraph' ); // Switch to Text Mode to check HTML Output From 9d46788bd90b3fa3697daa0dcad79a9324f327c4 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Tue, 30 Oct 2018 19:22:26 +0100 Subject: [PATCH 23/98] Revert using Icon in IconButton to avoid regression in plugin icons (pinned icons) (#11256) --- packages/components/src/icon-button/index.js | 6 +-- .../components/src/icon-button/test/index.js | 2 +- .../test/__snapshots__/index.js.snap | 45 +++++++++---------- 3 files changed, 24 insertions(+), 29 deletions(-) diff --git a/packages/components/src/icon-button/index.js b/packages/components/src/icon-button/index.js index eec8ecd913a51f..4cc8ac426ab4ee 100644 --- a/packages/components/src/icon-button/index.js +++ b/packages/components/src/icon-button/index.js @@ -2,7 +2,7 @@ * External dependencies */ import classnames from 'classnames'; -import { isArray } from 'lodash'; +import { isArray, isString } from 'lodash'; /** * WordPress dependencies @@ -14,7 +14,7 @@ import { Component } from '@wordpress/element'; */ import Tooltip from '../tooltip'; import Button from '../button'; -import Icon from '../icon'; +import Dashicon from '../dashicon'; // This is intentionally a Component class, not a function component because it // is common to apply a ref to the button element (only supported in class) @@ -42,7 +42,7 @@ class IconButton extends Component { let element = ( ); diff --git a/packages/components/src/icon-button/test/index.js b/packages/components/src/icon-button/test/index.js index b93f46a3c5bf66..d5b67eec7930f6 100644 --- a/packages/components/src/icon-button/test/index.js +++ b/packages/components/src/icon-button/test/index.js @@ -20,7 +20,7 @@ describe( 'IconButton', () => { it( 'should render a Dashicon component matching the wordpress icon', () => { const iconButton = shallow( ); - expect( iconButton.find( 'Icon' ).prop( 'icon' ) ).toBe( 'wordpress' ); + expect( iconButton.find( 'Dashicon' ).shallow().hasClass( 'dashicons-wordpress' ) ).toBe( true ); } ); it( 'should render child elements when passed as children', () => { diff --git a/packages/edit-post/src/components/header/more-menu/test/__snapshots__/index.js.snap b/packages/edit-post/src/components/header/more-menu/test/__snapshots__/index.js.snap index 425ee6a645971d..244a185376638c 100644 --- a/packages/edit-post/src/components/header/more-menu/test/__snapshots__/index.js.snap +++ b/packages/edit-post/src/components/header/more-menu/test/__snapshots__/index.js.snap @@ -42,16 +42,22 @@ exports[`MoreMenu should match snapshot 1`] = ` onMouseLeave={[Function]} type="button" > - - - - - - - + /> + + + + From 0f1130048daf87bfed12c3cbf5c1c754211b29f7 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Tue, 30 Oct 2018 15:25:59 -0400 Subject: [PATCH 24/98] Data: Use turbo-combine-reducers in place of Redux (#11255) * Data: Use turbo-combine-reducers in place of Redux * Data: Update package-lock.json for turbo-combine-reducers --- package-lock.json | 8 +++++++- packages/data/CHANGELOG.md | 10 ++++++++-- packages/data/package.json | 3 ++- packages/data/src/index.js | 2 +- 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index ed71b4aeeb4313..5d5dcb1b6c26b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2189,7 +2189,8 @@ "equivalent-key-map": "^0.2.2", "is-promise": "^2.1.0", "lodash": "^4.17.10", - "redux": "^4.0.0" + "redux": "^4.0.0", + "turbo-combine-reducers": "^1.0.2" } }, "@wordpress/date": { @@ -19933,6 +19934,11 @@ "safe-buffer": "^5.0.1" } }, + "turbo-combine-reducers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/turbo-combine-reducers/-/turbo-combine-reducers-1.0.2.tgz", + "integrity": "sha512-gHbdMZlA6Ym6Ur5pSH/UWrNQMIM9IqTH6SoL1DbHpqEdQ8i+cFunSmSlFykPt0eGQwZ4d/XTHOl74H0/kFBVWw==" + }, "tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", diff --git a/packages/data/CHANGELOG.md b/packages/data/CHANGELOG.md index bc8f8aee7bd42b..ce5e89b914db2f 100644 --- a/packages/data/CHANGELOG.md +++ b/packages/data/CHANGELOG.md @@ -1,3 +1,9 @@ +## 3.0.1 (Unreleased) + +### Internal + +- Replace Redux implementation of `combineReducers` with in-place-compatible `turbo-combine-reducers`. + ## 3.0.0 (2018-10-29) ### Breaking Changes @@ -10,7 +16,7 @@ ## 2.1.0 (2018-09-30) -## New Features +### New Features - Adding support for using controls in resolvers using the controls plugin. @@ -22,7 +28,7 @@ - Writing resolvers as async generators has been deprecated. Use the controls plugin instead. -## Bug Fixes +### Bug Fixes - Fix the promise middleware in Firefox. diff --git a/packages/data/package.json b/packages/data/package.json index 471486a547eb64..abe657181c52a8 100644 --- a/packages/data/package.json +++ b/packages/data/package.json @@ -29,7 +29,8 @@ "equivalent-key-map": "^0.2.2", "is-promise": "^2.1.0", "lodash": "^4.17.10", - "redux": "^4.0.0" + "redux": "^4.0.0", + "turbo-combine-reducers": "^1.0.2" }, "devDependencies": { "deep-freeze": "^0.0.1", diff --git a/packages/data/src/index.js b/packages/data/src/index.js index 2e71774abcdbbf..122f9c261b760b 100644 --- a/packages/data/src/index.js +++ b/packages/data/src/index.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { combineReducers } from 'redux'; +import combineReducers from 'turbo-combine-reducers'; /** * Internal dependencies From 39d5e21df83b0e59b62db6a9bceb47fae94a401f Mon Sep 17 00:00:00 2001 From: Miguel Fonseca Date: Tue, 30 Oct 2018 19:27:01 +0000 Subject: [PATCH 25/98] Update plugin version to 4.2.0. (#11258) --- gutenberg.php | 2 +- package-lock.json | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gutenberg.php b/gutenberg.php index 1d123094d09330..09ed86af7aa590 100644 --- a/gutenberg.php +++ b/gutenberg.php @@ -3,7 +3,7 @@ * Plugin Name: Gutenberg * Plugin URI: https://github.com/WordPress/gutenberg * Description: Printing since 1440. This is the development plugin for the new block editor in core. - * Version: 4.1.1 + * Version: 4.2.0-rc.1 * Author: Gutenberg Team * * @package gutenberg diff --git a/package-lock.json b/package-lock.json index 5d5dcb1b6c26b6..e34e319d22a7fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "4.1.1", + "version": "4.2.0-rc.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index e55538173a0f0e..5ecc9d58868ec5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "4.1.1", + "version": "4.2.0-rc.1", "private": true, "description": "A new WordPress editor experience", "repository": "git+https://github.com/WordPress/gutenberg.git", From 160cc0c7009de548af2cf26aee56f8fef9df202f Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski Date: Tue, 30 Oct 2018 21:10:52 +0100 Subject: [PATCH 26/98] chore(release): publish - @wordpress/api-fetch@2.2.1 - @wordpress/block-library@2.1.7 - @wordpress/blocks@5.1.1 - @wordpress/components@5.0.1 - @wordpress/core-data@2.0.8 - @wordpress/data@3.0.1 - @wordpress/edit-post@2.0.2 - @wordpress/editor@6.1.0 - @wordpress/format-library@1.0.2 - @wordpress/keycodes@2.0.3 - @wordpress/list-reusable-blocks@1.1.6 - @wordpress/notices@1.0.1 - @wordpress/nux@2.0.8 - @wordpress/rich-text@2.0.1 - @wordpress/viewport@2.0.7 --- packages/api-fetch/package.json | 2 +- packages/block-library/package.json | 2 +- packages/blocks/package.json | 2 +- packages/components/package.json | 2 +- packages/core-data/package.json | 2 +- packages/data/package.json | 2 +- packages/edit-post/package.json | 2 +- packages/editor/package.json | 2 +- packages/format-library/package.json | 2 +- packages/keycodes/package.json | 2 +- packages/list-reusable-blocks/package.json | 2 +- packages/notices/package.json | 2 +- packages/nux/package.json | 2 +- packages/rich-text/package.json | 2 +- packages/viewport/package.json | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/api-fetch/package.json b/packages/api-fetch/package.json index 5a540c1cb2a623..2fcc7ce88b236a 100644 --- a/packages/api-fetch/package.json +++ b/packages/api-fetch/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/api-fetch", - "version": "2.2.0", + "version": "2.2.1", "description": "Utility to make WordPress REST API requests.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/block-library/package.json b/packages/block-library/package.json index 99378cfd77f3fb..98f546fb9f186d 100644 --- a/packages/block-library/package.json +++ b/packages/block-library/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-library", - "version": "2.1.6", + "version": "2.1.7", "description": "Block library for the WordPress editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/blocks/package.json b/packages/blocks/package.json index 395a73310bada4..348287e08367c4 100644 --- a/packages/blocks/package.json +++ b/packages/blocks/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/blocks", - "version": "5.1.0", + "version": "5.1.1", "description": "Block API for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/components/package.json b/packages/components/package.json index 8c4df8e4dadb6c..d01a75826df0e5 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/components", - "version": "5.0.0", + "version": "5.0.1", "description": "UI components for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/core-data/package.json b/packages/core-data/package.json index 6ce9acdcdbb540..aa48096df8c46b 100644 --- a/packages/core-data/package.json +++ b/packages/core-data/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/core-data", - "version": "2.0.7", + "version": "2.0.8", "description": "Access to and manipulation of core WordPress entities.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/data/package.json b/packages/data/package.json index abe657181c52a8..596cad5d3a3c84 100644 --- a/packages/data/package.json +++ b/packages/data/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/data", - "version": "3.0.0", + "version": "3.0.1", "description": "Data module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/edit-post/package.json b/packages/edit-post/package.json index 3ae06ca6baa103..09b0d0ecbeec6c 100644 --- a/packages/edit-post/package.json +++ b/packages/edit-post/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/edit-post", - "version": "2.0.1", + "version": "2.0.2", "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 2d2253cd3d3607..3ac12f6921fa48 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/editor", - "version": "6.0.1", + "version": "6.1.0", "description": "Building blocks for WordPress editors.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/format-library/package.json b/packages/format-library/package.json index 2bbf6563d7de38..d1db967c54b29c 100644 --- a/packages/format-library/package.json +++ b/packages/format-library/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/format-library", - "version": "1.0.1", + "version": "1.0.2", "description": "Format library for the WordPress editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/keycodes/package.json b/packages/keycodes/package.json index 602472cd5025a1..331f0f40a03233 100644 --- a/packages/keycodes/package.json +++ b/packages/keycodes/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/keycodes", - "version": "2.0.2", + "version": "2.0.3", "description": "Keycodes utilities for WordPress. Used to check for keyboard events across browsers/operating systems.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/list-reusable-blocks/package.json b/packages/list-reusable-blocks/package.json index 026457330f64f5..68822f07f2db41 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.5", + "version": "1.1.6", "description": "Adding Export/Import support to the reusable blocks listing.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/notices/package.json b/packages/notices/package.json index 93e9ac42d9900e..27a27545c0c9f7 100644 --- a/packages/notices/package.json +++ b/packages/notices/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/notices", - "version": "1.0.0", + "version": "1.0.1", "description": "State management for notices.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/nux/package.json b/packages/nux/package.json index 46a2cf510f3f62..4f5cbaa9acff12 100644 --- a/packages/nux/package.json +++ b/packages/nux/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/nux", - "version": "2.0.7", + "version": "2.0.8", "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 6c2474383df7da..36a06be61f1d31 100644 --- a/packages/rich-text/package.json +++ b/packages/rich-text/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/rich-text", - "version": "2.0.0", + "version": "2.0.1", "description": "Rich text value and manipulation API.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/viewport/package.json b/packages/viewport/package.json index 3fcab4650d3bfb..c7eb6ef08dc1ab 100644 --- a/packages/viewport/package.json +++ b/packages/viewport/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/viewport", - "version": "2.0.6", + "version": "2.0.7", "description": "Viewport module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", From 3600c88fda01b62463769b5e8c3e795b064e12c1 Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski Date: Tue, 30 Oct 2018 21:18:50 +0100 Subject: [PATCH 27/98] chore(release): update changelog files --- packages/api-fetch/CHANGELOG.md | 4 +++- packages/block-library/CHANGELOG.md | 2 ++ packages/blocks/CHANGELOG.md | 2 ++ packages/components/CHANGELOG.md | 2 ++ packages/core-data/CHANGELOG.md | 2 ++ packages/data/CHANGELOG.md | 2 +- packages/edit-post/CHANGELOG.md | 2 ++ packages/editor/CHANGELOG.md | 2 +- packages/format-library/CHANGELOG.md | 2 ++ packages/keycodes/CHANGELOG.md | 2 ++ packages/list-reusable-blocks/CHANGELOG.md | 2 ++ packages/notices/CHANGELOG.md | 2 ++ packages/rich-text/CHANGELOG.md | 2 ++ packages/viewport/CHANGELOG.md | 2 ++ 14 files changed, 27 insertions(+), 3 deletions(-) diff --git a/packages/api-fetch/CHANGELOG.md b/packages/api-fetch/CHANGELOG.md index a10c2b53851316..a59aee8634fc73 100644 --- a/packages/api-fetch/CHANGELOG.md +++ b/packages/api-fetch/CHANGELOG.md @@ -1,4 +1,6 @@ -# 2.2.0 (2018-10-29) +## 2.2.1 (2018-10-30) + +## 2.2.0 (2018-10-29) ### New Feature diff --git a/packages/block-library/CHANGELOG.md b/packages/block-library/CHANGELOG.md index f3a7e80e7e4651..8df197bb991957 100644 --- a/packages/block-library/CHANGELOG.md +++ b/packages/block-library/CHANGELOG.md @@ -1,3 +1,5 @@ +## 2.1.7 (2018-10-30) + ## 2.1.6 (2018-10-30) ### Bug Fixes diff --git a/packages/blocks/CHANGELOG.md b/packages/blocks/CHANGELOG.md index 7db53884fe79e3..dd08eb8aa02756 100644 --- a/packages/blocks/CHANGELOG.md +++ b/packages/blocks/CHANGELOG.md @@ -1,3 +1,5 @@ +## 5.1.1 (2018-10-30) + ## 5.1.0 (2018-10-30) ### New feature diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 7351a93a587db3..d0c7e552baf1dd 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -1,3 +1,5 @@ +## 5.0.1 (2018-10-30) + ## 5.0.0 (2018-10-29) ### Breaking Change diff --git a/packages/core-data/CHANGELOG.md b/packages/core-data/CHANGELOG.md index efb7fe6c22c47d..f8286ef8c09abb 100644 --- a/packages/core-data/CHANGELOG.md +++ b/packages/core-data/CHANGELOG.md @@ -1,3 +1,5 @@ +## 2.0.8 (2018-10-30) + ## 2.0.6 (2018-10-22) ## 2.0.5 (2018-10-19) diff --git a/packages/data/CHANGELOG.md b/packages/data/CHANGELOG.md index ce5e89b914db2f..34ed8c5a73bb3c 100644 --- a/packages/data/CHANGELOG.md +++ b/packages/data/CHANGELOG.md @@ -1,4 +1,4 @@ -## 3.0.1 (Unreleased) +## 3.0.1 (2018-10-30) ### Internal diff --git a/packages/edit-post/CHANGELOG.md b/packages/edit-post/CHANGELOG.md index 105e2b38412604..bb14a1d78bb856 100644 --- a/packages/edit-post/CHANGELOG.md +++ b/packages/edit-post/CHANGELOG.md @@ -1,3 +1,5 @@ +## 2.0.2 (2018-10-30) + ## 2.0.1 (2018-10-30) ## 2.0.0 (2018-10-29) diff --git a/packages/editor/CHANGELOG.md b/packages/editor/CHANGELOG.md index 8ef9c5cce31eaf..3b0ed28447c317 100644 --- a/packages/editor/CHANGELOG.md +++ b/packages/editor/CHANGELOG.md @@ -1,4 +1,4 @@ -## 6.1.0 (Unreleased) +## 6.1.0 (2018-10-30) ### Deprecations diff --git a/packages/format-library/CHANGELOG.md b/packages/format-library/CHANGELOG.md index ab080e1495e9b6..60840015bcccfc 100644 --- a/packages/format-library/CHANGELOG.md +++ b/packages/format-library/CHANGELOG.md @@ -1,3 +1,5 @@ +## 1.0.2 (2018-10-30) + ## 1.0.1 (2018-10-30) ## 1.0.0 (2018-10-29) diff --git a/packages/keycodes/CHANGELOG.md b/packages/keycodes/CHANGELOG.md index a49cefcd0d75cf..9c0e0dcaa72dce 100644 --- a/packages/keycodes/CHANGELOG.md +++ b/packages/keycodes/CHANGELOG.md @@ -1,3 +1,5 @@ +## 2.0.3 (2018-10-30) + ## 2.0.0 (2018-09-05) ### Breaking Change diff --git a/packages/list-reusable-blocks/CHANGELOG.md b/packages/list-reusable-blocks/CHANGELOG.md index 6234aa478b9abb..0b07680b95a9aa 100644 --- a/packages/list-reusable-blocks/CHANGELOG.md +++ b/packages/list-reusable-blocks/CHANGELOG.md @@ -1,3 +1,5 @@ +## 1.1.6 (2018-10-30) + ## 1.1.4 (2018-10-22) ## 1.1.3 (2018-10-19) diff --git a/packages/notices/CHANGELOG.md b/packages/notices/CHANGELOG.md index 69606f9c9fc37d..7e9aad2e81b11a 100644 --- a/packages/notices/CHANGELOG.md +++ b/packages/notices/CHANGELOG.md @@ -1,3 +1,5 @@ +## 1.0.1 (2018-10-30) + ## 1.0.0 (2018-10-29) - Initial release. diff --git a/packages/rich-text/CHANGELOG.md b/packages/rich-text/CHANGELOG.md index 3efab18e80d667..07a1cf023dd80e 100644 --- a/packages/rich-text/CHANGELOG.md +++ b/packages/rich-text/CHANGELOG.md @@ -1,3 +1,5 @@ +## 2.0.1 (2018-10-30) + ## 2.0.0 (2018-10-30) - Remove `@wordpress/blocks` as a dependency. diff --git a/packages/viewport/CHANGELOG.md b/packages/viewport/CHANGELOG.md index 9b4bad0bd4aa56..7e7b548c9ef068 100644 --- a/packages/viewport/CHANGELOG.md +++ b/packages/viewport/CHANGELOG.md @@ -1,3 +1,5 @@ +## 2.0.7 (2018-10-30) + ## 2.0.5 (2018-10-19) ## 2.0.4 (2018-10-18) From 3d9e99bbf81a1270b85f176e0f761a8ae0bdbad5 Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Wed, 31 Oct 2018 11:20:00 +1100 Subject: [PATCH 28/98] Rename parentClientId to rootClientId for consistency (#11274) We refer to the same concept as rootClientId elsewhere in the application. --- docs/data/data-core-editor.md | 12 ++---- .../src/components/autocompleters/block.js | 8 ++-- .../src/components/inner-blocks/index.js | 4 +- packages/editor/src/store/selectors.js | 42 +++++++++---------- 4 files changed, 29 insertions(+), 37 deletions(-) diff --git a/docs/data/data-core-editor.md b/docs/data/data-core-editor.md index 78c2371a6e3e21..ebbc303649e17e 100644 --- a/docs/data/data-core-editor.md +++ b/docs/data/data-core-editor.md @@ -1074,17 +1074,13 @@ Post content. ### canInsertBlockType -Determines if the given block type is allowed to be inserted, and, if -parentClientId is provided, whether it is allowed to be nested within the -given parent. +Determines if the given block type is allowed to be inserted into the block list. *Parameters* * state: Editor state. - * blockName: The name of the given block type, e.g. - 'core/paragraph'. - * parentClientId: The parent that the given block is to be - nested within, or null. + * blockName: The name of the block type, e.g.' core/paragraph'. + * rootClientId: Optional root client ID of block list. *Returns* @@ -1114,7 +1110,7 @@ Items are returned ordered descendingly by their 'utility' and 'frecency'. *Parameters* * state: Editor state. - * parentClientId: The block we are inserting into, if any. + * rootClientId: Optional root client ID of block list. *Returns* diff --git a/packages/editor/src/components/autocompleters/block.js b/packages/editor/src/components/autocompleters/block.js index c864e2a77edb69..e48bf2f48e7380 100644 --- a/packages/editor/src/components/autocompleters/block.js +++ b/packages/editor/src/components/autocompleters/block.js @@ -23,14 +23,14 @@ function defaultGetBlockInsertionParentClientId() { /** * Returns the inserter items for the specified parent block. * - * @param {string} parentClientId Client ID of the block for which to retrieve - * inserter items. + * @param {string} rootClientId Client ID of the block for which to retrieve + * inserter items. * * @return {Array} The inserter items for the specified * parent. */ -function defaultGetInserterItems( parentClientId ) { - return select( 'core/editor' ).getInserterItems( parentClientId ); +function defaultGetInserterItems( rootClientId ) { + return select( 'core/editor' ).getInserterItems( rootClientId ); } /** diff --git a/packages/editor/src/components/inner-blocks/index.js b/packages/editor/src/components/inner-blocks/index.js index 0c06acd345eb5f..c9252e912b5df1 100644 --- a/packages/editor/src/components/inner-blocks/index.js +++ b/packages/editor/src/components/inner-blocks/index.js @@ -128,12 +128,12 @@ InnerBlocks = compose( [ getTemplateLock, } = select( 'core/editor' ); const { clientId } = ownProps; - const parentClientId = getBlockRootClientId( clientId ); + const rootClientId = getBlockRootClientId( clientId ); return { isSelectedBlockInRoot: isBlockSelected( clientId ) || hasSelectedInnerBlock( clientId ), block: getBlock( clientId ), blockListSettings: getBlockListSettings( clientId ), - parentLock: getTemplateLock( parentClientId ), + parentLock: getTemplateLock( rootClientId ), }; } ), withDispatch( ( dispatch, ownProps ) => { diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index 79cd4ca0fc8a05..b31ff36d46794f 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -1506,20 +1506,16 @@ export const getEditedPostContent = createSelector( ); /** - * Determines if the given block type is allowed to be inserted, and, if - * parentClientId is provided, whether it is allowed to be nested within the - * given parent. + * Determines if the given block type is allowed to be inserted into the block list. * - * @param {Object} state Editor state. - * @param {string} blockName The name of the given block type, e.g. - * 'core/paragraph'. - * @param {?string} parentClientId The parent that the given block is to be - * nested within, or null. + * @param {Object} state Editor state. + * @param {string} blockName The name of the block type, e.g.' core/paragraph'. + * @param {?string} rootClientId Optional root client ID of block list. * * @return {boolean} Whether the given block type is allowed to be inserted. */ export const canInsertBlockType = createSelector( - ( state, blockName, parentClientId = null ) => { + ( state, blockName, rootClientId = null ) => { const checkAllowList = ( list, item, defaultResult = null ) => { if ( isBoolean( list ) ) { return list; @@ -1542,17 +1538,17 @@ export const canInsertBlockType = createSelector( return false; } - const isLocked = !! getTemplateLock( state, parentClientId ); + const isLocked = !! getTemplateLock( state, rootClientId ); if ( isLocked ) { return false; } - const parentBlockListSettings = getBlockListSettings( state, parentClientId ); + const parentBlockListSettings = getBlockListSettings( state, rootClientId ); const parentAllowedBlocks = get( parentBlockListSettings, [ 'allowedBlocks' ] ); const hasParentAllowedBlock = checkAllowList( parentAllowedBlocks, blockName ); const blockAllowedParentBlocks = blockType.parent; - const parentName = getBlockName( state, parentClientId ); + const parentName = getBlockName( state, rootClientId ); const hasBlockAllowedParent = checkAllowList( blockAllowedParentBlocks, parentName ); if ( hasParentAllowedBlock !== null && hasBlockAllowedParent !== null ) { @@ -1565,9 +1561,9 @@ export const canInsertBlockType = createSelector( return true; }, - ( state, blockName, parentClientId ) => [ - state.blockListSettings[ parentClientId ], - state.editor.present.blocksByClientId[ parentClientId ], + ( state, blockName, rootClientId ) => [ + state.blockListSettings[ rootClientId ], + state.editor.present.blocksByClientId[ rootClientId ], state.settings.allowedBlockTypes, state.settings.templateLock, ], @@ -1607,8 +1603,8 @@ function getInsertUsage( state, id ) { * * Items are returned ordered descendingly by their 'utility' and 'frecency'. * - * @param {Object} state Editor state. - * @param {?string} parentClientId The block we are inserting into, if any. + * @param {Object} state Editor state. + * @param {?string} rootClientId Optional root client ID of block list. * * @return {Editor.InserterItem[]} Items that appear in inserter. * @@ -1626,7 +1622,7 @@ function getInsertUsage( state, id ) { * @property {number} frecency Hueristic that combines frequency and recency. */ export const getInserterItems = createSelector( - ( state, parentClientId = null ) => { + ( state, rootClientId = null ) => { const calculateUtility = ( category, count, isContextual ) => { if ( isContextual ) { return INSERTER_UTILITY_HIGH; @@ -1664,7 +1660,7 @@ export const getInserterItems = createSelector( return false; } - return canInsertBlockType( state, blockType.name, parentClientId ); + return canInsertBlockType( state, blockType.name, rootClientId ); }; const buildBlockTypeInserterItem = ( blockType ) => { @@ -1694,7 +1690,7 @@ export const getInserterItems = createSelector( }; const shouldIncludeReusableBlock = ( reusableBlock ) => { - if ( ! canInsertBlockType( state, 'core/block', parentClientId ) ) { + if ( ! canInsertBlockType( state, 'core/block', rootClientId ) ) { return false; } @@ -1708,7 +1704,7 @@ export const getInserterItems = createSelector( return false; } - if ( ! canInsertBlockType( state, referencedBlockType.name, parentClientId ) ) { + if ( ! canInsertBlockType( state, referencedBlockType.name, rootClientId ) ) { return false; } @@ -1753,8 +1749,8 @@ export const getInserterItems = createSelector( [ 'desc', 'desc' ] ); }, - ( state, parentClientId ) => [ - state.blockListSettings[ parentClientId ], + ( state, rootClientId ) => [ + state.blockListSettings[ rootClientId ], state.editor.present.blockOrder, state.editor.present.blocksByClientId, state.preferences.insertUsage, From 55d5aac9550ca423caeba8359ddc17a48e28f21b Mon Sep 17 00:00:00 2001 From: Marko Andrijasevic Date: Wed, 31 Oct 2018 08:01:29 +0100 Subject: [PATCH 29/98] Nux package: fix incorrect named deprecated import (#11283) DotTip component was attempting to import a named export of deprecated which is not provided by @wordpress/deprecated package. This switches it to use the deafult export instead. --- packages/nux/src/components/dot-tip/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nux/src/components/dot-tip/index.js b/packages/nux/src/components/dot-tip/index.js index dbd81c45d13a62..45c7a768e2089b 100644 --- a/packages/nux/src/components/dot-tip/index.js +++ b/packages/nux/src/components/dot-tip/index.js @@ -5,7 +5,7 @@ import { compose } from '@wordpress/compose'; import { Popover, Button, IconButton } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { withSelect, withDispatch } from '@wordpress/data'; -import { deprecated } from '@wordpress/deprecated'; +import deprecated from '@wordpress/deprecated'; function getAnchorRect( anchor ) { // The default getAnchorRect() excludes an element's top and bottom padding From 8f5c78c2af94fac569d5c69cc1c6feb0d8f66388 Mon Sep 17 00:00:00 2001 From: Eric Murphy Date: Wed, 31 Oct 2018 17:26:28 +0700 Subject: [PATCH 30/98] Fixed "artifact" misspelling in docs. (#11291) --- docs/language.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/language.md b/docs/language.md index 5a88f901bb81ba..8a71e707619fbc 100644 --- a/docs/language.md +++ b/docs/language.md @@ -22,7 +22,7 @@ Additionally, how do we even know this came from our editor? Maybe someone snuck A Gutenberg post is the proper block-aware representation of a post, a collection of semantically consistent descriptions of what each block is and what its essential data is. This representation only ever exists in memory. It is the [chase](https://en.wikipedia.org/wiki/Chase_(printing)) in the typesetter's workshop, ever-shifting as sorts are attached and repositioned. -A Gutenberg post is not the artefact it produces, namely the `post_content`. The latter is the printed page, optimized for the reader, but retaining its invisible markings for later editing. +A Gutenberg post is not the artifact it produces, namely the `post_content`. The latter is the printed page, optimized for the reader, but retaining its invisible markings for later editing. Later sections of this document will refer to _Gutenberg post_ and to _blocks_. These are to be assumed to not be the `post_content` or the invisible markings. From 529e3a2d081bef9c096226e6ce9302f9354194d7 Mon Sep 17 00:00:00 2001 From: Dominik Schilling Date: Wed, 31 Oct 2018 11:49:17 +0100 Subject: [PATCH 31/98] Increase specificity for active radio/checkbox input styling (#11290) --- packages/edit-post/src/style.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/edit-post/src/style.scss b/packages/edit-post/src/style.scss index fbc5dbfd755a4c..23c040743210c9 100644 --- a/packages/edit-post/src/style.scss +++ b/packages/edit-post/src/style.scss @@ -237,7 +237,7 @@ body.block-editor-page { input[type="checkbox"] { border-radius: $radius-round-rectangle / 2; - &::before { + &:checked::before { margin: -4px 0 0 -5px; color: $white; } @@ -246,7 +246,7 @@ body.block-editor-page { input[type="radio"] { border-radius: $radius-round; - &::before { + &:checked::before { margin: 3px 0 0 3px; background-color: $white; } From 73d9759116dde896931f4d152f186147a57889fe Mon Sep 17 00:00:00 2001 From: Timothy Jacobs Date: Wed, 31 Oct 2018 07:34:56 -0400 Subject: [PATCH 32/98] Add complete post type labels for Resuable Blocks (#11278) * Add complete post type labels for Resuable Blocks Fixes #11272. * Clean up PHPCS issues --- lib/register.php | 53 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/lib/register.php b/lib/register.php index 0401af2f8a7c75..b86df4fd7a37f0 100644 --- a/lib/register.php +++ b/lib/register.php @@ -444,9 +444,27 @@ function gutenberg_register_post_types() { 'wp_block', array( 'labels' => array( - 'name' => __( 'Blocks', 'gutenberg' ), - 'singular_name' => __( 'Block', 'gutenberg' ), - 'search_items' => __( 'Search Blocks', 'gutenberg' ), + 'name' => _x( 'Blocks', 'post type general name', 'gutenberg' ), + 'singular_name' => _x( 'Block', 'post type singular name', 'gutenberg' ), + 'menu_name' => _x( 'Blocks', 'admin menu', 'gutenberg' ), + 'name_admin_bar' => _x( 'Block', 'add new on admin bar', 'gutenberg' ), + 'add_new' => _x( 'Add New', 'Block', 'gutenberg' ), + 'add_new_item' => __( 'Add New Block', 'gutenberg' ), + 'new_item' => __( 'New Block', 'gutenberg' ), + 'edit_item' => __( 'Edit Block', 'gutenberg' ), + 'view_item' => __( 'View Block', 'gutenberg' ), + 'all_items' => __( 'All Blocks', 'gutenberg' ), + 'search_items' => __( 'Search Blocks', 'gutenberg' ), + 'not_found' => __( 'No blocks found.', 'gutenberg' ), + 'not_found_in_trash' => __( 'No blocks found in Trash.', 'gutenberg' ), + 'filter_items_list' => __( 'Filter blocks list', 'gutenberg' ), + 'items_list_navigation' => __( 'Blocks list navigation', 'gutenberg' ), + 'items_list' => __( 'Blocks list', 'gutenberg' ), + 'item_published' => __( 'Block published.', 'gutenberg' ), + 'item_published_privately' => __( 'Block published privately.', 'gutenberg' ), + 'item_reverted_to_draft' => __( 'Block reverted to draft.', 'gutenberg' ), + 'item_scheduled' => __( 'Block scheduled.', 'gutenberg' ), + 'item_updated' => __( 'Block updated.', 'gutenberg' ), ), 'public' => false, 'show_ui' => true, @@ -516,6 +534,35 @@ function gutenberg_register_post_types() { } add_action( 'init', 'gutenberg_register_post_types' ); +/** + * Apply the correct labels for Reusable Blocks in the bulk action updated messages. + * + * @since 4.3.0 + * + * @param array $messages Arrays of messages, each keyed by the corresponding post type. + * @param array $bulk_counts Array of item counts for each message, used to build internationalized strings. + * + * @return array + */ +function gutenberg_bulk_post_updated_messages( $messages, $bulk_counts ) { + $messages['wp_block'] = array( + // translators: Number of blocks updated. + 'updated' => _n( '%s block updated.', '%s blocks updated.', $bulk_counts['updated'], 'gutenberg' ), + // translators: Blocks not updated because they're locked. + 'locked' => ( 1 == $bulk_counts['locked'] ) ? __( '1 block not updated, somebody is editing it.', 'gutenberg' ) : _n( '%s block not updated, somebody is editing it.', '%s blocks not updated, somebody is editing them.', $bulk_counts['locked'], 'gutenberg' ), + // translators: Number of blocks deleted. + 'deleted' => _n( '%s block permanently deleted.', '%s blocks permanently deleted.', $bulk_counts['deleted'], 'gutenberg' ), + // translators: Number of blocks trashed. + 'trashed' => _n( '%s block moved to the Trash.', '%s blocks moved to the Trash.', $bulk_counts['trashed'], 'gutenberg' ), + // translators: Number of blocks untrashed. + 'untrashed' => _n( '%s block restored from the Trash.', '%s blocks restored from the Trash.', $bulk_counts['untrashed'], 'gutenberg' ), + ); + + return $messages; +} + +add_filter( 'bulk_post_updated_messages', 'gutenberg_bulk_post_updated_messages', 10, 2 ); + /** * Injects a hidden input in the edit form to propagate the information that classic editor is selected. * From a5ee143b965ff46d23d66d4782cf71b79575e46f Mon Sep 17 00:00:00 2001 From: Luke Pettway Date: Wed, 31 Oct 2018 08:02:38 -0400 Subject: [PATCH 33/98] added myself to the contributors list (#11260) --- CONTRIBUTORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 2b7c128f77cdcf..0b1cd656f6bdd9 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -111,3 +111,4 @@ This list is manually curated to include valuable contributions by volunteers th | @ajitbohra | | | @ChrisVanPatten | | | @tofumatt | @lonelyvegan | +| @LukePettway | @luke_pettway | \ No newline at end of file From 58725c42980f81dbcf18d921be7ca43507c968e0 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Wed, 31 Oct 2018 08:40:35 -0400 Subject: [PATCH 34/98] Components: Remove redundant onClickOutside handler from Dropdown (#11253) --- packages/components/src/dropdown/index.js | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/packages/components/src/dropdown/index.js b/packages/components/src/dropdown/index.js index c8ba61f4ad1837..c99c0c3e8ea47f 100644 --- a/packages/components/src/dropdown/index.js +++ b/packages/components/src/dropdown/index.js @@ -11,12 +11,13 @@ import Popover from '../popover'; class Dropdown extends Component { constructor() { super( ...arguments ); + this.toggle = this.toggle.bind( this ); this.close = this.close.bind( this ); - this.clickOutside = this.clickOutside.bind( this ); - this.bindContainer = this.bindContainer.bind( this ); this.refresh = this.refresh.bind( this ); + this.popoverRef = createRef(); + this.state = { isOpen: false, }; @@ -38,10 +39,6 @@ class Dropdown extends Component { } } - bindContainer( ref ) { - this.container = ref; - } - /** * When contents change height due to user interaction, * `refresh` can be called to re-render Popover with correct @@ -59,12 +56,6 @@ class Dropdown extends Component { } ) ); } - clickOutside( event ) { - if ( ! this.container.contains( event.target ) ) { - this.close(); - } - } - close() { this.setState( { isOpen: false } ); } @@ -84,7 +75,7 @@ class Dropdown extends Component { const args = { isOpen, onToggle: this.toggle, onClose: this.close }; return ( -
    +
    { renderToggle( args ) } { isOpen && ( From 8ad5fbd555a25afb103d14d0f2a4e19885429e38 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Wed, 31 Oct 2018 13:45:01 +0100 Subject: [PATCH 35/98] Remove findDOMNode from Tooltip component (#11169) * Remove findDOMNode from Tooltip component --- packages/components/CHANGELOG.md | 6 ++ packages/components/src/icon-button/index.js | 7 ++- packages/components/src/tooltip/index.js | 62 -------------------- 3 files changed, 11 insertions(+), 64 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index d0c7e552baf1dd..fe1414540b9cbd 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -1,3 +1,9 @@ +## 5.0.2 (Unreleased) + +### Polish + +- Tooltip are no longer removed when Button becomes disabled, it's left to the component rendering the Tooltip. + ## 5.0.1 (2018-10-30) ## 5.0.0 (2018-10-29) diff --git a/packages/components/src/icon-button/index.js b/packages/components/src/icon-button/index.js index 4cc8ac426ab4ee..b90ee85d2b6080 100644 --- a/packages/components/src/icon-button/index.js +++ b/packages/components/src/icon-button/index.js @@ -25,7 +25,7 @@ class IconButton extends Component { const tooltipText = tooltip || label; // Should show the tooltip if... - const showTooltip = ( + const showTooltip = ! additionalProps.disabled && ( // an explicit tooltip is passed or... tooltip || // there's a shortcut or... @@ -49,7 +49,10 @@ class IconButton extends Component { if ( showTooltip ) { element = ( - + { element } ); diff --git a/packages/components/src/tooltip/index.js b/packages/components/src/tooltip/index.js index de177eeb7492b1..3bb1f6c8b7ccb7 100644 --- a/packages/components/src/tooltip/index.js +++ b/packages/components/src/tooltip/index.js @@ -10,7 +10,6 @@ import { Component, Children, cloneElement, - findDOMNode, concatChildren, } from '@wordpress/element'; @@ -31,7 +30,6 @@ class Tooltip extends Component { constructor() { super( ...arguments ); - this.bindNode = this.bindNode.bind( this ); this.delayedSetIsOver = debounce( ( isOver ) => this.setState( { isOver } ), TOOLTIP_DELAY @@ -44,65 +42,6 @@ class Tooltip extends Component { componentWillUnmount() { this.delayedSetIsOver.cancel(); - this.disconnectDisabledAttributeObserver(); - } - - componentDidUpdate( prevProps, prevState ) { - const { isOver } = this.state; - if ( isOver !== prevState.isOver ) { - if ( isOver ) { - this.observeDisabledAttribute(); - } else { - this.disconnectDisabledAttributeObserver(); - } - } - } - - /** - * Assigns DOM node of the rendered component as an instance property. - * - * @param {Element} ref Rendered component reference. - */ - bindNode( ref ) { - // Disable reason: Because render clones the child, we don't know what - // type of element we have, but if it's a DOM node, we want to observe - // the disabled attribute. - // eslint-disable-next-line react/no-find-dom-node - this.node = findDOMNode( ref ); - } - - /** - * Disconnects any DOM observer attached to the rendered node. - */ - disconnectDisabledAttributeObserver() { - if ( this.observer ) { - this.observer.disconnect(); - } - } - - /** - * Adds a DOM observer to the rendered node, if supported and if the DOM - * node exists, to monitor for application of a disabled attribute. - */ - observeDisabledAttribute() { - if ( ! window.MutationObserver || ! this.node ) { - return; - } - - this.observer = new window.MutationObserver( ( [ mutation ] ) => { - if ( mutation.target.disabled ) { - // We can assume here that isOver is true, because mutation - // observer is only attached for duration of isOver active - this.setState( { isOver: false } ); - } - } ); - - // Monitor changes to the disable attribute on the DOM node - this.observer.observe( this.node, { - subtree: true, - attributes: true, - attributeFilter: [ 'disabled' ], - } ); } emitToChild( eventName, event ) { @@ -163,7 +102,6 @@ class Tooltip extends Component { const child = Children.only( children ); const { isOver } = this.state; return cloneElement( child, { - ref: this.bindNode, onMouseEnter: this.createToggleIsOver( 'onMouseEnter', true ), onMouseLeave: this.createToggleIsOver( 'onMouseLeave' ), onClick: this.createToggleIsOver( 'onClick' ), From 0001042c24179572cc9b32883a97c8fa03d32953 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Wed, 31 Oct 2018 08:45:45 -0400 Subject: [PATCH 36/98] RichText: Remove unused `ref` assignment to RichText (#11222) --- packages/editor/src/components/rich-text/index.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/editor/src/components/rich-text/index.js b/packages/editor/src/components/rich-text/index.js index 2548240a6671f9..6b5decb63331dd 100644 --- a/packages/editor/src/components/rich-text/index.js +++ b/packages/editor/src/components/rich-text/index.js @@ -15,7 +15,7 @@ import memize from 'memize'; /** * WordPress dependencies */ -import { Component, Fragment, RawHTML, createRef } from '@wordpress/element'; +import { Component, Fragment, RawHTML } from '@wordpress/element'; import { isHorizontalEdge, getRectangleFromRange, @@ -115,7 +115,6 @@ export class RichText extends Component { this.formatToValue = memize( this.formatToValue.bind( this ), { size: 1 } ); this.savedContent = value; - this.containerRef = createRef(); this.patterns = getPatterns( { onReplace, multilineTag: this.multilineTag, @@ -933,7 +932,6 @@ export class RichText extends Component { return (
    { isSelected && ! inlineToolbar && ( From 7b69168af5bb98c72f87e80016e35d3a48587c83 Mon Sep 17 00:00:00 2001 From: Danilo Ercoli Date: Wed, 31 Oct 2018 14:04:05 +0100 Subject: [PATCH 37/98] Export `switchToBlockType` to be used mobile side when merging two blocks. (#11294) --- packages/blocks/src/api/index.native.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/blocks/src/api/index.native.js b/packages/blocks/src/api/index.native.js index c83db0ad39e2c5..e987acbdaaf4e4 100644 --- a/packages/blocks/src/api/index.native.js +++ b/packages/blocks/src/api/index.native.js @@ -1,5 +1,6 @@ export { createBlock, + switchToBlockType, } from './factory'; export { default as parse, From ab5d5ceb61bc808cd92aadac0585ba76d3041fb9 Mon Sep 17 00:00:00 2001 From: Tim Wright Date: Wed, 31 Oct 2018 09:06:27 -0400 Subject: [PATCH 38/98] Fixed typos on block api documentation (#11298) * merge from upstream * fixed typos in block API documentation --- docs/block-api.md | 4 ++-- gutenberg-mobile | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/block-api.md b/docs/block-api.md index 769ff8105680c4..a3df73ffcb80e6 100644 --- a/docs/block-api.md +++ b/docs/block-api.md @@ -140,7 +140,7 @@ styles: [ ], ``` -Plugins and Themes can also register [custom block style](../docs/extensibility/extending-blocks/#block-style-variations) for exisiting blocks. +Plugins and Themes can also register [custom block style](../docs/extensibility/extending-blocks/#block-style-variations) for existing blocks. #### Attributes (optional) @@ -490,7 +490,7 @@ className: false, html: false, ``` -- `inserter` (default `true`): By default, all blocks will appear in the Gutenberg inserter. To hide a block so that it can only be inserted programatically, set `inserter` to `false`. +- `inserter` (default `true`): By default, all blocks will appear in the Gutenberg inserter. To hide a block so that it can only be inserted programmatically, set `inserter` to `false`. ```js // Hide this block from the inserter. diff --git a/gutenberg-mobile b/gutenberg-mobile index c90c36410d4c0c..776bb23f7f19ef 160000 --- a/gutenberg-mobile +++ b/gutenberg-mobile @@ -1 +1 @@ -Subproject commit c90c36410d4c0c3f2dea84ca666b1b585408a03f +Subproject commit 776bb23f7f19ef57b491f9708e9795b3a2ddb68e From 3503c0d08a18ad2036fc0329be82dcbbd8e03e4e Mon Sep 17 00:00:00 2001 From: William Earnhardt Date: Wed, 31 Oct 2018 09:38:11 -0400 Subject: [PATCH 39/98] Fix property path on get() call (#10962) --- .../src/components/post-taxonomies/flat-term-selector.js | 4 ++-- .../components/post-taxonomies/hierarchical-term-selector.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/editor/src/components/post-taxonomies/flat-term-selector.js b/packages/editor/src/components/post-taxonomies/flat-term-selector.js index 55e2f6e9c47c84..7e2a322d042847 100644 --- a/packages/editor/src/components/post-taxonomies/flat-term-selector.js +++ b/packages/editor/src/components/post-taxonomies/flat-term-selector.js @@ -174,12 +174,12 @@ class FlatTermSelector extends Component { const termNames = availableTerms.map( ( term ) => term.name ); const newTermLabel = get( taxonomy, - [ 'data', 'labels', 'add_new_item' ], + [ 'labels', 'add_new_item' ], slug === 'post_tag' ? __( 'Add New Tag' ) : __( 'Add New Term' ) ); const singularName = get( taxonomy, - [ 'data', 'labels', 'singular_name' ], + [ 'labels', 'singular_name' ], slug === 'post_tag' ? __( 'Tag' ) : __( 'Term' ) ); const termAddedLabel = sprintf( _x( '%s added', 'term' ), singularName ); diff --git a/packages/editor/src/components/post-taxonomies/hierarchical-term-selector.js b/packages/editor/src/components/post-taxonomies/hierarchical-term-selector.js index 482d65cf62abb8..7d3e60d17dcf29 100644 --- a/packages/editor/src/components/post-taxonomies/hierarchical-term-selector.js +++ b/packages/editor/src/components/post-taxonomies/hierarchical-term-selector.js @@ -247,7 +247,7 @@ class HierarchicalTermSelector extends Component { const { filteredTermsTree, formName, formParent, isRequestingTerms, showForm, filterValue } = this.state; const labelWithFallback = ( labelProperty, fallbackIsCategory, fallbackIsNotCategory ) => get( taxonomy, - [ 'data', 'labels', labelProperty ], + [ 'labels', labelProperty ], slug === 'category' ? fallbackIsCategory : fallbackIsNotCategory ); const newTermButtonLabel = labelWithFallback( From 08412463b92e38b43c9e296c4dcab51dbc643821 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20=28Greg=29=20Zi=C3=B3=C5=82kowski?= Date: Wed, 31 Oct 2018 18:03:36 +0100 Subject: [PATCH 40/98] Docs: Extends docs for withFilters HOC (#11313) --- .../src/higher-order/with-filters/README.md | 59 +++++++++++++++---- 1 file changed, 47 insertions(+), 12 deletions(-) diff --git a/packages/components/src/higher-order/with-filters/README.md b/packages/components/src/higher-order/with-filters/README.md index 244b226445c16d..7c2840ad90eac5 100644 --- a/packages/components/src/higher-order/with-filters/README.md +++ b/packages/components/src/higher-order/with-filters/README.md @@ -7,25 +7,60 @@ Wrapping a component with `withFilters` provides a filtering capability controll ## Usage ```jsx -import { withFilters } from '@wordpress/components'; +import { Fragment, withFilters } from '@wordpress/components'; import { addFilter } from '@wordpress/hooks'; -const ComposedComponent = () =>
    Composed component
    ; +const MyComponent = ( { title } ) =>

    { title }

    ; + +const ComponentToAppend = () =>
    Appended component
    ; + +function withComponentApended( FilteredComponent ) { + return ( props ) => ( + + + + + ); +} addFilter( 'MyHookName', - 'example/filtered-component', - ( FilteredComponent ) => () => ( -
    - - -
    - ) + 'my-plugin/with-component-appended', + withComponentApended ); -const MyComponentWithFilters = withFilters( 'MyHookName' )( - () =>
    My component
    -); +const MyComponentWithFilters = withFilters( 'MyHookName' )( MyComponent ); ``` `withFilters` expects a string argument which provides a hook name. It returns a function which can then be used in composing your component. The hook name allows plugin developers to customize or completely override the component passed to this higher-order component using `wp.hooks.addFilter` method. + +It is also possible to override props by implementing a higher-order component which works as follows: + +```jsx +import { Fragment, withFilters } from '@wordpress/components'; +import { addFilter } from '@wordpress/hooks'; + +const MyComponent = ( { hint, title } ) => ( + +

    { title }

    +

    { hint }

    +
    +); + +function withHintOverriden( FilteredComponent ) { + return ( props ) => ( + + ); + } + +addFilter( + 'MyHookName', + 'my-plugin/with-hint-overriden', + withHintOverriden +); + +const MyComponentWithFilters = withFilters( 'MyHookName' )( MyComponent ); +``` From 29f209a767937e3497f12296ecab0f2de09f592f Mon Sep 17 00:00:00 2001 From: Stefanos Togoulidis Date: Wed, 31 Oct 2018 21:04:53 +0200 Subject: [PATCH 41/98] Revert mobile RN testsuite integration (#11318) * Revert "Have Travis run mobile tests that use the parent code (#10034)" This reverts commit b1d9fb74dae09464cb7bf5903fd15f2d3021d023. * Revert "Integrate the mobile React Native testsuite with the main Gutenberg build (#9883)" This reverts commit cb908cff647b7adc3d251a0e1b2defdd71cc2f43. --- .eslintignore | 1 - .gitmodules | 3 --- .stylelintignore | 1 - .travis.yml | 41 ------------------------------ docs/reference/testing-overview.md | 25 ------------------ gutenberg-mobile | 1 - package-lock.json | 6 ----- package.json | 11 +++----- test/e2e/jest.config.json | 9 +------ test/unit/jest.config.json | 6 +---- 10 files changed, 5 insertions(+), 99 deletions(-) delete mode 100644 .gitmodules delete mode 100644 .stylelintignore delete mode 160000 gutenberg-mobile diff --git a/.eslintignore b/.eslintignore index 771c6bce35da5c..35fff82ce771c7 100644 --- a/.eslintignore +++ b/.eslintignore @@ -5,4 +5,3 @@ node_modules test/e2e/test-plugins vendor packages/block-serialization-spec-parser/index.js -gutenberg-mobile diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index c70c9c4d7133e6..00000000000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "gutenberg-mobile"] - path = gutenberg-mobile - url = ../../wordpress-mobile/gutenberg-mobile diff --git a/.stylelintignore b/.stylelintignore deleted file mode 100644 index e6c31cdc0d8e30..00000000000000 --- a/.stylelintignore +++ /dev/null @@ -1 +0,0 @@ -gutenberg-mobile diff --git a/.travis.yml b/.travis.yml index 817b6cb71081c1..51ae7a8eaa9e08 100644 --- a/.travis.yml +++ b/.travis.yml @@ -69,44 +69,3 @@ jobs: script: - npm install || exit 1 - ./bin/run-e2e-tests.sh || exit 1 - - stage: test - language: node_js - node_js: 8 - env: - LANE='node' - GUTENBERG_AS_PARENT=true - CHECK_CORRECTNESS='true' - cache: - yarn: true - script: - - cd ./gutenberg-mobile - - yarn install - - ./.travis/travis-checks-js.sh - - stage: test - language: node_js - node_js: 8 - env: - LANE='node' - GUTENBERG_AS_PARENT=true - CHECK_TESTS='true' - TEST_RN_PLATFORM='android' - cache: - yarn: true - script: - - cd ./gutenberg-mobile - - yarn install - - ./.travis/travis-checks-js.sh - - stage: test - language: node_js - node_js: 8 - env: - LANE='node' - GUTENBERG_AS_PARENT=true - CHECK_TESTS='true' - TEST_RN_PLATFORM='ios' - cache: - yarn: true - script: - - cd ./gutenberg-mobile - - yarn install - - ./.travis/travis-checks-js.sh diff --git a/docs/reference/testing-overview.md b/docs/reference/testing-overview.md index f3c3b731338599..fcfc951cfd90f3 100644 --- a/docs/reference/testing-overview.md +++ b/docs/reference/testing-overview.md @@ -379,30 +379,5 @@ Code style in PHP is enforced using [PHP_CodeSniffer](https://github.com/squizla To run unit tests only, without the linter, use `npm run test-unit-php` instead. -## Native Mobile Testing - -To enable automated testing against the native mobile app currently in development, the whole of the mobile source code is pulled in as a git submodule. Its testsuite is included in the Travis tests run but it can also be used locally, during development. - -To test locally, along with the typical Gutenberg setup and testing instructions already mentioned earlier, make sure you check out the code of the submodule: -``` -git submodule --init --recursive -``` -You can then use the available script to launch the mobile testsuite in isolation: -``` -npm run test-mobile -``` - -or the typical `npm run test` to run all the lint, unit and mobile tests in one go. - -The mobile tests pick up the compiled Gutenberg code/packages and so, don't forget to run `npm install` (while at the Gutenberg root) after you've made changes to the code. - -### Debugging native mobile - -Say you have made some changes to Gutenberg's code and turns out the mobile tests get broken. What's the path forward? Hopefully, the Jest tests output will have an error message and stacktrace that is indicative enough and helps point where the error happens and what went wrong. Oftenly, what happens is that the code being shared between the web and the mobile project is no longer compatible. - -For example, changing an intermediate interface can inadvertently bring the `file.js` and `file.native.js` pair out of sync. You'll then need to update the `.native.js` file to adhere to the new interface or iterate on the interface itself. Feel free to reach out to mobile devs for help if needed. - -In other usual cases, you might directly employ HTML elements in a `render()` function but those are not actually offered by React Native, the UI framework the native mobile is build on. Those elements are usually starting with a lower-case character like `div` or `span`. In any case that code is incompatible or doesn't make sense for mobile, what needs to be done is to wrap that code in a new component and provide a "nativized" variant of it to be loaded when on native mobile instead. To "nativize" a component just create a new `.native.js` file right alongside the web version one and in it return/run the code that is compatible with mobile. The proper variant will be picked up by the toolchain automatically. - [snapshot testing]: https://facebook.github.io/jest/docs/en/snapshot-testing.html [update snapshots]: https://facebook.github.io/jest/docs/en/snapshot-testing.html#updating-snapshots diff --git a/gutenberg-mobile b/gutenberg-mobile deleted file mode 160000 index 776bb23f7f19ef..00000000000000 --- a/gutenberg-mobile +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 776bb23f7f19ef57b491f9708e9795b3a2ddb68e diff --git a/package-lock.json b/package-lock.json index e34e319d22a7fe..0cb741133ff3fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21940,12 +21940,6 @@ "camelcase": "^4.1.0" } }, - "yarn": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/yarn/-/yarn-1.9.4.tgz", - "integrity": "sha1-O4LYRGtlJ3VyOQC0cNlmhhl2kks=", - "dev": true - }, "yauzl": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz", diff --git a/package.json b/package.json index 5ecc9d58868ec5..9d9d485054b7da 100644 --- a/package.json +++ b/package.json @@ -112,8 +112,7 @@ "webpack-bundle-analyzer": "3.0.2", "webpack-cli": "2.1.3", "webpack-livereload-plugin": "2.1.1", - "webpack-rtl-plugin": "github:yoavf/webpack-rtl-plugin#develop", - "yarn": "1.9.4" + "webpack-rtl-plugin": "github:yoavf/webpack-rtl-plugin#develop" }, "npmPackageJsonLintConfig": { "extends": "@wordpress/npm-package-json-lint-config", @@ -169,20 +168,16 @@ "lint-css": "stylelint '**/*.scss'", "lint-css:fix": "stylelint '**/*.scss' --fix", "package-plugin": "./bin/build-plugin-zip.sh", - "mobile-submodule-update": "git submodule update --init --recursive", - "mobile-install": "yarn --cwd gutenberg-mobile install", - "preinstall": "npm run mobile-submodule-update", - "postinstall": " npm run mobile-install && npm run check-licenses && npm run build:packages", + "postinstall": "npm run check-licenses && npm run build:packages", "pot-to-php": "./bin/pot-to-php.js", "precommit": "lint-staged", "publish:check": "npm run build:packages && lerna updated", "publish:dev": "npm run build:packages && lerna publish --npm-tag next", "publish:prod": "npm run build:packages && lerna publish", - "test": "concurrently \"npm run lint && npm run test-unit\" \"npm run test-mobile\"", + "test": "npm run lint && npm run test-unit", "pretest-e2e": "concurrently \"./bin/reset-e2e-tests.sh\" \"npm run build\"", "test-e2e": "cross-env JEST_PUPPETEER_CONFIG=test/e2e/puppeteer.config.js wp-scripts test-unit-js --config test/e2e/jest.config.json --runInBand", "test-e2e:watch": "npm run test-e2e -- --watch", - "test-mobile": "yarn --cwd gutenberg-mobile test:inside-gb", "test-php": "npm run lint-php && npm run test-unit-php", "test-unit": "wp-scripts test-unit-js --config test/unit/jest.config.json", "test-unit:coverage": "npm run test-unit -- --coverage", diff --git a/test/e2e/jest.config.json b/test/e2e/jest.config.json index 21c7aecb7809c3..354879e3d358a2 100644 --- a/test/e2e/jest.config.json +++ b/test/e2e/jest.config.json @@ -1,9 +1,5 @@ { "rootDir": "../../", - "coveragePathIgnorePatterns": [ - "/node_modules/", - "/gutenberg-mobile/" - ], "preset": "jest-puppeteer", "setupTestFrameworkScriptFile": "/test/e2e/support/setup-test-framework.js", "testMatch": [ @@ -11,8 +7,5 @@ ], "transform": { "^.+\\.jsx?$": "/node_modules/babel-jest" - }, - "modulePathIgnorePatterns": [ - "/gutenberg-mobile/" - ] + } } diff --git a/test/unit/jest.config.json b/test/unit/jest.config.json index 2519b3b4e2e972..8c5a2ef25e92fa 100644 --- a/test/unit/jest.config.json +++ b/test/unit/jest.config.json @@ -6,8 +6,7 @@ "/.*/build/", "/.*/build-module/", "/packages/.*/benchmark/", - "/packages/.*/test/", - "/gutenberg-mobile/" + "/packages/.*/test/" ], "moduleNameMapper": { "@wordpress\\/(block-serialization-spec-parser|is-shallow-equal)$": "packages/$1", @@ -23,8 +22,5 @@ "/test/e2e", "/.*/build/", "/.*/build-module/" - ], - "modulePathIgnorePatterns": [ - "/gutenberg-mobile/" ] } From bb9d9f1288d6c08b3fb1b2f905dce27dc676557f Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Wed, 31 Oct 2018 15:54:51 -0400 Subject: [PATCH 42/98] Editor: Reshape blocks state under own key (#11315) --- packages/editor/src/store/reducer.js | 390 +++--- packages/editor/src/store/selectors.js | 53 +- packages/editor/src/store/test/reducer.js | 232 ++-- packages/editor/src/store/test/selectors.js | 1177 +++++++++++-------- 4 files changed, 1029 insertions(+), 823 deletions(-) diff --git a/packages/editor/src/store/reducer.js b/packages/editor/src/store/reducer.js index fc6a1e387d9315..dca04e51edd8d2 100644 --- a/packages/editor/src/store/reducer.js +++ b/packages/editor/src/store/reducer.js @@ -86,7 +86,7 @@ function getFlattenedBlocks( blocks ) { const stack = [ ...blocks ]; while ( stack.length ) { // `innerBlocks` is redundant data which can fall out of sync, since - // this is reflected in `blockOrder`, so exclude from appended block. + // this is reflected in `blocks.order`, so exclude from appended block. const { innerBlocks, ...block } = stack.shift(); stack.push( ...innerBlocks ); @@ -181,7 +181,7 @@ const withInnerBlocksRemoveCascade = ( reducer ) => ( state, action ) => { // For each removed client ID, include its inner blocks to remove, // recursing into those so long as inner blocks exist. for ( let i = 0; i < clientIds.length; i++ ) { - clientIds.push( ...state.blockOrder[ clientIds[ i ] ] ); + clientIds.push( ...state.blocks.order[ clientIds[ i ] ] ); } action = { ...action, clientIds }; @@ -197,9 +197,7 @@ const withInnerBlocksRemoveCascade = ( reducer ) => ( state, action ) => { * Handles the following state keys: * - edits: an object describing changes to be made to the current post, in * the format accepted by the WP REST API - * - blocksByClientId: post content blocks keyed by client ID - * - blockOrder: object where each key is a client ID, its value an array of - * client IDs representing the order of its inner blocks + * - blocks: post content blocks * * @param {Object} state Current state. * @param {Object} action Dispatched action. @@ -277,242 +275,244 @@ export const editor = flow( [ return state; }, - blocksByClientId( state = {}, action ) { - switch ( action.type ) { - case 'RESET_BLOCKS': - case 'SETUP_EDITOR_STATE': - return getFlattenedBlocks( action.blocks ); + blocks: combineReducers( { + byClientId( state = {}, action ) { + switch ( action.type ) { + case 'RESET_BLOCKS': + case 'SETUP_EDITOR_STATE': + return getFlattenedBlocks( action.blocks ); - case 'RECEIVE_BLOCKS': - return { - ...state, - ...getFlattenedBlocks( action.blocks ), - }; + case 'RECEIVE_BLOCKS': + return { + ...state, + ...getFlattenedBlocks( action.blocks ), + }; - case 'UPDATE_BLOCK_ATTRIBUTES': - // Ignore updates if block isn't known - if ( ! state[ action.clientId ] ) { - return state; - } + case 'UPDATE_BLOCK_ATTRIBUTES': + // Ignore updates if block isn't known + if ( ! state[ action.clientId ] ) { + return state; + } - // Consider as updates only changed values - const nextAttributes = reduce( action.attributes, ( result, value, key ) => { - if ( value !== result[ key ] ) { - // Avoid mutating original block by creating shallow clone - if ( result === state[ action.clientId ].attributes ) { - result = { ...result }; + // Consider as updates only changed values + const nextAttributes = reduce( action.attributes, ( result, value, key ) => { + if ( value !== result[ key ] ) { + // Avoid mutating original block by creating shallow clone + if ( result === state[ action.clientId ].attributes ) { + result = { ...result }; + } + + result[ key ] = value; } - result[ key ] = value; + return result; + }, state[ action.clientId ].attributes ); + + // Skip update if nothing has been changed. The reference will + // match the original block if `reduce` had no changed values. + if ( nextAttributes === state[ action.clientId ].attributes ) { + return state; } - return result; - }, state[ action.clientId ].attributes ); + // Otherwise merge attributes into state + return { + ...state, + [ action.clientId ]: { + ...state[ action.clientId ], + attributes: nextAttributes, + }, + }; - // Skip update if nothing has been changed. The reference will - // match the original block if `reduce` had no changed values. - if ( nextAttributes === state[ action.clientId ].attributes ) { - return state; - } + case 'UPDATE_BLOCK': + // Ignore updates if block isn't known + if ( ! state[ action.clientId ] ) { + return state; + } - // Otherwise merge attributes into state - return { - ...state, - [ action.clientId ]: { - ...state[ action.clientId ], - attributes: nextAttributes, - }, - }; + return { + ...state, + [ action.clientId ]: { + ...state[ action.clientId ], + ...action.updates, + }, + }; - case 'UPDATE_BLOCK': - // Ignore updates if block isn't known - if ( ! state[ action.clientId ] ) { - return state; - } + case 'INSERT_BLOCKS': + return { + ...state, + ...getFlattenedBlocks( action.blocks ), + }; - return { - ...state, - [ action.clientId ]: { - ...state[ action.clientId ], - ...action.updates, - }, - }; + case 'REPLACE_BLOCKS': + if ( ! action.blocks ) { + return state; + } - case 'INSERT_BLOCKS': - return { - ...state, - ...getFlattenedBlocks( action.blocks ), - }; + return { + ...omit( state, action.clientIds ), + ...getFlattenedBlocks( action.blocks ), + }; - case 'REPLACE_BLOCKS': - if ( ! action.blocks ) { - return state; - } + case 'REMOVE_BLOCKS': + return omit( state, action.clientIds ); - return { - ...omit( state, action.clientIds ), - ...getFlattenedBlocks( action.blocks ), - }; + case 'SAVE_REUSABLE_BLOCK_SUCCESS': { + const { id, updatedId } = action; - case 'REMOVE_BLOCKS': - return omit( state, action.clientIds ); + // If a temporary reusable block is saved, we swap the temporary id with the final one + if ( id === updatedId ) { + return state; + } - case 'SAVE_REUSABLE_BLOCK_SUCCESS': { - const { id, updatedId } = action; + return mapValues( state, ( block ) => { + if ( block.name === 'core/block' && block.attributes.ref === id ) { + return { + ...block, + attributes: { + ...block.attributes, + ref: updatedId, + }, + }; + } - // If a temporary reusable block is saved, we swap the temporary id with the final one - if ( id === updatedId ) { - return state; + return block; + } ); } - - return mapValues( state, ( block ) => { - if ( block.name === 'core/block' && block.attributes.ref === id ) { - return { - ...block, - attributes: { - ...block.attributes, - ref: updatedId, - }, - }; - } - - return block; - } ); } - } - - return state; - }, - - blockOrder( state = {}, action ) { - switch ( action.type ) { - case 'RESET_BLOCKS': - case 'SETUP_EDITOR_STATE': - return mapBlockOrder( action.blocks ); - case 'RECEIVE_BLOCKS': - return { - ...state, - ...omit( mapBlockOrder( action.blocks ), '' ), - }; + return state; + }, - case 'INSERT_BLOCKS': { - const { rootClientId = '', blocks } = action; - const subState = state[ rootClientId ] || []; - const mappedBlocks = mapBlockOrder( blocks, rootClientId ); - const { index = subState.length } = action; + order( state = {}, action ) { + switch ( action.type ) { + case 'RESET_BLOCKS': + case 'SETUP_EDITOR_STATE': + return mapBlockOrder( action.blocks ); - return { - ...state, - ...mappedBlocks, - [ rootClientId ]: insertAt( subState, mappedBlocks[ rootClientId ], index ), - }; - } + case 'RECEIVE_BLOCKS': + return { + ...state, + ...omit( mapBlockOrder( action.blocks ), '' ), + }; - case 'MOVE_BLOCK_TO_POSITION': { - const { fromRootClientId = '', toRootClientId = '', clientId } = action; - const { index = state[ toRootClientId ].length } = action; + case 'INSERT_BLOCKS': { + const { rootClientId = '', blocks } = action; + const subState = state[ rootClientId ] || []; + const mappedBlocks = mapBlockOrder( blocks, rootClientId ); + const { index = subState.length } = action; - // Moving inside the same parent block - if ( fromRootClientId === toRootClientId ) { - const subState = state[ toRootClientId ]; - const fromIndex = subState.indexOf( clientId ); return { ...state, - [ toRootClientId ]: moveTo( state[ toRootClientId ], fromIndex, index ), + ...mappedBlocks, + [ rootClientId ]: insertAt( subState, mappedBlocks[ rootClientId ], index ), }; } - // Moving from a parent block to another - return { - ...state, - [ fromRootClientId ]: without( state[ fromRootClientId ], clientId ), - [ toRootClientId ]: insertAt( state[ toRootClientId ], clientId, index ), - }; - } + case 'MOVE_BLOCK_TO_POSITION': { + const { fromRootClientId = '', toRootClientId = '', clientId } = action; + const { index = state[ toRootClientId ].length } = action; - case 'MOVE_BLOCKS_UP': { - const { clientIds, rootClientId = '' } = action; - const firstClientId = first( clientIds ); - const subState = state[ rootClientId ]; + // Moving inside the same parent block + if ( fromRootClientId === toRootClientId ) { + const subState = state[ toRootClientId ]; + const fromIndex = subState.indexOf( clientId ); + return { + ...state, + [ toRootClientId ]: moveTo( state[ toRootClientId ], fromIndex, index ), + }; + } - if ( ! subState.length || firstClientId === first( subState ) ) { - return state; + // Moving from a parent block to another + return { + ...state, + [ fromRootClientId ]: without( state[ fromRootClientId ], clientId ), + [ toRootClientId ]: insertAt( state[ toRootClientId ], clientId, index ), + }; } - const firstIndex = subState.indexOf( firstClientId ); + case 'MOVE_BLOCKS_UP': { + const { clientIds, rootClientId = '' } = action; + const firstClientId = first( clientIds ); + const subState = state[ rootClientId ]; - return { - ...state, - [ rootClientId ]: moveTo( subState, firstIndex, firstIndex - 1, clientIds.length ), - }; - } + if ( ! subState.length || firstClientId === first( subState ) ) { + return state; + } - case 'MOVE_BLOCKS_DOWN': { - const { clientIds, rootClientId = '' } = action; - const firstClientId = first( clientIds ); - const lastClientId = last( clientIds ); - const subState = state[ rootClientId ]; + const firstIndex = subState.indexOf( firstClientId ); - if ( ! subState.length || lastClientId === last( subState ) ) { - return state; + return { + ...state, + [ rootClientId ]: moveTo( subState, firstIndex, firstIndex - 1, clientIds.length ), + }; } - const firstIndex = subState.indexOf( firstClientId ); + case 'MOVE_BLOCKS_DOWN': { + const { clientIds, rootClientId = '' } = action; + const firstClientId = first( clientIds ); + const lastClientId = last( clientIds ); + const subState = state[ rootClientId ]; - return { - ...state, - [ rootClientId ]: moveTo( subState, firstIndex, firstIndex + 1, clientIds.length ), - }; - } + if ( ! subState.length || lastClientId === last( subState ) ) { + return state; + } - case 'REPLACE_BLOCKS': { - const { blocks, clientIds } = action; - if ( ! blocks ) { - return state; - } + const firstIndex = subState.indexOf( firstClientId ); - const mappedBlocks = mapBlockOrder( blocks ); - - return flow( [ - ( nextState ) => omit( nextState, clientIds ), - ( nextState ) => ( { - ...nextState, - ...omit( mappedBlocks, '' ), - } ), - ( nextState ) => mapValues( nextState, ( subState ) => ( - reduce( subState, ( result, clientId ) => { - if ( clientId === clientIds[ 0 ] ) { - return [ - ...result, - ...mappedBlocks[ '' ], - ]; - } + return { + ...state, + [ rootClientId ]: moveTo( subState, firstIndex, firstIndex + 1, clientIds.length ), + }; + } - if ( clientIds.indexOf( clientId ) === -1 ) { - result.push( clientId ); - } + case 'REPLACE_BLOCKS': { + const { blocks, clientIds } = action; + if ( ! blocks ) { + return state; + } - return result; - }, [] ) - ) ), - ] )( state ); - } + const mappedBlocks = mapBlockOrder( blocks ); + + return flow( [ + ( nextState ) => omit( nextState, clientIds ), + ( nextState ) => ( { + ...nextState, + ...omit( mappedBlocks, '' ), + } ), + ( nextState ) => mapValues( nextState, ( subState ) => ( + reduce( subState, ( result, clientId ) => { + if ( clientId === clientIds[ 0 ] ) { + return [ + ...result, + ...mappedBlocks[ '' ], + ]; + } + + if ( clientIds.indexOf( clientId ) === -1 ) { + result.push( clientId ); + } + + return result; + }, [] ) + ) ), + ] )( state ); + } - case 'REMOVE_BLOCKS': - return flow( [ - // Remove inner block ordering for removed blocks - ( nextState ) => omit( nextState, action.clientIds ), + case 'REMOVE_BLOCKS': + return flow( [ + // Remove inner block ordering for removed blocks + ( nextState ) => omit( nextState, action.clientIds ), - // Remove deleted blocks from other blocks' orderings - ( nextState ) => mapValues( nextState, ( subState ) => ( - without( subState, ...action.clientIds ) - ) ), - ] )( state ); - } + // Remove deleted blocks from other blocks' orderings + ( nextState ) => mapValues( nextState, ( subState ) => ( + without( subState, ...action.clientIds ) + ) ), + ] )( state ); + } - return state; - }, + return state; + }, + } ), } ); /** diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index b31ff36d46794f..82ae8b6a7f4803 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -502,7 +502,7 @@ export const getBlockDependantsCacheBust = createSelector( * @return {string} Block name. */ export function getBlockName( state, clientId ) { - const block = state.editor.present.blocksByClientId[ clientId ]; + const block = state.editor.present.blocks.byClientId[ clientId ]; return block ? block.name : null; } @@ -519,7 +519,7 @@ export function getBlockName( state, clientId ) { */ export const getBlock = createSelector( ( state, clientId ) => { - const block = state.editor.present.blocksByClientId[ clientId ]; + const block = state.editor.present.blocks.byClientId[ clientId ]; if ( ! block ) { return null; } @@ -552,7 +552,7 @@ export const getBlock = createSelector( }; }, ( state, clientId ) => [ - state.editor.present.blocksByClientId[ clientId ], + state.editor.present.blocks.byClientId[ clientId ], getBlockDependantsCacheBust( state, clientId ), state.editor.present.edits.meta, state.currentPost.meta, @@ -585,8 +585,7 @@ export const getBlocks = createSelector( ); }, ( state ) => [ - state.editor.present.blockOrder, - state.editor.present.blocksByClientId, + state.editor.present.blocks, ] ); @@ -618,7 +617,7 @@ export const getClientIdsWithDescendants = createSelector( return [ ...topLevelIds, ...getClientIdsOfDescendants( state, topLevelIds ) ]; }, ( state ) => [ - state.editor.present.blockOrder, + state.editor.present.blocks.order, ] ); @@ -634,16 +633,16 @@ export const getClientIdsWithDescendants = createSelector( export const getGlobalBlockCount = createSelector( ( state, blockName ) => { if ( ! blockName ) { - return size( state.editor.present.blocksByClientId ); + return size( state.editor.present.blocks.byClientId ); } return reduce( - state.editor.present.blocksByClientId, + state.editor.present.blocks.byClientId, ( count, block ) => block.name === blockName ? count + 1 : count, 0 ); }, ( state ) => [ - state.editor.present.blocksByClientId, + state.editor.present.blocks.byClientId, ] ); @@ -662,11 +661,9 @@ export const getBlocksByClientId = createSelector( ( clientId ) => getBlock( state, clientId ) ), ( state ) => [ - state.editor.present.blocksByClientId, - state.editor.present.blockOrder, state.editor.present.edits.meta, state.currentPost.meta, - state.editor.present.blocksByClientId, + state.editor.present.blocks, ] ); @@ -774,10 +771,10 @@ export function getSelectedBlock( state ) { */ export const getBlockRootClientId = createSelector( ( state, clientId ) => { - const { blockOrder } = state.editor.present; + const { order } = state.editor.present.blocks; - for ( const rootClientId in blockOrder ) { - if ( includes( blockOrder[ rootClientId ], clientId ) ) { + for ( const rootClientId in order ) { + if ( includes( order[ rootClientId ], clientId ) ) { return rootClientId; } } @@ -785,7 +782,7 @@ export const getBlockRootClientId = createSelector( return null; }, ( state ) => [ - state.editor.present.blockOrder, + state.editor.present.blocks.order, ] ); @@ -809,7 +806,7 @@ export const getBlockHierarchyRootClientId = createSelector( return current; }, ( state ) => [ - state.editor.present.blockOrder, + state.editor.present.blocks.order, ] ); @@ -854,8 +851,8 @@ export function getAdjacentBlockClientId( state, startClientId, modifier = 1 ) { return null; } - const { blockOrder } = state.editor.present; - const orderSet = blockOrder[ rootClientId ]; + const { order } = state.editor.present.blocks; + const orderSet = order[ rootClientId ]; const index = orderSet.indexOf( startClientId ); const nextIndex = ( index + ( 1 * modifier ) ); @@ -954,7 +951,7 @@ export const getMultiSelectedBlockClientIds = createSelector( return blockOrder.slice( startIndex, endIndex + 1 ); }, ( state ) => [ - state.editor.present.blockOrder, + state.editor.present.blocks.order, state.blockSelection.start, state.blockSelection.end, ], @@ -978,10 +975,10 @@ export const getMultiSelectedBlocks = createSelector( return multiSelectedBlockClientIds.map( ( clientId ) => getBlock( state, clientId ) ); }, ( state ) => [ - state.editor.present.blockOrder, + state.editor.present.blocks.order, state.blockSelection.start, state.blockSelection.end, - state.editor.present.blocksByClientId, + state.editor.present.blocks.byClientId, state.editor.present.edits.meta, state.currentPost.meta, ] @@ -1059,7 +1056,7 @@ export const isAncestorMultiSelected = createSelector( return isMultiSelected; }, ( state ) => [ - state.editor.present.blockOrder, + state.editor.present.blocks.order, state.blockSelection.start, state.blockSelection.end, ], @@ -1115,7 +1112,7 @@ export function getMultiSelectedBlocksEndClientId( state ) { * @return {Array} Ordered client IDs of editor blocks. */ export function getBlockOrder( state, rootClientId ) { - return state.editor.present.blockOrder[ rootClientId || '' ] || EMPTY_ARRAY; + return state.editor.present.blocks.order[ rootClientId || '' ] || EMPTY_ARRAY; } /** @@ -1500,8 +1497,7 @@ export const getEditedPostContent = createSelector( }, ( state ) => [ state.editor.present.edits.content, - state.editor.present.blocksByClientId, - state.editor.present.blockOrder, + state.editor.present.blocks, ], ); @@ -1563,7 +1559,7 @@ export const canInsertBlockType = createSelector( }, ( state, blockName, rootClientId ) => [ state.blockListSettings[ rootClientId ], - state.editor.present.blocksByClientId[ rootClientId ], + state.editor.present.blocks.byClientId[ rootClientId ], state.settings.allowedBlockTypes, state.settings.templateLock, ], @@ -1751,8 +1747,7 @@ export const getInserterItems = createSelector( }, ( state, rootClientId ) => [ state.blockListSettings[ rootClientId ], - state.editor.present.blockOrder, - state.editor.present.blocksByClientId, + state.editor.present.blocks, state.preferences.insertUsage, state.settings.allowedBlockTypes, state.settings.templateLock, diff --git a/packages/editor/src/store/test/reducer.js b/packages/editor/src/store/test/reducer.js index 8ab789e987077a..60a9cbc256a624 100644 --- a/packages/editor/src/store/test/reducer.js +++ b/packages/editor/src/store/test/reducer.js @@ -277,14 +277,14 @@ describe( 'state', () => { unregisterBlockType( 'core/test-block' ); } ); - it( 'should return history (empty edits, blocksByClientId, blockOrder), dirty flag by default', () => { + it( 'should return history (empty edits, blocks), dirty flag by default', () => { const state = editor( undefined, {} ); expect( state.past ).toEqual( [] ); expect( state.future ).toEqual( [] ); expect( state.present.edits ).toEqual( {} ); - expect( state.present.blocksByClientId ).toEqual( {} ); - expect( state.present.blockOrder ).toEqual( {} ); + expect( state.present.blocks.byClientId ).toEqual( {} ); + expect( state.present.blocks.order ).toEqual( {} ); expect( state.isDirty ).toBe( false ); } ); @@ -295,9 +295,9 @@ describe( 'state', () => { blocks: [ { clientId: 'bananas', innerBlocks: [] } ], } ); - expect( Object.keys( state.present.blocksByClientId ) ).toHaveLength( 1 ); - expect( values( state.present.blocksByClientId )[ 0 ].clientId ).toBe( 'bananas' ); - expect( state.present.blockOrder ).toEqual( { + expect( Object.keys( state.present.blocks.byClientId ) ).toHaveLength( 1 ); + expect( values( state.present.blocks.byClientId )[ 0 ].clientId ).toBe( 'bananas' ); + expect( state.present.blocks.order ).toEqual( { '': [ 'bananas' ], bananas: [], } ); @@ -313,8 +313,8 @@ describe( 'state', () => { } ], } ); - expect( Object.keys( state.present.blocksByClientId ) ).toHaveLength( 2 ); - expect( state.present.blockOrder ).toEqual( { + expect( Object.keys( state.present.blocks.byClientId ) ).toHaveLength( 2 ); + expect( state.present.blocks.order ).toEqual( { '': [ 'bananas' ], apples: [], bananas: [ 'apples' ], @@ -340,9 +340,9 @@ describe( 'state', () => { } ], } ); - expect( Object.keys( state.present.blocksByClientId ) ).toHaveLength( 2 ); - expect( values( state.present.blocksByClientId )[ 1 ].clientId ).toBe( 'ribs' ); - expect( state.present.blockOrder ).toEqual( { + expect( Object.keys( state.present.blocks.byClientId ) ).toHaveLength( 2 ); + expect( values( state.present.blocks.byClientId )[ 1 ].clientId ).toBe( 'ribs' ); + expect( state.present.blocks.order ).toEqual( { '': [ 'chicken', 'ribs' ], chicken: [], ribs: [], @@ -369,10 +369,10 @@ describe( 'state', () => { } ], } ); - expect( Object.keys( state.present.blocksByClientId ) ).toHaveLength( 1 ); - expect( values( state.present.blocksByClientId )[ 0 ].name ).toBe( 'core/freeform' ); - expect( values( state.present.blocksByClientId )[ 0 ].clientId ).toBe( 'wings' ); - expect( state.present.blockOrder ).toEqual( { + expect( Object.keys( state.present.blocks.byClientId ) ).toHaveLength( 1 ); + expect( values( state.present.blocks.byClientId )[ 0 ].name ).toBe( 'core/freeform' ); + expect( values( state.present.blocks.byClientId )[ 0 ].clientId ).toBe( 'wings' ); + expect( state.present.blocks.order ).toEqual( { '': [ 'wings' ], wings: [], } ); @@ -393,7 +393,7 @@ describe( 'state', () => { blocks: [ replacementBlock ], } ); - expect( state.present.blockOrder ).toEqual( { + expect( state.present.blocks.order ).toEqual( { '': [ wrapperBlock.clientId ], [ wrapperBlock.clientId ]: [ replacementBlock.clientId ], [ replacementBlock.clientId ]: [], @@ -420,11 +420,11 @@ describe( 'state', () => { } ], } ); - expect( Object.keys( replacedState.present.blocksByClientId ) ).toHaveLength( 1 ); - expect( values( originalState.present.blocksByClientId )[ 0 ].name ).toBe( 'core/test-block' ); - expect( values( replacedState.present.blocksByClientId )[ 0 ].name ).toBe( 'core/freeform' ); - expect( values( replacedState.present.blocksByClientId )[ 0 ].clientId ).toBe( 'chicken' ); - expect( replacedState.present.blockOrder ).toEqual( { + expect( Object.keys( replacedState.present.blocks.byClientId ) ).toHaveLength( 1 ); + expect( values( originalState.present.blocks.byClientId )[ 0 ].name ).toBe( 'core/test-block' ); + expect( values( replacedState.present.blocks.byClientId )[ 0 ].name ).toBe( 'core/freeform' ); + expect( values( replacedState.present.blocks.byClientId )[ 0 ].clientId ).toBe( 'chicken' ); + expect( replacedState.present.blocks.order ).toEqual( { '': [ 'chicken' ], chicken: [], } ); @@ -454,14 +454,14 @@ describe( 'state', () => { blocks: [ replacementNestedBlock ], } ); - expect( replacedNestedState.present.blockOrder ).toEqual( { + expect( replacedNestedState.present.blocks.order ).toEqual( { '': [ wrapperBlock.clientId ], [ wrapperBlock.clientId ]: [ replacementNestedBlock.clientId ], [ replacementNestedBlock.clientId ]: [], } ); - expect( originalNestedState.present.blocksByClientId.chicken.name ).toBe( 'core/test-block' ); - expect( replacedNestedState.present.blocksByClientId.chicken.name ).toBe( 'core/freeform' ); + expect( originalNestedState.present.blocks.byClientId.chicken.name ).toBe( 'core/test-block' ); + expect( replacedNestedState.present.blocks.byClientId.chicken.name ).toBe( 'core/freeform' ); } ); it( 'should update the block', () => { @@ -484,7 +484,7 @@ describe( 'state', () => { }, } ); - expect( state.present.blocksByClientId.chicken ).toEqual( { + expect( state.present.blocks.byClientId.chicken ).toEqual( { clientId: 'chicken', name: 'core/test-block', attributes: { content: 'ribs' }, @@ -512,7 +512,7 @@ describe( 'state', () => { updatedId: 3, } ); - expect( state.present.blocksByClientId.chicken ).toEqual( { + expect( state.present.blocks.byClientId.chicken ).toEqual( { clientId: 'chicken', name: 'core/block', attributes: { @@ -542,7 +542,7 @@ describe( 'state', () => { clientIds: [ 'ribs' ], } ); - expect( state.present.blockOrder[ '' ] ).toEqual( [ 'ribs', 'chicken' ] ); + expect( state.present.blocks.order[ '' ] ).toEqual( [ 'ribs', 'chicken' ] ); } ); it( 'should move the nested block up', () => { @@ -559,7 +559,7 @@ describe( 'state', () => { rootClientId: wrapperBlock.clientId, } ); - expect( state.present.blockOrder ).toEqual( { + expect( state.present.blocks.order ).toEqual( { '': [ wrapperBlock.clientId ], [ wrapperBlock.clientId ]: [ movedBlock.clientId, siblingBlock.clientId ], [ movedBlock.clientId ]: [], @@ -592,7 +592,7 @@ describe( 'state', () => { clientIds: [ 'ribs', 'veggies' ], } ); - expect( state.present.blockOrder[ '' ] ).toEqual( [ 'ribs', 'veggies', 'chicken' ] ); + expect( state.present.blocks.order[ '' ] ).toEqual( [ 'ribs', 'veggies', 'chicken' ] ); } ); it( 'should move multiple nested blocks up', () => { @@ -610,7 +610,7 @@ describe( 'state', () => { rootClientId: wrapperBlock.clientId, } ); - expect( state.present.blockOrder ).toEqual( { + expect( state.present.blocks.order ).toEqual( { '': [ wrapperBlock.clientId ], [ wrapperBlock.clientId ]: [ movedBlockA.clientId, movedBlockB.clientId, siblingBlock.clientId ], [ movedBlockA.clientId ]: [], @@ -639,7 +639,7 @@ describe( 'state', () => { clientIds: [ 'chicken' ], } ); - expect( state.present.blockOrder ).toBe( original.present.blockOrder ); + expect( state.present.blocks.order ).toBe( original.present.blocks.order ); } ); it( 'should move the block down', () => { @@ -662,7 +662,7 @@ describe( 'state', () => { clientIds: [ 'chicken' ], } ); - expect( state.present.blockOrder[ '' ] ).toEqual( [ 'ribs', 'chicken' ] ); + expect( state.present.blocks.order[ '' ] ).toEqual( [ 'ribs', 'chicken' ] ); } ); it( 'should move the nested block down', () => { @@ -679,7 +679,7 @@ describe( 'state', () => { rootClientId: wrapperBlock.clientId, } ); - expect( state.present.blockOrder ).toEqual( { + expect( state.present.blocks.order ).toEqual( { '': [ wrapperBlock.clientId ], [ wrapperBlock.clientId ]: [ siblingBlock.clientId, movedBlock.clientId ], [ movedBlock.clientId ]: [], @@ -712,7 +712,7 @@ describe( 'state', () => { clientIds: [ 'chicken', 'ribs' ], } ); - expect( state.present.blockOrder[ '' ] ).toEqual( [ 'veggies', 'chicken', 'ribs' ] ); + expect( state.present.blocks.order[ '' ] ).toEqual( [ 'veggies', 'chicken', 'ribs' ] ); } ); it( 'should move multiple nested blocks down', () => { @@ -730,7 +730,7 @@ describe( 'state', () => { rootClientId: wrapperBlock.clientId, } ); - expect( state.present.blockOrder ).toEqual( { + expect( state.present.blocks.order ).toEqual( { '': [ wrapperBlock.clientId ], [ wrapperBlock.clientId ]: [ siblingBlock.clientId, movedBlockA.clientId, movedBlockB.clientId ], [ movedBlockA.clientId ]: [], @@ -759,7 +759,7 @@ describe( 'state', () => { clientIds: [ 'ribs' ], } ); - expect( state.present.blockOrder ).toBe( original.present.blockOrder ); + expect( state.present.blocks.order ).toBe( original.present.blocks.order ); } ); it( 'should remove the block', () => { @@ -782,9 +782,9 @@ describe( 'state', () => { clientIds: [ 'chicken' ], } ); - expect( state.present.blockOrder[ '' ] ).toEqual( [ 'ribs' ] ); - expect( state.present.blockOrder ).not.toHaveProperty( 'chicken' ); - expect( state.present.blocksByClientId ).toEqual( { + expect( state.present.blocks.order[ '' ] ).toEqual( [ 'ribs' ] ); + expect( state.present.blocks.order ).not.toHaveProperty( 'chicken' ); + expect( state.present.blocks.byClientId ).toEqual( { ribs: { clientId: 'ribs', name: 'core/test-block', @@ -818,10 +818,10 @@ describe( 'state', () => { clientIds: [ 'chicken', 'veggies' ], } ); - expect( state.present.blockOrder[ '' ] ).toEqual( [ 'ribs' ] ); - expect( state.present.blockOrder ).not.toHaveProperty( 'chicken' ); - expect( state.present.blockOrder ).not.toHaveProperty( 'veggies' ); - expect( state.present.blocksByClientId ).toEqual( { + expect( state.present.blocks.order[ '' ] ).toEqual( [ 'ribs' ] ); + expect( state.present.blocks.order ).not.toHaveProperty( 'chicken' ); + expect( state.present.blocks.order ).not.toHaveProperty( 'veggies' ); + expect( state.present.blocks.byClientId ).toEqual( { ribs: { clientId: 'ribs', name: 'core/test-block', @@ -847,8 +847,8 @@ describe( 'state', () => { clientIds: [ block.clientId ], } ); - expect( state.present.blocksByClientId ).toEqual( {} ); - expect( state.present.blockOrder ).toEqual( { + expect( state.present.blocks.byClientId ).toEqual( {} ); + expect( state.present.blocks.order ).toEqual( { '': [], } ); } ); @@ -879,8 +879,8 @@ describe( 'state', () => { } ], } ); - expect( Object.keys( state.present.blocksByClientId ) ).toHaveLength( 3 ); - expect( state.present.blockOrder[ '' ] ).toEqual( [ 'kumquat', 'persimmon', 'loquat' ] ); + expect( Object.keys( state.present.blocks.byClientId ) ).toHaveLength( 3 ); + expect( state.present.blocks.order[ '' ] ).toEqual( [ 'kumquat', 'persimmon', 'loquat' ] ); } ); it( 'should move block to lower index', () => { @@ -909,7 +909,7 @@ describe( 'state', () => { index: 0, } ); - expect( state.present.blockOrder[ '' ] ).toEqual( [ 'ribs', 'chicken', 'veggies' ] ); + expect( state.present.blocks.order[ '' ] ).toEqual( [ 'ribs', 'chicken', 'veggies' ] ); } ); it( 'should move block to higher index', () => { @@ -938,7 +938,7 @@ describe( 'state', () => { index: 2, } ); - expect( state.present.blockOrder[ '' ] ).toEqual( [ 'chicken', 'veggies', 'ribs' ] ); + expect( state.present.blocks.order[ '' ] ).toEqual( [ 'chicken', 'veggies', 'ribs' ] ); } ); it( 'should not move block if passed same index', () => { @@ -967,7 +967,7 @@ describe( 'state', () => { index: 1, } ); - expect( state.present.blockOrder[ '' ] ).toEqual( [ 'chicken', 'ribs', 'veggies' ] ); + expect( state.present.blocks.order[ '' ] ).toEqual( [ 'chicken', 'ribs', 'veggies' ] ); } ); describe( 'edits()', () => { @@ -1087,88 +1087,90 @@ describe( 'state', () => { } ); } ); - describe( 'blocksByClientId', () => { - it( 'should return with attribute block updates', () => { - const original = deepFreeze( editor( undefined, { - type: 'RESET_BLOCKS', - blocks: [ { + describe( 'blocks', () => { + describe( 'byClientId', () => { + it( 'should return with attribute block updates', () => { + const original = deepFreeze( editor( undefined, { + type: 'RESET_BLOCKS', + blocks: [ { + clientId: 'kumquat', + attributes: {}, + innerBlocks: [], + } ], + } ) ); + const state = editor( original, { + type: 'UPDATE_BLOCK_ATTRIBUTES', clientId: 'kumquat', - attributes: {}, - innerBlocks: [], - } ], - } ) ); - const state = editor( original, { - type: 'UPDATE_BLOCK_ATTRIBUTES', - clientId: 'kumquat', - attributes: { - updated: true, - }, - } ); + attributes: { + updated: true, + }, + } ); - expect( state.present.blocksByClientId.kumquat.attributes.updated ).toBe( true ); - } ); + expect( state.present.blocks.byClientId.kumquat.attributes.updated ).toBe( true ); + } ); - it( 'should accumulate attribute block updates', () => { - const original = deepFreeze( editor( undefined, { - type: 'RESET_BLOCKS', - blocks: [ { + it( 'should accumulate attribute block updates', () => { + const original = deepFreeze( editor( undefined, { + type: 'RESET_BLOCKS', + blocks: [ { + clientId: 'kumquat', + attributes: { + updated: true, + }, + innerBlocks: [], + } ], + } ) ); + const state = editor( original, { + type: 'UPDATE_BLOCK_ATTRIBUTES', clientId: 'kumquat', attributes: { - updated: true, + moreUpdated: true, }, - innerBlocks: [], - } ], - } ) ); - const state = editor( original, { - type: 'UPDATE_BLOCK_ATTRIBUTES', - clientId: 'kumquat', - attributes: { + } ); + + expect( state.present.blocks.byClientId.kumquat.attributes ).toEqual( { + updated: true, moreUpdated: true, - }, + } ); } ); - expect( state.present.blocksByClientId.kumquat.attributes ).toEqual( { - updated: true, - moreUpdated: true, - } ); - } ); + it( 'should ignore updates to non-existent block', () => { + const original = deepFreeze( editor( undefined, { + type: 'RESET_BLOCKS', + blocks: [], + } ) ); + const state = editor( original, { + type: 'UPDATE_BLOCK_ATTRIBUTES', + clientId: 'kumquat', + attributes: { + updated: true, + }, + } ); - it( 'should ignore updates to non-existent block', () => { - const original = deepFreeze( editor( undefined, { - type: 'RESET_BLOCKS', - blocks: [], - } ) ); - const state = editor( original, { - type: 'UPDATE_BLOCK_ATTRIBUTES', - clientId: 'kumquat', - attributes: { - updated: true, - }, + expect( state.present.blocks.byClientId ).toBe( original.present.blocks.byClientId ); } ); - expect( state.present.blocksByClientId ).toBe( original.present.blocksByClientId ); - } ); - - it( 'should return with same reference if no changes in updates', () => { - const original = deepFreeze( editor( undefined, { - type: 'RESET_BLOCKS', - blocks: [ { + it( 'should return with same reference if no changes in updates', () => { + const original = deepFreeze( editor( undefined, { + type: 'RESET_BLOCKS', + blocks: [ { + clientId: 'kumquat', + attributes: { + updated: true, + }, + innerBlocks: [], + } ], + } ) ); + const state = editor( original, { + type: 'UPDATE_BLOCK_ATTRIBUTES', clientId: 'kumquat', attributes: { updated: true, }, - innerBlocks: [], - } ], - } ) ); - const state = editor( original, { - type: 'UPDATE_BLOCK_ATTRIBUTES', - clientId: 'kumquat', - attributes: { - updated: true, - }, - } ); + } ); - expect( state.present.blocksByClientId ).toBe( state.present.blocksByClientId ); + expect( state.present.blocks.byClientId ).toBe( state.present.blocks.byClientId ); + } ); } ); } ); diff --git a/packages/editor/src/store/test/selectors.js b/packages/editor/src/store/test/selectors.js index 157bdb62a5926a..b0a7878a8b5568 100644 --- a/packages/editor/src/store/test/selectors.js +++ b/packages/editor/src/store/test/selectors.js @@ -919,8 +919,10 @@ describe( 'selectors', () => { const state = { editor: { present: { - blocksByClientId: {}, - blockOrder: {}, + blocks: { + byClientId: {}, + order: {}, + }, edits: {}, }, }, @@ -935,8 +937,10 @@ describe( 'selectors', () => { const state = { editor: { present: { - blocksByClientId: {}, - blockOrder: {}, + blocks: { + byClientId: {}, + order: {}, + }, edits: {}, }, }, @@ -955,8 +959,10 @@ describe( 'selectors', () => { const state = { editor: { present: { - blocksByClientId: {}, - blockOrder: {}, + blocks: { + byClientId: {}, + order: {}, + }, edits: {}, }, }, @@ -973,8 +979,10 @@ describe( 'selectors', () => { const state = { editor: { present: { - blocksByClientId: {}, - blockOrder: {}, + blocks: { + byClientId: {}, + order: {}, + }, edits: {}, }, }, @@ -991,17 +999,19 @@ describe( 'selectors', () => { const state = { editor: { present: { - blocksByClientId: { - 123: { - clientId: 123, - name: 'core/test-block', - attributes: { - text: '', + blocks: { + byClientId: { + 123: { + clientId: 123, + name: 'core/test-block', + attributes: { + text: '', + }, }, }, - }, - blockOrder: { - '': [ 123 ], + order: { + '': [ 123 ], + }, }, edits: {}, }, @@ -1019,8 +1029,10 @@ describe( 'selectors', () => { const state = { editor: { present: { - blocksByClientId: {}, - blockOrder: {}, + blocks: { + byClientId: {}, + order: {}, + }, edits: {}, }, }, @@ -1042,8 +1054,10 @@ describe( 'selectors', () => { const state = { editor: { present: { - blocksByClientId: {}, - blockOrder: {}, + blocks: { + byClientId: {}, + order: {}, + }, edits: {}, }, }, @@ -1061,8 +1075,10 @@ describe( 'selectors', () => { const state = { editor: { present: { - blocksByClientId: {}, - blockOrder: {}, + blocks: { + byClientId: {}, + order: {}, + }, edits: { content: 'foo', }, @@ -1090,8 +1106,10 @@ describe( 'selectors', () => { const state = { editor: { present: { - blocksByClientId: {}, - blockOrder: {}, + blocks: { + byClientId: {}, + order: {}, + }, edits: { content: 'foo', }, @@ -1163,8 +1181,10 @@ describe( 'selectors', () => { const state = { editor: { present: { - blocksByClientId: {}, - blockOrder: {}, + blocks: { + byClientId: {}, + order: {}, + }, edits: {}, }, }, @@ -1178,17 +1198,19 @@ describe( 'selectors', () => { const state = { editor: { present: { - blocksByClientId: { - 123: { - clientId: 123, - name: 'core/test-block', - attributes: { - text: '', + blocks: { + byClientId: { + 123: { + clientId: 123, + name: 'core/test-block', + attributes: { + text: '', + }, }, }, - }, - blockOrder: { - '': [ 123 ], + order: { + '': [ 123 ], + }, }, edits: {}, }, @@ -1203,8 +1225,10 @@ describe( 'selectors', () => { const state = { editor: { present: { - blocksByClientId: {}, - blockOrder: {}, + blocks: { + byClientId: {}, + order: {}, + }, edits: {}, }, }, @@ -1220,8 +1244,10 @@ describe( 'selectors', () => { const state = { editor: { present: { - blocksByClientId: {}, - blockOrder: {}, + blocks: { + byClientId: {}, + order: {}, + }, edits: { content: 'sassel', }, @@ -1348,12 +1374,14 @@ describe( 'selectors', () => { currentPost: {}, editor: { present: { - blocksByClientId: { - 123: rootBlock, - }, - blockOrder: { - '': rootOrder, - 123: rootBlockOrder, + blocks: { + byClientId: { + 123: rootBlock, + }, + order: { + '': rootOrder, + 123: rootBlockOrder, + }, }, edits: {}, }, @@ -1364,12 +1392,14 @@ describe( 'selectors', () => { currentPost: {}, editor: { present: { - blocksByClientId: { - 123: rootBlock, - }, - blockOrder: { - '': rootOrder, - 123: rootBlockOrder, + blocks: { + byClientId: { + 123: rootBlock, + }, + order: { + '': rootOrder, + 123: rootBlockOrder, + }, }, edits: {}, }, @@ -1386,12 +1416,14 @@ describe( 'selectors', () => { currentPost: {}, editor: { present: { - blocksByClientId: { - 123: rootBlock, - }, - blockOrder: { - '': rootOrder, - 123: [], + blocks: { + byClientId: { + 123: rootBlock, + }, + order: { + '': rootOrder, + 123: [], + }, }, edits: {}, }, @@ -1402,14 +1434,16 @@ describe( 'selectors', () => { currentPost: {}, editor: { present: { - blocksByClientId: { - 123: rootBlock, - 456: { clientId: 456, name: 'core/paragraph', attributes: {} }, - }, - blockOrder: { - '': rootOrder, - 123: [ 456 ], - 456: [], + blocks: { + byClientId: { + 123: rootBlock, + 456: { clientId: 456, name: 'core/paragraph', attributes: {} }, + }, + order: { + '': rootOrder, + 123: [ 456 ], + 456: [], + }, }, edits: {}, }, @@ -1430,14 +1464,16 @@ describe( 'selectors', () => { currentPost: {}, editor: { present: { - blocksByClientId: { - 123: rootBlock, - 456: childBlock, - }, - blockOrder: { - '': rootOrder, - 123: rootBlockOrder, - 456: childBlockOrder, + blocks: { + byClientId: { + 123: rootBlock, + 456: childBlock, + }, + order: { + '': rootOrder, + 123: rootBlockOrder, + 456: childBlockOrder, + }, }, edits: {}, }, @@ -1448,14 +1484,16 @@ describe( 'selectors', () => { currentPost: {}, editor: { present: { - blocksByClientId: { - 123: rootBlock, - 456: childBlock, - }, - blockOrder: { - '': rootOrder, - 123: rootBlockOrder, - 456: childBlockOrder, + blocks: { + byClientId: { + 123: rootBlock, + 456: childBlock, + }, + order: { + '': rootOrder, + 123: rootBlockOrder, + 456: childBlockOrder, + }, }, edits: {}, }, @@ -1475,14 +1513,16 @@ describe( 'selectors', () => { currentPost: {}, editor: { present: { - blocksByClientId: { - 123: rootBlock, - 456: { clientId: 456, name: 'core/paragraph', attributes: {} }, - }, - blockOrder: { - '': rootOrder, - 123: rootBlockOrder, - 456: childBlockOrder, + blocks: { + byClientId: { + 123: rootBlock, + 456: { clientId: 456, name: 'core/paragraph', attributes: {} }, + }, + order: { + '': rootOrder, + 123: rootBlockOrder, + 456: childBlockOrder, + }, }, edits: {}, }, @@ -1493,14 +1533,16 @@ describe( 'selectors', () => { currentPost: {}, editor: { present: { - blocksByClientId: { - 123: rootBlock, - 456: { clientId: 456, name: 'core/paragraph', attributes: { content: [ 'foo' ] } }, - }, - blockOrder: { - '': rootOrder, - 123: rootBlockOrder, - 456: childBlockOrder, + blocks: { + byClientId: { + 123: rootBlock, + 456: { clientId: 456, name: 'core/paragraph', attributes: { content: [ 'foo' ] } }, + }, + order: { + '': rootOrder, + 123: rootBlockOrder, + 456: childBlockOrder, + }, }, edits: {}, }, @@ -1522,16 +1564,18 @@ describe( 'selectors', () => { currentPost: {}, editor: { present: { - blocksByClientId: { - 123: rootBlock, - 456: childBlock, - 789: { clientId: 789, name: 'core/paragraph', attributes: {} }, - }, - blockOrder: { - '': rootOrder, - 123: rootBlockOrder, - 456: childBlockOrder, - 789: grandChildBlockOrder, + blocks: { + byClientId: { + 123: rootBlock, + 456: childBlock, + 789: { clientId: 789, name: 'core/paragraph', attributes: {} }, + }, + order: { + '': rootOrder, + 123: rootBlockOrder, + 456: childBlockOrder, + 789: grandChildBlockOrder, + }, }, edits: {}, }, @@ -1542,16 +1586,18 @@ describe( 'selectors', () => { currentPost: {}, editor: { present: { - blocksByClientId: { - 123: rootBlock, - 456: childBlock, - 789: { clientId: 789, name: 'core/paragraph', attributes: { content: [ 'foo' ] } }, - }, - blockOrder: { - '': rootOrder, - 123: rootBlockOrder, - 456: childBlockOrder, - 789: grandChildBlockOrder, + blocks: { + byClientId: { + 123: rootBlock, + 456: childBlock, + 789: { clientId: 789, name: 'core/paragraph', attributes: { content: [ 'foo' ] } }, + }, + order: { + '': rootOrder, + 123: rootBlockOrder, + 456: childBlockOrder, + 789: grandChildBlockOrder, + }, }, edits: {}, }, @@ -1570,8 +1616,10 @@ describe( 'selectors', () => { currentPost: {}, editor: { present: { - blocksByClientId: {}, - blockOrder: {}, + blocks: { + byClientId: {}, + order: {}, + }, edits: {}, }, }, @@ -1587,16 +1635,18 @@ describe( 'selectors', () => { currentPost: {}, editor: { present: { - blocksByClientId: { - 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1': { - clientId: 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1', - name: 'core/paragraph', - attributes: {}, + blocks: { + byClientId: { + 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1': { + clientId: 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1', + name: 'core/paragraph', + attributes: {}, + }, + }, + order: { + '': [ 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1' ], + 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1': [], }, - }, - blockOrder: { - '': [ 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1' ], - 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1': [], }, edits: {}, }, @@ -1615,12 +1665,14 @@ describe( 'selectors', () => { currentPost: {}, editor: { present: { - blocksByClientId: { - 123: { clientId: 123, name: 'core/paragraph', attributes: {} }, - }, - blockOrder: { - '': [ 123 ], - 123: [], + blocks: { + byClientId: { + 123: { clientId: 123, name: 'core/paragraph', attributes: {} }, + }, + order: { + '': [ 123 ], + 123: [], + }, }, edits: {}, }, @@ -1640,8 +1692,10 @@ describe( 'selectors', () => { currentPost: {}, editor: { present: { - blocksByClientId: {}, - blockOrder: {}, + blocks: { + byClientId: {}, + order: {}, + }, edits: {}, }, }, @@ -1655,14 +1709,16 @@ describe( 'selectors', () => { currentPost: {}, editor: { present: { - blocksByClientId: { - 123: { clientId: 123, name: 'core/paragraph', attributes: {} }, - 456: { clientId: 456, name: 'core/paragraph', attributes: {} }, - }, - blockOrder: { - '': [ 123 ], - 123: [ 456 ], - 456: [], + blocks: { + byClientId: { + 123: { clientId: 123, name: 'core/paragraph', attributes: {} }, + 456: { clientId: 456, name: 'core/paragraph', attributes: {} }, + }, + order: { + '': [ 123 ], + 123: [ 456 ], + 456: [], + }, }, edits: {}, }, @@ -1704,12 +1760,14 @@ describe( 'selectors', () => { }, editor: { present: { - blocksByClientId: { - 123: { clientId: 123, name: 'core/meta-block', attributes: {} }, - }, - blockOrder: { - '': [ 123 ], - 123: [], + blocks: { + byClientId: { + 123: { clientId: 123, name: 'core/meta-block', attributes: {} }, + }, + order: { + '': [ 123 ], + 123: [], + }, }, edits: {}, }, @@ -1735,12 +1793,14 @@ describe( 'selectors', () => { currentPost: {}, editor: { present: { - blocksByClientId: { - 23: { clientId: 23, name: 'core/heading', attributes: {} }, - 123: { clientId: 123, name: 'core/paragraph', attributes: {} }, - }, - blockOrder: { - '': [ 123, 23 ], + blocks: { + byClientId: { + 23: { clientId: 23, name: 'core/heading', attributes: {} }, + 123: { clientId: 123, name: 'core/paragraph', attributes: {} }, + }, + order: { + '': [ 123, 23 ], + }, }, edits: {}, }, @@ -1760,39 +1820,41 @@ describe( 'selectors', () => { currentPost: {}, editor: { present: { - blocksByClientId: { - 'uuid-2': { clientId: 'uuid-2', name: 'core/image', attributes: {} }, - 'uuid-4': { clientId: 'uuid-4', name: 'core/paragraph', attributes: {} }, - 'uuid-6': { clientId: 'uuid-6', name: 'core/paragraph', attributes: {} }, - 'uuid-8': { clientId: 'uuid-8', name: 'core/block', attributes: {} }, - 'uuid-10': { clientId: 'uuid-10', name: 'core/columns', attributes: {} }, - 'uuid-12': { clientId: 'uuid-12', name: 'core/column', attributes: {} }, - 'uuid-14': { clientId: 'uuid-14', name: 'core/column', attributes: {} }, - 'uuid-16': { clientId: 'uuid-16', name: 'core/quote', attributes: {} }, - 'uuid-18': { clientId: 'uuid-18', name: 'core/block', attributes: {} }, - 'uuid-20': { clientId: 'uuid-20', name: 'core/gallery', attributes: {} }, - 'uuid-22': { clientId: 'uuid-22', name: 'core/block', attributes: {} }, - 'uuid-24': { clientId: 'uuid-24', name: 'core/columns', attributes: {} }, - 'uuid-26': { clientId: 'uuid-26', name: 'core/column', attributes: {} }, - 'uuid-28': { clientId: 'uuid-28', name: 'core/column', attributes: {} }, - 'uuid-30': { clientId: 'uuid-30', name: 'core/paragraph', attributes: {} }, - }, - blockOrder: { - '': [ 'uuid-6', 'uuid-8', 'uuid-10', 'uuid-22' ], - 'uuid-2': [ ], - 'uuid-4': [ ], - 'uuid-6': [ ], - 'uuid-8': [ ], - 'uuid-10': [ 'uuid-12', 'uuid-14' ], - 'uuid-12': [ 'uuid-16' ], - 'uuid-14': [ 'uuid-18' ], - 'uuid-16': [ ], - 'uuid-18': [ 'uuid-24' ], - 'uuid-20': [ ], - 'uuid-22': [ ], - 'uuid-24': [ 'uuid-26', 'uuid-28' ], - 'uuid-26': [ ], - 'uuid-28': [ 'uuid-30' ], + blocks: { + byClientId: { + 'uuid-2': { clientId: 'uuid-2', name: 'core/image', attributes: {} }, + 'uuid-4': { clientId: 'uuid-4', name: 'core/paragraph', attributes: {} }, + 'uuid-6': { clientId: 'uuid-6', name: 'core/paragraph', attributes: {} }, + 'uuid-8': { clientId: 'uuid-8', name: 'core/block', attributes: {} }, + 'uuid-10': { clientId: 'uuid-10', name: 'core/columns', attributes: {} }, + 'uuid-12': { clientId: 'uuid-12', name: 'core/column', attributes: {} }, + 'uuid-14': { clientId: 'uuid-14', name: 'core/column', attributes: {} }, + 'uuid-16': { clientId: 'uuid-16', name: 'core/quote', attributes: {} }, + 'uuid-18': { clientId: 'uuid-18', name: 'core/block', attributes: {} }, + 'uuid-20': { clientId: 'uuid-20', name: 'core/gallery', attributes: {} }, + 'uuid-22': { clientId: 'uuid-22', name: 'core/block', attributes: {} }, + 'uuid-24': { clientId: 'uuid-24', name: 'core/columns', attributes: {} }, + 'uuid-26': { clientId: 'uuid-26', name: 'core/column', attributes: {} }, + 'uuid-28': { clientId: 'uuid-28', name: 'core/column', attributes: {} }, + 'uuid-30': { clientId: 'uuid-30', name: 'core/paragraph', attributes: {} }, + }, + order: { + '': [ 'uuid-6', 'uuid-8', 'uuid-10', 'uuid-22' ], + 'uuid-2': [ ], + 'uuid-4': [ ], + 'uuid-6': [ ], + 'uuid-8': [ ], + 'uuid-10': [ 'uuid-12', 'uuid-14' ], + 'uuid-12': [ 'uuid-16' ], + 'uuid-14': [ 'uuid-18' ], + 'uuid-16': [ ], + 'uuid-18': [ 'uuid-24' ], + 'uuid-20': [ ], + 'uuid-22': [ ], + 'uuid-24': [ 'uuid-26', 'uuid-28' ], + 'uuid-26': [ ], + 'uuid-28': [ 'uuid-30' ], + }, }, edits: {}, }, @@ -1817,39 +1879,41 @@ describe( 'selectors', () => { currentPost: {}, editor: { present: { - blocksByClientId: { - 'uuid-2': { clientId: 'uuid-2', name: 'core/image', attributes: {} }, - 'uuid-4': { clientId: 'uuid-4', name: 'core/paragraph', attributes: {} }, - 'uuid-6': { clientId: 'uuid-6', name: 'core/paragraph', attributes: {} }, - 'uuid-8': { clientId: 'uuid-8', name: 'core/block', attributes: {} }, - 'uuid-10': { clientId: 'uuid-10', name: 'core/columns', attributes: {} }, - 'uuid-12': { clientId: 'uuid-12', name: 'core/column', attributes: {} }, - 'uuid-14': { clientId: 'uuid-14', name: 'core/column', attributes: {} }, - 'uuid-16': { clientId: 'uuid-16', name: 'core/quote', attributes: {} }, - 'uuid-18': { clientId: 'uuid-18', name: 'core/block', attributes: {} }, - 'uuid-20': { clientId: 'uuid-20', name: 'core/gallery', attributes: {} }, - 'uuid-22': { clientId: 'uuid-22', name: 'core/block', attributes: {} }, - 'uuid-24': { clientId: 'uuid-24', name: 'core/columns', attributes: {} }, - 'uuid-26': { clientId: 'uuid-26', name: 'core/column', attributes: {} }, - 'uuid-28': { clientId: 'uuid-28', name: 'core/column', attributes: {} }, - 'uuid-30': { clientId: 'uuid-30', name: 'core/paragraph', attributes: {} }, - }, - blockOrder: { - '': [ 'uuid-6', 'uuid-8', 'uuid-10', 'uuid-22' ], - 'uuid-2': [ ], - 'uuid-4': [ ], - 'uuid-6': [ ], - 'uuid-8': [ ], - 'uuid-10': [ 'uuid-12', 'uuid-14' ], - 'uuid-12': [ 'uuid-16' ], - 'uuid-14': [ 'uuid-18' ], - 'uuid-16': [ ], - 'uuid-18': [ 'uuid-24' ], - 'uuid-20': [ ], - 'uuid-22': [ ], - 'uuid-24': [ 'uuid-26', 'uuid-28' ], - 'uuid-26': [ ], - 'uuid-28': [ 'uuid-30' ], + blocks: { + byClientId: { + 'uuid-2': { clientId: 'uuid-2', name: 'core/image', attributes: {} }, + 'uuid-4': { clientId: 'uuid-4', name: 'core/paragraph', attributes: {} }, + 'uuid-6': { clientId: 'uuid-6', name: 'core/paragraph', attributes: {} }, + 'uuid-8': { clientId: 'uuid-8', name: 'core/block', attributes: {} }, + 'uuid-10': { clientId: 'uuid-10', name: 'core/columns', attributes: {} }, + 'uuid-12': { clientId: 'uuid-12', name: 'core/column', attributes: {} }, + 'uuid-14': { clientId: 'uuid-14', name: 'core/column', attributes: {} }, + 'uuid-16': { clientId: 'uuid-16', name: 'core/quote', attributes: {} }, + 'uuid-18': { clientId: 'uuid-18', name: 'core/block', attributes: {} }, + 'uuid-20': { clientId: 'uuid-20', name: 'core/gallery', attributes: {} }, + 'uuid-22': { clientId: 'uuid-22', name: 'core/block', attributes: {} }, + 'uuid-24': { clientId: 'uuid-24', name: 'core/columns', attributes: {} }, + 'uuid-26': { clientId: 'uuid-26', name: 'core/column', attributes: {} }, + 'uuid-28': { clientId: 'uuid-28', name: 'core/column', attributes: {} }, + 'uuid-30': { clientId: 'uuid-30', name: 'core/paragraph', attributes: {} }, + }, + order: { + '': [ 'uuid-6', 'uuid-8', 'uuid-10', 'uuid-22' ], + 'uuid-2': [ ], + 'uuid-4': [ ], + 'uuid-6': [ ], + 'uuid-8': [ ], + 'uuid-10': [ 'uuid-12', 'uuid-14' ], + 'uuid-12': [ 'uuid-16' ], + 'uuid-14': [ 'uuid-18' ], + 'uuid-16': [ ], + 'uuid-18': [ 'uuid-24' ], + 'uuid-20': [ ], + 'uuid-22': [ ], + 'uuid-24': [ 'uuid-26', 'uuid-28' ], + 'uuid-26': [ ], + 'uuid-28': [ 'uuid-30' ], + }, }, edits: {}, }, @@ -1877,12 +1941,14 @@ describe( 'selectors', () => { const state = { editor: { present: { - blocksByClientId: { - 23: { clientId: 23, name: 'core/heading', attributes: {} }, - 123: { clientId: 123, name: 'core/paragraph', attributes: {} }, - }, - blockOrder: { - '': [ 123, 23 ], + blocks: { + byClientId: { + 23: { clientId: 23, name: 'core/heading', attributes: {} }, + 123: { clientId: 123, name: 'core/paragraph', attributes: {} }, + }, + order: { + '': [ 123, 23 ], + }, }, }, }, @@ -1895,14 +1961,16 @@ describe( 'selectors', () => { const state = { editor: { present: { - blocksByClientId: { - 123: { clientId: 123, name: 'core/columns', attributes: {} }, - 456: { clientId: 456, name: 'core/paragraph', attributes: {} }, - 789: { clientId: 789, name: 'core/paragraph', attributes: {} }, - }, - blockOrder: { - '': [ 123 ], - 123: [ 456, 789 ], + blocks: { + byClientId: { + 123: { clientId: 123, name: 'core/columns', attributes: {} }, + 456: { clientId: 456, name: 'core/paragraph', attributes: {} }, + 789: { clientId: 789, name: 'core/paragraph', attributes: {} }, + }, + order: { + '': [ 123 ], + 123: [ 456, 789 ], + }, }, }, }, @@ -1952,9 +2020,11 @@ describe( 'selectors', () => { const state = { editor: { present: { - blocksByClientId: { - 23: { clientId: 23, name: 'core/heading', attributes: {} }, - 123: { clientId: 123, name: 'core/paragraph', attributes: {} }, + blocks: { + byClientId: { + 23: { clientId: 23, name: 'core/heading', attributes: {} }, + 123: { clientId: 123, name: 'core/paragraph', attributes: {} }, + }, }, }, }, @@ -1967,11 +2037,13 @@ describe( 'selectors', () => { const state = { editor: { present: { - blocksByClientId: { - 123: { clientId: 123, name: 'core/columns', attributes: {} }, - 456: { clientId: 456, name: 'core/paragraph', attributes: {} }, - 789: { clientId: 789, name: 'core/paragraph', attributes: {} }, - 124: { clientId: 123, name: 'core/heading', attributes: {} }, + blocks: { + byClientId: { + 123: { clientId: 123, name: 'core/columns', attributes: {} }, + 456: { clientId: 456, name: 'core/paragraph', attributes: {} }, + 789: { clientId: 789, name: 'core/paragraph', attributes: {} }, + 124: { clientId: 123, name: 'core/heading', attributes: {} }, + }, }, }, }, @@ -1984,7 +2056,8 @@ describe( 'selectors', () => { const state = { editor: { present: { - blocksByClientId: { + blocks: { + byClientId: {}, }, }, }, @@ -2026,14 +2099,16 @@ describe( 'selectors', () => { currentPost: {}, editor: { present: { - blocksByClientId: { - 23: { clientId: 23, name: 'core/heading', attributes: {} }, - 123: { clientId: 123, name: 'core/paragraph', attributes: {} }, - }, - blockOrder: { - '': [ 23, 123 ], - 23: [], - 123: [], + blocks: { + byClientId: { + 23: { clientId: 23, name: 'core/heading', attributes: {} }, + 123: { clientId: 123, name: 'core/paragraph', attributes: {} }, + }, + order: { + '': [ 23, 123 ], + 23: [], + 123: [], + }, }, edits: {}, }, @@ -2049,14 +2124,16 @@ describe( 'selectors', () => { currentPost: {}, editor: { present: { - blocksByClientId: { - 23: { clientId: 23, name: 'core/heading', attributes: {} }, - 123: { clientId: 123, name: 'core/paragraph', attributes: {} }, - }, - blockOrder: { - '': [ 23, 123 ], - 23: [], - 123: [], + blocks: { + byClientId: { + 23: { clientId: 23, name: 'core/heading', attributes: {} }, + 123: { clientId: 123, name: 'core/paragraph', attributes: {} }, + }, + order: { + '': [ 23, 123 ], + 23: [], + 123: [], + }, }, edits: {}, }, @@ -2072,14 +2149,16 @@ describe( 'selectors', () => { currentPost: {}, editor: { present: { - blocksByClientId: { - 23: { clientId: 23, name: 'core/heading', attributes: {} }, - 123: { clientId: 123, name: 'core/paragraph', attributes: {} }, - }, - blockOrder: { - '': [ 23, 123 ], - 23: [], - 123: [], + blocks: { + byClientId: { + 23: { clientId: 23, name: 'core/heading', attributes: {} }, + 123: { clientId: 123, name: 'core/paragraph', attributes: {} }, + }, + order: { + '': [ 23, 123 ], + 23: [], + 123: [], + }, }, edits: {}, }, @@ -2101,7 +2180,9 @@ describe( 'selectors', () => { const state = { editor: { present: { - blockOrder: {}, + blocks: { + order: {}, + }, }, }, }; @@ -2113,9 +2194,11 @@ describe( 'selectors', () => { const state = { editor: { present: { - blockOrder: { - '': [ 123, 23 ], - 123: [ 456, 56 ], + blocks: { + order: { + '': [ 123, 23 ], + 123: [ 456, 56 ], + }, }, }, }, @@ -2130,7 +2213,9 @@ describe( 'selectors', () => { const state = { editor: { present: { - blockOrder: {}, + blocks: { + order: {}, + }, }, }, }; @@ -2142,9 +2227,11 @@ describe( 'selectors', () => { const state = { editor: { present: { - blockOrder: { - '': [ 123, 23 ], - 123: [ 456, 56 ], + blocks: { + order: { + '': [ 123, 23 ], + 123: [ 456, 56 ], + }, }, }, }, @@ -2157,10 +2244,12 @@ describe( 'selectors', () => { const state = { editor: { present: { - blockOrder: { - '': [ '123', '23' ], - 123: [ '456', '56' ], - 56: [ '12' ], + blocks: { + order: { + '': [ '123', '23' ], + 123: [ '456', '56' ], + 56: [ '12' ], + }, }, }, }, @@ -2175,8 +2264,10 @@ describe( 'selectors', () => { const state = { editor: { present: { - blockOrder: { - '': [ 123, 23 ], + blocks: { + order: { + '': [ 123, 23 ], + }, }, }, }, @@ -2190,8 +2281,10 @@ describe( 'selectors', () => { const state = { editor: { present: { - blockOrder: { - '': [ 5, 4, 3, 2, 1 ], + blocks: { + order: { + '': [ 5, 4, 3, 2, 1 ], + }, }, }, }, @@ -2205,9 +2298,11 @@ describe( 'selectors', () => { const state = { editor: { present: { - blockOrder: { - '': [ 5, 4, 3, 2, 1 ], - 4: [ 9, 8, 7, 6 ], + blocks: { + order: { + '': [ 5, 4, 3, 2, 1 ], + 4: [ 9, 8, 7, 6 ], + }, }, }, }, @@ -2223,8 +2318,10 @@ describe( 'selectors', () => { const state = { editor: { present: { - blocksByClientId: {}, - blockOrder: {}, + blocks: { + byClientId: {}, + order: {}, + }, edits: {}, }, }, @@ -2279,8 +2376,10 @@ describe( 'selectors', () => { const state = { editor: { present: { - blockOrder: { - '': [ 123, 23 ], + blocks: { + order: { + '': [ 123, 23 ], + }, }, }, }, @@ -2293,9 +2392,11 @@ describe( 'selectors', () => { const state = { editor: { present: { - blockOrder: { - '': [ 123, 23 ], - 123: [ 456 ], + blocks: { + order: { + '': [ 123, 23 ], + 123: [ 456 ], + }, }, }, }, @@ -2310,8 +2411,10 @@ describe( 'selectors', () => { const state = { editor: { present: { - blockOrder: { - '': [ 123, 23 ], + blocks: { + order: { + '': [ 123, 23 ], + }, }, }, }, @@ -2324,9 +2427,11 @@ describe( 'selectors', () => { const state = { editor: { present: { - blockOrder: { - '': [ 123, 23 ], - 123: [ 456, 56 ], + blocks: { + order: { + '': [ 123, 23 ], + 123: [ 456, 56 ], + }, }, }, }, @@ -2341,8 +2446,10 @@ describe( 'selectors', () => { const state = { editor: { present: { - blockOrder: { - '': [ 123, 23 ], + blocks: { + order: { + '': [ 123, 23 ], + }, }, }, }, @@ -2355,9 +2462,11 @@ describe( 'selectors', () => { const state = { editor: { present: { - blockOrder: { - '': [ 123, 23 ], - 123: [ 456, 56 ], + blocks: { + order: { + '': [ 123, 23 ], + 123: [ 456, 56 ], + }, }, }, }, @@ -2370,8 +2479,10 @@ describe( 'selectors', () => { const state = { editor: { present: { - blockOrder: { - '': [ 123, 23 ], + blocks: { + order: { + '': [ 123, 23 ], + }, }, }, }, @@ -2384,9 +2495,11 @@ describe( 'selectors', () => { const state = { editor: { present: { - blockOrder: { - '': [ 123, 23 ], - 123: [ 456, 56 ], + blocks: { + order: { + '': [ 123, 23 ], + 123: [ 456, 56 ], + }, }, }, }, @@ -2401,8 +2514,10 @@ describe( 'selectors', () => { const state = { editor: { present: { - blockOrder: { - '': [ 123, 23 ], + blocks: { + order: { + '': [ 123, 23 ], + }, }, }, }, @@ -2415,9 +2530,11 @@ describe( 'selectors', () => { const state = { editor: { present: { - blockOrder: { - '': [ 123, 23 ], - 123: [ 456, 56 ], + blocks: { + order: { + '': [ 123, 23 ], + 123: [ 456, 56 ], + }, }, }, }, @@ -2430,8 +2547,10 @@ describe( 'selectors', () => { const state = { editor: { present: { - blockOrder: { - '': [ 123, 23 ], + blocks: { + order: { + '': [ 123, 23 ], + }, }, }, }, @@ -2444,9 +2563,11 @@ describe( 'selectors', () => { const state = { editor: { present: { - blockOrder: { - '': [ 123, 23 ], - 123: [ 456, 56 ], + blocks: { + order: { + '': [ 123, 23 ], + 123: [ 456, 56 ], + }, }, }, }, @@ -2488,8 +2609,10 @@ describe( 'selectors', () => { blockSelection: { start: 5, end: 5 }, editor: { present: { - blockOrder: { - 4: [ 3, 2, 1 ], + blocks: { + order: { + 4: [ 3, 2, 1 ], + }, }, }, }, @@ -2503,8 +2626,10 @@ describe( 'selectors', () => { blockSelection: { start: 3, end: 3 }, editor: { present: { - blockOrder: { - 4: [ 3, 2, 1 ], + blocks: { + order: { + 4: [ 3, 2, 1 ], + }, }, }, }, @@ -2517,8 +2642,10 @@ describe( 'selectors', () => { const state = { editor: { present: { - blockOrder: { - 6: [ 5, 4, 3, 2, 1 ], + blocks: { + order: { + 6: [ 5, 4, 3, 2, 1 ], + }, }, }, }, @@ -2531,9 +2658,11 @@ describe( 'selectors', () => { const state = { editor: { present: { - blockOrder: { - 3: [ 2, 1 ], - 6: [ 5, 4 ], + blocks: { + order: { + 3: [ 2, 1 ], + 6: [ 5, 4 ], + }, }, }, }, @@ -2549,8 +2678,10 @@ describe( 'selectors', () => { blockSelection: { start: 5, end: 3 }, editor: { present: { - blockOrder: { - '': [ 5, 4, 3, 2, 1 ], + blocks: { + order: { + '': [ 5, 4, 3, 2, 1 ], + }, }, }, }, @@ -2564,8 +2695,10 @@ describe( 'selectors', () => { blockSelection: { start: 5, end: 3 }, editor: { present: { - blockOrder: { - '': [ 5, 4, 3, 2, 1 ], + blocks: { + order: { + '': [ 5, 4, 3, 2, 1 ], + }, }, }, }, @@ -2579,8 +2712,10 @@ describe( 'selectors', () => { blockSelection: { start: 5, end: 3 }, editor: { present: { - blockOrder: { - '': [ 5, 4, 3, 2, 1 ], + blocks: { + order: { + '': [ 5, 4, 3, 2, 1 ], + }, }, }, }, @@ -2594,8 +2729,10 @@ describe( 'selectors', () => { blockSelection: {}, editor: { present: { - blockOrder: { - '': [ 5, 4, 3, 2, 1 ], + blocks: { + order: { + '': [ 5, 4, 3, 2, 1 ], + }, }, }, }, @@ -2644,8 +2781,10 @@ describe( 'selectors', () => { const state = { editor: { present: { - blockOrder: { - '': [ 5, 4, 3, 2, 1 ], + blocks: { + order: { + '': [ 5, 4, 3, 2, 1 ], + }, }, }, }, @@ -2665,8 +2804,10 @@ describe( 'selectors', () => { const state = { editor: { present: { - blockOrder: { - '': [ 5, 4, 3, 2, 1 ], + blocks: { + order: { + '': [ 5, 4, 3, 2, 1 ], + }, }, }, }, @@ -2771,14 +2912,16 @@ describe( 'selectors', () => { }, editor: { present: { - blocksByClientId: { - clientId1: { clientId: 'clientId1' }, - clientId2: { clientId: 'clientId2' }, - }, - blockOrder: { - '': [ 'clientId1' ], - clientId1: [ 'clientId2' ], - clientId2: [], + blocks: { + byClientId: { + clientId1: { clientId: 'clientId1' }, + clientId2: { clientId: 'clientId2' }, + }, + order: { + '': [ 'clientId1' ], + clientId1: [ 'clientId2' ], + clientId2: [], + }, }, edits: {}, }, @@ -2805,12 +2948,14 @@ describe( 'selectors', () => { }, editor: { present: { - blocksByClientId: { - clientId1: { clientId: 'clientId1' }, - }, - blockOrder: { - '': [ 'clientId1' ], - clientId1: [], + blocks: { + byClientId: { + clientId1: { clientId: 'clientId1' }, + }, + order: { + '': [ 'clientId1' ], + clientId1: [], + }, }, edits: {}, }, @@ -2834,14 +2979,16 @@ describe( 'selectors', () => { }, editor: { present: { - blocksByClientId: { - clientId1: { clientId: 'clientId1' }, - clientId2: { clientId: 'clientId2' }, - }, - blockOrder: { - '': [ 'clientId1' ], - clientId1: [ 'clientId2' ], - clientId2: [], + blocks: { + byClientId: { + clientId1: { clientId: 'clientId1' }, + clientId2: { clientId: 'clientId2' }, + }, + order: { + '': [ 'clientId1' ], + clientId1: [ 'clientId2' ], + clientId2: [], + }, }, edits: {}, }, @@ -2865,14 +3012,16 @@ describe( 'selectors', () => { }, editor: { present: { - blocksByClientId: { - clientId1: { clientId: 'clientId1' }, - clientId2: { clientId: 'clientId2' }, - }, - blockOrder: { - '': [ 'clientId1', 'clientId2' ], - clientId1: [], - clientId2: [], + blocks: { + byClientId: { + clientId1: { clientId: 'clientId1' }, + clientId2: { clientId: 'clientId2' }, + }, + order: { + '': [ 'clientId1', 'clientId2' ], + clientId1: [], + clientId2: [], + }, }, edits: {}, }, @@ -2896,14 +3045,16 @@ describe( 'selectors', () => { }, editor: { present: { - blocksByClientId: { - clientId1: { clientId: 'clientId1' }, - clientId2: { clientId: 'clientId2' }, - }, - blockOrder: { - '': [ 'clientId1', 'clientId2' ], - clientId1: [], - clientId2: [], + blocks: { + byClientId: { + clientId1: { clientId: 'clientId1' }, + clientId2: { clientId: 'clientId2' }, + }, + order: { + '': [ 'clientId1', 'clientId2' ], + clientId1: [], + clientId2: [], + }, }, edits: {}, }, @@ -3010,8 +3161,10 @@ describe( 'selectors', () => { const state = { editor: { present: { - blockOrder: {}, - blocksByClientId: {}, + blocks: { + byClientId: {}, + order: {}, + }, edits: {}, }, }, @@ -3025,12 +3178,14 @@ describe( 'selectors', () => { const state = { editor: { present: { - blockOrder: { - '': [ 123, 456 ], - }, - blocksByClientId: { - 123: { clientId: 123, name: 'core/image', attributes: {} }, - 456: { clientId: 456, name: 'core/quote', attributes: {} }, + blocks: { + byClientId: { + 123: { clientId: 123, name: 'core/image', attributes: {} }, + 456: { clientId: 456, name: 'core/quote', attributes: {} }, + }, + order: { + '': [ 123, 456 ], + }, }, edits: {}, }, @@ -3045,11 +3200,13 @@ describe( 'selectors', () => { const state = { editor: { present: { - blockOrder: { - '': [ 123 ], - }, - blocksByClientId: { - 123: { clientId: 123, name: 'core/image', attributes: {} }, + blocks: { + byClientId: { + 123: { clientId: 123, name: 'core/image', attributes: {} }, + }, + order: { + '': [ 123 ], + }, }, edits: {}, }, @@ -3064,11 +3221,13 @@ describe( 'selectors', () => { const state = { editor: { present: { - blockOrder: { - '': [ 456 ], - }, - blocksByClientId: { - 456: { clientId: 456, name: 'core/quote', attributes: {} }, + blocks: { + byClientId: { + 456: { clientId: 456, name: 'core/quote', attributes: {} }, + }, + order: { + '': [ 456 ], + }, }, edits: {}, }, @@ -3083,11 +3242,13 @@ describe( 'selectors', () => { const state = { editor: { present: { - blockOrder: { - '': [ 567 ], - }, - blocksByClientId: { - 567: { clientId: 567, name: 'core-embed/youtube', attributes: {} }, + blocks: { + byClientId: { + 567: { clientId: 567, name: 'core-embed/youtube', attributes: {} }, + }, + order: { + '': [ 567 ], + }, }, edits: {}, }, @@ -3102,12 +3263,14 @@ describe( 'selectors', () => { const state = { editor: { present: { - blockOrder: { - '': [ 456, 789 ], - }, - blocksByClientId: { - 456: { clientId: 456, name: 'core/quote', attributes: {} }, - 789: { clientId: 789, name: 'core/paragraph', attributes: {} }, + blocks: { + byClientId: { + 456: { clientId: 456, name: 'core/quote', attributes: {} }, + 789: { clientId: 789, name: 'core/paragraph', attributes: {} }, + }, + order: { + '': [ 456, 789 ], + }, }, edits: {}, }, @@ -3165,11 +3328,13 @@ describe( 'selectors', () => { const state = { editor: { present: { - blockOrder: { - '': [ block.clientId ], - }, - blocksByClientId: { - [ block.clientId ]: block, + blocks: { + byClientId: { + [ block.clientId ]: block, + }, + order: { + '': [ block.clientId ], + }, }, edits: { content: 'custom edit', @@ -3190,11 +3355,13 @@ describe( 'selectors', () => { const state = { editor: { present: { - blockOrder: { - '': [ block.clientId ], - }, - blocksByClientId: { - [ block.clientId ]: block, + blocks: { + byClientId: { + [ block.clientId ]: block, + }, + order: { + '': [ block.clientId ], + }, }, edits: {}, }, @@ -3214,11 +3381,13 @@ describe( 'selectors', () => { const state = { editor: { present: { - blockOrder: { - '': [ unknownBlock.clientId ], - }, - blocksByClientId: { - [ unknownBlock.clientId ]: unknownBlock, + blocks: { + byClientId: { + [ unknownBlock.clientId ]: unknownBlock, + }, + order: { + '': [ unknownBlock.clientId ], + }, }, edits: {}, }, @@ -3241,12 +3410,14 @@ describe( 'selectors', () => { const state = { editor: { present: { - blockOrder: { - '': [ firstUnknown.clientId, secondUnknown.clientId ], - }, - blocksByClientId: { - [ firstUnknown.clientId ]: firstUnknown, - [ secondUnknown.clientId ]: secondUnknown, + blocks: { + byClientId: { + [ firstUnknown.clientId ]: firstUnknown, + [ secondUnknown.clientId ]: secondUnknown, + }, + order: { + '': [ firstUnknown.clientId, secondUnknown.clientId ], + }, }, edits: {}, }, @@ -3264,11 +3435,13 @@ describe( 'selectors', () => { const state = { editor: { present: { - blockOrder: { - '': [ defaultBlock.clientId ], - }, - blocksByClientId: { - [ defaultBlock.clientId ]: defaultBlock, + blocks: { + byClientId: { + [ defaultBlock.clientId ]: defaultBlock, + }, + order: { + '': [ defaultBlock.clientId ], + }, }, edits: {}, }, @@ -3286,17 +3459,19 @@ describe( 'selectors', () => { const state = { editor: { present: { - blockOrder: { - '': [ defaultBlock.clientId ], - }, - blocksByClientId: { - [ defaultBlock.clientId ]: { - ...defaultBlock, - attributes: { - ...defaultBlock.attributes, - modified: true, + blocks: { + byClientId: { + [ defaultBlock.clientId ]: { + ...defaultBlock, + attributes: { + ...defaultBlock.attributes, + modified: true, + }, }, }, + order: { + '': [ defaultBlock.clientId ], + }, }, edits: {}, }, @@ -3315,7 +3490,9 @@ describe( 'selectors', () => { const state = { editor: { present: { - blocksByClientId: {}, + blocks: { + byClientId: {}, + }, }, }, blockListSettings: {}, @@ -3328,7 +3505,9 @@ describe( 'selectors', () => { const state = { editor: { present: { - blocksByClientId: {}, + blocks: { + byClientId: {}, + }, }, }, blockListSettings: {}, @@ -3343,7 +3522,9 @@ describe( 'selectors', () => { const state = { editor: { present: { - blocksByClientId: {}, + blocks: { + byClientId: {}, + }, }, }, blockListSettings: {}, @@ -3358,7 +3539,9 @@ describe( 'selectors', () => { const state = { editor: { present: { - blocksByClientId: {}, + blocks: { + byClientId: {}, + }, }, }, blockListSettings: {}, @@ -3373,7 +3556,9 @@ describe( 'selectors', () => { const state = { editor: { present: { - blocksByClientId: {}, + blocks: { + byClientId: {}, + }, }, }, blockListSettings: {}, @@ -3386,8 +3571,10 @@ describe( 'selectors', () => { const state = { editor: { present: { - blocksByClientId: { - block1: { name: 'core/test-block-a' }, + blocks: { + byClientId: { + block1: { name: 'core/test-block-a' }, + }, }, }, }, @@ -3401,8 +3588,10 @@ describe( 'selectors', () => { const state = { editor: { present: { - blocksByClientId: { - block1: { name: 'core/test-block-b' }, + blocks: { + byClientId: { + block1: { name: 'core/test-block-b' }, + }, }, }, }, @@ -3416,8 +3605,10 @@ describe( 'selectors', () => { const state = { editor: { present: { - blocksByClientId: { - block1: { name: 'core/test-block-a' }, + blocks: { + byClientId: { + block1: { name: 'core/test-block-a' }, + }, }, }, }, @@ -3435,8 +3626,10 @@ describe( 'selectors', () => { const state = { editor: { present: { - blocksByClientId: { - block1: { name: 'core/test-block-a' }, + blocks: { + byClientId: { + block1: { name: 'core/test-block-a' }, + }, }, }, }, @@ -3454,8 +3647,10 @@ describe( 'selectors', () => { const state = { editor: { present: { - blocksByClientId: { - block1: { name: 'core/test-block-b' }, + blocks: { + byClientId: { + block1: { name: 'core/test-block-b' }, + }, }, }, }, @@ -3475,10 +3670,12 @@ describe( 'selectors', () => { const state = { editor: { present: { - blocksByClientId: { - block1: { name: 'core/test-block-a' }, + blocks: { + byClientId: { + block1: { name: 'core/test-block-a' }, + }, + order: {}, }, - blockOrder: {}, edits: {}, }, }, @@ -3532,11 +3729,13 @@ describe( 'selectors', () => { const state = { editor: { present: { - blocksByClientId: { - block1: { name: 'core/test-block-a' }, - block2: { name: 'core/test-block-a' }, + blocks: { + byClientId: { + block1: { name: 'core/test-block-a' }, + block2: { name: 'core/test-block-a' }, + }, + order: {}, }, - blockOrder: {}, edits: {}, }, }, @@ -3569,11 +3768,13 @@ describe( 'selectors', () => { const state = { editor: { present: { - blocksByClientId: { - block1: { name: 'core/test-block-a' }, - block2: { name: 'core/test-block-a' }, + blocks: { + byClientId: { + block1: { name: 'core/test-block-a' }, + block2: { name: 'core/test-block-a' }, + }, + order: {}, }, - blockOrder: {}, edits: {}, }, }, @@ -3627,11 +3828,13 @@ describe( 'selectors', () => { const state = { editor: { present: { - blocksByClientId: { - block1: { clientId: 'block1', name: 'core/test-block-b' }, - }, - blockOrder: { - '': [ 'block1' ], + blocks: { + byClientId: { + block1: { clientId: 'block1', name: 'core/test-block-b' }, + }, + order: { + '': [ 'block1' ], + }, }, edits: {}, }, @@ -3655,8 +3858,10 @@ describe( 'selectors', () => { const state = { editor: { present: { - blocksByClientId: {}, - blockOrder: {}, + blocks: { + byClientId: {}, + order: {}, + }, edits: {}, }, }, @@ -3679,8 +3884,10 @@ describe( 'selectors', () => { const state = { editor: { present: { - blocksByClientId: {}, - blockOrder: {}, + blocks: { + byClientId: {}, + order: {}, + }, edits: {}, }, }, @@ -3706,11 +3913,13 @@ describe( 'selectors', () => { const state = { editor: { present: { - blocksByClientId: { - block1: { name: 'core/test-block-b' }, - }, - blockOrder: { - '': [ 'block1' ], + blocks: { + byClientId: { + block1: { name: 'core/test-block-b' }, + }, + order: { + '': [ 'block1' ], + }, }, edits: {}, }, From a824a49d9762e529ddd31308930e4a26fb2088cf Mon Sep 17 00:00:00 2001 From: Rafael Ramos Date: Wed, 31 Oct 2018 16:50:07 -0400 Subject: [PATCH 43/98] Update documentation link paths (#11324) --- packages/edit-post/src/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/edit-post/src/README.md b/packages/edit-post/src/README.md index 74db4920f4c047..7dceb8f8b08a73 100644 --- a/packages/edit-post/src/README.md +++ b/packages/edit-post/src/README.md @@ -2,11 +2,11 @@ Extending the editor UI can be accomplished with the `registerPlugin` API, allowing you to define all your plugin's UI elements in one place. -Refer to [the plugins module documentation](../packages/plugins/) for more information. +Refer to [the plugins module documentation](../../plugins/) for more information. ## Plugin Components -The following components can be used with the `registerPlugin` ([see documentation](../packages/plugins)) API. +The following components can be used with the `registerPlugin` ([see documentation](../../plugins)) API. They can be found in the global variable `wp.editPost` when defining `wp-edit-post` as a script dependency. ### `PluginBlockSettingsMenuItem` From f3451ad9fce93149a823a62436e228f0d31295ac Mon Sep 17 00:00:00 2001 From: Zebulan Stanphill Date: Wed, 31 Oct 2018 15:59:24 -0500 Subject: [PATCH 44/98] Fix "Mac OS" typo + use fancy quotes consistently (#11310) --- docs/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/readme.md b/docs/readme.md index 8b571f3e0be958..4d1c74172ec236 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -2,7 +2,7 @@ Gutenberg began as a transformation of the WordPress editor — a new interface for adding, editing, and manipulating content. It seeks to make it easy for anyone to create rich, flexible content layouts with a block-based UI. All types of page components are represented as modular blocks, which means they can be accessed from a unified block menu, dropped anywhere on a page, and directly edited to create the custom presentation the user wants. -It is a fundamental modernization and transformation of how the WordPress experience works, creating new opportunities for both users and developers. Gutenberg introduces new frameworks, interaction patterns, functionality, and user experiences for WordPress. And similar to a new Mac OS version, we will talk about "Gutenberg", and all the new possibilities it enables, until eventually the idea of Gutenberg as a separate entity will fade and it will simply be WordPress. +It is a fundamental modernization and transformation of how the WordPress experience works, creating new opportunities for both users and developers. Gutenberg introduces new frameworks, interaction patterns, functionality, and user experiences for WordPress. And similar to a new macOS version, we will talk about “Gutenberg”, and all the new possibilities it enables, until eventually the idea of Gutenberg as a separate entity will fade and it will simply be WordPress. ![Gutenberg Demo](https://cldup.com/kZXGDcGPMU.gif) From fae7777e768bd70457669c4a948fed89b0bc68c7 Mon Sep 17 00:00:00 2001 From: Rahul Prajapati Date: Thu, 1 Nov 2018 02:36:53 +0530 Subject: [PATCH 45/98] Stop trying to autosave when title and classic block content both are empty. (#10404) * Do not auto-update if title and classic block content is empty. * Update isEditedPostEmpty: use hasKnownBlocks and check if freeform or unregistered or empty content block is available in post. * Update isEditablePost logic to check length of availalbe block first. * Editor: Improve selector consideration of empty content edit --- packages/editor/src/store/selectors.js | 34 ++- packages/editor/src/store/test/selectors.js | 271 ++++++++++++++++++-- 2 files changed, 274 insertions(+), 31 deletions(-) diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index 82ae8b6a7f4803..61d8cf4c2d5591 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -372,14 +372,32 @@ export function isEditedPostSaveable( state ) { * @return {boolean} Whether post has content. */ export function isEditedPostEmpty( state ) { - // While the condition of truthy content string would be sufficient for - // determining emptiness, testing saveable blocks length is a trivial - // operation by comparison. Since this function can be called frequently, - // optimize for the fast case where saveable blocks are non-empty. - return ( - ! getBlocksForSerialization( state ).length && - ! getEditedPostAttribute( state, 'content' ) - ); + const blocks = getBlocksForSerialization( state ); + + // While the condition of truthy content string is sufficient to determine + // emptiness, testing saveable blocks length is a trivial operation. Since + // this function can be called frequently, optimize for the fast case as a + // condition of the mere existence of blocks. Note that the value of edited + // content is used in place of blocks, thus allowed to fall through. + if ( blocks.length && ! ( 'content' in getPostEdits( state ) ) ) { + // Pierce the abstraction of the serializer in knowing that blocks are + // joined with with newlines such that even if every individual block + // produces an empty save result, the serialized content is non-empty. + if ( blocks.length > 1 ) { + return false; + } + + // Freeform and unregistered blocks omit comment delimiters in their + // output. The freeform block specifically may produce an empty string + // to save. In the case of a single freeform block, fall through to the + // full serialize. Otherwise, the single block is assumed non-empty by + // virtue of its comment delimiters. + if ( blocks[ 0 ].name !== getFreeformContentHandlerName() ) { + return false; + } + } + + return ! getEditedPostContent( state ); } /** diff --git a/packages/editor/src/store/test/selectors.js b/packages/editor/src/store/test/selectors.js index b0a7878a8b5568..0daa48afa97fe4 100644 --- a/packages/editor/src/store/test/selectors.js +++ b/packages/editor/src/store/test/selectors.js @@ -13,7 +13,6 @@ import { getBlockTypes, getDefaultBlockName, setDefaultBlockName, - getFreeformContentHandlerName, setFreeformContentHandlerName, } from '@wordpress/blocks'; import { moment } from '@wordpress/date'; @@ -159,6 +158,20 @@ describe( 'selectors', () => { parent: [ 'core/test-block-b' ], } ); + registerBlockType( 'core/test-freeform', { + save: ( props ) => { props.attributes.content }, + category: 'common', + title: 'Test Freeform Content Handler', + icon: 'test', + attributes: { + content: { + type: 'string', + }, + }, + } ); + + setFreeformContentHandlerName( 'core/test-freeform' ); + cachedSelectors.forEach( ( { clear } ) => clear() ); } ); @@ -167,6 +180,9 @@ describe( 'selectors', () => { unregisterBlockType( 'core/test-block-a' ); unregisterBlockType( 'core/test-block-b' ); unregisterBlockType( 'core/test-block-c' ); + unregisterBlockType( 'core/test-freeform' ); + + setFreeformContentHandlerName( undefined ); } ); describe( 'hasEditorUndo', () => { @@ -1003,7 +1019,8 @@ describe( 'selectors', () => { byClientId: { 123: { clientId: 123, - name: 'core/test-block', + name: 'core/test-block-a', + isValid: true, attributes: { text: '', }, @@ -1022,6 +1039,65 @@ describe( 'selectors', () => { expect( isEditedPostSaveable( state ) ).toBe( true ); } ); + + it( 'should return false if the post has no title, excerpt and empty classic block', () => { + const state = { + editor: { + present: { + blocks: { + byClientId: { + 123: { + clientId: 123, + name: 'core/test-freeform', + attributes: { + content: '', + }, + }, + }, + order: { + '': [ 123 ], + }, + }, + edits: {}, + }, + }, + currentPost: {}, + saving: {}, + }; + + expect( isEditedPostSaveable( state ) ).toBe( false ); + } ); + + it( 'should return true if the post has a title and empty classic block', () => { + const state = { + editor: { + present: { + blocks: { + byClientId: { + 123: { + clientId: 123, + name: 'core/test-freeform', + isValid: true, + attributes: { + content: '', + }, + }, + }, + order: { + '': [ 123 ], + }, + }, + edits: {}, + }, + }, + currentPost: { + title: 'sassel', + }, + saving: {}, + }; + + expect( isEditedPostSaveable( state ) ).toBe( true ); + } ); } ); describe( 'isEditedPostAutosaveable', () => { @@ -1202,7 +1278,8 @@ describe( 'selectors', () => { byClientId: { 123: { clientId: 123, - name: 'core/test-block', + name: 'core/test-block-a', + isValid: true, attributes: { text: '', }, @@ -1221,6 +1298,38 @@ describe( 'selectors', () => { expect( isEditedPostEmpty( state ) ).toBe( false ); } ); + it( 'should return true if blocks, but empty content edit', () => { + const state = { + editor: { + present: { + blocks: { + byClientId: { + 123: { + clientId: 123, + name: 'core/test-block-a', + isValid: true, + attributes: { + text: '', + }, + }, + }, + order: { + '': [ 123 ], + }, + }, + edits: { + content: '', + }, + }, + }, + currentPost: { + content: '', + }, + }; + + expect( isEditedPostEmpty( state ) ).toBe( true ); + } ); + it( 'should return true if the post has an empty content property', () => { const state = { editor: { @@ -1258,6 +1367,132 @@ describe( 'selectors', () => { expect( isEditedPostEmpty( state ) ).toBe( false ); } ); + + it( 'should return true if empty classic block', () => { + const state = { + editor: { + present: { + blocks: { + byClientId: { + 123: { + clientId: 123, + name: 'core/test-freeform', + isValid: true, + attributes: { + content: '', + }, + }, + }, + order: { + '': [ 123 ], + }, + }, + edits: {}, + }, + }, + currentPost: {}, + }; + + expect( isEditedPostEmpty( state ) ).toBe( true ); + } ); + + it( 'should return true if empty content freeform block', () => { + const state = { + editor: { + present: { + blocks: { + byClientId: { + 123: { + clientId: 123, + name: 'core/test-freeform', + isValid: true, + attributes: { + content: '', + }, + }, + }, + order: { + '': [ 123 ], + }, + }, + edits: {}, + }, + }, + currentPost: { + content: '', + }, + }; + + expect( isEditedPostEmpty( state ) ).toBe( true ); + } ); + + it( 'should return false if non-empty content freeform block', () => { + const state = { + editor: { + present: { + blocks: { + byClientId: { + 123: { + clientId: 123, + name: 'core/test-freeform', + isValid: true, + attributes: { + content: 'Test Data', + }, + }, + }, + order: { + '': [ 123 ], + }, + }, + edits: {}, + }, + }, + currentPost: { + content: 'Test Data', + }, + }; + + expect( isEditedPostEmpty( state ) ).toBe( false ); + } ); + + it( 'should return false for multiple empty freeform blocks', () => { + const state = { + editor: { + present: { + blocks: { + byClientId: { + 123: { + clientId: 123, + name: 'core/test-freeform', + isValid: true, + attributes: { + content: '', + }, + }, + 456: { + clientId: 456, + name: 'core/test-freeform', + isValid: true, + attributes: { + content: '', + }, + }, + }, + order: { + '': [ 123, 456 ], + }, + }, + edits: {}, + }, + }, + currentPost: { + content: '\n\n', + }, + }; + + expect( isEditedPostEmpty( state ) ).toBe( false ); + } ); } ); describe( 'isEditedPostBeingScheduled', () => { @@ -3283,11 +3518,10 @@ describe( 'selectors', () => { } ); describe( 'getEditedPostContent', () => { - let originalDefaultBlockName, originalFreeformContentHandlerName; + let originalDefaultBlockName; beforeAll( () => { originalDefaultBlockName = getDefaultBlockName(); - originalFreeformContentHandlerName = getFreeformContentHandlerName(); registerBlockType( 'core/default', { category: 'common', @@ -3300,23 +3534,11 @@ describe( 'selectors', () => { }, save: () => null, } ); - registerBlockType( 'core/unknown', { - category: 'common', - title: 'unknown', - attributes: { - html: { - type: 'string', - }, - }, - save: ( { attributes } ) => { attributes.html }, - } ); setDefaultBlockName( 'core/default' ); - setFreeformContentHandlerName( 'core/unknown' ); } ); afterAll( () => { setDefaultBlockName( originalDefaultBlockName ); - setFreeformContentHandlerName( originalFreeformContentHandlerName ); getBlockTypes().forEach( ( block ) => { unregisterBlockType( block.name ); } ); @@ -3375,8 +3597,8 @@ describe( 'selectors', () => { } ); it( 'returns removep\'d serialization of blocks for single unknown', () => { - const unknownBlock = createBlock( getFreeformContentHandlerName(), { - html: '

    foo

    ', + const unknownBlock = createBlock( 'core/test-freeform', { + content: '

    foo

    ', } ); const state = { editor: { @@ -3401,11 +3623,11 @@ describe( 'selectors', () => { } ); it( 'returns non-removep\'d serialization of blocks for multiple unknown', () => { - const firstUnknown = createBlock( getFreeformContentHandlerName(), { - html: '

    foo

    ', + const firstUnknown = createBlock( 'core/test-freeform', { + content: '

    foo

    ', } ); - const secondUnknown = createBlock( getFreeformContentHandlerName(), { - html: '

    bar

    ', + const secondUnknown = createBlock( 'core/test-freeform', { + content: '

    bar

    ', } ); const state = { editor: { @@ -3760,6 +3982,7 @@ describe( 'selectors', () => { 'core/block/2', 'core/block/1', 'core/test-block-b', + 'core/test-freeform', 'core/test-block-a', ] ); } ); @@ -3806,6 +4029,7 @@ describe( 'selectors', () => { expect( firstBlockFirstCall ).toBe( firstBlockSecondCall ); expect( firstBlockFirstCall.map( ( item ) => item.id ) ).toEqual( [ 'core/test-block-b', + 'core/test-freeform', 'core/test-block-a', 'core/block/1', 'core/block/2', @@ -3815,6 +4039,7 @@ describe( 'selectors', () => { const secondBlockSecondCall = getInserterItems( stateSecondBlockRestricted, 'block2' ); expect( secondBlockFirstCall.map( ( item ) => item.id ) ).toEqual( [ 'core/test-block-b', + 'core/test-freeform', 'core/test-block-a', 'core/block/1', 'core/block/2', From b7d8fbfd0b97d579f732d9fc8f026e2919c2642d Mon Sep 17 00:00:00 2001 From: Dennis Snell Date: Wed, 31 Oct 2018 14:12:33 -0700 Subject: [PATCH 46/98] Parser: Runs all parser implementations against the same tests (#11320) So far we haven't added too many tests to the parsers but the ones we _have_ added are in separate places and each implementation runs against a different set of tests. In this patch we're creating `shared-tests.js` in the spec parser that defines a suite of conformance tests for the parser and then we're using that base test-builder to dynamically create test suites for each implementation such that they all run the same suite. It's probably easier to understand by reading the code than this summary. Of note: by calling to `php` directly we're able to run the PHP parsers against the same tests as we are the JavaScript implementations. We should be able to do this for any implementation as long as the required binaries are available in the CI environment (Rust, for example). --- .eslintignore | 2 +- .../package.json | 3 + .../test/__snapshots__/index.js.snap | 23 + .../test/index.js | 75 +- .../test/test-parser.php | 7 + .../block-serialization-spec-parser/index.js | 1626 +---------------- .../package.json | 2 +- .../block-serialization-spec-parser/parser.js | 1624 ++++++++++++++++ .../shared-tests.js | 89 + .../test/__snapshots__/index.js.snap | 13 +- .../test/index.js | 17 +- .../test/test-parser.php | 7 + 12 files changed, 1785 insertions(+), 1703 deletions(-) create mode 100644 packages/block-serialization-default-parser/test/__snapshots__/index.js.snap create mode 100644 packages/block-serialization-default-parser/test/test-parser.php create mode 100644 packages/block-serialization-spec-parser/parser.js create mode 100644 packages/block-serialization-spec-parser/shared-tests.js create mode 100644 packages/block-serialization-spec-parser/test/test-parser.php diff --git a/.eslintignore b/.eslintignore index 35fff82ce771c7..8ed0cd62953add 100644 --- a/.eslintignore +++ b/.eslintignore @@ -4,4 +4,4 @@ coverage node_modules test/e2e/test-plugins vendor -packages/block-serialization-spec-parser/index.js +packages/block-serialization-spec-parser/parser.js diff --git a/packages/block-serialization-default-parser/package.json b/packages/block-serialization-default-parser/package.json index 806020751aab0a..d3c99b992ebff9 100644 --- a/packages/block-serialization-default-parser/package.json +++ b/packages/block-serialization-default-parser/package.json @@ -23,6 +23,9 @@ "dependencies": { "@babel/runtime": "^7.0.0" }, + "devDependencies": { + "@wordpress/block-serialization-spec-parser": "file:../block-serialization-spec-parser" + }, "publishConfig": { "access": "public" } diff --git a/packages/block-serialization-default-parser/test/__snapshots__/index.js.snap b/packages/block-serialization-default-parser/test/__snapshots__/index.js.snap new file mode 100644 index 00000000000000..5b5abb61a3d2e5 --- /dev/null +++ b/packages/block-serialization-default-parser/test/__snapshots__/index.js.snap @@ -0,0 +1,23 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`block-serialization-default-parser-js basic parsing parse() works properly 1`] = ` +Array [ + Object { + "attrs": Object {}, + "blockName": "core/more", + "innerBlocks": Array [], + "innerHTML": "", + }, +] +`; + +exports[`block-serialization-default-parser-php basic parsing parse() works properly 1`] = ` +Array [ + Object { + "attrs": Object {}, + "blockName": "core/more", + "innerBlocks": Array [], + "innerHTML": "", + }, +] +`; diff --git a/packages/block-serialization-default-parser/test/index.js b/packages/block-serialization-default-parser/test/index.js index 116a35ce93487b..a8749c1c0f2624 100644 --- a/packages/block-serialization-default-parser/test/index.js +++ b/packages/block-serialization-default-parser/test/index.js @@ -1,73 +1,14 @@ +/** + * External dependencies + */ +import path from 'path'; + /** * Internal dependencies */ +import { jsTester, phpTester } from '@wordpress/block-serialization-spec-parser'; import { parse } from '../'; -describe( 'block-serialization-spec-parser', () => { - test( 'parse() accepts inputs with multiple Reusable blocks', () => { - const result = parse( - '' - ); - - expect( result ).toEqual( [ - { - blockName: 'core/block', - attrs: { ref: 313 }, - innerBlocks: [], - innerHTML: '', - }, - { - blockName: 'core/block', - attrs: { ref: 482 }, - innerBlocks: [], - innerHTML: '', - }, - ] ); - } ); - - test( 'treats void blocks and empty blocks identically', () => { - expect( parse( - '' - ) ).toEqual( parse( - '' - ) ); - - expect( parse( - '' - ) ).toEqual( parse( - '' - ) ); - } ); - - test( 'should grab HTML soup before block openers', () => { - [ - '

    Break me

    ', - '

    Break me

    ', - ].forEach( ( input ) => expect( parse( input ) ).toEqual( [ - expect.objectContaining( { innerHTML: '

    Break me

    ' } ), - expect.objectContaining( { blockName: 'core/block', innerHTML: '' } ), - ] ) ); - } ); - - test( 'should grab HTML soup before inner block openers', () => { - [ - '

    Break me

    ', - '

    Break me

    ', - ].forEach( ( input ) => expect( parse( input ) ).toEqual( [ - expect.objectContaining( { - innerBlocks: [ expect.objectContaining( { blockName: 'core/block', innerHTML: '' } ) ], - innerHTML: '

    Break me

    ', - } ), - ] ) ); - } ); +describe( 'block-serialization-default-parser-js', jsTester( parse ) ); - test( 'should grab HTML soup after blocks', () => { - [ - '

    Break me

    ', - '

    Break me

    ', - ].forEach( ( input ) => expect( parse( input ) ).toEqual( [ - expect.objectContaining( { blockName: 'core/block', innerHTML: '' } ), - expect.objectContaining( { innerHTML: '

    Break me

    ' } ), - ] ) ); - } ); -} ); +phpTester( 'block-serialization-default-parser-php', path.join( __dirname, 'test-parser.php' ) ); diff --git a/packages/block-serialization-default-parser/test/test-parser.php b/packages/block-serialization-default-parser/test/test-parser.php new file mode 100644 index 00000000000000..a1ab00b6d7bf45 --- /dev/null +++ b/packages/block-serialization-default-parser/test/test-parser.php @@ -0,0 +1,7 @@ +parse( file_get_contents( 'php://stdin' ) ) ); diff --git a/packages/block-serialization-spec-parser/index.js b/packages/block-serialization-spec-parser/index.js index e716efb9e3e8d2..b217f8f5c5ad76 100644 --- a/packages/block-serialization-spec-parser/index.js +++ b/packages/block-serialization-spec-parser/index.js @@ -1,1624 +1,2 @@ -/* - * Generated by PEG.js 0.10.0. - * - * http://pegjs.org/ - */ -(function(root, factory) { - if (typeof define === "function" && define.amd) { - define([], factory); - } else if (typeof module === "object" && module.exports) { - module.exports = factory(); - } -})(this, function() { - "use strict"; - - function peg$subclass(child, parent) { - function ctor() { this.constructor = child; } - ctor.prototype = parent.prototype; - child.prototype = new ctor(); - } - - function peg$SyntaxError(message, expected, found, location) { - this.message = message; - this.expected = expected; - this.found = found; - this.location = location; - this.name = "SyntaxError"; - - if (typeof Error.captureStackTrace === "function") { - Error.captureStackTrace(this, peg$SyntaxError); - } - } - - peg$subclass(peg$SyntaxError, Error); - - peg$SyntaxError.buildMessage = function(expected, found) { - var DESCRIBE_EXPECTATION_FNS = { - literal: function(expectation) { - return "\"" + literalEscape(expectation.text) + "\""; - }, - - "class": function(expectation) { - var escapedParts = "", - i; - - for (i = 0; i < expectation.parts.length; i++) { - escapedParts += expectation.parts[i] instanceof Array - ? classEscape(expectation.parts[i][0]) + "-" + classEscape(expectation.parts[i][1]) - : classEscape(expectation.parts[i]); - } - - return "[" + (expectation.inverted ? "^" : "") + escapedParts + "]"; - }, - - any: function(expectation) { - return "any character"; - }, - - end: function(expectation) { - return "end of input"; - }, - - other: function(expectation) { - return expectation.description; - } - }; - - function hex(ch) { - return ch.charCodeAt(0).toString(16).toUpperCase(); - } - - function literalEscape(s) { - return s - .replace(/\\/g, '\\\\') - .replace(/"/g, '\\"') - .replace(/\0/g, '\\0') - .replace(/\t/g, '\\t') - .replace(/\n/g, '\\n') - .replace(/\r/g, '\\r') - .replace(/[\x00-\x0F]/g, function(ch) { return '\\x0' + hex(ch); }) - .replace(/[\x10-\x1F\x7F-\x9F]/g, function(ch) { return '\\x' + hex(ch); }); - } - - function classEscape(s) { - return s - .replace(/\\/g, '\\\\') - .replace(/\]/g, '\\]') - .replace(/\^/g, '\\^') - .replace(/-/g, '\\-') - .replace(/\0/g, '\\0') - .replace(/\t/g, '\\t') - .replace(/\n/g, '\\n') - .replace(/\r/g, '\\r') - .replace(/[\x00-\x0F]/g, function(ch) { return '\\x0' + hex(ch); }) - .replace(/[\x10-\x1F\x7F-\x9F]/g, function(ch) { return '\\x' + hex(ch); }); - } - - function describeExpectation(expectation) { - return DESCRIBE_EXPECTATION_FNS[expectation.type](expectation); - } - - function describeExpected(expected) { - var descriptions = new Array(expected.length), - i, j; - - for (i = 0; i < expected.length; i++) { - descriptions[i] = describeExpectation(expected[i]); - } - - descriptions.sort(); - - if (descriptions.length > 0) { - for (i = 1, j = 1; i < descriptions.length; i++) { - if (descriptions[i - 1] !== descriptions[i]) { - descriptions[j] = descriptions[i]; - j++; - } - } - descriptions.length = j; - } - - switch (descriptions.length) { - case 1: - return descriptions[0]; - - case 2: - return descriptions[0] + " or " + descriptions[1]; - - default: - return descriptions.slice(0, -1).join(", ") - + ", or " - + descriptions[descriptions.length - 1]; - } - } - - function describeFound(found) { - return found ? "\"" + literalEscape(found) + "\"" : "end of input"; - } - - return "Expected " + describeExpected(expected) + " but " + describeFound(found) + " found."; - }; - - function peg$parse(input, options) { - options = options !== void 0 ? options : {}; - - var peg$FAILED = {}, - - peg$startRuleFunctions = { Block_List: peg$parseBlock_List }, - peg$startRuleFunction = peg$parseBlock_List, - - peg$c0 = peg$anyExpectation(), - peg$c1 = function(pre, b, html) { /** **/ return [ b, html ] }, - peg$c2 = function(pre, bs, post) { /** **/ - return joinBlocks( pre, bs, post ); - }, - peg$c3 = "", - peg$c9 = peg$literalExpectation("/-->", false), - peg$c10 = function(blockName, attrs) { - /** $blockName, - 'attrs' => isset( $attrs ) ? $attrs : array(), - 'innerBlocks' => array(), - 'innerHTML' => '', - ); - ?> **/ - - return { - blockName: blockName, - attrs: attrs || {}, - innerBlocks: [], - innerHTML: '' - }; - }, - peg$c11 = function(s, children, e) { - /** $s['blockName'], - 'attrs' => $s['attrs'], - 'innerBlocks' => $innerBlocks, - 'innerHTML' => implode( '', $innerHTML ), - ); - ?> **/ - - var innerContent = partition( function( a ) { return 'string' === typeof a }, children ); - var innerHTML = innerContent[ 0 ]; - var innerBlocks = innerContent[ 1 ]; - - return { - blockName: s.blockName, - attrs: s.attrs, - innerBlocks: innerBlocks, - innerHTML: innerHTML.join( '' ) - }; - }, - peg$c12 = "-->", - peg$c13 = peg$literalExpectation("-->", false), - peg$c14 = function(blockName, attrs) { - /** $blockName, - 'attrs' => isset( $attrs ) ? $attrs : array(), - ); - ?> **/ - - return { - blockName: blockName, - attrs: attrs || {} - }; - }, - peg$c15 = "/wp:", - peg$c16 = peg$literalExpectation("/wp:", false), - peg$c17 = function(blockName) { - /** $blockName, - ); - ?> **/ - - return { - blockName: blockName - }; - }, - peg$c18 = "/", - peg$c19 = peg$literalExpectation("/", false), - peg$c20 = function(type) { - /** **/ - return 'core/' + type; - }, - peg$c21 = /^[a-z]/, - peg$c22 = peg$classExpectation([["a", "z"]], false, false), - peg$c23 = /^[a-z0-9_\-]/, - peg$c24 = peg$classExpectation([["a", "z"], ["0", "9"], "_", "-"], false, false), - peg$c25 = peg$otherExpectation("JSON-encoded attributes embedded in a block's opening comment"), - peg$c26 = "{", - peg$c27 = peg$literalExpectation("{", false), - peg$c28 = "}", - peg$c29 = peg$literalExpectation("}", false), - peg$c30 = "", - peg$c31 = function(attrs) { - /** **/ - return maybeJSON( attrs ); - }, - peg$c32 = /^[ \t\r\n]/, - peg$c33 = peg$classExpectation([" ", "\t", "\r", "\n"], false, false), - - peg$currPos = 0, - peg$savedPos = 0, - peg$posDetailsCache = [{ line: 1, column: 1 }], - peg$maxFailPos = 0, - peg$maxFailExpected = [], - peg$silentFails = 0, - - peg$result; - - if ("startRule" in options) { - if (!(options.startRule in peg$startRuleFunctions)) { - throw new Error("Can't start parsing from rule \"" + options.startRule + "\"."); - } - - peg$startRuleFunction = peg$startRuleFunctions[options.startRule]; - } - - function text() { - return input.substring(peg$savedPos, peg$currPos); - } - - function location() { - return peg$computeLocation(peg$savedPos, peg$currPos); - } - - function expected(description, location) { - location = location !== void 0 ? location : peg$computeLocation(peg$savedPos, peg$currPos) - - throw peg$buildStructuredError( - [peg$otherExpectation(description)], - input.substring(peg$savedPos, peg$currPos), - location - ); - } - - function error(message, location) { - location = location !== void 0 ? location : peg$computeLocation(peg$savedPos, peg$currPos) - - throw peg$buildSimpleError(message, location); - } - - function peg$literalExpectation(text, ignoreCase) { - return { type: "literal", text: text, ignoreCase: ignoreCase }; - } - - function peg$classExpectation(parts, inverted, ignoreCase) { - return { type: "class", parts: parts, inverted: inverted, ignoreCase: ignoreCase }; - } - - function peg$anyExpectation() { - return { type: "any" }; - } - - function peg$endExpectation() { - return { type: "end" }; - } - - function peg$otherExpectation(description) { - return { type: "other", description: description }; - } - - function peg$computePosDetails(pos) { - var details = peg$posDetailsCache[pos], p; - - if (details) { - return details; - } else { - p = pos - 1; - while (!peg$posDetailsCache[p]) { - p--; - } - - details = peg$posDetailsCache[p]; - details = { - line: details.line, - column: details.column - }; - - while (p < pos) { - if (input.charCodeAt(p) === 10) { - details.line++; - details.column = 1; - } else { - details.column++; - } - - p++; - } - - peg$posDetailsCache[pos] = details; - return details; - } - } - - function peg$computeLocation(startPos, endPos) { - var startPosDetails = peg$computePosDetails(startPos), - endPosDetails = peg$computePosDetails(endPos); - - return { - start: { - offset: startPos, - line: startPosDetails.line, - column: startPosDetails.column - }, - end: { - offset: endPos, - line: endPosDetails.line, - column: endPosDetails.column - } - }; - } - - function peg$fail(expected) { - if (peg$currPos < peg$maxFailPos) { return; } - - if (peg$currPos > peg$maxFailPos) { - peg$maxFailPos = peg$currPos; - peg$maxFailExpected = []; - } - - peg$maxFailExpected.push(expected); - } - - function peg$buildSimpleError(message, location) { - return new peg$SyntaxError(message, null, null, location); - } - - function peg$buildStructuredError(expected, found, location) { - return new peg$SyntaxError( - peg$SyntaxError.buildMessage(expected, found), - expected, - found, - location - ); - } - - function peg$parseBlock_List() { - var s0, s1, s2, s3, s4, s5, s6, s7, s8, s9; - - s0 = peg$currPos; - s1 = peg$currPos; - s2 = []; - s3 = peg$currPos; - s4 = peg$currPos; - peg$silentFails++; - s5 = peg$parseBlock(); - peg$silentFails--; - if (s5 === peg$FAILED) { - s4 = void 0; - } else { - peg$currPos = s4; - s4 = peg$FAILED; - } - if (s4 !== peg$FAILED) { - if (input.length > peg$currPos) { - s5 = input.charAt(peg$currPos); - peg$currPos++; - } else { - s5 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c0); } - } - if (s5 !== peg$FAILED) { - s4 = [s4, s5]; - s3 = s4; - } else { - peg$currPos = s3; - s3 = peg$FAILED; - } - } else { - peg$currPos = s3; - s3 = peg$FAILED; - } - while (s3 !== peg$FAILED) { - s2.push(s3); - s3 = peg$currPos; - s4 = peg$currPos; - peg$silentFails++; - s5 = peg$parseBlock(); - peg$silentFails--; - if (s5 === peg$FAILED) { - s4 = void 0; - } else { - peg$currPos = s4; - s4 = peg$FAILED; - } - if (s4 !== peg$FAILED) { - if (input.length > peg$currPos) { - s5 = input.charAt(peg$currPos); - peg$currPos++; - } else { - s5 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c0); } - } - if (s5 !== peg$FAILED) { - s4 = [s4, s5]; - s3 = s4; - } else { - peg$currPos = s3; - s3 = peg$FAILED; - } - } else { - peg$currPos = s3; - s3 = peg$FAILED; - } - } - if (s2 !== peg$FAILED) { - s1 = input.substring(s1, peg$currPos); - } else { - s1 = s2; - } - if (s1 !== peg$FAILED) { - s2 = []; - s3 = peg$currPos; - s4 = peg$parseBlock(); - if (s4 !== peg$FAILED) { - s5 = peg$currPos; - s6 = []; - s7 = peg$currPos; - s8 = peg$currPos; - peg$silentFails++; - s9 = peg$parseBlock(); - peg$silentFails--; - if (s9 === peg$FAILED) { - s8 = void 0; - } else { - peg$currPos = s8; - s8 = peg$FAILED; - } - if (s8 !== peg$FAILED) { - if (input.length > peg$currPos) { - s9 = input.charAt(peg$currPos); - peg$currPos++; - } else { - s9 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c0); } - } - if (s9 !== peg$FAILED) { - s8 = [s8, s9]; - s7 = s8; - } else { - peg$currPos = s7; - s7 = peg$FAILED; - } - } else { - peg$currPos = s7; - s7 = peg$FAILED; - } - while (s7 !== peg$FAILED) { - s6.push(s7); - s7 = peg$currPos; - s8 = peg$currPos; - peg$silentFails++; - s9 = peg$parseBlock(); - peg$silentFails--; - if (s9 === peg$FAILED) { - s8 = void 0; - } else { - peg$currPos = s8; - s8 = peg$FAILED; - } - if (s8 !== peg$FAILED) { - if (input.length > peg$currPos) { - s9 = input.charAt(peg$currPos); - peg$currPos++; - } else { - s9 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c0); } - } - if (s9 !== peg$FAILED) { - s8 = [s8, s9]; - s7 = s8; - } else { - peg$currPos = s7; - s7 = peg$FAILED; - } - } else { - peg$currPos = s7; - s7 = peg$FAILED; - } - } - if (s6 !== peg$FAILED) { - s5 = input.substring(s5, peg$currPos); - } else { - s5 = s6; - } - if (s5 !== peg$FAILED) { - peg$savedPos = s3; - s4 = peg$c1(s1, s4, s5); - s3 = s4; - } else { - peg$currPos = s3; - s3 = peg$FAILED; - } - } else { - peg$currPos = s3; - s3 = peg$FAILED; - } - while (s3 !== peg$FAILED) { - s2.push(s3); - s3 = peg$currPos; - s4 = peg$parseBlock(); - if (s4 !== peg$FAILED) { - s5 = peg$currPos; - s6 = []; - s7 = peg$currPos; - s8 = peg$currPos; - peg$silentFails++; - s9 = peg$parseBlock(); - peg$silentFails--; - if (s9 === peg$FAILED) { - s8 = void 0; - } else { - peg$currPos = s8; - s8 = peg$FAILED; - } - if (s8 !== peg$FAILED) { - if (input.length > peg$currPos) { - s9 = input.charAt(peg$currPos); - peg$currPos++; - } else { - s9 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c0); } - } - if (s9 !== peg$FAILED) { - s8 = [s8, s9]; - s7 = s8; - } else { - peg$currPos = s7; - s7 = peg$FAILED; - } - } else { - peg$currPos = s7; - s7 = peg$FAILED; - } - while (s7 !== peg$FAILED) { - s6.push(s7); - s7 = peg$currPos; - s8 = peg$currPos; - peg$silentFails++; - s9 = peg$parseBlock(); - peg$silentFails--; - if (s9 === peg$FAILED) { - s8 = void 0; - } else { - peg$currPos = s8; - s8 = peg$FAILED; - } - if (s8 !== peg$FAILED) { - if (input.length > peg$currPos) { - s9 = input.charAt(peg$currPos); - peg$currPos++; - } else { - s9 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c0); } - } - if (s9 !== peg$FAILED) { - s8 = [s8, s9]; - s7 = s8; - } else { - peg$currPos = s7; - s7 = peg$FAILED; - } - } else { - peg$currPos = s7; - s7 = peg$FAILED; - } - } - if (s6 !== peg$FAILED) { - s5 = input.substring(s5, peg$currPos); - } else { - s5 = s6; - } - if (s5 !== peg$FAILED) { - peg$savedPos = s3; - s4 = peg$c1(s1, s4, s5); - s3 = s4; - } else { - peg$currPos = s3; - s3 = peg$FAILED; - } - } else { - peg$currPos = s3; - s3 = peg$FAILED; - } - } - if (s2 !== peg$FAILED) { - s3 = peg$currPos; - s4 = []; - if (input.length > peg$currPos) { - s5 = input.charAt(peg$currPos); - peg$currPos++; - } else { - s5 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c0); } - } - while (s5 !== peg$FAILED) { - s4.push(s5); - if (input.length > peg$currPos) { - s5 = input.charAt(peg$currPos); - peg$currPos++; - } else { - s5 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c0); } - } - } - if (s4 !== peg$FAILED) { - s3 = input.substring(s3, peg$currPos); - } else { - s3 = s4; - } - if (s3 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c2(s1, s2, s3); - s0 = s1; - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - - return s0; - } - - function peg$parseBlock() { - var s0; - - s0 = peg$parseBlock_Void(); - if (s0 === peg$FAILED) { - s0 = peg$parseBlock_Balanced(); - } - - return s0; - } - - function peg$parseBlock_Void() { - var s0, s1, s2, s3, s4, s5, s6, s7, s8; - - s0 = peg$currPos; - if (input.substr(peg$currPos, 4) === peg$c3) { - s1 = peg$c3; - peg$currPos += 4; - } else { - s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c4); } - } - if (s1 !== peg$FAILED) { - s2 = peg$parse__(); - if (s2 !== peg$FAILED) { - if (input.substr(peg$currPos, 3) === peg$c5) { - s3 = peg$c5; - peg$currPos += 3; - } else { - s3 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c6); } - } - if (s3 !== peg$FAILED) { - s4 = peg$parseBlock_Name(); - if (s4 !== peg$FAILED) { - s5 = peg$parse__(); - if (s5 !== peg$FAILED) { - s6 = peg$currPos; - s7 = peg$parseBlock_Attributes(); - if (s7 !== peg$FAILED) { - s8 = peg$parse__(); - if (s8 !== peg$FAILED) { - peg$savedPos = s6; - s7 = peg$c7(s4, s7); - s6 = s7; - } else { - peg$currPos = s6; - s6 = peg$FAILED; - } - } else { - peg$currPos = s6; - s6 = peg$FAILED; - } - if (s6 === peg$FAILED) { - s6 = null; - } - if (s6 !== peg$FAILED) { - if (input.substr(peg$currPos, 4) === peg$c8) { - s7 = peg$c8; - peg$currPos += 4; - } else { - s7 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c9); } - } - if (s7 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c10(s4, s6); - s0 = s1; - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - - return s0; - } - - function peg$parseBlock_Balanced() { - var s0, s1, s2, s3, s4, s5, s6; - - s0 = peg$currPos; - s1 = peg$parseBlock_Start(); - if (s1 !== peg$FAILED) { - s2 = []; - s3 = peg$parseBlock(); - if (s3 === peg$FAILED) { - s3 = peg$currPos; - s4 = peg$currPos; - s5 = peg$currPos; - peg$silentFails++; - s6 = peg$parseBlock_End(); - peg$silentFails--; - if (s6 === peg$FAILED) { - s5 = void 0; - } else { - peg$currPos = s5; - s5 = peg$FAILED; - } - if (s5 !== peg$FAILED) { - if (input.length > peg$currPos) { - s6 = input.charAt(peg$currPos); - peg$currPos++; - } else { - s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c0); } - } - if (s6 !== peg$FAILED) { - s5 = [s5, s6]; - s4 = s5; - } else { - peg$currPos = s4; - s4 = peg$FAILED; - } - } else { - peg$currPos = s4; - s4 = peg$FAILED; - } - if (s4 !== peg$FAILED) { - s3 = input.substring(s3, peg$currPos); - } else { - s3 = s4; - } - } - while (s3 !== peg$FAILED) { - s2.push(s3); - s3 = peg$parseBlock(); - if (s3 === peg$FAILED) { - s3 = peg$currPos; - s4 = peg$currPos; - s5 = peg$currPos; - peg$silentFails++; - s6 = peg$parseBlock_End(); - peg$silentFails--; - if (s6 === peg$FAILED) { - s5 = void 0; - } else { - peg$currPos = s5; - s5 = peg$FAILED; - } - if (s5 !== peg$FAILED) { - if (input.length > peg$currPos) { - s6 = input.charAt(peg$currPos); - peg$currPos++; - } else { - s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c0); } - } - if (s6 !== peg$FAILED) { - s5 = [s5, s6]; - s4 = s5; - } else { - peg$currPos = s4; - s4 = peg$FAILED; - } - } else { - peg$currPos = s4; - s4 = peg$FAILED; - } - if (s4 !== peg$FAILED) { - s3 = input.substring(s3, peg$currPos); - } else { - s3 = s4; - } - } - } - if (s2 !== peg$FAILED) { - s3 = peg$parseBlock_End(); - if (s3 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c11(s1, s2, s3); - s0 = s1; - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - - return s0; - } - - function peg$parseBlock_Start() { - var s0, s1, s2, s3, s4, s5, s6, s7, s8; - - s0 = peg$currPos; - if (input.substr(peg$currPos, 4) === peg$c3) { - s1 = peg$c3; - peg$currPos += 4; - } else { - s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c4); } - } - if (s1 !== peg$FAILED) { - s2 = peg$parse__(); - if (s2 !== peg$FAILED) { - if (input.substr(peg$currPos, 3) === peg$c5) { - s3 = peg$c5; - peg$currPos += 3; - } else { - s3 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c6); } - } - if (s3 !== peg$FAILED) { - s4 = peg$parseBlock_Name(); - if (s4 !== peg$FAILED) { - s5 = peg$parse__(); - if (s5 !== peg$FAILED) { - s6 = peg$currPos; - s7 = peg$parseBlock_Attributes(); - if (s7 !== peg$FAILED) { - s8 = peg$parse__(); - if (s8 !== peg$FAILED) { - peg$savedPos = s6; - s7 = peg$c7(s4, s7); - s6 = s7; - } else { - peg$currPos = s6; - s6 = peg$FAILED; - } - } else { - peg$currPos = s6; - s6 = peg$FAILED; - } - if (s6 === peg$FAILED) { - s6 = null; - } - if (s6 !== peg$FAILED) { - if (input.substr(peg$currPos, 3) === peg$c12) { - s7 = peg$c12; - peg$currPos += 3; - } else { - s7 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c13); } - } - if (s7 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c14(s4, s6); - s0 = s1; - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - - return s0; - } - - function peg$parseBlock_End() { - var s0, s1, s2, s3, s4, s5, s6; - - s0 = peg$currPos; - if (input.substr(peg$currPos, 4) === peg$c3) { - s1 = peg$c3; - peg$currPos += 4; - } else { - s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c4); } - } - if (s1 !== peg$FAILED) { - s2 = peg$parse__(); - if (s2 !== peg$FAILED) { - if (input.substr(peg$currPos, 4) === peg$c15) { - s3 = peg$c15; - peg$currPos += 4; - } else { - s3 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c16); } - } - if (s3 !== peg$FAILED) { - s4 = peg$parseBlock_Name(); - if (s4 !== peg$FAILED) { - s5 = peg$parse__(); - if (s5 !== peg$FAILED) { - if (input.substr(peg$currPos, 3) === peg$c12) { - s6 = peg$c12; - peg$currPos += 3; - } else { - s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c13); } - } - if (s6 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c17(s4); - s0 = s1; - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - - return s0; - } - - function peg$parseBlock_Name() { - var s0; - - s0 = peg$parseNamespaced_Block_Name(); - if (s0 === peg$FAILED) { - s0 = peg$parseCore_Block_Name(); - } - - return s0; - } - - function peg$parseNamespaced_Block_Name() { - var s0, s1, s2, s3, s4; - - s0 = peg$currPos; - s1 = peg$currPos; - s2 = peg$parseBlock_Name_Part(); - if (s2 !== peg$FAILED) { - if (input.charCodeAt(peg$currPos) === 47) { - s3 = peg$c18; - peg$currPos++; - } else { - s3 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c19); } - } - if (s3 !== peg$FAILED) { - s4 = peg$parseBlock_Name_Part(); - if (s4 !== peg$FAILED) { - s2 = [s2, s3, s4]; - s1 = s2; - } else { - peg$currPos = s1; - s1 = peg$FAILED; - } - } else { - peg$currPos = s1; - s1 = peg$FAILED; - } - } else { - peg$currPos = s1; - s1 = peg$FAILED; - } - if (s1 !== peg$FAILED) { - s0 = input.substring(s0, peg$currPos); - } else { - s0 = s1; - } - - return s0; - } - - function peg$parseCore_Block_Name() { - var s0, s1, s2; - - s0 = peg$currPos; - s1 = peg$currPos; - s2 = peg$parseBlock_Name_Part(); - if (s2 !== peg$FAILED) { - s1 = input.substring(s1, peg$currPos); - } else { - s1 = s2; - } - if (s1 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c20(s1); - } - s0 = s1; - - return s0; - } - - function peg$parseBlock_Name_Part() { - var s0, s1, s2, s3, s4; - - s0 = peg$currPos; - s1 = peg$currPos; - if (peg$c21.test(input.charAt(peg$currPos))) { - s2 = input.charAt(peg$currPos); - peg$currPos++; - } else { - s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c22); } - } - if (s2 !== peg$FAILED) { - s3 = []; - if (peg$c23.test(input.charAt(peg$currPos))) { - s4 = input.charAt(peg$currPos); - peg$currPos++; - } else { - s4 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c24); } - } - while (s4 !== peg$FAILED) { - s3.push(s4); - if (peg$c23.test(input.charAt(peg$currPos))) { - s4 = input.charAt(peg$currPos); - peg$currPos++; - } else { - s4 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c24); } - } - } - if (s3 !== peg$FAILED) { - s2 = [s2, s3]; - s1 = s2; - } else { - peg$currPos = s1; - s1 = peg$FAILED; - } - } else { - peg$currPos = s1; - s1 = peg$FAILED; - } - if (s1 !== peg$FAILED) { - s0 = input.substring(s0, peg$currPos); - } else { - s0 = s1; - } - - return s0; - } - - function peg$parseBlock_Attributes() { - var s0, s1, s2, s3, s4, s5, s6, s7, s8, s9, s10, s11, s12; - - peg$silentFails++; - s0 = peg$currPos; - s1 = peg$currPos; - s2 = peg$currPos; - if (input.charCodeAt(peg$currPos) === 123) { - s3 = peg$c26; - peg$currPos++; - } else { - s3 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c27); } - } - if (s3 !== peg$FAILED) { - s4 = []; - s5 = peg$currPos; - s6 = peg$currPos; - peg$silentFails++; - s7 = peg$currPos; - if (input.charCodeAt(peg$currPos) === 125) { - s8 = peg$c28; - peg$currPos++; - } else { - s8 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c29); } - } - if (s8 !== peg$FAILED) { - s9 = peg$parse__(); - if (s9 !== peg$FAILED) { - s10 = peg$c30; - if (s10 !== peg$FAILED) { - if (input.charCodeAt(peg$currPos) === 47) { - s11 = peg$c18; - peg$currPos++; - } else { - s11 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c19); } - } - if (s11 === peg$FAILED) { - s11 = null; - } - if (s11 !== peg$FAILED) { - if (input.substr(peg$currPos, 3) === peg$c12) { - s12 = peg$c12; - peg$currPos += 3; - } else { - s12 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c13); } - } - if (s12 !== peg$FAILED) { - s8 = [s8, s9, s10, s11, s12]; - s7 = s8; - } else { - peg$currPos = s7; - s7 = peg$FAILED; - } - } else { - peg$currPos = s7; - s7 = peg$FAILED; - } - } else { - peg$currPos = s7; - s7 = peg$FAILED; - } - } else { - peg$currPos = s7; - s7 = peg$FAILED; - } - } else { - peg$currPos = s7; - s7 = peg$FAILED; - } - peg$silentFails--; - if (s7 === peg$FAILED) { - s6 = void 0; - } else { - peg$currPos = s6; - s6 = peg$FAILED; - } - if (s6 !== peg$FAILED) { - if (input.length > peg$currPos) { - s7 = input.charAt(peg$currPos); - peg$currPos++; - } else { - s7 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c0); } - } - if (s7 !== peg$FAILED) { - s6 = [s6, s7]; - s5 = s6; - } else { - peg$currPos = s5; - s5 = peg$FAILED; - } - } else { - peg$currPos = s5; - s5 = peg$FAILED; - } - while (s5 !== peg$FAILED) { - s4.push(s5); - s5 = peg$currPos; - s6 = peg$currPos; - peg$silentFails++; - s7 = peg$currPos; - if (input.charCodeAt(peg$currPos) === 125) { - s8 = peg$c28; - peg$currPos++; - } else { - s8 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c29); } - } - if (s8 !== peg$FAILED) { - s9 = peg$parse__(); - if (s9 !== peg$FAILED) { - s10 = peg$c30; - if (s10 !== peg$FAILED) { - if (input.charCodeAt(peg$currPos) === 47) { - s11 = peg$c18; - peg$currPos++; - } else { - s11 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c19); } - } - if (s11 === peg$FAILED) { - s11 = null; - } - if (s11 !== peg$FAILED) { - if (input.substr(peg$currPos, 3) === peg$c12) { - s12 = peg$c12; - peg$currPos += 3; - } else { - s12 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c13); } - } - if (s12 !== peg$FAILED) { - s8 = [s8, s9, s10, s11, s12]; - s7 = s8; - } else { - peg$currPos = s7; - s7 = peg$FAILED; - } - } else { - peg$currPos = s7; - s7 = peg$FAILED; - } - } else { - peg$currPos = s7; - s7 = peg$FAILED; - } - } else { - peg$currPos = s7; - s7 = peg$FAILED; - } - } else { - peg$currPos = s7; - s7 = peg$FAILED; - } - peg$silentFails--; - if (s7 === peg$FAILED) { - s6 = void 0; - } else { - peg$currPos = s6; - s6 = peg$FAILED; - } - if (s6 !== peg$FAILED) { - if (input.length > peg$currPos) { - s7 = input.charAt(peg$currPos); - peg$currPos++; - } else { - s7 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c0); } - } - if (s7 !== peg$FAILED) { - s6 = [s6, s7]; - s5 = s6; - } else { - peg$currPos = s5; - s5 = peg$FAILED; - } - } else { - peg$currPos = s5; - s5 = peg$FAILED; - } - } - if (s4 !== peg$FAILED) { - if (input.charCodeAt(peg$currPos) === 125) { - s5 = peg$c28; - peg$currPos++; - } else { - s5 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c29); } - } - if (s5 !== peg$FAILED) { - s3 = [s3, s4, s5]; - s2 = s3; - } else { - peg$currPos = s2; - s2 = peg$FAILED; - } - } else { - peg$currPos = s2; - s2 = peg$FAILED; - } - } else { - peg$currPos = s2; - s2 = peg$FAILED; - } - if (s2 !== peg$FAILED) { - s1 = input.substring(s1, peg$currPos); - } else { - s1 = s2; - } - if (s1 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c31(s1); - } - s0 = s1; - peg$silentFails--; - if (s0 === peg$FAILED) { - s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c25); } - } - - return s0; - } - - function peg$parse__() { - var s0, s1; - - s0 = []; - if (peg$c32.test(input.charAt(peg$currPos))) { - s1 = input.charAt(peg$currPos); - peg$currPos++; - } else { - s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c33); } - } - if (s1 !== peg$FAILED) { - while (s1 !== peg$FAILED) { - s0.push(s1); - if (peg$c32.test(input.charAt(peg$currPos))) { - s1 = input.charAt(peg$currPos); - peg$currPos++; - } else { - s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c33); } - } - } - } else { - s0 = peg$FAILED; - } - - return s0; - } - - - - /* - * - * _____ _ _ - * / ____| | | | | - * | | __ _ _| |_ ___ _ __ | |__ ___ _ __ __ _ - * | | |_ | | | | __/ _ \ '_ \| '_ \ / _ \ '__/ _` | - * | |__| | |_| | || __/ | | | |_) | __/ | | (_| | - * \_____|\__,_|\__\___|_| |_|_.__/ \___|_| \__, | - * __/ | - * GRAMMAR |___/ - * - * - * Welcome to the grammar file for Gutenberg posts! - * - * Please don't be distracted by the functions at the top - * here - they're just helpers for the grammar below. We - * try to keep them as minimal and simple as possible, - * but the parser generator forces us to declare them at - * the beginning of the file. - * - * What follows is the official specification grammar for - * documents created or edited in Gutenberg. It starts at - * the top-level rule `Block_List` - * - * The grammar is defined by a series of _rules_ and ways - * to return matches on those rules. It's a _PEG_, a - * parsing expression grammar, which simply means that for - * each of our rules we have a set of sub-rules to match - * on and the generated parser will try them in order - * until it finds the first match. - * - * This grammar is a _specification_ (with as little actual - * code as we can get away with) which is used by the - * parser generator to generate the actual _parser_ which - * is used by Gutenberg. We generate two parsers: one in - * JavaScript for use the browser and one in PHP for - * WordPress itself. PEG parser generators are available - * in many languages, though different libraries may require - * some translation of this grammar into their syntax. - * - * For more information: - * @see https://pegjs.org - * @see https://en.wikipedia.org/wiki/Parsing_expression_grammar - * - */ - - /** null, - 'attrs' => array(), - 'innerBlocks' => array(), - 'innerHTML' => $pre - ); - } - - foreach ( $tokens as $token ) { - list( $token, $html ) = $token; - - $blocks[] = $token; - - if ( ! empty( $html ) ) { - $blocks[] = array( - 'blockName' => null, - 'attrs' => array(), - 'innerBlocks' => array(), - 'innerHTML' => $html - ); - } - } - - if ( ! empty( $post ) ) { - $blocks[] = array( - 'blockName' => null, - 'attrs' => array(), - 'innerBlocks' => array(), - 'innerHTML' => $post - ); - } - - return $blocks; - } - } - - ?> **/ - - function freeform( s ) { - return s.length && { - blockName: null, - attrs: {}, - innerBlocks: [], - innerHTML: s, - }; - } - - function joinBlocks( pre, tokens, post ) { - var blocks = [], i, l, html, item, token; - - if ( pre.length ) { - blocks.push( freeform( pre ) ); - } - - for ( i = 0, l = tokens.length; i < l; i++ ) { - item = tokens[ i ]; - token = item[ 0 ]; - html = item[ 1 ]; - - blocks.push( token ); - if ( html.length ) { - blocks.push( freeform( html ) ); - } - } - - if ( post.length ) { - blocks.push( freeform( post ) ); - } - - return blocks; - } - - function maybeJSON( s ) { - try { - return JSON.parse( s ); - } catch (e) { - return null; - } - } - - function partition( predicate, list ) { - var i, l, item; - var truthy = []; - var falsey = []; - - // nod to performance over a simpler reduce - // and clone model we could have taken here - for ( i = 0, l = list.length; i < l; i++ ) { - item = list[ i ]; - - predicate( item ) - ? truthy.push( item ) - : falsey.push( item ) - }; - - return [ truthy, falsey ]; - } - - - - peg$result = peg$startRuleFunction(); - - if (peg$result !== peg$FAILED && peg$currPos === input.length) { - return peg$result; - } else { - if (peg$result !== peg$FAILED && peg$currPos < input.length) { - peg$fail(peg$endExpectation()); - } - - throw peg$buildStructuredError( - peg$maxFailExpected, - peg$maxFailPos < input.length ? input.charAt(peg$maxFailPos) : null, - peg$maxFailPos < input.length - ? peg$computeLocation(peg$maxFailPos, peg$maxFailPos + 1) - : peg$computeLocation(peg$maxFailPos, peg$maxFailPos) - ); - } - } - - return { - SyntaxError: peg$SyntaxError, - parse: peg$parse - }; -}); +export { parse } from './parser'; +export { jsTester, phpTester } from './shared-tests'; diff --git a/packages/block-serialization-spec-parser/package.json b/packages/block-serialization-spec-parser/package.json index 761c46afc828b6..eaa51e43cebee8 100644 --- a/packages/block-serialization-spec-parser/package.json +++ b/packages/block-serialization-spec-parser/package.json @@ -25,6 +25,6 @@ "access": "public" }, "scripts": { - "build": "pegjs --format umd -o ./index.js ./grammar.pegjs" + "build": "pegjs --format umd -o ./parser.js ./grammar.pegjs" } } diff --git a/packages/block-serialization-spec-parser/parser.js b/packages/block-serialization-spec-parser/parser.js new file mode 100644 index 00000000000000..e716efb9e3e8d2 --- /dev/null +++ b/packages/block-serialization-spec-parser/parser.js @@ -0,0 +1,1624 @@ +/* + * Generated by PEG.js 0.10.0. + * + * http://pegjs.org/ + */ +(function(root, factory) { + if (typeof define === "function" && define.amd) { + define([], factory); + } else if (typeof module === "object" && module.exports) { + module.exports = factory(); + } +})(this, function() { + "use strict"; + + function peg$subclass(child, parent) { + function ctor() { this.constructor = child; } + ctor.prototype = parent.prototype; + child.prototype = new ctor(); + } + + function peg$SyntaxError(message, expected, found, location) { + this.message = message; + this.expected = expected; + this.found = found; + this.location = location; + this.name = "SyntaxError"; + + if (typeof Error.captureStackTrace === "function") { + Error.captureStackTrace(this, peg$SyntaxError); + } + } + + peg$subclass(peg$SyntaxError, Error); + + peg$SyntaxError.buildMessage = function(expected, found) { + var DESCRIBE_EXPECTATION_FNS = { + literal: function(expectation) { + return "\"" + literalEscape(expectation.text) + "\""; + }, + + "class": function(expectation) { + var escapedParts = "", + i; + + for (i = 0; i < expectation.parts.length; i++) { + escapedParts += expectation.parts[i] instanceof Array + ? classEscape(expectation.parts[i][0]) + "-" + classEscape(expectation.parts[i][1]) + : classEscape(expectation.parts[i]); + } + + return "[" + (expectation.inverted ? "^" : "") + escapedParts + "]"; + }, + + any: function(expectation) { + return "any character"; + }, + + end: function(expectation) { + return "end of input"; + }, + + other: function(expectation) { + return expectation.description; + } + }; + + function hex(ch) { + return ch.charCodeAt(0).toString(16).toUpperCase(); + } + + function literalEscape(s) { + return s + .replace(/\\/g, '\\\\') + .replace(/"/g, '\\"') + .replace(/\0/g, '\\0') + .replace(/\t/g, '\\t') + .replace(/\n/g, '\\n') + .replace(/\r/g, '\\r') + .replace(/[\x00-\x0F]/g, function(ch) { return '\\x0' + hex(ch); }) + .replace(/[\x10-\x1F\x7F-\x9F]/g, function(ch) { return '\\x' + hex(ch); }); + } + + function classEscape(s) { + return s + .replace(/\\/g, '\\\\') + .replace(/\]/g, '\\]') + .replace(/\^/g, '\\^') + .replace(/-/g, '\\-') + .replace(/\0/g, '\\0') + .replace(/\t/g, '\\t') + .replace(/\n/g, '\\n') + .replace(/\r/g, '\\r') + .replace(/[\x00-\x0F]/g, function(ch) { return '\\x0' + hex(ch); }) + .replace(/[\x10-\x1F\x7F-\x9F]/g, function(ch) { return '\\x' + hex(ch); }); + } + + function describeExpectation(expectation) { + return DESCRIBE_EXPECTATION_FNS[expectation.type](expectation); + } + + function describeExpected(expected) { + var descriptions = new Array(expected.length), + i, j; + + for (i = 0; i < expected.length; i++) { + descriptions[i] = describeExpectation(expected[i]); + } + + descriptions.sort(); + + if (descriptions.length > 0) { + for (i = 1, j = 1; i < descriptions.length; i++) { + if (descriptions[i - 1] !== descriptions[i]) { + descriptions[j] = descriptions[i]; + j++; + } + } + descriptions.length = j; + } + + switch (descriptions.length) { + case 1: + return descriptions[0]; + + case 2: + return descriptions[0] + " or " + descriptions[1]; + + default: + return descriptions.slice(0, -1).join(", ") + + ", or " + + descriptions[descriptions.length - 1]; + } + } + + function describeFound(found) { + return found ? "\"" + literalEscape(found) + "\"" : "end of input"; + } + + return "Expected " + describeExpected(expected) + " but " + describeFound(found) + " found."; + }; + + function peg$parse(input, options) { + options = options !== void 0 ? options : {}; + + var peg$FAILED = {}, + + peg$startRuleFunctions = { Block_List: peg$parseBlock_List }, + peg$startRuleFunction = peg$parseBlock_List, + + peg$c0 = peg$anyExpectation(), + peg$c1 = function(pre, b, html) { /** **/ return [ b, html ] }, + peg$c2 = function(pre, bs, post) { /** **/ + return joinBlocks( pre, bs, post ); + }, + peg$c3 = "", + peg$c9 = peg$literalExpectation("/-->", false), + peg$c10 = function(blockName, attrs) { + /** $blockName, + 'attrs' => isset( $attrs ) ? $attrs : array(), + 'innerBlocks' => array(), + 'innerHTML' => '', + ); + ?> **/ + + return { + blockName: blockName, + attrs: attrs || {}, + innerBlocks: [], + innerHTML: '' + }; + }, + peg$c11 = function(s, children, e) { + /** $s['blockName'], + 'attrs' => $s['attrs'], + 'innerBlocks' => $innerBlocks, + 'innerHTML' => implode( '', $innerHTML ), + ); + ?> **/ + + var innerContent = partition( function( a ) { return 'string' === typeof a }, children ); + var innerHTML = innerContent[ 0 ]; + var innerBlocks = innerContent[ 1 ]; + + return { + blockName: s.blockName, + attrs: s.attrs, + innerBlocks: innerBlocks, + innerHTML: innerHTML.join( '' ) + }; + }, + peg$c12 = "-->", + peg$c13 = peg$literalExpectation("-->", false), + peg$c14 = function(blockName, attrs) { + /** $blockName, + 'attrs' => isset( $attrs ) ? $attrs : array(), + ); + ?> **/ + + return { + blockName: blockName, + attrs: attrs || {} + }; + }, + peg$c15 = "/wp:", + peg$c16 = peg$literalExpectation("/wp:", false), + peg$c17 = function(blockName) { + /** $blockName, + ); + ?> **/ + + return { + blockName: blockName + }; + }, + peg$c18 = "/", + peg$c19 = peg$literalExpectation("/", false), + peg$c20 = function(type) { + /** **/ + return 'core/' + type; + }, + peg$c21 = /^[a-z]/, + peg$c22 = peg$classExpectation([["a", "z"]], false, false), + peg$c23 = /^[a-z0-9_\-]/, + peg$c24 = peg$classExpectation([["a", "z"], ["0", "9"], "_", "-"], false, false), + peg$c25 = peg$otherExpectation("JSON-encoded attributes embedded in a block's opening comment"), + peg$c26 = "{", + peg$c27 = peg$literalExpectation("{", false), + peg$c28 = "}", + peg$c29 = peg$literalExpectation("}", false), + peg$c30 = "", + peg$c31 = function(attrs) { + /** **/ + return maybeJSON( attrs ); + }, + peg$c32 = /^[ \t\r\n]/, + peg$c33 = peg$classExpectation([" ", "\t", "\r", "\n"], false, false), + + peg$currPos = 0, + peg$savedPos = 0, + peg$posDetailsCache = [{ line: 1, column: 1 }], + peg$maxFailPos = 0, + peg$maxFailExpected = [], + peg$silentFails = 0, + + peg$result; + + if ("startRule" in options) { + if (!(options.startRule in peg$startRuleFunctions)) { + throw new Error("Can't start parsing from rule \"" + options.startRule + "\"."); + } + + peg$startRuleFunction = peg$startRuleFunctions[options.startRule]; + } + + function text() { + return input.substring(peg$savedPos, peg$currPos); + } + + function location() { + return peg$computeLocation(peg$savedPos, peg$currPos); + } + + function expected(description, location) { + location = location !== void 0 ? location : peg$computeLocation(peg$savedPos, peg$currPos) + + throw peg$buildStructuredError( + [peg$otherExpectation(description)], + input.substring(peg$savedPos, peg$currPos), + location + ); + } + + function error(message, location) { + location = location !== void 0 ? location : peg$computeLocation(peg$savedPos, peg$currPos) + + throw peg$buildSimpleError(message, location); + } + + function peg$literalExpectation(text, ignoreCase) { + return { type: "literal", text: text, ignoreCase: ignoreCase }; + } + + function peg$classExpectation(parts, inverted, ignoreCase) { + return { type: "class", parts: parts, inverted: inverted, ignoreCase: ignoreCase }; + } + + function peg$anyExpectation() { + return { type: "any" }; + } + + function peg$endExpectation() { + return { type: "end" }; + } + + function peg$otherExpectation(description) { + return { type: "other", description: description }; + } + + function peg$computePosDetails(pos) { + var details = peg$posDetailsCache[pos], p; + + if (details) { + return details; + } else { + p = pos - 1; + while (!peg$posDetailsCache[p]) { + p--; + } + + details = peg$posDetailsCache[p]; + details = { + line: details.line, + column: details.column + }; + + while (p < pos) { + if (input.charCodeAt(p) === 10) { + details.line++; + details.column = 1; + } else { + details.column++; + } + + p++; + } + + peg$posDetailsCache[pos] = details; + return details; + } + } + + function peg$computeLocation(startPos, endPos) { + var startPosDetails = peg$computePosDetails(startPos), + endPosDetails = peg$computePosDetails(endPos); + + return { + start: { + offset: startPos, + line: startPosDetails.line, + column: startPosDetails.column + }, + end: { + offset: endPos, + line: endPosDetails.line, + column: endPosDetails.column + } + }; + } + + function peg$fail(expected) { + if (peg$currPos < peg$maxFailPos) { return; } + + if (peg$currPos > peg$maxFailPos) { + peg$maxFailPos = peg$currPos; + peg$maxFailExpected = []; + } + + peg$maxFailExpected.push(expected); + } + + function peg$buildSimpleError(message, location) { + return new peg$SyntaxError(message, null, null, location); + } + + function peg$buildStructuredError(expected, found, location) { + return new peg$SyntaxError( + peg$SyntaxError.buildMessage(expected, found), + expected, + found, + location + ); + } + + function peg$parseBlock_List() { + var s0, s1, s2, s3, s4, s5, s6, s7, s8, s9; + + s0 = peg$currPos; + s1 = peg$currPos; + s2 = []; + s3 = peg$currPos; + s4 = peg$currPos; + peg$silentFails++; + s5 = peg$parseBlock(); + peg$silentFails--; + if (s5 === peg$FAILED) { + s4 = void 0; + } else { + peg$currPos = s4; + s4 = peg$FAILED; + } + if (s4 !== peg$FAILED) { + if (input.length > peg$currPos) { + s5 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s5 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c0); } + } + if (s5 !== peg$FAILED) { + s4 = [s4, s5]; + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$currPos; + s4 = peg$currPos; + peg$silentFails++; + s5 = peg$parseBlock(); + peg$silentFails--; + if (s5 === peg$FAILED) { + s4 = void 0; + } else { + peg$currPos = s4; + s4 = peg$FAILED; + } + if (s4 !== peg$FAILED) { + if (input.length > peg$currPos) { + s5 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s5 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c0); } + } + if (s5 !== peg$FAILED) { + s4 = [s4, s5]; + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } + if (s2 !== peg$FAILED) { + s1 = input.substring(s1, peg$currPos); + } else { + s1 = s2; + } + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$currPos; + s4 = peg$parseBlock(); + if (s4 !== peg$FAILED) { + s5 = peg$currPos; + s6 = []; + s7 = peg$currPos; + s8 = peg$currPos; + peg$silentFails++; + s9 = peg$parseBlock(); + peg$silentFails--; + if (s9 === peg$FAILED) { + s8 = void 0; + } else { + peg$currPos = s8; + s8 = peg$FAILED; + } + if (s8 !== peg$FAILED) { + if (input.length > peg$currPos) { + s9 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s9 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c0); } + } + if (s9 !== peg$FAILED) { + s8 = [s8, s9]; + s7 = s8; + } else { + peg$currPos = s7; + s7 = peg$FAILED; + } + } else { + peg$currPos = s7; + s7 = peg$FAILED; + } + while (s7 !== peg$FAILED) { + s6.push(s7); + s7 = peg$currPos; + s8 = peg$currPos; + peg$silentFails++; + s9 = peg$parseBlock(); + peg$silentFails--; + if (s9 === peg$FAILED) { + s8 = void 0; + } else { + peg$currPos = s8; + s8 = peg$FAILED; + } + if (s8 !== peg$FAILED) { + if (input.length > peg$currPos) { + s9 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s9 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c0); } + } + if (s9 !== peg$FAILED) { + s8 = [s8, s9]; + s7 = s8; + } else { + peg$currPos = s7; + s7 = peg$FAILED; + } + } else { + peg$currPos = s7; + s7 = peg$FAILED; + } + } + if (s6 !== peg$FAILED) { + s5 = input.substring(s5, peg$currPos); + } else { + s5 = s6; + } + if (s5 !== peg$FAILED) { + peg$savedPos = s3; + s4 = peg$c1(s1, s4, s5); + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$currPos; + s4 = peg$parseBlock(); + if (s4 !== peg$FAILED) { + s5 = peg$currPos; + s6 = []; + s7 = peg$currPos; + s8 = peg$currPos; + peg$silentFails++; + s9 = peg$parseBlock(); + peg$silentFails--; + if (s9 === peg$FAILED) { + s8 = void 0; + } else { + peg$currPos = s8; + s8 = peg$FAILED; + } + if (s8 !== peg$FAILED) { + if (input.length > peg$currPos) { + s9 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s9 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c0); } + } + if (s9 !== peg$FAILED) { + s8 = [s8, s9]; + s7 = s8; + } else { + peg$currPos = s7; + s7 = peg$FAILED; + } + } else { + peg$currPos = s7; + s7 = peg$FAILED; + } + while (s7 !== peg$FAILED) { + s6.push(s7); + s7 = peg$currPos; + s8 = peg$currPos; + peg$silentFails++; + s9 = peg$parseBlock(); + peg$silentFails--; + if (s9 === peg$FAILED) { + s8 = void 0; + } else { + peg$currPos = s8; + s8 = peg$FAILED; + } + if (s8 !== peg$FAILED) { + if (input.length > peg$currPos) { + s9 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s9 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c0); } + } + if (s9 !== peg$FAILED) { + s8 = [s8, s9]; + s7 = s8; + } else { + peg$currPos = s7; + s7 = peg$FAILED; + } + } else { + peg$currPos = s7; + s7 = peg$FAILED; + } + } + if (s6 !== peg$FAILED) { + s5 = input.substring(s5, peg$currPos); + } else { + s5 = s6; + } + if (s5 !== peg$FAILED) { + peg$savedPos = s3; + s4 = peg$c1(s1, s4, s5); + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } + if (s2 !== peg$FAILED) { + s3 = peg$currPos; + s4 = []; + if (input.length > peg$currPos) { + s5 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s5 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c0); } + } + while (s5 !== peg$FAILED) { + s4.push(s5); + if (input.length > peg$currPos) { + s5 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s5 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c0); } + } + } + if (s4 !== peg$FAILED) { + s3 = input.substring(s3, peg$currPos); + } else { + s3 = s4; + } + if (s3 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c2(s1, s2, s3); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseBlock() { + var s0; + + s0 = peg$parseBlock_Void(); + if (s0 === peg$FAILED) { + s0 = peg$parseBlock_Balanced(); + } + + return s0; + } + + function peg$parseBlock_Void() { + var s0, s1, s2, s3, s4, s5, s6, s7, s8; + + s0 = peg$currPos; + if (input.substr(peg$currPos, 4) === peg$c3) { + s1 = peg$c3; + peg$currPos += 4; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c4); } + } + if (s1 !== peg$FAILED) { + s2 = peg$parse__(); + if (s2 !== peg$FAILED) { + if (input.substr(peg$currPos, 3) === peg$c5) { + s3 = peg$c5; + peg$currPos += 3; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c6); } + } + if (s3 !== peg$FAILED) { + s4 = peg$parseBlock_Name(); + if (s4 !== peg$FAILED) { + s5 = peg$parse__(); + if (s5 !== peg$FAILED) { + s6 = peg$currPos; + s7 = peg$parseBlock_Attributes(); + if (s7 !== peg$FAILED) { + s8 = peg$parse__(); + if (s8 !== peg$FAILED) { + peg$savedPos = s6; + s7 = peg$c7(s4, s7); + s6 = s7; + } else { + peg$currPos = s6; + s6 = peg$FAILED; + } + } else { + peg$currPos = s6; + s6 = peg$FAILED; + } + if (s6 === peg$FAILED) { + s6 = null; + } + if (s6 !== peg$FAILED) { + if (input.substr(peg$currPos, 4) === peg$c8) { + s7 = peg$c8; + peg$currPos += 4; + } else { + s7 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c9); } + } + if (s7 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c10(s4, s6); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseBlock_Balanced() { + var s0, s1, s2, s3, s4, s5, s6; + + s0 = peg$currPos; + s1 = peg$parseBlock_Start(); + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$parseBlock(); + if (s3 === peg$FAILED) { + s3 = peg$currPos; + s4 = peg$currPos; + s5 = peg$currPos; + peg$silentFails++; + s6 = peg$parseBlock_End(); + peg$silentFails--; + if (s6 === peg$FAILED) { + s5 = void 0; + } else { + peg$currPos = s5; + s5 = peg$FAILED; + } + if (s5 !== peg$FAILED) { + if (input.length > peg$currPos) { + s6 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s6 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c0); } + } + if (s6 !== peg$FAILED) { + s5 = [s5, s6]; + s4 = s5; + } else { + peg$currPos = s4; + s4 = peg$FAILED; + } + } else { + peg$currPos = s4; + s4 = peg$FAILED; + } + if (s4 !== peg$FAILED) { + s3 = input.substring(s3, peg$currPos); + } else { + s3 = s4; + } + } + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$parseBlock(); + if (s3 === peg$FAILED) { + s3 = peg$currPos; + s4 = peg$currPos; + s5 = peg$currPos; + peg$silentFails++; + s6 = peg$parseBlock_End(); + peg$silentFails--; + if (s6 === peg$FAILED) { + s5 = void 0; + } else { + peg$currPos = s5; + s5 = peg$FAILED; + } + if (s5 !== peg$FAILED) { + if (input.length > peg$currPos) { + s6 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s6 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c0); } + } + if (s6 !== peg$FAILED) { + s5 = [s5, s6]; + s4 = s5; + } else { + peg$currPos = s4; + s4 = peg$FAILED; + } + } else { + peg$currPos = s4; + s4 = peg$FAILED; + } + if (s4 !== peg$FAILED) { + s3 = input.substring(s3, peg$currPos); + } else { + s3 = s4; + } + } + } + if (s2 !== peg$FAILED) { + s3 = peg$parseBlock_End(); + if (s3 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c11(s1, s2, s3); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseBlock_Start() { + var s0, s1, s2, s3, s4, s5, s6, s7, s8; + + s0 = peg$currPos; + if (input.substr(peg$currPos, 4) === peg$c3) { + s1 = peg$c3; + peg$currPos += 4; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c4); } + } + if (s1 !== peg$FAILED) { + s2 = peg$parse__(); + if (s2 !== peg$FAILED) { + if (input.substr(peg$currPos, 3) === peg$c5) { + s3 = peg$c5; + peg$currPos += 3; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c6); } + } + if (s3 !== peg$FAILED) { + s4 = peg$parseBlock_Name(); + if (s4 !== peg$FAILED) { + s5 = peg$parse__(); + if (s5 !== peg$FAILED) { + s6 = peg$currPos; + s7 = peg$parseBlock_Attributes(); + if (s7 !== peg$FAILED) { + s8 = peg$parse__(); + if (s8 !== peg$FAILED) { + peg$savedPos = s6; + s7 = peg$c7(s4, s7); + s6 = s7; + } else { + peg$currPos = s6; + s6 = peg$FAILED; + } + } else { + peg$currPos = s6; + s6 = peg$FAILED; + } + if (s6 === peg$FAILED) { + s6 = null; + } + if (s6 !== peg$FAILED) { + if (input.substr(peg$currPos, 3) === peg$c12) { + s7 = peg$c12; + peg$currPos += 3; + } else { + s7 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c13); } + } + if (s7 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c14(s4, s6); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseBlock_End() { + var s0, s1, s2, s3, s4, s5, s6; + + s0 = peg$currPos; + if (input.substr(peg$currPos, 4) === peg$c3) { + s1 = peg$c3; + peg$currPos += 4; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c4); } + } + if (s1 !== peg$FAILED) { + s2 = peg$parse__(); + if (s2 !== peg$FAILED) { + if (input.substr(peg$currPos, 4) === peg$c15) { + s3 = peg$c15; + peg$currPos += 4; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c16); } + } + if (s3 !== peg$FAILED) { + s4 = peg$parseBlock_Name(); + if (s4 !== peg$FAILED) { + s5 = peg$parse__(); + if (s5 !== peg$FAILED) { + if (input.substr(peg$currPos, 3) === peg$c12) { + s6 = peg$c12; + peg$currPos += 3; + } else { + s6 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c13); } + } + if (s6 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c17(s4); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseBlock_Name() { + var s0; + + s0 = peg$parseNamespaced_Block_Name(); + if (s0 === peg$FAILED) { + s0 = peg$parseCore_Block_Name(); + } + + return s0; + } + + function peg$parseNamespaced_Block_Name() { + var s0, s1, s2, s3, s4; + + s0 = peg$currPos; + s1 = peg$currPos; + s2 = peg$parseBlock_Name_Part(); + if (s2 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 47) { + s3 = peg$c18; + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c19); } + } + if (s3 !== peg$FAILED) { + s4 = peg$parseBlock_Name_Part(); + if (s4 !== peg$FAILED) { + s2 = [s2, s3, s4]; + s1 = s2; + } else { + peg$currPos = s1; + s1 = peg$FAILED; + } + } else { + peg$currPos = s1; + s1 = peg$FAILED; + } + } else { + peg$currPos = s1; + s1 = peg$FAILED; + } + if (s1 !== peg$FAILED) { + s0 = input.substring(s0, peg$currPos); + } else { + s0 = s1; + } + + return s0; + } + + function peg$parseCore_Block_Name() { + var s0, s1, s2; + + s0 = peg$currPos; + s1 = peg$currPos; + s2 = peg$parseBlock_Name_Part(); + if (s2 !== peg$FAILED) { + s1 = input.substring(s1, peg$currPos); + } else { + s1 = s2; + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c20(s1); + } + s0 = s1; + + return s0; + } + + function peg$parseBlock_Name_Part() { + var s0, s1, s2, s3, s4; + + s0 = peg$currPos; + s1 = peg$currPos; + if (peg$c21.test(input.charAt(peg$currPos))) { + s2 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c22); } + } + if (s2 !== peg$FAILED) { + s3 = []; + if (peg$c23.test(input.charAt(peg$currPos))) { + s4 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s4 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c24); } + } + while (s4 !== peg$FAILED) { + s3.push(s4); + if (peg$c23.test(input.charAt(peg$currPos))) { + s4 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s4 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c24); } + } + } + if (s3 !== peg$FAILED) { + s2 = [s2, s3]; + s1 = s2; + } else { + peg$currPos = s1; + s1 = peg$FAILED; + } + } else { + peg$currPos = s1; + s1 = peg$FAILED; + } + if (s1 !== peg$FAILED) { + s0 = input.substring(s0, peg$currPos); + } else { + s0 = s1; + } + + return s0; + } + + function peg$parseBlock_Attributes() { + var s0, s1, s2, s3, s4, s5, s6, s7, s8, s9, s10, s11, s12; + + peg$silentFails++; + s0 = peg$currPos; + s1 = peg$currPos; + s2 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 123) { + s3 = peg$c26; + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c27); } + } + if (s3 !== peg$FAILED) { + s4 = []; + s5 = peg$currPos; + s6 = peg$currPos; + peg$silentFails++; + s7 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 125) { + s8 = peg$c28; + peg$currPos++; + } else { + s8 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c29); } + } + if (s8 !== peg$FAILED) { + s9 = peg$parse__(); + if (s9 !== peg$FAILED) { + s10 = peg$c30; + if (s10 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 47) { + s11 = peg$c18; + peg$currPos++; + } else { + s11 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c19); } + } + if (s11 === peg$FAILED) { + s11 = null; + } + if (s11 !== peg$FAILED) { + if (input.substr(peg$currPos, 3) === peg$c12) { + s12 = peg$c12; + peg$currPos += 3; + } else { + s12 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c13); } + } + if (s12 !== peg$FAILED) { + s8 = [s8, s9, s10, s11, s12]; + s7 = s8; + } else { + peg$currPos = s7; + s7 = peg$FAILED; + } + } else { + peg$currPos = s7; + s7 = peg$FAILED; + } + } else { + peg$currPos = s7; + s7 = peg$FAILED; + } + } else { + peg$currPos = s7; + s7 = peg$FAILED; + } + } else { + peg$currPos = s7; + s7 = peg$FAILED; + } + peg$silentFails--; + if (s7 === peg$FAILED) { + s6 = void 0; + } else { + peg$currPos = s6; + s6 = peg$FAILED; + } + if (s6 !== peg$FAILED) { + if (input.length > peg$currPos) { + s7 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s7 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c0); } + } + if (s7 !== peg$FAILED) { + s6 = [s6, s7]; + s5 = s6; + } else { + peg$currPos = s5; + s5 = peg$FAILED; + } + } else { + peg$currPos = s5; + s5 = peg$FAILED; + } + while (s5 !== peg$FAILED) { + s4.push(s5); + s5 = peg$currPos; + s6 = peg$currPos; + peg$silentFails++; + s7 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 125) { + s8 = peg$c28; + peg$currPos++; + } else { + s8 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c29); } + } + if (s8 !== peg$FAILED) { + s9 = peg$parse__(); + if (s9 !== peg$FAILED) { + s10 = peg$c30; + if (s10 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 47) { + s11 = peg$c18; + peg$currPos++; + } else { + s11 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c19); } + } + if (s11 === peg$FAILED) { + s11 = null; + } + if (s11 !== peg$FAILED) { + if (input.substr(peg$currPos, 3) === peg$c12) { + s12 = peg$c12; + peg$currPos += 3; + } else { + s12 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c13); } + } + if (s12 !== peg$FAILED) { + s8 = [s8, s9, s10, s11, s12]; + s7 = s8; + } else { + peg$currPos = s7; + s7 = peg$FAILED; + } + } else { + peg$currPos = s7; + s7 = peg$FAILED; + } + } else { + peg$currPos = s7; + s7 = peg$FAILED; + } + } else { + peg$currPos = s7; + s7 = peg$FAILED; + } + } else { + peg$currPos = s7; + s7 = peg$FAILED; + } + peg$silentFails--; + if (s7 === peg$FAILED) { + s6 = void 0; + } else { + peg$currPos = s6; + s6 = peg$FAILED; + } + if (s6 !== peg$FAILED) { + if (input.length > peg$currPos) { + s7 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s7 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c0); } + } + if (s7 !== peg$FAILED) { + s6 = [s6, s7]; + s5 = s6; + } else { + peg$currPos = s5; + s5 = peg$FAILED; + } + } else { + peg$currPos = s5; + s5 = peg$FAILED; + } + } + if (s4 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 125) { + s5 = peg$c28; + peg$currPos++; + } else { + s5 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c29); } + } + if (s5 !== peg$FAILED) { + s3 = [s3, s4, s5]; + s2 = s3; + } else { + peg$currPos = s2; + s2 = peg$FAILED; + } + } else { + peg$currPos = s2; + s2 = peg$FAILED; + } + } else { + peg$currPos = s2; + s2 = peg$FAILED; + } + if (s2 !== peg$FAILED) { + s1 = input.substring(s1, peg$currPos); + } else { + s1 = s2; + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c31(s1); + } + s0 = s1; + peg$silentFails--; + if (s0 === peg$FAILED) { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c25); } + } + + return s0; + } + + function peg$parse__() { + var s0, s1; + + s0 = []; + if (peg$c32.test(input.charAt(peg$currPos))) { + s1 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c33); } + } + if (s1 !== peg$FAILED) { + while (s1 !== peg$FAILED) { + s0.push(s1); + if (peg$c32.test(input.charAt(peg$currPos))) { + s1 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c33); } + } + } + } else { + s0 = peg$FAILED; + } + + return s0; + } + + + + /* + * + * _____ _ _ + * / ____| | | | | + * | | __ _ _| |_ ___ _ __ | |__ ___ _ __ __ _ + * | | |_ | | | | __/ _ \ '_ \| '_ \ / _ \ '__/ _` | + * | |__| | |_| | || __/ | | | |_) | __/ | | (_| | + * \_____|\__,_|\__\___|_| |_|_.__/ \___|_| \__, | + * __/ | + * GRAMMAR |___/ + * + * + * Welcome to the grammar file for Gutenberg posts! + * + * Please don't be distracted by the functions at the top + * here - they're just helpers for the grammar below. We + * try to keep them as minimal and simple as possible, + * but the parser generator forces us to declare them at + * the beginning of the file. + * + * What follows is the official specification grammar for + * documents created or edited in Gutenberg. It starts at + * the top-level rule `Block_List` + * + * The grammar is defined by a series of _rules_ and ways + * to return matches on those rules. It's a _PEG_, a + * parsing expression grammar, which simply means that for + * each of our rules we have a set of sub-rules to match + * on and the generated parser will try them in order + * until it finds the first match. + * + * This grammar is a _specification_ (with as little actual + * code as we can get away with) which is used by the + * parser generator to generate the actual _parser_ which + * is used by Gutenberg. We generate two parsers: one in + * JavaScript for use the browser and one in PHP for + * WordPress itself. PEG parser generators are available + * in many languages, though different libraries may require + * some translation of this grammar into their syntax. + * + * For more information: + * @see https://pegjs.org + * @see https://en.wikipedia.org/wiki/Parsing_expression_grammar + * + */ + + /** null, + 'attrs' => array(), + 'innerBlocks' => array(), + 'innerHTML' => $pre + ); + } + + foreach ( $tokens as $token ) { + list( $token, $html ) = $token; + + $blocks[] = $token; + + if ( ! empty( $html ) ) { + $blocks[] = array( + 'blockName' => null, + 'attrs' => array(), + 'innerBlocks' => array(), + 'innerHTML' => $html + ); + } + } + + if ( ! empty( $post ) ) { + $blocks[] = array( + 'blockName' => null, + 'attrs' => array(), + 'innerBlocks' => array(), + 'innerHTML' => $post + ); + } + + return $blocks; + } + } + + ?> **/ + + function freeform( s ) { + return s.length && { + blockName: null, + attrs: {}, + innerBlocks: [], + innerHTML: s, + }; + } + + function joinBlocks( pre, tokens, post ) { + var blocks = [], i, l, html, item, token; + + if ( pre.length ) { + blocks.push( freeform( pre ) ); + } + + for ( i = 0, l = tokens.length; i < l; i++ ) { + item = tokens[ i ]; + token = item[ 0 ]; + html = item[ 1 ]; + + blocks.push( token ); + if ( html.length ) { + blocks.push( freeform( html ) ); + } + } + + if ( post.length ) { + blocks.push( freeform( post ) ); + } + + return blocks; + } + + function maybeJSON( s ) { + try { + return JSON.parse( s ); + } catch (e) { + return null; + } + } + + function partition( predicate, list ) { + var i, l, item; + var truthy = []; + var falsey = []; + + // nod to performance over a simpler reduce + // and clone model we could have taken here + for ( i = 0, l = list.length; i < l; i++ ) { + item = list[ i ]; + + predicate( item ) + ? truthy.push( item ) + : falsey.push( item ) + }; + + return [ truthy, falsey ]; + } + + + + peg$result = peg$startRuleFunction(); + + if (peg$result !== peg$FAILED && peg$currPos === input.length) { + return peg$result; + } else { + if (peg$result !== peg$FAILED && peg$currPos < input.length) { + peg$fail(peg$endExpectation()); + } + + throw peg$buildStructuredError( + peg$maxFailExpected, + peg$maxFailPos < input.length ? input.charAt(peg$maxFailPos) : null, + peg$maxFailPos < input.length + ? peg$computeLocation(peg$maxFailPos, peg$maxFailPos + 1) + : peg$computeLocation(peg$maxFailPos, peg$maxFailPos) + ); + } + } + + return { + SyntaxError: peg$SyntaxError, + parse: peg$parse + }; +}); diff --git a/packages/block-serialization-spec-parser/shared-tests.js b/packages/block-serialization-spec-parser/shared-tests.js new file mode 100644 index 00000000000000..e9b90337061eeb --- /dev/null +++ b/packages/block-serialization-spec-parser/shared-tests.js @@ -0,0 +1,89 @@ +export const jsTester = ( parse ) => () => { + describe( 'basic parsing', () => { + test( 'parse() works properly', () => { + expect( parse( '' ) ).toMatchSnapshot(); + } ); + } ); + + describe( 'generic tests', () => { + test( 'parse() accepts inputs with multiple Reusable blocks', () => { + expect( parse( '' ) ).toEqual( [ + expect.objectContaining( { + blockName: 'core/block', + attrs: { ref: 313 }, + } ), + expect.objectContaining( { + blockName: 'core/block', + attrs: { ref: 482 }, + } ), + ] ); + } ); + + test( 'treats void blocks and empty blocks identically', () => { + expect( parse( + '' + ) ).toEqual( parse( + '' + ) ); + + expect( parse( + '' + ) ).toEqual( parse( + '' + ) ); + } ); + + test( 'should grab HTML soup before block openers', () => { + [ + '

    Break me

    ', + '

    Break me

    ', + ].forEach( ( input ) => expect( parse( input ) ).toEqual( [ + expect.objectContaining( { innerHTML: '

    Break me

    ' } ), + expect.objectContaining( { blockName: 'core/block', innerHTML: '' } ), + ] ) ); + } ); + + test( 'should grab HTML soup before inner block openers', () => [ + '

    Break me

    ', + '

    Break me

    ', + ].forEach( ( input ) => expect( parse( input ) ).toEqual( [ + expect.objectContaining( { + innerBlocks: [ expect.objectContaining( { blockName: 'core/block', innerHTML: '' } ) ], + innerHTML: '

    Break me

    ', + } ), + ] ) ) ); + + test( 'should grab HTML soup after blocks', () => [ + '

    Break me

    ', + '

    Break me

    ', + ].forEach( ( input ) => expect( parse( input ) ).toEqual( [ + expect.objectContaining( { blockName: 'core/block', innerHTML: '' } ), + expect.objectContaining( { innerHTML: '

    Break me

    ' } ), + ] ) ) ); + } ); +}; + +const hasPHP = 'test' === process.env.NODE_ENV ? ( () => { + const process = require( 'child_process' ).spawnSync( 'php', [ '-r', 'echo 1;' ], { encoding: 'utf8' } ); + + return process.status === 0 && process.stdout === '1'; +} )() : false; + +// skipping if `php` isn't available to us, such as in local dev without it +// skipping preserves snapshots while commenting out or simply +// not injecting the tests prompts `jest` to remove "obsolete snapshots" +// eslint-disable-next-line jest/no-disabled-tests +const makeTest = hasPHP ? ( ...args ) => describe( ...args ) : ( ...args ) => describe.skip( ...args ); + +export const phpTester = ( name, filename ) => makeTest( + name, + 'test' === process.env.NODE_ENV ? jsTester( ( doc ) => JSON.parse( require( 'child_process' ).spawnSync( + 'php', + [ '-f', filename ], + { + input: doc, + encoding: 'utf8', + timeout: 30 * 1000, // abort after 30 seconds, that's too long anyway + } + ).stdout ) ) : () => {} +); diff --git a/packages/block-serialization-spec-parser/test/__snapshots__/index.js.snap b/packages/block-serialization-spec-parser/test/__snapshots__/index.js.snap index 0a3dbe3973d649..1a014545d98744 100644 --- a/packages/block-serialization-spec-parser/test/__snapshots__/index.js.snap +++ b/packages/block-serialization-spec-parser/test/__snapshots__/index.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`block-serialization-spec-parser parse() works properly 1`] = ` +exports[`block-serialization-spec-parser-js basic parsing parse() works properly 1`] = ` Array [ Object { "attrs": Object {}, @@ -10,3 +10,14 @@ Array [ }, ] `; + +exports[`block-serialization-spec-parser-php basic parsing parse() works properly 1`] = ` +Array [ + Object { + "attrs": Array [], + "blockName": "core/more", + "innerBlocks": Array [], + "innerHTML": "", + }, +] +`; diff --git a/packages/block-serialization-spec-parser/test/index.js b/packages/block-serialization-spec-parser/test/index.js index 98cca5053ef209..7bdbe9f053f169 100644 --- a/packages/block-serialization-spec-parser/test/index.js +++ b/packages/block-serialization-spec-parser/test/index.js @@ -1,14 +1,13 @@ +/** + * External dependencies + */ +import path from 'path'; + /** * Internal dependencies */ -import { parse } from '../'; +import { jsTester, phpTester, parse } from '../'; -describe( 'block-serialization-spec-parser', () => { - test( 'parse() works properly', () => { - const result = parse( - '' - ); +describe( 'block-serialization-spec-parser-js', jsTester( parse ) ); - expect( result ).toMatchSnapshot(); - } ); -} ); +phpTester( 'block-serialization-spec-parser-php', path.join( __dirname, 'test-parser.php' ) ); diff --git a/packages/block-serialization-spec-parser/test/test-parser.php b/packages/block-serialization-spec-parser/test/test-parser.php new file mode 100644 index 00000000000000..2300facd5857cd --- /dev/null +++ b/packages/block-serialization-spec-parser/test/test-parser.php @@ -0,0 +1,7 @@ +parse( file_get_contents( 'php://stdin' ) ) ); From 2388f1742338370877a682c00e5b6e6ace9dd3ca Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Thu, 1 Nov 2018 10:19:59 +0100 Subject: [PATCH 47/98] Remove code coverage setup (#11198) --- .eslintignore | 1 - .gitignore | 1 - README.md | 1 - codecov.yml | 9 -------- docs/reference/testing-overview.md | 5 ---- jsconfig.json | 1 - package-lock.json | 23 ------------------- package.json | 6 ++--- packages/jest-preset-default/CHANGELOG.md | 6 +++++ packages/jest-preset-default/README.md | 2 -- packages/jest-preset-default/jest-preset.json | 7 ------ test/unit/jest.config.json | 8 ------- 12 files changed, 8 insertions(+), 62 deletions(-) delete mode 100644 codecov.yml diff --git a/.eslintignore b/.eslintignore index 8ed0cd62953add..cfc995e9e9aef4 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,6 +1,5 @@ build build-module -coverage node_modules test/e2e/test-plugins vendor diff --git a/.gitignore b/.gitignore index 5da44088f5fa31..5ba04948b2faf7 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,6 @@ build build-module build-style -coverage node_modules gutenberg.zip languages/gutenberg.pot diff --git a/README.md b/README.md index ca4e1fa0dd5287..2f662221785b79 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ # Gutenberg [![Build Status](https://img.shields.io/travis/WordPress/gutenberg/master.svg)](https://travis-ci.org/WordPress/gutenberg) -[![Coverage](https://img.shields.io/codecov/c/github/WordPress/gutenberg/master.svg)](https://codecov.io/gh/WordPress/gutenberg) [![lerna](https://img.shields.io/badge/maintained%20with-lerna-cc00ff.svg)](https://lernajs.io/) Printing since 1440. diff --git a/codecov.yml b/codecov.yml deleted file mode 100644 index ad136e69553e1a..00000000000000 --- a/codecov.yml +++ /dev/null @@ -1,9 +0,0 @@ -coverage: - status: - project: - default: - target: auto - threshold: 0.5% - patch: off - -comment: false diff --git a/docs/reference/testing-overview.md b/docs/reference/testing-overview.md index fcfc951cfd90f3..c54ef5a7e6033b 100644 --- a/docs/reference/testing-overview.md +++ b/docs/reference/testing-overview.md @@ -342,11 +342,6 @@ test( 'should contain mars if planets is true', () => { It's tempting to snapshot deep renders, but that makes for huge snapshots. Additionally, deep renders no longer test a single component, but an entire tree. With `shallow`, we snapshot just the components that are directly rendered by the component we want to test. -### Code Coverage - -Code coverage is measured for each PR using the [codecov.io](https://codecov.io/gh/WordPress/gutenberg) tool. -[Code coverage](https://en.wikipedia.org/wiki/Code_coverage) is a way of measuring the amount of code covered by the tests in the test suite of a project. In Gutenberg, it is currently measured for JavaScript code only. - ## End to end Testing If you're using the built-in [local environment](https://github.com/WordPress/gutenberg/blob/master/CONTRIBUTING.md#local-environment), you can run the e2e tests locally using this command: diff --git a/jsconfig.json b/jsconfig.json index f7492f5ce8f350..7e800a9c053b43 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -8,7 +8,6 @@ "exclude": [ "build", "build-module", - "coverage", "node_modules", "test/e2e/test-plugins", "vendor" diff --git a/package-lock.json b/package-lock.json index 0cb741133ff3fc..861bc2d2624774 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2751,12 +2751,6 @@ } } }, - "argv": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/argv/-/argv-0.0.2.tgz", - "integrity": "sha1-7L0W+JSbFXGDcRsb2jNPN4QBhas=", - "dev": true - }, "aria-query": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-0.7.1.tgz", @@ -5263,17 +5257,6 @@ "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, - "codecov": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/codecov/-/codecov-3.0.2.tgz", - "integrity": "sha512-9ljtIROIjPIUmMRqO+XuDITDoV8xRrZmA0jcEq6p2hg2+wY9wGmLfreAZGIL72IzUfdEDZaU8+Vjidg1fBQ8GQ==", - "dev": true, - "requires": { - "argv": "0.0.2", - "request": "^2.81.0", - "urlgrey": "0.4.4" - } - }, "collapse-white-space": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-1.0.4.tgz", @@ -20390,12 +20373,6 @@ "integrity": "sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k=", "dev": true }, - "urlgrey": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/urlgrey/-/urlgrey-0.4.4.tgz", - "integrity": "sha1-iS/pWWCAXoVRnxzUOJ8stMu3ZS8=", - "dev": true - }, "use": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", diff --git a/package.json b/package.json index 9d9d485054b7da..b5ce8b03ad7a00 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,6 @@ "babel-loader": "8.0.0", "chalk": "2.4.1", "check-node-version": "3.1.1", - "codecov": "3.0.2", "concurrently": "3.5.0", "copy-webpack-plugin": "4.5.2", "core-js": "2.5.7", @@ -151,7 +150,7 @@ "check-licenses": "concurrently \"wp-scripts check-licenses --prod --gpl2\" \"wp-scripts check-licenses --dev\"", "precheck-local-changes": "npm run docs:build", "check-local-changes": "( git diff -U0 | xargs -0 node bin/process-git-diff ) || ( echo \"There are local uncommitted changes after one or both of 'npm install' or 'npm run docs:build'!\" && exit 1 );", - "ci": "concurrently \"npm run lint\" \"npm run test-unit:coverage-ci\" \"npm run check-local-changes\"", + "ci": "concurrently \"npm run lint\" \"npm run test-unit:ci\" \"npm run check-local-changes\"", "predev": "npm run check-engines", "dev": "npm run build:packages && concurrently \"cross-env webpack --watch\" \"npm run dev:packages\"", "dev:packages": "node ./bin/packages/watch.js", @@ -180,10 +179,9 @@ "test-e2e:watch": "npm run test-e2e -- --watch", "test-php": "npm run lint-php && npm run test-unit-php", "test-unit": "wp-scripts test-unit-js --config test/unit/jest.config.json", - "test-unit:coverage": "npm run test-unit -- --coverage", - "test-unit:coverage-ci": "npm run test-unit -- --coverage --maxWorkers 1 && codecov", "test-unit:update": "npm run test-unit -- --updateSnapshot", "test-unit:watch": "npm run test-unit -- --watch", + "test-unit:ci": "npm run test-unit -- --ci --runInBand", "test-unit-php": "docker-compose run --rm wordpress_phpunit phpunit", "test-unit-php-multisite": "docker-compose run -e WP_MULTISITE=1 --rm wordpress_phpunit phpunit" }, diff --git a/packages/jest-preset-default/CHANGELOG.md b/packages/jest-preset-default/CHANGELOG.md index fcac1256024078..591e202d86602d 100644 --- a/packages/jest-preset-default/CHANGELOG.md +++ b/packages/jest-preset-default/CHANGELOG.md @@ -1,3 +1,9 @@ +## 3.0.0 (Unreleased) + +### Breaking Change + +- Remove coverage support. + ## 2.0.0 (2018-07-12) ### Breaking Change diff --git a/packages/jest-preset-default/README.md b/packages/jest-preset-default/README.md index 826e11af09d22b..ee32878ae3fe84 100644 --- a/packages/jest-preset-default/README.md +++ b/packages/jest-preset-default/README.md @@ -24,8 +24,6 @@ npm install @wordpress/jest-preset-default --save-dev #### Brief explanations of options included -* `coverageDirectory` - the directory where Jest outputs its coverage files is set to `coverage`. -* `coveragePathIgnorePatterns` - coverage information will be skipped for all folders named `build` and `build-module`. * `moduleNameMapper` - all `css` and `scss` files containing CSS styles will be stubbed out. * `modulePaths` - the root dir of the project is used as a location to search when resolving modules. * `setupFiles` - runs code before each test which sets up global variables required in the testing environment. diff --git a/packages/jest-preset-default/jest-preset.json b/packages/jest-preset-default/jest-preset.json index b2327e5646c8c3..3995497bfb5ba3 100644 --- a/packages/jest-preset-default/jest-preset.json +++ b/packages/jest-preset-default/jest-preset.json @@ -1,11 +1,4 @@ { - "coverageDirectory": "coverage", - "coveragePathIgnorePatterns": [ - "/node_modules/", - "/.*/build/", - "/.*/build-module/", - "/.*/test/" - ], "moduleNameMapper": { "\\.(scss|css)$": "/node_modules/@wordpress/jest-preset-default/scripts/style-mock.js" }, diff --git a/test/unit/jest.config.json b/test/unit/jest.config.json index 8c5a2ef25e92fa..6060917fb7ff21 100644 --- a/test/unit/jest.config.json +++ b/test/unit/jest.config.json @@ -1,13 +1,5 @@ { "rootDir": "../../", - "collectCoverageFrom": [ "packages/**/*.js" ], - "coveragePathIgnorePatterns": [ - "/node_modules/", - "/.*/build/", - "/.*/build-module/", - "/packages/.*/benchmark/", - "/packages/.*/test/" - ], "moduleNameMapper": { "@wordpress\\/(block-serialization-spec-parser|is-shallow-equal)$": "packages/$1", "@wordpress\\/([a-z0-9-]+)$": "packages/$1/src" From a38c8164fc62826ccf43e4289c63efdbca3078ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20Van=C2=A0Dorpe?= Date: Thu, 1 Nov 2018 10:21:51 +0100 Subject: [PATCH 48/98] RichText: fix buggy enter/delete behaviour (#11287) * Fix buggy enter/delete behaviour in RichText * Fix setting selection on merge when value stays the same * Re-add horizontal navigation comment --- .../editor/src/components/rich-text/index.js | 185 ++++++------------ .../src/components/rich-text/tinymce.js | 85 +++++++- test/e2e/specs/block-deletion.test.js | 4 - test/e2e/specs/splitting-merging.test.js | 6 - test/e2e/support/utils.js | 31 --- 5 files changed, 140 insertions(+), 171 deletions(-) diff --git a/packages/editor/src/components/rich-text/index.js b/packages/editor/src/components/rich-text/index.js index 6b5decb63331dd..04837e079e93f5 100644 --- a/packages/editor/src/components/rich-text/index.js +++ b/packages/editor/src/components/rich-text/index.js @@ -6,7 +6,6 @@ import { defer, find, isNil, - noop, isEqual, omit, } from 'lodash'; @@ -22,7 +21,7 @@ import { getScrollContainer, } from '@wordpress/dom'; import { createBlobURL } from '@wordpress/blob'; -import { BACKSPACE, DELETE, ENTER, LEFT, RIGHT, rawShortcut } from '@wordpress/keycodes'; +import { BACKSPACE, DELETE, ENTER, rawShortcut } from '@wordpress/keycodes'; import { withDispatch, withSelect } from '@wordpress/data'; import { rawHandler, children, getBlockTransforms, findTransform } from '@wordpress/blocks'; import { withInstanceId, withSafeTimeout, compose } from '@wordpress/compose'; @@ -41,8 +40,6 @@ import { unstableToDom, getSelectionStart, getSelectionEnd, - charAt, - LINE_SEPARATOR, remove, isCollapsed, } from '@wordpress/rich-text'; @@ -55,7 +52,7 @@ import Autocomplete from '../autocomplete'; import BlockFormatControls from '../block-format-controls'; import FormatEdit from './format-edit'; import FormatToolbar from './format-toolbar'; -import TinyMCE from './tinymce'; +import TinyMCE, { TINYMCE_ZWSP } from './tinymce'; import { pickAriaProps } from './aria'; import { getPatterns } from './patterns'; import { withBlockEditContext } from '../block-edit/context'; @@ -64,17 +61,7 @@ import { withBlockEditContext } from '../block-edit/context'; * Browser dependencies */ -const { Node, getSelection } = window; - -/** - * Zero-width space character used by TinyMCE as a caret landing point for - * inline boundary nodes. - * - * @see tinymce/src/core/main/ts/text/Zwsp.ts - * - * @type {string} - */ -const TINYMCE_ZWSP = '\uFEFF'; +const { getSelection } = window; export class RichText extends Component { constructor( { value, onReplace, multiline } ) { @@ -95,7 +82,6 @@ export class RichText extends Component { this.onChange = this.onChange.bind( this ); this.onNodeChange = this.onNodeChange.bind( this ); this.onDeleteKeyDown = this.onDeleteKeyDown.bind( this ); - this.onHorizontalNavigationKeyDown = this.onHorizontalNavigationKeyDown.bind( this ); this.onKeyDown = this.onKeyDown.bind( this ); this.onKeyUp = this.onKeyUp.bind( this ); this.onPropagateUndo = this.onPropagateUndo.bind( this ); @@ -183,10 +169,7 @@ export class RichText extends Component { editor.on( 'init', this.onInit ); editor.on( 'nodechange', this.onNodeChange ); - editor.on( 'keydown', this.onKeyDown ); - editor.on( 'keyup', this.onKeyUp ); editor.on( 'BeforeExecCommand', this.onPropagateUndo ); - editor.on( 'focus', this.onFocus ); // The change event in TinyMCE fires every time an undo level is added. editor.on( 'change', this.onCreateUndoLevel ); @@ -244,7 +227,7 @@ export class RichText extends Component { } createRecord() { - const range = window.getSelection().getRangeAt( 0 ); + const range = getSelection().getRangeAt( 0 ); return create( { element: this.editableRef, @@ -277,11 +260,11 @@ export class RichText extends Component { } /** - * Handles a paste event from TinyMCE. + * Handles a paste event. * * Saves the pasted data as plain text in `pastedPlainText`. * - * @param {PasteEvent} event The paste event as triggered by TinyMCE. + * @param {PasteEvent} event The paste event. */ onPaste( event ) { const clipboardData = event.clipboardData; @@ -508,7 +491,9 @@ export class RichText extends Component { * * @link https://en.wikipedia.org/wiki/Caret_navigation * - * @param {tinymce.EditorEvent} event Keydown event. + * @param {KeyboardEvent} event Keydown event. + * + * @return {?boolean} True if the event was handled. */ onDeleteKeyDown( event ) { const { onMerge, onRemove } = this.props; @@ -520,7 +505,7 @@ export class RichText extends Component { const isReverse = keyCode === BACKSPACE; // Only process delete if the key press occurs at uncollapsed edge. - if ( ! getSelection().isCollapsed ) { + if ( ! isCollapsed( this.createRecord() ) ) { return; } @@ -548,118 +533,46 @@ export class RichText extends Component { onRemove( ! isReverse ); } - event.preventDefault(); - - // Calling onMerge() or onRemove() will destroy the editor, so it's - // important that we stop other handlers (e.g. ones registered by - // TinyMCE) from also handling this event. - event.stopImmediatePropagation(); + return true; } /** - * Handles a horizontal navigation key down event to handle the case where - * TinyMCE attempts to preventDefault when on the outside edge of an inline - * boundary when arrowing _away_ from the boundary, not within it. Replaces - * the TinyMCE event `preventDefault` behavior with a noop, such that those - * relying on `defaultPrevented` are not misinformed about the arrow event. - * - * If TinyMCE#4476 is resolved, this handling may be removed. - * - * @see https://github.com/tinymce/tinymce/issues/4476 + * Handles a keydown event. * - * @param {tinymce.EditorEvent} event Keydown event. - */ - onHorizontalNavigationKeyDown( event ) { - const { focusNode } = getSelection(); - const { nodeType, nodeValue } = focusNode; - - if ( nodeType !== Node.TEXT_NODE ) { - return; - } - - if ( nodeValue.length !== 1 || nodeValue[ 0 ] !== TINYMCE_ZWSP ) { - return; - } - - const { keyCode } = event; - - // Consider to be moving away from inline boundary based on: - // - // 1. Within a text fragment consisting only of ZWSP. - // 2. If in reverse, there is no previous sibling. If forward, there is - // no next sibling (i.e. end of node). - const isReverse = keyCode === LEFT; - const edgeSibling = isReverse ? 'previousSibling' : 'nextSibling'; - if ( ! focusNode[ edgeSibling ] ) { - // Note: This is not reassigning on the native event, rather the - // "fixed" TinyMCE copy, which proxies its preventDefault to the - // native event. By reassigning here, we're effectively preventing - // the proxied call on the native event, but not otherwise mutating - // the original event object. - event.preventDefault = noop; - } - } - - /** - * Handles a keydown event from TinyMCE. - * - * @param {KeydownEvent} event The keydown event as triggered by TinyMCE. + * @param {KeyboardEvent} event The keydown event. */ onKeyDown( event ) { const { keyCode } = event; - const isHorizontalNavigation = keyCode === LEFT || keyCode === RIGHT; - if ( isHorizontalNavigation ) { - this.onHorizontalNavigationKeyDown( event ); - } - if ( keyCode === DELETE || keyCode === BACKSPACE ) { - if ( this.multilineTag ) { - const value = this.createRecord(); - const start = getSelectionStart( value ); - const end = getSelectionEnd( value ); - - let newValue; - - if ( keyCode === BACKSPACE ) { - if ( charAt( value, start - 1 ) === LINE_SEPARATOR ) { - newValue = remove( - value, - // Only remove the line if the selection is - // collapsed. - isCollapsed( value ) ? start - 1 : start, - end - ); - } - } else if ( charAt( value, end ) === LINE_SEPARATOR ) { - newValue = remove( - value, - start, - // Only remove the line if the selection is collapsed. - isCollapsed( value ) ? end + 1 : end, - ); - } - - if ( newValue ) { - this.onChange( newValue ); + event.preventDefault(); - event.preventDefault(); - // It's important that we stop other handlers (e.g. ones - // registered by TinyMCE) from also handling this event. - event.stopImmediatePropagation(); - } + if ( this.onDeleteKeyDown( event ) ) { + return; } - this.onDeleteKeyDown( event ); - } - - // If we click shift+Enter on inline RichTexts, we avoid creating two contenteditables - // We also split the content and call the onSplit prop if provided. - if ( keyCode === ENTER ) { + const value = this.createRecord(); + const start = getSelectionStart( value ); + const end = getSelectionEnd( value ); + + if ( keyCode === BACKSPACE ) { + this.onChange( remove( + value, + // Only remove the line if the selection is + // collapsed. + isCollapsed( value ) ? start - 1 : start, + end + ) ); + } else { + this.onChange( remove( + value, + start, + // Only remove the line if the selection is collapsed. + isCollapsed( value ) ? end + 1 : end, + ) ); + } + } else if ( keyCode === ENTER ) { event.preventDefault(); - // It's important that we stop other handlers (e.g. ones registered - // by TinyMCE) from also handling this event. - event.stopImmediatePropagation(); const record = this.createRecord(); @@ -706,9 +619,10 @@ export class RichText extends Component { } /** - * Handles TinyMCE key up event. + * Handles a keyup event. * - * @param {number} keyCode The key code that has been pressed on the keyboard. + * @param {number} $1.keyCode The key code that has been pressed on the + * keyboard. */ onKeyUp( { keyCode } ) { // The input event does not fire when the whole field is selected and @@ -825,7 +739,7 @@ export class RichText extends Component { } componentDidUpdate( prevProps ) { - const { tagName, value } = this.props; + const { tagName, value, isSelected } = this.props; if ( tagName === prevProps.tagName && @@ -844,7 +758,7 @@ export class RichText extends Component { const record = this.formatToValue( value ); - if ( this.isActive() ) { + if ( isSelected ) { const prevRecord = this.formatToValue( prevProps.value ); const length = getTextContent( prevRecord ).length; record.start = length; @@ -854,6 +768,18 @@ export class RichText extends Component { this.applyRecord( record ); this.savedContent = value; } + + // If blocks are merged, but the content remains the same, e.g. merging + // an empty paragraph into another, then also set the selection to the + // end. + if ( isSelected && ! prevProps.isSelected && ! this.isActive() ) { + const record = this.formatToValue( value ); + const prevRecord = this.formatToValue( prevProps.value ); + const length = getTextContent( prevRecord ).length; + record.start = length; + record.end = length; + this.applyRecord( record ); + } } formatToValue( value ) { @@ -969,6 +895,9 @@ export class RichText extends Component { key={ key } onPaste={ this.onPaste } onInput={ this.onInput } + onKeyDown={ this.onKeyDown } + onKeyUp={ this.onKeyUp } + onFocus={ this.onFocus } multilineTag={ this.multilineTag } multilineWrapperTags={ this.multilineWrapperTags } setRef={ this.setRef } diff --git a/packages/editor/src/components/rich-text/tinymce.js b/packages/editor/src/components/rich-text/tinymce.js index c589dde327075d..d2a300a8d9cdb2 100644 --- a/packages/editor/src/components/rich-text/tinymce.js +++ b/packages/editor/src/components/rich-text/tinymce.js @@ -2,14 +2,14 @@ * External dependencies */ import tinymce from 'tinymce'; -import { isEqual } from 'lodash'; +import { isEqual, noop } from 'lodash'; import classnames from 'classnames'; /** * WordPress dependencies */ import { Component, createElement } from '@wordpress/element'; -import { BACKSPACE, DELETE } from '@wordpress/keycodes'; +import { BACKSPACE, DELETE, ENTER, LEFT, RIGHT } from '@wordpress/keycodes'; import { toHTMLString } from '@wordpress/rich-text'; import { children } from '@wordpress/blocks'; @@ -18,6 +18,23 @@ import { children } from '@wordpress/blocks'; */ import { diffAriaProps, pickAriaProps } from './aria'; +/** + * Browser dependencies + */ + +const { getSelection } = window; +const { TEXT_NODE } = window.Node; + +/** + * Zero-width space character used by TinyMCE as a caret landing point for + * inline boundary nodes. + * + * @see tinymce/src/core/main/ts/text/Zwsp.ts + * + * @type {string} + */ +export const TINYMCE_ZWSP = '\uFEFF'; + /** * Determines whether we need a fix to provide `input` events for contenteditable. * @@ -102,9 +119,14 @@ export default class TinyMCE extends Component { super(); this.bindEditorNode = this.bindEditorNode.bind( this ); this.onFocus = this.onFocus.bind( this ); + this.onKeyDown = this.onKeyDown.bind( this ); } onFocus() { + if ( this.props.onFocus ) { + this.props.onFocus(); + } + this.initialize(); } @@ -198,6 +220,8 @@ export default class TinyMCE extends Component { editor.dom.setHTML = setHTML; } ); + + editor.on( 'keydown', this.onKeyDown, true ); }, } ); } @@ -223,6 +247,59 @@ export default class TinyMCE extends Component { } } + onKeyDown( event ) { + const { keyCode } = event; + + // Disables TinyMCE behaviour. + if ( keyCode === ENTER || keyCode === BACKSPACE || keyCode === DELETE ) { + event.preventDefault(); + // For some reason this is needed to also prevent the insertion of + // line breaks. + return false; + } + + // Handles a horizontal navigation key down event to handle the case + // where TinyMCE attempts to preventDefault when on the outside edge of + // an inline boundary when arrowing _away_ from the boundary, not within + // it. Replaces the TinyMCE event `preventDefault` behavior with a noop, + // such that those relying on `defaultPrevented` are not misinformed + // about the arrow event. + // + // If TinyMCE#4476 is resolved, this handling may be removed. + // + // @see https://github.com/tinymce/tinymce/issues/4476 + if ( keyCode !== LEFT && keyCode !== RIGHT ) { + return; + } + + const { focusNode } = getSelection(); + const { nodeType, nodeValue } = focusNode; + + if ( nodeType !== TEXT_NODE ) { + return; + } + + if ( nodeValue.length !== 1 || nodeValue[ 0 ] !== TINYMCE_ZWSP ) { + return; + } + + // Consider to be moving away from inline boundary based on: + // + // 1. Within a text fragment consisting only of ZWSP. + // 2. If in reverse, there is no previous sibling. If forward, there is + // no next sibling (i.e. end of node). + const isReverse = event.keyCode === LEFT; + const edgeSibling = isReverse ? 'previousSibling' : 'nextSibling'; + if ( ! focusNode[ edgeSibling ] ) { + // Note: This is not reassigning on the native event, rather the + // "fixed" TinyMCE copy, which proxies its preventDefault to the + // native event. By reassigning here, we're effectively preventing + // the proxied call on the native event, but not otherwise mutating + // the original event object. + event.preventDefault = noop; + } + } + render() { const ariaProps = pickAriaProps( this.props ); const { @@ -235,6 +312,8 @@ export default class TinyMCE extends Component { onInput, multilineTag, multilineWrapperTags, + onKeyDown, + onKeyUp, } = this.props; /* @@ -284,6 +363,8 @@ export default class TinyMCE extends Component { onPaste, onInput, onFocus: this.onFocus, + onKeyDown, + onKeyUp, } ); } } diff --git a/test/e2e/specs/block-deletion.test.js b/test/e2e/specs/block-deletion.test.js index fb6dd2b40abd5b..7da0695f1ab436 100644 --- a/test/e2e/specs/block-deletion.test.js +++ b/test/e2e/specs/block-deletion.test.js @@ -7,7 +7,6 @@ import { newPost, pressWithModifier, ACCESS_MODIFIER_KEYS, - waitForRichTextInitialization, } from '../support/utils'; const addThreeParagraphsToNewPost = async () => { @@ -17,10 +16,8 @@ const addThreeParagraphsToNewPost = async () => { await clickBlockAppender(); await page.keyboard.type( 'First paragraph' ); await page.keyboard.press( 'Enter' ); - await waitForRichTextInitialization(); await page.keyboard.type( 'Second paragraph' ); await page.keyboard.press( 'Enter' ); - await waitForRichTextInitialization(); }; const clickOnBlockSettingsMenuItem = async ( buttonLabel ) => { @@ -99,7 +96,6 @@ describe( 'block deletion -', () => { // Add a third paragraph for this test. await page.keyboard.type( 'Third paragraph' ); await page.keyboard.press( 'Enter' ); - await waitForRichTextInitialization(); // Press the up arrow once to select the third and fourth blocks. await pressWithModifier( 'Shift', 'ArrowUp' ); diff --git a/test/e2e/specs/splitting-merging.test.js b/test/e2e/specs/splitting-merging.test.js index fca2a8820a0135..d059fbf42ea63d 100644 --- a/test/e2e/specs/splitting-merging.test.js +++ b/test/e2e/specs/splitting-merging.test.js @@ -8,7 +8,6 @@ import { pressTimes, pressWithModifier, META_KEY, - waitForRichTextInitialization, } from '../support/utils'; describe( 'splitting and merging blocks', () => { @@ -133,9 +132,7 @@ describe( 'splitting and merging blocks', () => { await pressWithModifier( META_KEY, 'b' ); await page.keyboard.press( 'ArrowRight' ); await page.keyboard.press( 'Enter' ); - await waitForRichTextInitialization(); await page.keyboard.press( 'Enter' ); - await waitForRichTextInitialization(); await page.keyboard.press( 'Backspace' ); @@ -175,11 +172,8 @@ describe( 'splitting and merging blocks', () => { await insertBlock( 'Paragraph' ); await page.keyboard.type( 'First' ); await page.keyboard.press( 'Enter' ); - await waitForRichTextInitialization(); await page.keyboard.press( 'Enter' ); - await waitForRichTextInitialization(); await page.keyboard.press( 'Enter' ); - await waitForRichTextInitialization(); await page.keyboard.type( 'Second' ); await page.keyboard.press( 'ArrowUp' ); await page.keyboard.press( 'ArrowUp' ); diff --git a/test/e2e/support/utils.js b/test/e2e/support/utils.js index 7275da4d1d2fcf..ee7380de6cf198 100644 --- a/test/e2e/support/utils.js +++ b/test/e2e/support/utils.js @@ -86,34 +86,6 @@ async function login() { ] ); } -/** - * Returns a promise which resolves once it's determined that the active DOM - * element is not within a RichText field, or the RichText field's TinyMCE has - * completed initialization. This is an unfortunate workaround to address an - * issue where TinyMCE takes its time to become ready for user input. - * - * TODO: This is a code smell, indicating that "too fast" resulting in breakage - * could be equally problematic for a fast human. It should be explored whether - * all event bindings we assign to TinyMCE to handle could be handled through - * the DOM directly instead. - * - * @return {Promise} Promise resolving once RichText is initialized, or is - * determined to not be a container of the active element. - */ -export async function waitForRichTextInitialization() { - const isInRichText = await page.evaluate( () => { - return !! document.activeElement.closest( '.editor-rich-text__tinymce' ); - } ); - - if ( ! isInRichText ) { - return; - } - - return page.waitForFunction( () => { - return !! document.activeElement.closest( '.mce-content-body' ); - } ); -} - export async function visitAdmin( adminPath, query ) { await goToWPPath( join( 'wp-admin', adminPath ), query ); @@ -238,7 +210,6 @@ export async function ensureSidebarOpened() { */ export async function clickBlockAppender() { await page.click( '.editor-default-block-appender__content' ); - await waitForRichTextInitialization(); } /** @@ -268,7 +239,6 @@ export async function insertBlock( searchTerm, panelName = null ) { await panelButton.click(); } await page.click( `button[aria-label="${ searchTerm }"]` ); - await waitForRichTextInitialization(); } export async function convertBlock( name ) { @@ -276,7 +246,6 @@ export async function convertBlock( name ) { await page.mouse.move( 250, 350, { steps: 10 } ); await page.click( '.editor-block-switcher__toggle' ); await page.click( `.editor-block-types-list__item[aria-label="${ name }"]` ); - await waitForRichTextInitialization(); } /** From 9b4ef60178eb85bd6618c6c1517da6a98c432360 Mon Sep 17 00:00:00 2001 From: Joen Asmussen Date: Thu, 1 Nov 2018 10:23:44 +0100 Subject: [PATCH 49/98] Fix RTL block alignments (#11293) * Fix RTL block alignments When you explicitly pick an alignment in the editor, this alignment should be respected regardless of text direction. "Align left" is always _align left_. This PR fixes that by adding ignore comments to the auto RTL prefixer * Fix issue with toolbar as well. --- packages/block-library/src/button/style.scss | 1 + packages/block-library/src/categories/style.scss | 2 ++ packages/block-library/src/classic/editor.scss | 4 ++++ packages/block-library/src/file/style.scss | 1 + packages/block-library/src/image/style.scss | 4 ++++ packages/block-library/src/latest-posts/style.scss | 2 ++ packages/editor/src/components/block-list/style.scss | 11 +++++++++++ 7 files changed, 25 insertions(+) diff --git a/packages/block-library/src/button/style.scss b/packages/block-library/src/button/style.scss index 26575cfa2fcf26..49ae444c831495 100644 --- a/packages/block-library/src/button/style.scss +++ b/packages/block-library/src/button/style.scss @@ -29,6 +29,7 @@ $blocks-button__line-height: $big-font-size + 6px; } &.alignright { + /*rtl:ignore*/ text-align: right; } } diff --git a/packages/block-library/src/categories/style.scss b/packages/block-library/src/categories/style.scss index cb490a56e167fe..0b5eeafd989ea0 100644 --- a/packages/block-library/src/categories/style.scss +++ b/packages/block-library/src/categories/style.scss @@ -1,8 +1,10 @@ .wp-block-categories { &.alignleft { + /*rtl:ignore*/ margin-right: 2em; } &.alignright { + /*rtl:ignore*/ margin-left: 2em; } } diff --git a/packages/block-library/src/classic/editor.scss b/packages/block-library/src/classic/editor.scss index 1a31c69c7a65f9..bc4e371702a875 100644 --- a/packages/block-library/src/classic/editor.scss +++ b/packages/block-library/src/classic/editor.scss @@ -88,12 +88,16 @@ } .alignright { + /*rtl:ignore*/ float: right; + /*rtl:ignore*/ margin: 0.5em 0 0.5em 1em; } .alignleft { + /*rtl:ignore*/ float: left; + /*rtl:ignore*/ margin: 0.5em 1em 0.5em 0; } diff --git a/packages/block-library/src/file/style.scss b/packages/block-library/src/file/style.scss index 973de64b24ad64..61cf2ad25ec1be 100644 --- a/packages/block-library/src/file/style.scss +++ b/packages/block-library/src/file/style.scss @@ -6,6 +6,7 @@ } &.alignright { + /*rtl:ignore*/ text-align: right; } diff --git a/packages/block-library/src/image/style.scss b/packages/block-library/src/image/style.scss index 9268acdeeb920b..01c3585be4a1fc 100644 --- a/packages/block-library/src/image/style.scss +++ b/packages/block-library/src/image/style.scss @@ -41,12 +41,16 @@ } .alignleft { + /*rtl:ignore*/ float: left; + /*rtl:ignore*/ margin-right: 1em; } .alignright { + /*rtl:ignore*/ float: right; + /*rtl:ignore*/ margin-left: 1em; } diff --git a/packages/block-library/src/latest-posts/style.scss b/packages/block-library/src/latest-posts/style.scss index 2bbfb7384558bb..0877c3d5dee9cc 100644 --- a/packages/block-library/src/latest-posts/style.scss +++ b/packages/block-library/src/latest-posts/style.scss @@ -1,8 +1,10 @@ .wp-block-latest-posts { &.alignleft { + /*rtl:ignore*/ margin-right: 2em; } &.alignright { + /*rtl:ignore*/ margin-left: 2em; } &.is-grid { diff --git a/packages/editor/src/components/block-list/style.scss b/packages/editor/src/components/block-list/style.scss index 67f1aaa00a61d1..c66a2027411757 100644 --- a/packages/editor/src/components/block-list/style.scss +++ b/packages/editor/src/components/block-list/style.scss @@ -814,12 +814,23 @@ // It behaves as relative, in other words, until it reaches an edge and then behaves as fixed. // But by applying a float, we take it out of this flow. The benefit is that we don't need to compensate for margins. // In turn, this allows margins on sibling elements to collapse to parent elements. + // RTL note: this rule does need to be auto-flipped based on direction. + float: left; + } + } + + .editor-block-list__block[data-align="left"] & { + @include break-small() { + // RTL note: this rule should not be auto-flipped based on direction. + /*rtl:ignore*/ float: left; } } .editor-block-list__block[data-align="right"] & { @include break-small() { + // RTL note: this rule should not be auto-flipped based on direction. + /*rtl:ignore*/ float: right; } } From 6808261cd429d2d9ed838123836d723a422153d3 Mon Sep 17 00:00:00 2001 From: Dion Hulse Date: Thu, 1 Nov 2018 19:38:17 +1000 Subject: [PATCH 50/98] Nonce Middleware: Wrap the nonce middleware function into it's own function that isn't regenerated on every API request. (#11347) This allows the nonce to be updated during pageloads. --- packages/api-fetch/src/middlewares/nonce.js | 47 +++++++++++---------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/packages/api-fetch/src/middlewares/nonce.js b/packages/api-fetch/src/middlewares/nonce.js index 5081e00700ad15..706715a55a9e09 100644 --- a/packages/api-fetch/src/middlewares/nonce.js +++ b/packages/api-fetch/src/middlewares/nonce.js @@ -3,8 +3,9 @@ */ import { addAction } from '@wordpress/hooks'; -const createNonceMiddleware = ( nonce ) => ( options, next ) => { +const createNonceMiddleware = ( nonce ) => { let usedNonce = nonce; + /** * This is not ideal but it's fine for now. * @@ -17,31 +18,33 @@ const createNonceMiddleware = ( nonce ) => ( options, next ) => { } } ); - let headers = options.headers || {}; - // If an 'X-WP-Nonce' header (or any case-insensitive variation - // thereof) was specified, no need to add a nonce header. - let addNonceHeader = true; - for ( const headerName in headers ) { - if ( headers.hasOwnProperty( headerName ) ) { - if ( headerName.toLowerCase() === 'x-wp-nonce' ) { - addNonceHeader = false; - break; + return function( options, next ) { + let headers = options.headers || {}; + // If an 'X-WP-Nonce' header (or any case-insensitive variation + // thereof) was specified, no need to add a nonce header. + let addNonceHeader = true; + for ( const headerName in headers ) { + if ( headers.hasOwnProperty( headerName ) ) { + if ( headerName.toLowerCase() === 'x-wp-nonce' ) { + addNonceHeader = false; + break; + } } } - } - if ( addNonceHeader ) { - // Do not mutate the original headers object, if any. - headers = { - ...headers, - 'X-WP-Nonce': usedNonce, - }; - } + if ( addNonceHeader ) { + // Do not mutate the original headers object, if any. + headers = { + ...headers, + 'X-WP-Nonce': usedNonce, + }; + } - return next( { - ...options, - headers, - } ); + return next( { + ...options, + headers, + } ); + }; }; export default createNonceMiddleware; From 52119c332b5569c3b586a495c7bc0c2e257cb480 Mon Sep 17 00:00:00 2001 From: Nicola Heald Date: Thu, 1 Nov 2018 10:47:54 +0000 Subject: [PATCH 51/98] Embed block refactor and tidy (#10958) * Edit using separate components for each state * Change maybe to upgrade * Docs explaining why we need attributes passed in separately * Moved class name calculations into a util function * Tests for class name calculation * Components in separate files, better variable naming for support * Removed unneeded editingUrl state change --- packages/block-library/src/embed/edit.js | 297 ++++-------------- .../block-library/src/embed/embed-controls.js | 49 +++ .../block-library/src/embed/embed-loading.js | 14 + .../src/embed/embed-placeholder.js | 31 ++ .../block-library/src/embed/embed-preview.js | 69 ++++ packages/block-library/src/embed/settings.js | 2 +- .../block-library/src/embed/test/index.js | 20 +- packages/block-library/src/embed/util.js | 130 +++++++- 8 files changed, 363 insertions(+), 249 deletions(-) create mode 100644 packages/block-library/src/embed/embed-controls.js create mode 100644 packages/block-library/src/embed/embed-loading.js create mode 100644 packages/block-library/src/embed/embed-placeholder.js create mode 100644 packages/block-library/src/embed/embed-preview.js diff --git a/packages/block-library/src/embed/edit.js b/packages/block-library/src/embed/edit.js index 84f220d22b8c36..d5ef390bfa34b6 100644 --- a/packages/block-library/src/embed/edit.js +++ b/packages/block-library/src/embed/edit.js @@ -1,32 +1,22 @@ /** * Internal dependencies */ -import { findBlock, isFromWordPress } from './util'; -import { HOSTS_NO_PREVIEWS, ASPECT_RATIOS, DEFAULT_EMBED_BLOCK, WORDPRESS_EMBED_BLOCK } from './constants'; +import { isFromWordPress, createUpgradedEmbedBlock, getClassNames } from './util'; +import EmbedControls from './embed-controls'; +import EmbedLoading from './embed-loading'; +import EmbedPlaceholder from './embed-placeholder'; +import EmbedPreview from './embed-preview'; + /** * External dependencies */ -import { parse } from 'url'; -import { includes, kebabCase, toLower } from 'lodash'; -import classnames from 'classnames/dedupe'; +import { kebabCase, toLower } from 'lodash'; /** * WordPress dependencies */ -import { __, _x, sprintf } from '@wordpress/i18n'; -import { Component, renderToString, Fragment } from '@wordpress/element'; -import { - Button, - Placeholder, - Spinner, - SandBox, - IconButton, - Toolbar, - PanelBody, - ToggleControl, -} from '@wordpress/components'; -import { createBlock } from '@wordpress/blocks'; -import { RichText, BlockControls, BlockIcon, InspectorControls } from '@wordpress/editor'; +import { __, sprintf } from '@wordpress/i18n'; +import { Component, Fragment } from '@wordpress/element'; export function getEmbedEditComponent( title, icon, responsive = true ) { return class extends Component { @@ -34,10 +24,8 @@ export function getEmbedEditComponent( title, icon, responsive = true ) { super( ...arguments ); this.switchBackToURLInput = this.switchBackToURLInput.bind( this ); this.setUrl = this.setUrl.bind( this ); - this.maybeSwitchBlock = this.maybeSwitchBlock.bind( this ); this.getAttributesFromPreview = this.getAttributesFromPreview.bind( this ); this.setAttributesFromPreview = this.setAttributesFromPreview.bind( this ); - this.setAspectRatioClassNames = this.setAspectRatioClassNames.bind( this ); this.getResponsiveHelp = this.getResponsiveHelp.bind( this ); this.toggleResponsive = this.toggleResponsive.bind( this ); this.handleIncomingPreview = this.handleIncomingPreview.bind( this ); @@ -53,8 +41,15 @@ export function getEmbedEditComponent( title, icon, responsive = true ) { } handleIncomingPreview() { + const { allowResponsive } = this.props.attributes; this.setAttributesFromPreview(); - this.maybeSwitchBlock(); + const upgradedBlock = createUpgradedEmbedBlock( + this.props, + this.getAttributesFromPreview( this.props.preview, allowResponsive ) + ); + if ( upgradedBlock ) { + this.props.onReplace( upgradedBlock ); + } } componentDidUpdate( prevProps ) { @@ -63,29 +58,15 @@ export function getEmbedEditComponent( title, icon, responsive = true ) { const switchedPreview = this.props.preview && this.props.attributes.url !== prevProps.attributes.url; const switchedURL = this.props.attributes.url !== prevProps.attributes.url; - if ( ( switchedURL || ( hasPreview && ! hadPreview ) ) && this.maybeSwitchBlock() ) { - // Dont do anything if we are going to switch to a different block, - // and we've just changed the URL, or we've just received a preview. - return; - } - - if ( ( hasPreview && ! hadPreview ) || switchedPreview ) { + if ( ( hasPreview && ! hadPreview ) || switchedPreview || switchedURL ) { if ( this.props.cannotEmbed ) { // Can't embed this URL, and we've just received or switched the preview. - this.setState( { editingURL: true } ); return; } this.handleIncomingPreview(); } } - getPhotoHtml( photo ) { - // 100% width for the preview so it fits nicely into the document, some "thumbnails" are - // acually the full size photo. - const photoPreview =

    {

    ; - return renderToString( photoPreview ); - } - setUrl( event ) { if ( event ) { event.preventDefault(); @@ -96,114 +77,6 @@ export function getEmbedEditComponent( title, icon, responsive = true ) { setAttributes( { url } ); } - /*** - * Switches to a different embed block type, based on the URL - * and the HTML in the preview, if the preview or URL match a different block. - * - * @return {boolean} Whether the block was switched. - */ - maybeSwitchBlock() { - const { preview } = this.props; - const { url } = this.props.attributes; - - if ( ! url ) { - return false; - } - - const matchingBlock = findBlock( url ); - - // WordPress blocks can work on multiple sites, and so don't have patterns, - // so if we're in a WordPress block, assume the user has chosen it for a WordPress URL. - if ( WORDPRESS_EMBED_BLOCK !== this.props.name && DEFAULT_EMBED_BLOCK !== matchingBlock ) { - // At this point, we have discovered a more suitable block for this url, so transform it. - if ( this.props.name !== matchingBlock ) { - this.props.onReplace( createBlock( matchingBlock, { url } ) ); - return true; - } - } - - if ( preview ) { - const { html } = preview; - - // We can't match the URL for WordPress embeds, we have to check the HTML instead. - if ( isFromWordPress( html ) ) { - // If this is not the WordPress embed block, transform it into one. - if ( WORDPRESS_EMBED_BLOCK !== this.props.name ) { - this.props.onReplace( - createBlock( - WORDPRESS_EMBED_BLOCK, - { - url, - // By now we have the preview, but when the new block first renders, it - // won't have had all the attributes set, and so won't get the correct - // type and it won't render correctly. So, we work out the attributes - // here so that the initial render works when we switch to the WordPress - // block. This only affects the WordPress block because it can't be - // rendered in the usual Sandbox (it has a sandbox of its own) and it - // relies on the preview to set the correct render type. - ...this.getAttributesFromPreview( - this.props.preview, this.props.attributes.allowResponsive - ), - } - ) - ); - return true; - } - } - } - - return false; - } - - /** - * Gets the appropriate CSS class names to enforce an aspect ratio when the embed is resized - * if the HTML has an iframe with width and height set. - * - * @param {string} html The preview HTML that possibly contains an iframe with width and height set. - * @param {boolean} allowResponsive If the classes should be added, or removed. - * @return {Object} Object with classnames set for use with `classnames`. - */ - getAspectRatioClassNames( html, allowResponsive = true ) { - const previewDocument = document.implementation.createHTMLDocument( '' ); - previewDocument.body.innerHTML = html; - const iframe = previewDocument.body.querySelector( 'iframe' ); - - // If we have a fixed aspect iframe, and it's a responsive embed block. - if ( responsive && iframe && iframe.height && iframe.width ) { - const aspectRatio = ( iframe.width / iframe.height ).toFixed( 2 ); - // Given the actual aspect ratio, find the widest ratio to support it. - for ( let ratioIndex = 0; ratioIndex < ASPECT_RATIOS.length; ratioIndex++ ) { - const potentialRatio = ASPECT_RATIOS[ ratioIndex ]; - if ( aspectRatio >= potentialRatio.ratio ) { - return { - [ potentialRatio.className ]: allowResponsive, - 'wp-has-aspect-ratio': allowResponsive, - }; - } - } - } - - return this.props.attributes.className; - } - - /** - * Sets the aspect ratio related class names returned by `getAspectRatioClassNames` - * if `allowResponsive` is truthy. - * - * @param {string} html The preview HTML. - */ - setAspectRatioClassNames( html ) { - const { allowResponsive } = this.props.attributes; - if ( ! allowResponsive ) { - return; - } - const className = classnames( - this.props.attributes.className, - this.getAspectRatioClassNames( html ) - ); - this.props.setAttributes( { className } ); - } - /*** * Gets block attributes based on the preview and responsive state. * @@ -229,10 +102,7 @@ export function getEmbedEditComponent( title, icon, responsive = true ) { attributes.providerNameSlug = providerNameSlug; } - attributes.className = classnames( - this.props.attributes.className, - this.getAspectRatioClassNames( html, allowResponsive ) - ); + attributes.className = getClassNames( html, this.props.attributes.className, responsive && allowResponsive ); return attributes; } @@ -257,12 +127,12 @@ export function getEmbedEditComponent( title, icon, responsive = true ) { toggleResponsive() { const { allowResponsive, className } = this.props.attributes; const { html } = this.props.preview; - const responsiveClassNames = this.getAspectRatioClassNames( html, ! allowResponsive ); + const newAllowResponsive = ! allowResponsive; this.props.setAttributes( { - allowResponsive: ! allowResponsive, - className: classnames( className, responsiveClassNames ), + allowResponsive: newAllowResponsive, + className: getClassNames( html, className, responsive && newAllowResponsive ), } ); } @@ -270,42 +140,11 @@ export function getEmbedEditComponent( title, icon, responsive = true ) { render() { const { url, editingURL } = this.state; const { caption, type, allowResponsive } = this.props.attributes; - const { fetching, setAttributes, isSelected, className, preview, cannotEmbed, supportsResponsive } = this.props; - const controls = ( - - - - { preview && ! cannotEmbed && ( - - ) } - - - { supportsResponsive && ( - - - - - - ) } - - ); + const { fetching, setAttributes, isSelected, className, preview, cannotEmbed, themeSupportsResponsive } = this.props; if ( fetching ) { return ( -
    - -

    { __( 'Embedding…' ) }

    -
    + ); } @@ -315,68 +154,40 @@ export function getEmbedEditComponent( title, icon, responsive = true ) { // No preview, or we can't embed the current URL, or we've clicked the edit button. if ( ! preview || cannotEmbed || editingURL ) { return ( - } label={ label } className="wp-block-embed"> -
    - this.setState( { url: event.target.value } ) } /> - - { cannotEmbed &&

    { __( 'Sorry, we could not embed that content.' ) }

    } -
    -
    + this.setState( { url: event.target.value } ) } + /> ); } - const html = 'photo' === type ? this.getPhotoHtml( preview ) : preview.html; - const { scripts } = preview; - const parsedUrl = parse( url ); - const cannotPreview = includes( HOSTS_NO_PREVIEWS, parsedUrl.host.replace( /^www\./, '' ) ); - // translators: %s: host providing embed content e.g: www.youtube.com - const iframeTitle = sprintf( __( 'Embedded content from %s' ), parsedUrl.host ); - const sandboxClassnames = classnames( type, className ); - const embedWrapper = 'wp-embed' === type ? ( -
    - ) : ( -
    - -
    - ); - return ( -
    - { controls } - { ( cannotPreview ) ? ( - } label={ label }> -

    { url }

    -

    { __( 'Previews for this are unavailable in the editor, sorry!' ) }

    - - ) : embedWrapper } - { ( ! RichText.isEmpty( caption ) || isSelected ) && ( - setAttributes( { caption: value } ) } - inlineToolbar - /> - ) } -
    + + + setAttributes( { caption: value } ) } + isSelected={ isSelected } + icon={ icon } + label={ label } + /> + ); } }; diff --git a/packages/block-library/src/embed/embed-controls.js b/packages/block-library/src/embed/embed-controls.js new file mode 100644 index 00000000000000..e16b91ba7552f9 --- /dev/null +++ b/packages/block-library/src/embed/embed-controls.js @@ -0,0 +1,49 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { Fragment } from '@wordpress/element'; +import { IconButton, Toolbar, PanelBody, ToggleControl } from '@wordpress/components'; +import { BlockControls, InspectorControls } from '@wordpress/editor'; + +const EmbedControls = ( props ) => { + const { + blockSupportsResponsive, + showEditButton, + themeSupportsResponsive, + allowResponsive, + getResponsiveHelp, + toggleResponsive, + switchBackToURLInput, + } = props; + return ( + + + + { showEditButton && ( + + ) } + + + { themeSupportsResponsive && blockSupportsResponsive && ( + + + + + + ) } + + ); +}; + +export default EmbedControls; diff --git a/packages/block-library/src/embed/embed-loading.js b/packages/block-library/src/embed/embed-loading.js new file mode 100644 index 00000000000000..f643cdc9c1dc1a --- /dev/null +++ b/packages/block-library/src/embed/embed-loading.js @@ -0,0 +1,14 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { Spinner } from '@wordpress/components'; + +const EmbedLoading = () => ( +
    + +

    { __( 'Embedding…' ) }

    +
    +); + +export default EmbedLoading; diff --git a/packages/block-library/src/embed/embed-placeholder.js b/packages/block-library/src/embed/embed-placeholder.js new file mode 100644 index 00000000000000..1d54279c9027fa --- /dev/null +++ b/packages/block-library/src/embed/embed-placeholder.js @@ -0,0 +1,31 @@ +/** + * WordPress dependencies + */ +import { __, _x } from '@wordpress/i18n'; +import { Button, Placeholder } from '@wordpress/components'; +import { BlockIcon } from '@wordpress/editor'; + +const EmbedPlaceholder = ( props ) => { + const { icon, label, value, onSubmit, onChange, cannotEmbed } = props; + return ( + } label={ label } className="wp-block-embed"> +
    + + + { cannotEmbed &&

    { __( 'Sorry, we could not embed that content.' ) }

    } +
    +
    + ); +}; + +export default EmbedPlaceholder; diff --git a/packages/block-library/src/embed/embed-preview.js b/packages/block-library/src/embed/embed-preview.js new file mode 100644 index 00000000000000..b3da26ce757529 --- /dev/null +++ b/packages/block-library/src/embed/embed-preview.js @@ -0,0 +1,69 @@ +/** + * Internal dependencies + */ +import { HOSTS_NO_PREVIEWS } from './constants'; +import { getPhotoHtml } from './util'; + +/** + * External dependencies + */ +import { parse } from 'url'; +import { includes } from 'lodash'; +import classnames from 'classnames/dedupe'; + +/** + * WordPress dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; +import { Placeholder, SandBox } from '@wordpress/components'; +import { RichText, BlockIcon } from '@wordpress/editor'; + +const EmbedPreview = ( props ) => { + const { preview, url, type, caption, onCaptionChange, isSelected, className, icon, label } = props; + const { scripts } = preview; + + const html = 'photo' === type ? getPhotoHtml( preview ) : preview.html; + const parsedUrl = parse( url ); + const cannotPreview = includes( HOSTS_NO_PREVIEWS, parsedUrl.host.replace( /^www\./, '' ) ); + // translators: %s: host providing embed content e.g: www.youtube.com + const iframeTitle = sprintf( __( 'Embedded content from %s' ), parsedUrl.host ); + const sandboxClassnames = classnames( type, className, 'wp-block-embed__wrapper' ); + + const embedWrapper = 'wp-embed' === type ? ( +
    + ) : ( +
    + +
    + ); + + return ( +
    + { ( cannotPreview ) ? ( + } label={ label }> +

    { url }

    +

    { __( 'Previews for this are unavailable in the editor, sorry!' ) }

    + + ) : embedWrapper } + { ( ! RichText.isEmpty( caption ) || isSelected ) && ( + + ) } +
    + ); +}; + +export default EmbedPreview; diff --git a/packages/block-library/src/embed/settings.js b/packages/block-library/src/embed/settings.js index 3a3de098c0dbfa..a6d68ae80c9b20 100644 --- a/packages/block-library/src/embed/settings.js +++ b/packages/block-library/src/embed/settings.js @@ -76,7 +76,7 @@ export function getEmbedBlockSettings( { title, description, icon, category = 'e return { preview: validPreview ? preview : undefined, fetching, - supportsResponsive: themeSupports[ 'responsive-embeds' ], + themeSupportsResponsive: themeSupports[ 'responsive-embeds' ], cannotEmbed, }; } ) diff --git a/packages/block-library/src/embed/test/index.js b/packages/block-library/src/embed/test/index.js index ff625b01ae1b47..3931ae7b581e18 100644 --- a/packages/block-library/src/embed/test/index.js +++ b/packages/block-library/src/embed/test/index.js @@ -7,7 +7,7 @@ import { render } from 'enzyme'; * Internal dependencies */ import { getEmbedEditComponent } from '../edit'; -import { findBlock } from '../util'; +import { findBlock, getClassNames } from '../util'; describe( 'core/embed', () => { test( 'block edit matches snapshot', () => { @@ -26,4 +26,22 @@ describe( 'core/embed', () => { expect( findBlock( youtubeURL ) ).toEqual( 'core-embed/youtube' ); expect( findBlock( unknownURL ) ).toEqual( 'core/embed' ); } ); + + test( 'getClassNames returns aspect ratio class names for iframes with width and height', () => { + const html = ''; + const expected = 'wp-embed-aspect-16-9 wp-has-aspect-ratio'; + expect( getClassNames( html ) ).toEqual( expected ); + } ); + + test( 'getClassNames does not return aspect ratio class names if we do not allow responsive', () => { + const html = ''; + const expected = ''; + expect( getClassNames( html, '', false ) ).toEqual( expected ); + } ); + + test( 'getClassNames preserves exsiting class names when removing responsive classes', () => { + const html = ''; + const expected = 'lovely'; + expect( getClassNames( html, 'lovely wp-embed-aspect-16-9 wp-has-aspect-ratio', false ) ).toEqual( expected ); + } ); } ); diff --git a/packages/block-library/src/embed/util.js b/packages/block-library/src/embed/util.js index afb32f81ca79a5..58b07b0a627743 100644 --- a/packages/block-library/src/embed/util.js +++ b/packages/block-library/src/embed/util.js @@ -2,18 +2,25 @@ * Internal dependencies */ import { common, others } from './core-embeds'; -import { DEFAULT_EMBED_BLOCK } from './constants'; +import { DEFAULT_EMBED_BLOCK, WORDPRESS_EMBED_BLOCK, ASPECT_RATIOS } from './constants'; /** * External dependencies */ import { includes } from 'lodash'; +import classnames from 'classnames/dedupe'; + +/** + * WordPress dependencies + */ +import { renderToString } from '@wordpress/element'; +import { createBlock } from '@wordpress/blocks'; /** * Returns true if any of the regular expressions match the URL. * - * @param {string} url The URL to test. - * @param {Array} patterns The list of regular expressions to test agains. + * @param {string} url The URL to test. + * @param {Array} patterns The list of regular expressions to test agains. * @return {boolean} True if any of the regular expressions match the URL. */ export const matchesPatterns = ( url, patterns = [] ) => { @@ -26,7 +33,7 @@ export const matchesPatterns = ( url, patterns = [] ) => { * Finds the block name that should be used for the URL, based on the * structure of the URL. * - * @param {string} url The URL to test. + * @param {string} url The URL to test. * @return {string} The name of the block that should be used for this URL, e.g. core-embed/twitter */ export const findBlock = ( url ) => { @@ -41,3 +48,118 @@ export const findBlock = ( url ) => { export const isFromWordPress = ( html ) => { return includes( html, 'class="wp-embedded-content" data-secret' ); }; + +export const getPhotoHtml = ( photo ) => { + // 100% width for the preview so it fits nicely into the document, some "thumbnails" are + // acually the full size photo. + const photoPreview =

    {

    ; + return renderToString( photoPreview ); +}; + +/*** + * Creates a more suitable embed block based on the passed in props + * and attributes generated from an embed block's preview. + * + * We require `attributesFromPreview` to be generated from the latest attributes + * and preview, and because of the way the react lifecycle operates, we can't + * guarantee that the attributes contained in the block's props are the latest + * versions, so we require that these are generated separately. + * See `getAttributesFromPreview` in the generated embed edit component. + * + * @param {Object} props The block's props. + * @param {Object} attributesFromPreview Attributes generated from the block's most up to date preview. + * @return {Object|undefined} A more suitable embed block if one exists. + */ +export const createUpgradedEmbedBlock = ( props, attributesFromPreview ) => { + const { preview, name } = props; + const { url } = props.attributes; + + if ( ! url ) { + return; + } + + const matchingBlock = findBlock( url ); + + // WordPress blocks can work on multiple sites, and so don't have patterns, + // so if we're in a WordPress block, assume the user has chosen it for a WordPress URL. + if ( WORDPRESS_EMBED_BLOCK !== name && DEFAULT_EMBED_BLOCK !== matchingBlock ) { + // At this point, we have discovered a more suitable block for this url, so transform it. + if ( name !== matchingBlock ) { + return createBlock( matchingBlock, { url } ); + } + } + + if ( preview ) { + const { html } = preview; + + // We can't match the URL for WordPress embeds, we have to check the HTML instead. + if ( isFromWordPress( html ) ) { + // If this is not the WordPress embed block, transform it into one. + if ( WORDPRESS_EMBED_BLOCK !== name ) { + return createBlock( + WORDPRESS_EMBED_BLOCK, + { + url, + // By now we have the preview, but when the new block first renders, it + // won't have had all the attributes set, and so won't get the correct + // type and it won't render correctly. So, we pass through the current attributes + // here so that the initial render works when we switch to the WordPress + // block. This only affects the WordPress block because it can't be + // rendered in the usual Sandbox (it has a sandbox of its own) and it + // relies on the preview to set the correct render type. + ...attributesFromPreview, + } + ); + } + } + } +}; + +/** + * Returns class names with any relevant responsive aspect ratio names. + * + * @param {string} html The preview HTML that possibly contains an iframe with width and height set. + * @param {string} existingClassNames Any existing class names. + * @param {boolean} allowResponsive If the responsive class names should be added, or removed. + * @return {string} Deduped class names. + */ +export function getClassNames( html, existingClassNames = '', allowResponsive = true ) { + if ( ! allowResponsive ) { + // Remove all of the aspect ratio related class names. + const aspectRatioClassNames = { + 'wp-has-aspect-ratio': false, + }; + for ( let ratioIndex = 0; ratioIndex < ASPECT_RATIOS.length; ratioIndex++ ) { + const aspectRatioToRemove = ASPECT_RATIOS[ ratioIndex ]; + aspectRatioClassNames[ aspectRatioToRemove.className ] = false; + } + return classnames( + existingClassNames, + aspectRatioClassNames + ); + } + + const previewDocument = document.implementation.createHTMLDocument( '' ); + previewDocument.body.innerHTML = html; + const iframe = previewDocument.body.querySelector( 'iframe' ); + + // If we have a fixed aspect iframe, and it's a responsive embed block. + if ( iframe && iframe.height && iframe.width ) { + const aspectRatio = ( iframe.width / iframe.height ).toFixed( 2 ); + // Given the actual aspect ratio, find the widest ratio to support it. + for ( let ratioIndex = 0; ratioIndex < ASPECT_RATIOS.length; ratioIndex++ ) { + const potentialRatio = ASPECT_RATIOS[ ratioIndex ]; + if ( aspectRatio >= potentialRatio.ratio ) { + return classnames( + existingClassNames, + { + [ potentialRatio.className ]: allowResponsive, + 'wp-has-aspect-ratio': allowResponsive, + } + ); + } + } + } + + return existingClassNames; +} From 563e2926a68b10e098b1387c9af1c35ee2446fb8 Mon Sep 17 00:00:00 2001 From: Pinar Olguc Date: Thu, 1 Nov 2018 14:23:37 +0300 Subject: [PATCH 52/98] Enhance styling of nextpage block using the Hr element (#11354) * Add a line into nextpage block using the Hr element * Fix lint problems * Fix scss lint issue --- packages/block-library/src/nextpage/edit.native.js | 7 +++++-- packages/block-library/src/nextpage/editor.native.scss | 9 +++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/block-library/src/nextpage/edit.native.js b/packages/block-library/src/nextpage/edit.native.js index e486337e1c47b6..03cd3bfe3c7d18 100644 --- a/packages/block-library/src/nextpage/edit.native.js +++ b/packages/block-library/src/nextpage/edit.native.js @@ -1,7 +1,8 @@ /** * External dependencies */ -import { View, Text } from 'react-native'; +import { View } from 'react-native'; +import Hr from 'react-native-hr'; /** * WordPress dependencies @@ -18,7 +19,9 @@ export default function NextPageEdit( { attributes } ) { return ( - { customText } +
    ); } diff --git a/packages/block-library/src/nextpage/editor.native.scss b/packages/block-library/src/nextpage/editor.native.scss index 7101c63e82962a..0e75d4a7595c28 100644 --- a/packages/block-library/src/nextpage/editor.native.scss +++ b/packages/block-library/src/nextpage/editor.native.scss @@ -4,3 +4,12 @@ align-items: center; padding: 4px 4px 4px 4px; } + +.block-library-nextpage__line { + background-color: #555d66; + height: 2; +} + +.block-library-nextpage__text { + text-decoration-style: solid; +} From 5bdaf3b178d78b85418e6fd5bd5af5266d4dd6a6 Mon Sep 17 00:00:00 2001 From: William Earnhardt Date: Thu, 1 Nov 2018 08:35:11 -0400 Subject: [PATCH 53/98] Image Block: Use source_url for media file link (#11254) * Use source_url for media file link URL in image block * Account for external images in media file link --- packages/block-library/src/image/edit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-library/src/image/edit.js b/packages/block-library/src/image/edit.js index 1ff353c1ada4d6..ce52f131075bcc 100644 --- a/packages/block-library/src/image/edit.js +++ b/packages/block-library/src/image/edit.js @@ -168,7 +168,7 @@ class ImageEdit extends Component { if ( value === LINK_DESTINATION_NONE ) { href = undefined; } else if ( value === LINK_DESTINATION_MEDIA ) { - href = this.props.attributes.url; + href = ( this.props.image && this.props.image.source_url ) || this.props.attributes.url; } else if ( value === LINK_DESTINATION_ATTACHMENT ) { href = this.props.image && this.props.image.link; } else { From 20c7215b70d7519adec37ade9421089d50e4010d Mon Sep 17 00:00:00 2001 From: Gary Pendergast Date: Thu, 1 Nov 2018 23:51:56 +1100 Subject: [PATCH 54/98] Remove the Cloudflare warning (#11350) --- lib/compat.php | 155 +-------------------- packages/api-fetch/CHANGELOG.md | 2 + packages/api-fetch/src/index.js | 22 +-- packages/api-fetch/src/test/index.js | 3 - packages/editor/CHANGELOG.md | 2 + packages/editor/src/store/effects/posts.js | 20 --- 6 files changed, 7 insertions(+), 197 deletions(-) diff --git a/lib/compat.php b/lib/compat.php index d4d223c42b7605..ff0938c496828a 100644 --- a/lib/compat.php +++ b/lib/compat.php @@ -134,7 +134,7 @@ function gutenberg_check_if_classic_needs_warning_about_blocks() { return; } - if ( ! has_blocks( $post ) && ! isset( $_REQUEST['cloudflare-error'] ) ) { + if ( ! has_blocks( $post ) ) { return; } @@ -142,11 +142,7 @@ function gutenberg_check_if_classic_needs_warning_about_blocks() { wp_enqueue_script( 'wp-a11y' ); wp_enqueue_script( 'wp-sanitize' ); - if ( isset( $_REQUEST['cloudflare-error'] ) ) { - add_action( 'admin_footer', 'gutenberg_warn_classic_about_cloudflare' ); - } else { - add_action( 'admin_footer', 'gutenberg_warn_classic_about_blocks' ); - } + add_action( 'admin_footer', 'gutenberg_warn_classic_about_blocks' ); } add_action( 'admin_enqueue_scripts', 'gutenberg_check_if_classic_needs_warning_about_blocks' ); @@ -308,150 +304,3 @@ function gutenberg_warn_classic_about_blocks() { - - -
    -
    -
    -
    -

    -

    -

    -
      -
    • -
    • - change the REST API URL, to avoid triggering the WAF rules. Please be aware that this may cause issues with other plugins that use the REST API, and removes any other protection Cloudflare may be offering for the REST API.', 'gutenberg' ), - 'https://github.com/WordPress/gutenberg/issues/2704#issuecomment-410582252' - ); - ?> -
    • -
    -

    - follow this issue for updates. We hope to have this issue rectified soon!', 'gutenberg' ), - 'https://github.com/WordPress/gutenberg/issues/2704' - ); - ?> -

    -
    -

    - -

    -
    -
    - - - = 0 ) { - throw { - code: 'cloudflare_error', - }; - } -} - function apiFetch( options ) { const raw = ( nextOptions ) => { const { url, path, body, data, parse = true, ...remainingOptions } = nextOptions; @@ -82,18 +74,8 @@ function apiFetch( options ) { throw invalidJsonError; } - /* - * Response data is a stream, which will be consumed by the .json() call. - * If we need to re-use this data to send to the Cloudflare error handler, - * we need a clone of the original response, so the stream can be consumed - * in the .text() call, instead. - */ - const responseClone = response.clone(); - return response.json() - .catch( async () => { - const text = await responseClone.text(); - checkCloudflareError( text ); + .catch( () => { throw invalidJsonError; } ) .then( ( error ) => { @@ -102,8 +84,6 @@ function apiFetch( options ) { message: __( 'An unknown error occurred.' ), }; - checkCloudflareError( error ); - throw error || unknownError; } ); } ); diff --git a/packages/api-fetch/src/test/index.js b/packages/api-fetch/src/test/index.js index 220a618f17a3db..a01e0e75781599 100644 --- a/packages/api-fetch/src/test/index.js +++ b/packages/api-fetch/src/test/index.js @@ -32,9 +32,6 @@ describe( 'apiFetch', () => { message: 'Bad Request', } ); }, - clone() { - return null; - }, } ) ); return apiFetch( { path: '/random' } ).catch( ( body ) => { diff --git a/packages/editor/CHANGELOG.md b/packages/editor/CHANGELOG.md index 3b0ed28447c317..aefa03d7f56436 100644 --- a/packages/editor/CHANGELOG.md +++ b/packages/editor/CHANGELOG.md @@ -1,3 +1,5 @@ +## 6.1.1 (Unreleased) + ## 6.1.0 (2018-10-30) ### Deprecations diff --git a/packages/editor/src/store/effects/posts.js b/packages/editor/src/store/effects/posts.js index a7354ad101a585..6d6d23917c2bca 100644 --- a/packages/editor/src/store/effects/posts.js +++ b/packages/editor/src/store/effects/posts.js @@ -9,7 +9,6 @@ import { pick, includes } from 'lodash'; */ import apiFetch from '@wordpress/api-fetch'; import { __ } from '@wordpress/i18n'; -import { addQueryArgs } from '@wordpress/url'; // TODO: Ideally this would be the only dispatch in scope. This requires either // refactoring editor actions to yielded controls, or replacing direct dispatch // on the editor store with action creators (e.g. `REQUEST_POST_UPDATE_START`). @@ -266,25 +265,6 @@ export const requestPostUpdateFailure = ( action ) => { dataDispatch( 'core/notices' ).createErrorNotice( noticeMessage, { id: SAVE_POST_NOTICE_ID, } ); - - if ( error && 'cloudflare_error' === error.code ) { - dataDispatch( 'core/notices' ).createErrorNotice( - __( 'Cloudflare is blocking REST API requests.' ), - { - actions: [ - { - label: __( 'Learn More' ), - url: addQueryArgs( 'post.php', { - post: post.id, - action: 'edit', - 'classic-editor': '', - 'cloudflare-error': '', - } ), - }, - ], - }, - ); - } }; /** From 1acf0626c097e2c2400d7acca6f9f161e4dff744 Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Thu, 1 Nov 2018 23:59:21 +1100 Subject: [PATCH 55/98] Remove _wpGutenbergCodeEditorSettings and wp.codeEditor assets (#11342) Remove _wpGutenbergCodeEditorSettings and the assets required to use wp.codeEditor. Gutenberg no longer uses this as of cc5bf5c. --- lib/client-assets.php | 39 --------------------------------------- 1 file changed, 39 deletions(-) diff --git a/lib/client-assets.php b/lib/client-assets.php index c34e03edc7d5f1..b68c2fa5c99036 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -1239,32 +1239,6 @@ function gutenberg_default_post_format_template( $settings, $post ) { } add_filter( 'block_editor_settings', 'gutenberg_default_post_format_template', 10, 2 ); -/** - * The code editor settings that were last captured by - * gutenberg_capture_code_editor_settings(). - * - * @var array|false - */ -$gutenberg_captured_code_editor_settings = false; - -/** - * When passed to the wp_code_editor_settings filter, this function captures - * the code editor settings given to it and then prevents - * wp_enqueue_code_editor() from enqueuing any assets. - * - * This is a workaround until e.g. code_editor_settings() is added to Core. - * - * @since 2.1.0 - * - * @param array $settings Code editor settings. - * @return false - */ -function gutenberg_capture_code_editor_settings( $settings ) { - global $gutenberg_captured_code_editor_settings; - $gutenberg_captured_code_editor_settings = $settings; - return false; -} - /** * Retrieve a stored autosave that is newer than the post save. * @@ -1509,19 +1483,6 @@ function gutenberg_editor_scripts_and_styles( $hook ) { ); wp_localize_script( 'wp-editor', '_wpMetaBoxUrl', $meta_box_url ); - // Populate default code editor settings by short-circuiting wp_enqueue_code_editor. - global $gutenberg_captured_code_editor_settings; - add_filter( 'wp_code_editor_settings', 'gutenberg_capture_code_editor_settings' ); - wp_enqueue_code_editor( array( 'type' => 'text/html' ) ); - remove_filter( 'wp_code_editor_settings', 'gutenberg_capture_code_editor_settings' ); - wp_add_inline_script( - 'wp-editor', - sprintf( - 'window._wpGutenbergCodeEditorSettings = %s;', - wp_json_encode( $gutenberg_captured_code_editor_settings ) - ) - ); - // Initialize the editor. $gutenberg_theme_support = get_theme_support( 'gutenberg' ); $align_wide = get_theme_support( 'align-wide' ); From b3e0e89287a834e5aca554d484abc3e8d76fe479 Mon Sep 17 00:00:00 2001 From: Jorge Date: Thu, 1 Nov 2018 13:06:50 +0000 Subject: [PATCH 56/98] Fix: make meta+A behaviour of selecting all blocks work on safari and firefox. (#8180) On safari and firefox isEntirelySelected( target ) called right after meta+A event is fired when meta key is not released in the middle returns false but the content gets selected anyway. If the target is editable it is safe to assume TinyMCE will make sure all content gets selected so in this cases we set the value to true without calling isEntirelySelected. --- packages/editor/src/components/writing-flow/index.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/editor/src/components/writing-flow/index.js b/packages/editor/src/components/writing-flow/index.js index b90458368c6614..235e32fb1dc949 100644 --- a/packages/editor/src/components/writing-flow/index.js +++ b/packages/editor/src/components/writing-flow/index.js @@ -254,8 +254,9 @@ class WritingFlow extends Component { event.preventDefault(); } - // Set in case the meta key doesn't get released. - this.isEntirelySelected = isEntirelySelected( target ); + // After pressing primary + A we can assume isEntirelySelected is true. + // Calling right away isEntirelySelected after primary + A may still return false on some browsers. + this.isEntirelySelected = true; } return; From 506187b0151b00de45bb0580467d49df040814b6 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Thu, 1 Nov 2018 18:06:04 +0100 Subject: [PATCH 57/98] Try avoiding the deprecated findDOMNode API from DropZone Provider (#11168) * Try avoiding the deprecated findDOMNode API * Components: Use event handler in place of `ref` --- packages/components/src/drop-zone/provider.js | 19 +++++++------------ packages/components/src/drop-zone/style.scss | 4 ++++ test/e2e/specs/adding-blocks.test.js | 16 +++++++++++++++- 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/packages/components/src/drop-zone/provider.js b/packages/components/src/drop-zone/provider.js index 07e46c37e9a3a5..5e77c74d5b9580 100644 --- a/packages/components/src/drop-zone/provider.js +++ b/packages/components/src/drop-zone/provider.js @@ -6,7 +6,7 @@ import { isEqual, find, some, filter, throttle, includes } from 'lodash'; /** * WordPress dependencies */ -import { Component, createContext, findDOMNode } from '@wordpress/element'; +import { Component, createContext } from '@wordpress/element'; import isShallowEqual from '@wordpress/is-shallow-equal'; const { Provider, Consumer } = createContext( { @@ -77,17 +77,11 @@ class DropZoneProvider extends Component { componentDidMount() { window.addEventListener( 'dragover', this.onDragOver ); - window.addEventListener( 'drop', this.onDrop ); window.addEventListener( 'mouseup', this.resetDragState ); - - // Disable reason: Can't use a ref since this component just renders its children - // eslint-disable-next-line react/no-find-dom-node - this.container = findDOMNode( this ); } componentWillUnmount() { window.removeEventListener( 'dragover', this.onDragOver ); - window.removeEventListener( 'drop', this.onDrop ); window.removeEventListener( 'mouseup', this.resetDragState ); } @@ -212,10 +206,9 @@ class DropZoneProvider extends Component { const { position, hoveredDropZone } = this.state; const dragEventType = getDragEventType( event ); const dropZone = this.dropZones[ hoveredDropZone ]; - const isValidDropzone = !! dropZone && this.container.contains( event.target ); this.resetDragState(); - if ( isValidDropzone ) { + if ( dropZone ) { switch ( dragEventType ) { case 'file': dropZone.onFilesDrop( [ ...event.dataTransfer.files ], position ); @@ -234,9 +227,11 @@ class DropZoneProvider extends Component { render() { return ( - - { this.props.children } - +
    + + { this.props.children } + +
    ); } } diff --git a/packages/components/src/drop-zone/style.scss b/packages/components/src/drop-zone/style.scss index 65b67953004246..0103f4af011588 100644 --- a/packages/components/src/drop-zone/style.scss +++ b/packages/components/src/drop-zone/style.scss @@ -58,3 +58,7 @@ .components-drop-zone__content-text { font-family: $default-font; } + +.components-drop-zone__provider { + height: 100%; +} diff --git a/test/e2e/specs/adding-blocks.test.js b/test/e2e/specs/adding-blocks.test.js index 0c38f03fdbfa13..b46bff8600254f 100644 --- a/test/e2e/specs/adding-blocks.test.js +++ b/test/e2e/specs/adding-blocks.test.js @@ -13,9 +13,23 @@ describe( 'adding blocks', () => { await newPost(); } ); + /** + * Given a Puppeteer ElementHandle, clicks below its bounding box. + * + * @param {Puppeteer.ElementHandle} elementHandle Element handle. + * + * @return {Promise} Promise resolving when click occurs. + */ + async function clickBelow( elementHandle ) { + const box = await elementHandle.boundingBox(); + const x = box.x + ( box.width / 2 ); + const y = box.y + box.height + 100; + return page.mouse.click( x, y ); + } + it( 'Should insert content using the placeholder and the regular inserter', async () => { // Click below editor to focus last field (block appender) - await page.click( '.editor-writing-flow__click-redirect' ); + await clickBelow( await page.$( '.editor-default-block-appender' ) ); expect( await page.$( '[data-type="core/paragraph"]' ) ).not.toBeNull(); await page.keyboard.type( 'Paragraph block' ); From de7121c365bfcd6d7b8596ca3a37ffbd8f06ec35 Mon Sep 17 00:00:00 2001 From: Dominik Schilling Date: Thu, 1 Nov 2018 18:07:39 +0100 Subject: [PATCH 58/98] Create file blocks when dropping multiple files at once (#11297) * Create file blocks when dropping multiple files at once * Add changelog entry --- packages/block-library/CHANGELOG.md | 6 ++++++ packages/block-library/src/file/index.js | 23 +++++++++++++++-------- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/packages/block-library/CHANGELOG.md b/packages/block-library/CHANGELOG.md index 8df197bb991957..39fbb90aa0c65f 100644 --- a/packages/block-library/CHANGELOG.md +++ b/packages/block-library/CHANGELOG.md @@ -1,3 +1,9 @@ +## 2.1.8 (Unreleased) + +### Polish + +- File Block: Create file blocks when dropping multiple files at once. + ## 2.1.7 (2018-10-30) ## 2.1.6 (2018-10-30) diff --git a/packages/block-library/src/file/index.js b/packages/block-library/src/file/index.js index 7f26a4abc025e4..43471765187c8a 100644 --- a/packages/block-library/src/file/index.js +++ b/packages/block-library/src/file/index.js @@ -77,20 +77,27 @@ export const settings = { from: [ { type: 'files', - isMatch: ( files ) => files.length === 1, + isMatch( files ) { + return files.length > 0; + }, // We define a lower priorty (higher number) than the default of 10. This // ensures that the File block is only created as a fallback. priority: 15, transform: ( files ) => { - const file = files[ 0 ]; - const blobURL = createBlobURL( file ); + const blocks = []; - // File will be uploaded in componentDidMount() - return createBlock( 'core/file', { - href: blobURL, - fileName: file.name, - textLinkHref: blobURL, + files.map( ( file ) => { + const blobURL = createBlobURL( file ); + + // File will be uploaded in componentDidMount() + blocks.push( createBlock( 'core/file', { + href: blobURL, + fileName: file.name, + textLinkHref: blobURL, + } ) ); } ); + + return blocks; }, }, { From 9c8923a4db22ceda7aa42d2ff170badb91fa2894 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20Van=C2=A0Dorpe?= Date: Thu, 1 Nov 2018 18:28:28 +0100 Subject: [PATCH 59/98] Remove deprecated componentWillReceiveProps from TinyMCE component (#11368) --- .../editor/src/components/rich-text/tinymce.js | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/editor/src/components/rich-text/tinymce.js b/packages/editor/src/components/rich-text/tinymce.js index d2a300a8d9cdb2..4e6ab5c17562b1 100644 --- a/packages/editor/src/components/rich-text/tinymce.js +++ b/packages/editor/src/components/rich-text/tinymce.js @@ -130,15 +130,7 @@ export default class TinyMCE extends Component { this.initialize(); } - shouldComponentUpdate() { - // We must prevent rerenders because TinyMCE will modify the DOM, thus - // breaking React's ability to reconcile changes. - // - // See: https://github.com/facebook/react/issues/6802 - return false; - } - - componentWillReceiveProps( nextProps ) { + shouldComponentUpdate( nextProps ) { this.configureIsPlaceholderVisible( nextProps.isPlaceholderVisible ); if ( ! isEqual( this.props.style, nextProps.style ) ) { @@ -155,6 +147,12 @@ export default class TinyMCE extends Component { this.editorNode.removeAttribute( key ) ); updatedKeys.forEach( ( key ) => this.editorNode.setAttribute( key, nextProps[ key ] ) ); + + // We must prevent rerenders because TinyMCE will modify the DOM, thus + // breaking React's ability to reconcile changes. + // + // See: https://github.com/facebook/react/issues/6802 + return false; } componentWillUnmount() { From 6919650587d379a02fe3b4f7b5ea40eaa277455a Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Thu, 1 Nov 2018 18:31:56 +0100 Subject: [PATCH 60/98] Remove findDOMNode usage from the inserter (#11363) --- .../src/more/test/__snapshots__/edit.js.snap | 8 +- packages/components/CHANGELOG.md | 1 + packages/components/src/panel/body.js | 15 ++-- .../panel/test/__snapshots__/color.js.snap | 2 +- packages/components/src/panel/test/body.js | 2 +- .../test/__snapshots__/index.js.snap | 76 +------------------ .../plugin-post-publish-panel/test/index.js | 16 ++-- .../test/__snapshots__/index.js.snap | 76 +------------------ .../plugin-pre-publish-panel/test/index.js | 14 ++-- packages/editor/CHANGELOG.md | 4 + .../editor/src/components/inserter/menu.js | 4 +- .../test/__snapshots__/index.js.snap | 12 +-- 12 files changed, 44 insertions(+), 186 deletions(-) diff --git a/packages/block-library/src/more/test/__snapshots__/edit.js.snap b/packages/block-library/src/more/test/__snapshots__/edit.js.snap index 53da28ea534284..7c03b45fa5df21 100644 --- a/packages/block-library/src/more/test/__snapshots__/edit.js.snap +++ b/packages/block-library/src/more/test/__snapshots__/edit.js.snap @@ -3,13 +3,13 @@ exports[`core/more/edit should match snapshot when noTeaser is false 1`] = ` - + - +
    - + - +
    +
    { !! title && (

    - -

    - My panel content -
    - -`; +exports[`PluginPostPublishPanel renders fill properly 1`] = `"

    My panel content
    "`; diff --git a/packages/edit-post/src/components/sidebar/plugin-post-publish-panel/test/index.js b/packages/edit-post/src/components/sidebar/plugin-post-publish-panel/test/index.js index fb491baadc564f..66b159bb255337 100644 --- a/packages/edit-post/src/components/sidebar/plugin-post-publish-panel/test/index.js +++ b/packages/edit-post/src/components/sidebar/plugin-post-publish-panel/test/index.js @@ -1,12 +1,8 @@ -/** - * External dependencies - */ -import { mount } from 'enzyme'; - /** * WordPress dependencies */ import { SlotFillProvider } from '@wordpress/components'; +import { render } from '@wordpress/element'; /** * Internal dependencies @@ -17,19 +13,21 @@ jest.mock( '../../../../../../components/src/button' ); describe( 'PluginPostPublishPanel', () => { test( 'renders fill properly', () => { - const wrapper = mount( + const div = document.createElement( 'div' ); + render( - My panel content + My panel content - + , + div ); - expect( wrapper.find( 'Slot' ).children() ).toMatchSnapshot(); + expect( div.innerHTML ).toMatchSnapshot(); } ); } ); diff --git a/packages/edit-post/src/components/sidebar/plugin-pre-publish-panel/test/__snapshots__/index.js.snap b/packages/edit-post/src/components/sidebar/plugin-pre-publish-panel/test/__snapshots__/index.js.snap index 837d2e8b6c2ab0..0321e1b11b72ed 100644 --- a/packages/edit-post/src/components/sidebar/plugin-pre-publish-panel/test/__snapshots__/index.js.snap +++ b/packages/edit-post/src/components/sidebar/plugin-pre-publish-panel/test/__snapshots__/index.js.snap @@ -1,77 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`PluginPrePublishPanel renders fill properly 1`] = ` - -
    -

    - - -

    - My panel content -
    -
    -`; +exports[`PluginPrePublishPanel renders fill properly 1`] = `"

    My panel content
    "`; diff --git a/packages/edit-post/src/components/sidebar/plugin-pre-publish-panel/test/index.js b/packages/edit-post/src/components/sidebar/plugin-pre-publish-panel/test/index.js index 4d9569b0162860..1383341bfe7050 100644 --- a/packages/edit-post/src/components/sidebar/plugin-pre-publish-panel/test/index.js +++ b/packages/edit-post/src/components/sidebar/plugin-pre-publish-panel/test/index.js @@ -1,12 +1,8 @@ -/** - * External dependencies - */ -import { mount } from 'enzyme'; - /** * WordPress dependencies */ import { SlotFillProvider } from '@wordpress/components'; +import { render } from '@wordpress/element'; /** * Internal dependencies @@ -17,7 +13,8 @@ jest.mock( '../../../../../../components/src/button' ); describe( 'PluginPrePublishPanel', () => { test( 'renders fill properly', () => { - const wrapper = mount( + const div = document.createElement( 'div' ); + render( { My panel content - + , + div ); - expect( wrapper.find( 'Slot' ).children() ).toMatchSnapshot(); + expect( div.innerHTML ).toMatchSnapshot(); } ); } ); diff --git a/packages/editor/CHANGELOG.md b/packages/editor/CHANGELOG.md index aefa03d7f56436..f3e6172184c4d0 100644 --- a/packages/editor/CHANGELOG.md +++ b/packages/editor/CHANGELOG.md @@ -1,5 +1,9 @@ ## 6.1.1 (Unreleased) +### Polish + +- Remove `findDOMNode` usage from the `Inserter` component. + ## 6.1.0 (2018-10-30) ### Deprecations diff --git a/packages/editor/src/components/inserter/menu.js b/packages/editor/src/components/inserter/menu.js index be7623a6a50aaf..57131b5d2e7d2a 100644 --- a/packages/editor/src/components/inserter/menu.js +++ b/packages/editor/src/components/inserter/menu.js @@ -21,7 +21,7 @@ import scrollIntoView from 'dom-scroll-into-view'; * WordPress dependencies */ import { __, _n, _x, sprintf } from '@wordpress/i18n'; -import { Component, findDOMNode, createRef } from '@wordpress/element'; +import { Component, createRef } from '@wordpress/element'; import { withSpokenMessages, PanelBody } from '@wordpress/components'; import { getCategories, @@ -163,7 +163,7 @@ export class InserterMenu extends Component { this.props.setTimeout( () => { // We need a generic way to access the panel's container // eslint-disable-next-line react/no-find-dom-node - scrollIntoView( findDOMNode( this.panels[ panel ] ), this.inserterResults.current, { + scrollIntoView( this.panels[ panel ], this.inserterResults.current, { alignWithTop: true, } ); } ); diff --git a/packages/editor/src/components/panel-color-settings/test/__snapshots__/index.js.snap b/packages/editor/src/components/panel-color-settings/test/__snapshots__/index.js.snap index 46f3bb8e1328df..7564cd036d6892 100644 --- a/packages/editor/src/components/panel-color-settings/test/__snapshots__/index.js.snap +++ b/packages/editor/src/components/panel-color-settings/test/__snapshots__/index.js.snap @@ -22,7 +22,7 @@ exports[`PanelColorSettings matches the snapshot 1`] = ` `; exports[`PanelColorSettings matches the snapshot 2`] = ` - - + `; exports[`PanelColorSettings should render a color panel if at least one setting specifies some colors to choose 1`] = ` @@ -87,7 +87,7 @@ exports[`PanelColorSettings should render a color panel if at least one setting `; exports[`PanelColorSettings should render a color panel if at least one setting specifies some colors to choose 2`] = ` - - + `; exports[`PanelColorSettings should render a color panel if at least one setting supports custom colors 1`] = ` @@ -156,7 +156,7 @@ exports[`PanelColorSettings should render a color panel if at least one setting `; exports[`PanelColorSettings should render a color panel if at least one setting supports custom colors 2`] = ` - - + `; From 5d3c7a139381e3a34469c88cf0aa0d66dd708fef Mon Sep 17 00:00:00 2001 From: Mayuko Moriyama Date: Fri, 2 Nov 2018 02:33:15 +0900 Subject: [PATCH 61/98] Add removed periods to block descriptions. (#11367) * Fix missing period in block discriptions. * Update CONTRIBUTORS.md --- CONTRIBUTORS.md | 1 + .../block-library/src/embed/core-embeds.js | 66 +++++++++---------- 2 files changed, 34 insertions(+), 33 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 0b1cd656f6bdd9..fb8f6ba79298be 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -110,5 +110,6 @@ This list is manually curated to include valuable contributions by volunteers th | @jorgefilipecosta | | | @ajitbohra | | | @ChrisVanPatten | | +| @mayukojpn | @mayukojpn | | @tofumatt | @lonelyvegan | | @LukePettway | @luke_pettway | \ No newline at end of file diff --git a/packages/block-library/src/embed/core-embeds.js b/packages/block-library/src/embed/core-embeds.js index a38c47d5be21bd..3a2e861e0c0aa1 100644 --- a/packages/block-library/src/embed/core-embeds.js +++ b/packages/block-library/src/embed/core-embeds.js @@ -31,7 +31,7 @@ export const common = [ title: 'Twitter', icon: embedTwitterIcon, keywords: [ 'tweet' ], - description: __( 'Embed a tweet' ), + description: __( 'Embed a tweet.' ), }, patterns: [ /^https?:\/\/(www\.)?twitter\.com\/.+/i ], }, @@ -41,7 +41,7 @@ export const common = [ title: 'YouTube', icon: embedYouTubeIcon, keywords: [ __( 'music' ), __( 'video' ) ], - description: __( 'Embed a YouTube video' ), + description: __( 'Embed a YouTube video.' ), }, patterns: [ /^https?:\/\/((m|www)\.)?youtube\.com\/.+/i, /^https?:\/\/youtu\.be\/.+/i ], }, @@ -50,7 +50,7 @@ export const common = [ settings: { title: 'Facebook', icon: embedFacebookIcon, - description: __( 'Embed a Facebook post' ), + description: __( 'Embed a Facebook post.' ), }, patterns: [ /^https?:\/\/www\.facebook.com\/.+/i ], }, @@ -60,7 +60,7 @@ export const common = [ title: 'Instagram', icon: embedInstagramIcon, keywords: [ __( 'image' ) ], - description: __( 'Embed an Instagram post' ), + description: __( 'Embed an Instagram post.' ), }, patterns: [ /^https?:\/\/(www\.)?instagr(\.am|am\.com)\/.+/i ], }, @@ -71,7 +71,7 @@ export const common = [ icon: embedWordPressIcon, keywords: [ __( 'post' ), __( 'blog' ) ], responsive: false, - description: __( 'Embed a WordPress post' ), + description: __( 'Embed a WordPress post.' ), }, }, { @@ -80,7 +80,7 @@ export const common = [ title: 'SoundCloud', icon: embedAudioIcon, keywords: [ __( 'music' ), __( 'audio' ) ], - description: __( 'Embed SoundCloud content' ), + description: __( 'Embed SoundCloud content.' ), }, patterns: [ /^https?:\/\/(www\.)?soundcloud\.com\/.+/i ], }, @@ -90,7 +90,7 @@ export const common = [ title: 'Spotify', icon: embedSpotifyIcon, keywords: [ __( 'music' ), __( 'audio' ) ], - description: __( 'Embed Spotify content' ), + description: __( 'Embed Spotify content.' ), }, patterns: [ /^https?:\/\/(open|play)\.spotify\.com\/.+/i ], }, @@ -100,7 +100,7 @@ export const common = [ title: 'Flickr', icon: embedFlickrIcon, keywords: [ __( 'image' ) ], - description: __( 'Embed Flickr content' ), + description: __( 'Embed Flickr content.' ), }, patterns: [ /^https?:\/\/(www\.)?flickr\.com\/.+/i, /^https?:\/\/flic\.kr\/.+/i ], }, @@ -110,7 +110,7 @@ export const common = [ title: 'Vimeo', icon: embedVimeoIcon, keywords: [ __( 'video' ) ], - description: __( 'Embed a Vimeo video' ), + description: __( 'Embed a Vimeo video.' ), }, patterns: [ /^https?:\/\/(www\.)?vimeo\.com\/.+/i ], }, @@ -122,7 +122,7 @@ export const others = [ settings: { title: 'Animoto', icon: embedVideoIcon, - description: __( 'Embed an Animoto video' ), + description: __( 'Embed an Animoto video.' ), }, patterns: [ /^https?:\/\/(www\.)?(animoto|video214)\.com\/.+/i ], }, @@ -131,7 +131,7 @@ export const others = [ settings: { title: 'Cloudup', icon: embedContentIcon, - description: __( 'Embed Cloudup content' ), + description: __( 'Embed Cloudup content.' ), }, patterns: [ /^https?:\/\/cloudup\.com\/.+/i ], }, @@ -140,7 +140,7 @@ export const others = [ settings: { title: 'CollegeHumor', icon: embedVideoIcon, - description: __( 'Embed CollegeHumor content' ), + description: __( 'Embed CollegeHumor content.' ), }, patterns: [ /^https?:\/\/(www\.)?collegehumor\.com\/.+/i ], }, @@ -149,7 +149,7 @@ export const others = [ settings: { title: 'Dailymotion', icon: embedVideoIcon, - description: __( 'Embed a Dailymotion video' ), + description: __( 'Embed a Dailymotion video.' ), }, patterns: [ /^https?:\/\/(www\.)?dailymotion\.com\/.+/i ], }, @@ -158,7 +158,7 @@ export const others = [ settings: { title: 'Funny or Die', icon: embedVideoIcon, - description: __( 'Embed Funny or Die content' ), + description: __( 'Embed Funny or Die content.' ), }, patterns: [ /^https?:\/\/(www\.)?funnyordie\.com\/.+/i ], }, @@ -167,7 +167,7 @@ export const others = [ settings: { title: 'Hulu', icon: embedVideoIcon, - description: __( 'Embed Hulu content' ), + description: __( 'Embed Hulu content.' ), }, patterns: [ /^https?:\/\/(www\.)?hulu\.com\/.+/i ], }, @@ -176,7 +176,7 @@ export const others = [ settings: { title: 'Imgur', icon: embedPhotoIcon, - description: __( 'Embed Imgur content' ), + description: __( 'Embed Imgur content.' ), }, patterns: [ /^https?:\/\/(.+\.)?imgur\.com\/.+/i ], }, @@ -185,7 +185,7 @@ export const others = [ settings: { title: 'Issuu', icon: embedContentIcon, - description: __( 'Embed Issuu content' ), + description: __( 'Embed Issuu content.' ), }, patterns: [ /^https?:\/\/(www\.)?issuu\.com\/.+/i ], }, @@ -194,7 +194,7 @@ export const others = [ settings: { title: 'Kickstarter', icon: embedContentIcon, - description: __( 'Embed Kickstarter content' ), + description: __( 'Embed Kickstarter content.' ), }, patterns: [ /^https?:\/\/(www\.)?kickstarter\.com\/.+/i, /^https?:\/\/kck\.st\/.+/i ], }, @@ -203,7 +203,7 @@ export const others = [ settings: { title: 'Meetup.com', icon: embedContentIcon, - description: __( 'Embed Meetup.com content' ), + description: __( 'Embed Meetup.com content.' ), }, patterns: [ /^https?:\/\/(www\.)?meetu(\.ps|p\.com)\/.+/i ], }, @@ -213,7 +213,7 @@ export const others = [ title: 'Mixcloud', icon: embedAudioIcon, keywords: [ __( 'music' ), __( 'audio' ) ], - description: __( 'Embed Mixcloud content' ), + description: __( 'Embed Mixcloud content.' ), }, patterns: [ /^https?:\/\/(www\.)?mixcloud\.com\/.+/i ], }, @@ -222,7 +222,7 @@ export const others = [ settings: { title: 'Photobucket', icon: embedPhotoIcon, - description: __( 'Embed a Photobucket image' ), + description: __( 'Embed a Photobucket image.' ), }, patterns: [ /^http:\/\/g?i*\.photobucket\.com\/.+/i ], }, @@ -231,7 +231,7 @@ export const others = [ settings: { title: 'Polldaddy', icon: embedContentIcon, - description: __( 'Embed Polldaddy content' ), + description: __( 'Embed Polldaddy content.' ), }, patterns: [ /^https?:\/\/(www\.)?polldaddy\.com\/.+/i ], }, @@ -240,7 +240,7 @@ export const others = [ settings: { title: 'Reddit', icon: embedRedditIcon, - description: __( 'Embed a Reddit thread' ), + description: __( 'Embed a Reddit thread.' ), }, patterns: [ /^https?:\/\/(www\.)?reddit\.com\/.+/i ], }, @@ -249,7 +249,7 @@ export const others = [ settings: { title: 'ReverbNation', icon: embedAudioIcon, - description: __( 'Embed ReverbNation content' ), + description: __( 'Embed ReverbNation content.' ), }, patterns: [ /^https?:\/\/(www\.)?reverbnation\.com\/.+/i ], }, @@ -258,7 +258,7 @@ export const others = [ settings: { title: 'Screencast', icon: embedVideoIcon, - description: __( 'Embed Screencast content' ), + description: __( 'Embed Screencast content.' ), }, patterns: [ /^https?:\/\/(www\.)?screencast\.com\/.+/i ], }, @@ -267,7 +267,7 @@ export const others = [ settings: { title: 'Scribd', icon: embedContentIcon, - description: __( 'Embed Scribd content' ), + description: __( 'Embed Scribd content.' ), }, patterns: [ /^https?:\/\/(www\.)?scribd\.com\/.+/i ], }, @@ -276,7 +276,7 @@ export const others = [ settings: { title: 'Slideshare', icon: embedContentIcon, - description: __( 'Embed Slideshare content' ), + description: __( 'Embed Slideshare content.' ), }, patterns: [ /^https?:\/\/(.+?\.)?slideshare\.net\/.+/i ], }, @@ -285,7 +285,7 @@ export const others = [ settings: { title: 'SmugMug', icon: embedPhotoIcon, - description: __( 'Embed SmugMug content' ), + description: __( 'Embed SmugMug content.' ), }, patterns: [ /^https?:\/\/(www\.)?smugmug\.com\/.+/i ], }, @@ -315,7 +315,7 @@ export const others = [ } ); }, } ], - description: __( 'Embed Speaker Deck content' ), + description: __( 'Embed Speaker Deck content.' ), }, patterns: [ /^https?:\/\/(www\.)?speakerdeck\.com\/.+/i ], }, @@ -324,7 +324,7 @@ export const others = [ settings: { title: 'TED', icon: embedVideoIcon, - description: __( 'Embed a TED video' ), + description: __( 'Embed a TED video.' ), }, patterns: [ /^https?:\/\/(www\.|embed\.)?ted\.com\/.+/i ], }, @@ -333,7 +333,7 @@ export const others = [ settings: { title: 'Tumblr', icon: embedTumbrIcon, - description: __( 'Embed a Tumblr post' ), + description: __( 'Embed a Tumblr post.' ), }, patterns: [ /^https?:\/\/(www\.)?tumblr\.com\/.+/i ], }, @@ -343,7 +343,7 @@ export const others = [ title: 'VideoPress', icon: embedVideoIcon, keywords: [ __( 'video' ) ], - description: __( 'Embed a VideoPress video' ), + description: __( 'Embed a VideoPress video.' ), }, patterns: [ /^https?:\/\/videopress\.com\/.+/i ], }, @@ -352,7 +352,7 @@ export const others = [ settings: { title: 'WordPress.tv', icon: embedVideoIcon, - description: __( 'Embed a WordPress.tv video' ), + description: __( 'Embed a WordPress.tv video.' ), }, patterns: [ /^https?:\/\/wordpress\.tv\/.+/i ], }, From b8f2849e35e150e469d67c8b972d4559280b08e5 Mon Sep 17 00:00:00 2001 From: Pavlo Tkachov Date: Thu, 1 Nov 2018 14:07:51 -0500 Subject: [PATCH 62/98] Docs: Remove duplicate word "page". (#11372) ## Description Removed duplicated word. --- docs/extensibility/meta-box.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/extensibility/meta-box.md b/docs/extensibility/meta-box.md index ca5e9783b8be90..eb96bfc38e0ffb 100644 --- a/docs/extensibility/meta-box.md +++ b/docs/extensibility/meta-box.md @@ -73,7 +73,7 @@ So an example url would look like: This url is automatically passed into React via a `_wpMetaBoxUrl` global variable. -This page page mimics the `post.php` post form, so when it is submitted it will fire all of the normal hooks and actions, and have the proper global state to correctly fire any PHP meta box mumbo jumbo without needing to modify any existing code. On successful submission, React will signal a `handleMetaBoxReload` to remove the updating overlay. +This page mimics the `post.php` post form, so when it is submitted it will fire all of the normal hooks and actions, and have the proper global state to correctly fire any PHP meta box mumbo jumbo without needing to modify any existing code. On successful submission, React will signal a `handleMetaBoxReload` to remove the updating overlay. ### Common Compatibility Issues From 29bdf31d69164d8ba3d122638b5e96d9f318b2f4 Mon Sep 17 00:00:00 2001 From: Timothy Jacobs Date: Thu, 1 Nov 2018 16:53:45 -0400 Subject: [PATCH 63/98] api-fetch: Add Accept json header to l10n requests (#11336) * api-fetch: Add Accept json header to l10n requests Fixes #11327. The server side localization switching requires an 'Accept' or 'Content-Type' header set to 'application/json` to detect that it is a JSON request. * Add tests to check if the accept header is properly applied. * Widen the Accept header to include */*. This indicates that JSON is preferred and is enough for the server to determine that it is a JSON request. * Use assign to simplify applying headers. --- packages/api-fetch/src/index.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/api-fetch/src/index.js b/packages/api-fetch/src/index.js index 07d61dc4cf950d..746f19304af94a 100644 --- a/packages/api-fetch/src/index.js +++ b/packages/api-fetch/src/index.js @@ -23,10 +23,11 @@ function registerMiddleware( middleware ) { function apiFetch( options ) { const raw = ( nextOptions ) => { const { url, path, body, data, parse = true, ...remainingOptions } = nextOptions; - const headers = remainingOptions.headers || {}; - if ( ! headers[ 'Content-Type' ] && data ) { - headers[ 'Content-Type' ] = 'application/json'; - } + const headers = { + Accept: 'application/json, */*;q=0.1', + 'Content-Type': 'application/json', + ...remainingOptions.headers, + }; const responsePromise = window.fetch( url || path, From 062089236b9ab32994af2595e44ee9791bcb5570 Mon Sep 17 00:00:00 2001 From: Ajit Bohra Date: Fri, 2 Nov 2018 02:31:37 +0530 Subject: [PATCH 64/98] Docs: add icon info to custom block category (#11213) * Docs: custom block category add icon info * Update icon note Co-Authored-By: ajitbohra * Update example to use svg icon --- docs/extensibility/extending-blocks.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/extensibility/extending-blocks.md b/docs/extensibility/extending-blocks.md index 11daff62329ed3..d00282e6b4665a 100644 --- a/docs/extensibility/extending-blocks.md +++ b/docs/extensibility/extending-blocks.md @@ -333,9 +333,13 @@ function my_plugin_block_categories( $categories, $post ) { array( 'slug' => 'my-category', 'title' => __( 'My category', 'my-plugin' ), + 'icon' => '', ), ) ); } add_filter( 'block_categories', 'my_plugin_block_categories', 10, 2 ); ``` + +You can also display an icon with your block category by setting an `icon` attribute. The value can be the slug of a [WordPress Dashicon](https://developer.wordpress.org/resource/dashicons/), or a custom `svg` element. + From e30f36f54a391c9b1d700066e70872b949bafb21 Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Fri, 2 Nov 2018 14:12:16 +1100 Subject: [PATCH 65/98] Advanced Panels: Add support for the 'Custom Fields' meta box (#11084) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Advanced Panels: Add support for the 'Custom Fields' meta box - Adds an option which allows the user to enable the existing 'postcustom' meta box included in WordPress. - Don't load the 'postcustom' assets when the option is disabled. - Stores the option as a site option. - Reload the editor when the option is changed. * Fix E2E tests The 'Enable Tips' checkbox is obscured by the first Tip now that the Options modal is taller. Work around this by only testing that tips can be re-enabled. * Custom Fields: Remove unnecessary jQuery selector We can simply interpolate $post->ID when initializing Custom Fields, rather than selecting from the #post_ID hidden field. * Custom Fields: Use user meta field instead of site option By using `get_user_meta()` instead of `get_option()`, the Custom Fields setting won't affect other users. * Custom Fields: Kill the process after redirecting * Custom Fields: Use hidden
    instead of redirect Trigger the toggle_custom_fields action using a hidden so that the action is triggered by a POST request rather than a GET request. * Custom Fields: Move meta box options into their own component * Custom Fields: Rename isEnabled → isChecked * Custom Fields: Only show option if custom fields are supported --- lib/client-assets.php | 6 +- lib/meta-box-partial-page.php | 58 ++++++++++++- .../src/components/options-modal/index.js | 20 ++--- .../options-modal/meta-boxes-section.js | 50 +++++++++++ .../src/components/options-modal/options.js | 85 ------------------- .../components/options-modal/options/base.js | 17 ++++ .../options-modal/options/deferred.js | 36 ++++++++ .../options/enable-custom-fields.js | 47 ++++++++++ .../options-modal/options/enable-panel.js | 19 +++++ .../options/enable-publish-sidebar.js | 26 ++++++ .../options-modal/options/enable-tips.js | 27 ++++++ .../components/options-modal/options/index.js | 4 + .../test/__snapshots__/index.js.snap | 13 +-- test/e2e/specs/nux.test.js | 14 ++- 14 files changed, 310 insertions(+), 112 deletions(-) create mode 100644 packages/edit-post/src/components/options-modal/meta-boxes-section.js delete mode 100644 packages/edit-post/src/components/options-modal/options.js create mode 100644 packages/edit-post/src/components/options-modal/options/base.js create mode 100644 packages/edit-post/src/components/options-modal/options/deferred.js create mode 100644 packages/edit-post/src/components/options-modal/options/enable-custom-fields.js create mode 100644 packages/edit-post/src/components/options-modal/options/enable-panel.js create mode 100644 packages/edit-post/src/components/options-modal/options/enable-publish-sidebar.js create mode 100644 packages/edit-post/src/components/options-modal/options/enable-tips.js create mode 100644 packages/edit-post/src/components/options-modal/options/index.js diff --git a/lib/client-assets.php b/lib/client-assets.php index b68c2fa5c99036..d8792b5fa2e963 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -1602,14 +1602,18 @@ function gutenberg_editor_scripts_and_styles( $hook ) { 'maxUploadFileSize' => $max_upload_size, 'allowedMimeTypes' => get_allowed_mime_types(), 'styles' => $styles, - 'postLock' => $lock_details, // Ideally, we'd remove this and rely on a REST API endpoint. + 'postLock' => $lock_details, 'postLockUtils' => array( 'nonce' => wp_create_nonce( 'lock-post_' . $post->ID ), 'unlockNonce' => wp_create_nonce( 'update-post_' . $post->ID ), 'ajaxUrl' => admin_url( 'admin-ajax.php' ), ), + + // Whether or not to load the 'postcustom' meta box is stored as a user meta + // field so that we're not always loading its assets. + 'enableCustomFields' => (bool) get_user_meta( get_current_user_id(), 'enable_custom_fields', true ), ); $post_autosave = gutenberg_get_autosave_newer_than_post_save( $post ); diff --git a/lib/meta-box-partial-page.php b/lib/meta-box-partial-page.php index e50aaffb8c3a40..b77492bbbb87ac 100644 --- a/lib/meta-box-partial-page.php +++ b/lib/meta-box-partial-page.php @@ -113,7 +113,6 @@ function gutenberg_filter_meta_boxes( $meta_boxes ) { $core_normal_meta_boxes = array( 'revisionsdiv', 'postexcerpt', - 'postcustom', 'trackbacksdiv', 'commentstatusdiv', 'commentsdiv', @@ -121,6 +120,13 @@ function gutenberg_filter_meta_boxes( $meta_boxes ) { 'authordiv', ); + // Whether or not to load the 'postcustom' meta box is stored as a user meta + // field so that we're not always loading its assets. + $enable_custom_fields = (bool) get_user_meta( get_current_user_id(), 'enable_custom_fields', true ); + if ( ! $enable_custom_fields ) { + $core_normal_meta_boxes[] = 'postcustom'; + } + $taxonomy_callbacks_to_unset = array( 'post_tags_meta_box', 'post_categories_meta_box', @@ -298,6 +304,10 @@ function the_gutenberg_metaboxes() {
    +
    + + +