From dfc2b5ff3915bab793fff1410136eda1587e51ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20Van=C2=A0Dorpe?= Date: Mon, 5 Nov 2018 21:15:14 +0100 Subject: [PATCH 001/106] Fix applying formats on multiline values without wrapper tags (#11500) --- .../src/test/__snapshots__/to-dom.js.snap | 16 ++++++++++++++++ packages/rich-text/src/test/create.js | 4 ++++ packages/rich-text/src/test/helpers/index.js | 10 ++++++++++ packages/rich-text/src/to-tree.js | 2 +- 4 files changed, 31 insertions(+), 1 deletion(-) 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 0b3d69d139c95..ce980dcd7589b 100644 --- a/packages/rich-text/src/test/__snapshots__/to-dom.js.snap +++ b/packages/rich-text/src/test/__snapshots__/to-dom.js.snap @@ -301,6 +301,22 @@ exports[`recordToDom should handle selection before br 1`] = ` `; +exports[`recordToDom should ignore formats at line separator 1`] = ` + +

+ + one + +

+

+ + two + + +

+ +`; + exports[`recordToDom should ignore line breaks to format HTML 1`] = ` diff --git a/packages/rich-text/src/test/create.js b/packages/rich-text/src/test/create.js index f4bbbad889b22..79d11f23134f5 100644 --- a/packages/rich-text/src/test/create.js +++ b/packages/rich-text/src/test/create.js @@ -32,6 +32,10 @@ describe( 'create', () => { createRange, record, } ) => { + if ( html === undefined ) { + return; + } + it( description, () => { const element = createElement( document, html ); const range = createRange( element ); diff --git a/packages/rich-text/src/test/helpers/index.js b/packages/rich-text/src/test/helpers/index.js index cb8ad42d9997b..a220d4ac70561 100644 --- a/packages/rich-text/src/test/helpers/index.js +++ b/packages/rich-text/src/test/helpers/index.js @@ -530,6 +530,16 @@ export const spec = [ text: '', }, }, + { + description: 'should ignore formats at line separator', + multilineTag: 'p', + startPath: [], + endPath: [], + record: { + formats: [ [ em ], [ em ], [ em ], [ em ], [ em ], [ em ], [ em ] ], + text: 'one\u2028two', + }, + }, { description: 'should remove br with settings', settings: { diff --git a/packages/rich-text/src/to-tree.js b/packages/rich-text/src/to-tree.js index 893b331c44a37..fa8810b8e17ea 100644 --- a/packages/rich-text/src/to-tree.js +++ b/packages/rich-text/src/to-tree.js @@ -45,7 +45,7 @@ function fromFormat( { type, attributes, object } ) { export function toTree( { value, multilineTag, - multilineWrapperTags, + multilineWrapperTags = [], createEmpty, append, getLastChild, From 70c80680b14bfd12a660651261eb4079578f20ea Mon Sep 17 00:00:00 2001 From: Tim Elsass Date: Mon, 5 Nov 2018 15:36:43 -0500 Subject: [PATCH 002/106] Update/i18n pullquote and quote translator comments (#11496) ## Description Fixes: #11467 ## How has this been tested? Ran through building the translation strings. Opened #11494 as a result, noticing these comments aren't even being parsed and included in the final build. ## Types of changes Accessibility - Updated translator comments for pullquote and quote. ## Checklist: - [ x ] My code is tested. - [ x ] My code follows the WordPress code style. - [ x ] My code follows the accessibility standards. - [ x ] My code has proper inline documentation. --- packages/block-library/src/pullquote/edit.js | 4 ++-- packages/block-library/src/quote/index.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/block-library/src/pullquote/edit.js b/packages/block-library/src/pullquote/edit.js index 9d16aca82d427..2e16d06d48125 100644 --- a/packages/block-library/src/pullquote/edit.js +++ b/packages/block-library/src/pullquote/edit.js @@ -84,14 +84,14 @@ class PullQuoteEdit extends Component { value: nextValue, } ) } - /* translators: the text of the quotation */ + /* translators: placeholder text used for the quote */ placeholder={ __( 'Write quote…' ) } wrapperClassName="block-library-pullquote__content" /> { ( ! RichText.isEmpty( citation ) || isSelected ) && ( setAttributes( { diff --git a/packages/block-library/src/quote/index.js b/packages/block-library/src/quote/index.js index 49eef577f907a..f99f725b245d6 100644 --- a/packages/block-library/src/quote/index.js +++ b/packages/block-library/src/quote/index.js @@ -215,7 +215,7 @@ export const settings = { onReplace( [] ); } } } - /* translators: the text of the quotation */ + /* translators: placeholder text used for the quote */ placeholder={ __( 'Write quote…' ) } /> { ( ! RichText.isEmpty( citation ) || isSelected ) && ( @@ -226,7 +226,7 @@ export const settings = { citation: nextCitation, } ) } - /* translators: the individual or entity quoted */ + /* translators: placeholder text used for the citation */ placeholder={ __( 'Write citation…' ) } className="wp-block-quote__citation" /> From 5c6f480740241e9fa2c2815cb7cc5eeb43f6db4d Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Tue, 6 Nov 2018 10:43:55 +1100 Subject: [PATCH 003/106] Hide custom fields option when the meta box is disabled (#11476) * Custom Fields: Don't show option when enableCustomFields is not set Don't render the Custom Fields option when the enableCustomFields editor setting is undefined. This means that, in 5.0, plugins can properly disable custom fields. See https://core.trac.wordpress.org/ticket/45282 * Add tests for MetaBoxesSection & EnableCustomFieldsOption * Don't render MetaBoxesSection if postcustom is the only meta box --- .../options-modal/meta-boxes-section.js | 30 ++++----- .../options/enable-custom-fields.js | 4 +- .../enable-custom-fields.js.snap | 17 +++++ .../options/test/enable-custom-fields.js | 65 +++++++++++++++++++ .../__snapshots__/meta-boxes-section.js.snap | 48 ++++++++++++++ .../options-modal/test/meta-boxes-section.js | 62 ++++++++++++++++++ 6 files changed, 206 insertions(+), 20 deletions(-) create mode 100644 packages/edit-post/src/components/options-modal/options/test/__snapshots__/enable-custom-fields.js.snap create mode 100644 packages/edit-post/src/components/options-modal/options/test/enable-custom-fields.js create mode 100644 packages/edit-post/src/components/options-modal/test/__snapshots__/meta-boxes-section.js.snap create mode 100644 packages/edit-post/src/components/options-modal/test/meta-boxes-section.js diff --git a/packages/edit-post/src/components/options-modal/meta-boxes-section.js b/packages/edit-post/src/components/options-modal/meta-boxes-section.js index 414ed37d34b01..bbec30e0eadb7 100644 --- a/packages/edit-post/src/components/options-modal/meta-boxes-section.js +++ b/packages/edit-post/src/components/options-modal/meta-boxes-section.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { map } from 'lodash'; +import { filter, map } from 'lodash'; /** * WordPress dependencies @@ -15,36 +15,30 @@ import { withSelect } from '@wordpress/data'; import Section from './section'; import { EnableCustomFieldsOption, EnablePanelOption } from './options'; -function MetaBoxesSection( { hasCustomFieldsSupport, metaBoxes, ...sectionProps } ) { - if ( ! hasCustomFieldsSupport && metaBoxes.length === 0 ) { +export function MetaBoxesSection( { areCustomFieldsRegistered, metaBoxes, ...sectionProps } ) { + // The 'Custom Fields' meta box is a special case that we handle separately. + const thirdPartyMetaBoxes = filter( metaBoxes, ( { id } ) => id !== 'postcustom' ); + + if ( ! areCustomFieldsRegistered && thirdPartyMetaBoxes.length === 0 ) { return null; } return (
- { hasCustomFieldsSupport && ( - - ) } - { map( - metaBoxes, - ( { id, title } ) => - // The 'Custom Fields' meta box is a special case handled above. - id !== 'postcustom' && ( - - ) - ) } + { areCustomFieldsRegistered && } + { map( thirdPartyMetaBoxes, ( { id, title } ) => ( + + ) ) }
); } export default withSelect( ( select ) => { - const { getEditedPostAttribute } = select( 'core/editor' ); - const { getPostType } = select( 'core' ); + const { getEditorSettings } = select( 'core/editor' ); const { getAllMetaBoxes } = select( 'core/edit-post' ); - const postType = getPostType( getEditedPostAttribute( 'type' ) ); return { - hasCustomFieldsSupport: postType.supports[ 'custom-fields' ], + areCustomFieldsRegistered: getEditorSettings().enableCustomFields !== undefined, metaBoxes: getAllMetaBoxes(), }; } )( MetaBoxesSection ); diff --git a/packages/edit-post/src/components/options-modal/options/enable-custom-fields.js b/packages/edit-post/src/components/options-modal/options/enable-custom-fields.js index a22f96250ccad..140c6f1c46d2d 100644 --- a/packages/edit-post/src/components/options-modal/options/enable-custom-fields.js +++ b/packages/edit-post/src/components/options-modal/options/enable-custom-fields.js @@ -9,7 +9,7 @@ import { withSelect } from '@wordpress/data'; */ import BaseOption from './base'; -class EnableCustomFieldsOption extends Component { +export class EnableCustomFieldsOption extends Component { constructor( { isChecked } ) { super( ...arguments ); @@ -43,5 +43,5 @@ class EnableCustomFieldsOption extends Component { } export default withSelect( ( select ) => ( { - isChecked: select( 'core/editor' ).getEditorSettings().enableCustomFields, + isChecked: !! select( 'core/editor' ).getEditorSettings().enableCustomFields, } ) )( EnableCustomFieldsOption ); diff --git a/packages/edit-post/src/components/options-modal/options/test/__snapshots__/enable-custom-fields.js.snap b/packages/edit-post/src/components/options-modal/options/test/__snapshots__/enable-custom-fields.js.snap new file mode 100644 index 0000000000000..14bed9e08237e --- /dev/null +++ b/packages/edit-post/src/components/options-modal/options/test/__snapshots__/enable-custom-fields.js.snap @@ -0,0 +1,17 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`EnableCustomFieldsOption renders properly when checked 1`] = ` + +`; + +exports[`EnableCustomFieldsOption renders properly when unchecked 1`] = ` + +`; diff --git a/packages/edit-post/src/components/options-modal/options/test/enable-custom-fields.js b/packages/edit-post/src/components/options-modal/options/test/enable-custom-fields.js new file mode 100644 index 0000000000000..8ce8f78eb4be6 --- /dev/null +++ b/packages/edit-post/src/components/options-modal/options/test/enable-custom-fields.js @@ -0,0 +1,65 @@ +/** + * External dependencies + */ +import { shallow } from 'enzyme'; + +/** + * Internal dependencies + */ +import { EnableCustomFieldsOption } from '../enable-custom-fields'; + +describe( 'EnableCustomFieldsOption', () => { + it( 'renders properly when checked', () => { + const wrapper = shallow( ); + expect( wrapper ).toMatchSnapshot(); + } ); + + it( 'can be unchecked', () => { + const submit = jest.fn(); + const getElementById = jest.spyOn( document, 'getElementById' ).mockImplementation( () => ( { + submit, + } ) ); + + const wrapper = shallow( ); + + expect( wrapper.prop( 'isChecked' ) ).toBe( true ); + + wrapper.prop( 'onChange' )(); + wrapper.update(); + + expect( wrapper.prop( 'isChecked' ) ).toBe( false ); + expect( getElementById ).toHaveBeenCalledWith( 'toggle-custom-fields-form' ); + expect( submit ).toHaveBeenCalled(); + + getElementById.mockRestore(); + } ); + + it( 'renders properly when unchecked', () => { + const wrapper = shallow( + + ); + expect( wrapper ).toMatchSnapshot(); + } ); + + it( 'can be checked', () => { + const submit = jest.fn(); + const getElementById = jest.spyOn( document, 'getElementById' ).mockImplementation( () => ( { + submit, + } ) ); + + const wrapper = shallow( + + ); + + expect( wrapper.prop( 'isChecked' ) ).toBe( false ); + + wrapper.prop( 'onChange' )(); + wrapper.update(); + + expect( wrapper.prop( 'isChecked' ) ).toBe( true ); + expect( getElementById ).toHaveBeenCalledWith( 'toggle-custom-fields-form' ); + expect( submit ).toHaveBeenCalled(); + + getElementById.mockRestore(); + } ); +} ); diff --git a/packages/edit-post/src/components/options-modal/test/__snapshots__/meta-boxes-section.js.snap b/packages/edit-post/src/components/options-modal/test/__snapshots__/meta-boxes-section.js.snap new file mode 100644 index 0000000000000..42c22d9030718 --- /dev/null +++ b/packages/edit-post/src/components/options-modal/test/__snapshots__/meta-boxes-section.js.snap @@ -0,0 +1,48 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MetaBoxesSection renders a Custom Fields option 1`] = ` +
+ +
+`; + +exports[`MetaBoxesSection renders a Custom Fields option and meta box options 1`] = ` +
+ + + +
+`; + +exports[`MetaBoxesSection renders meta box options 1`] = ` +
+ + +
+`; diff --git a/packages/edit-post/src/components/options-modal/test/meta-boxes-section.js b/packages/edit-post/src/components/options-modal/test/meta-boxes-section.js new file mode 100644 index 0000000000000..edf45cb94c9e2 --- /dev/null +++ b/packages/edit-post/src/components/options-modal/test/meta-boxes-section.js @@ -0,0 +1,62 @@ +/** + * External dependencies + */ +import { shallow } from 'enzyme'; + +/** + * Internal dependencies + */ +import { MetaBoxesSection } from '../meta-boxes-section'; + +describe( 'MetaBoxesSection', () => { + it( 'does not render if there are no options', () => { + const wrapper = shallow( + + ); + expect( wrapper.isEmptyRender() ).toBe( true ); + } ); + + it( 'renders a Custom Fields option', () => { + const wrapper = shallow( + + ); + expect( wrapper ).toMatchSnapshot(); + } ); + + it( 'renders meta box options', () => { + const wrapper = shallow( + + ); + expect( wrapper ).toMatchSnapshot(); + } ); + + it( 'renders a Custom Fields option and meta box options', () => { + const wrapper = shallow( + + ); + expect( wrapper ).toMatchSnapshot(); + } ); +} ); From c9610242b5b937b3b593723306f58662f4781c21 Mon Sep 17 00:00:00 2001 From: Andrew Munro Date: Tue, 6 Nov 2018 14:48:39 +1300 Subject: [PATCH 004/106] Update contributors.md (#11521) --- CONTRIBUTORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 0fe6c435be696..3b526bb045107 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -114,3 +114,4 @@ This list is manually curated to include valuable contributions by volunteers th | @tofumatt | @lonelyvegan | | @LukePettway | @luke_pettway | | @pratikthink | @pratikthink | +| @amdrew | @sumobi | From 57d01f60f042c998ad118c4760827366ba358be3 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Tue, 6 Nov 2018 09:11:19 +0100 Subject: [PATCH 005/106] Remove findDOMNode usage from the NavigableToolbar component (#11401) --- package-lock.json | 130 +++++++----- packages/block-library/package.json | 2 +- packages/components/CHANGELOG.md | 1 + packages/components/package.json | 2 +- .../test/__snapshots__/index.js.snap | 4 +- .../src/navigable-container/container.js | 131 ++++++++++++ .../src/navigable-container/index.js | 194 +----------------- .../src/navigable-container/menu.js | 58 ++++++ .../src/navigable-container/tabbable.js | 46 +++++ .../test/{index.js => menu.js} | 85 +------- .../src/navigable-container/test/tabbable.js | 123 +++++++++++ .../panel/test/__snapshots__/color.js.snap | 2 +- .../components/src/scroll-lock/test/index.js | 2 +- packages/compose/package.json | 2 +- packages/data/package.json | 2 +- packages/edit-post/package.json | 2 +- .../test/__snapshots__/index.js.snap | 8 +- packages/editor/CHANGELOG.md | 1 + packages/editor/package.json | 2 +- .../test/__snapshots__/block-view.js.snap | 4 +- .../src/components/navigable-toolbar/index.js | 15 +- .../test/__snapshots__/index.js.snap | 12 +- .../test/__snapshots__/index.js.snap | 4 +- .../components/post-taxonomies/test/index.js | 4 +- packages/element/package.json | 2 +- packages/jest-preset-default/package.json | 4 +- packages/nux/package.json | 2 +- 27 files changed, 473 insertions(+), 371 deletions(-) create mode 100644 packages/components/src/navigable-container/container.js create mode 100644 packages/components/src/navigable-container/menu.js create mode 100644 packages/components/src/navigable-container/tabbable.js rename packages/components/src/navigable-container/test/{index.js => menu.js} (70%) create mode 100644 packages/components/src/navigable-container/test/tabbable.js diff --git a/package-lock.json b/package-lock.json index b467ccda26076..3a02588023f2a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1832,9 +1832,9 @@ "dev": true }, "@types/node": { - "version": "10.5.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.5.2.tgz", - "integrity": "sha512-m9zXmifkZsMHZBOyxZWilMwmTlpC8x5Ty360JKTiXvlXZfBWYpsg9ZZvP/Ye+iZUh+Q+MxDLjItVTWIsfwz+8Q==", + "version": "10.12.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.2.tgz", + "integrity": "sha512-53ElVDSnZeFUUFIYzI8WLQ25IhWzb6vbddNp8UHlXQyU0ET2RhV5zg0NfubzU7iNMh5bBXb0htCzfvrSVNgzaQ==", "dev": true }, "@webassemblyjs/ast": { @@ -2366,8 +2366,8 @@ "requires": { "@wordpress/jest-console": "file:packages/jest-console", "babel-jest": "^23.4.2", - "enzyme": "^3.3.0", - "enzyme-adapter-react-16": "^1.1.1", + "enzyme": "^3.7.0", + "enzyme-adapter-react-16": "^1.6.0", "jest-enzyme": "^6.0.2" } }, @@ -6935,41 +6935,52 @@ "dev": true }, "enzyme": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/enzyme/-/enzyme-3.3.0.tgz", - "integrity": "sha512-l8csyPyLmtxskTz6pX9W8eDOyH1ckEtDttXk/vlFWCjv00SkjTjtoUrogqp4yEvMyneU9dUJoOLnqFoiHb8IHA==", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/enzyme/-/enzyme-3.7.0.tgz", + "integrity": "sha512-QLWx+krGK6iDNyR1KlH5YPZqxZCQaVF6ike1eDJAOg0HvSkSCVImPsdWaNw6v+VrnK92Kg8jIOYhuOSS9sBpyg==", "dev": true, "requires": { + "array.prototype.flat": "^1.2.1", "cheerio": "^1.0.0-rc.2", - "function.prototype.name": "^1.0.3", - "has": "^1.0.1", + "function.prototype.name": "^1.1.0", + "has": "^1.0.3", "is-boolean-object": "^1.0.0", - "is-callable": "^1.1.3", + "is-callable": "^1.1.4", "is-number-object": "^1.0.3", "is-string": "^1.0.4", "is-subset": "^0.1.1", - "lodash": "^4.17.4", - "object-inspect": "^1.5.0", + "lodash.escape": "^4.0.1", + "lodash.isequal": "^4.5.0", + "object-inspect": "^1.6.0", "object-is": "^1.0.1", "object.assign": "^4.1.0", "object.entries": "^1.0.4", "object.values": "^1.0.4", "raf": "^3.4.0", - "rst-selector-parser": "^2.2.3" + "rst-selector-parser": "^2.2.3", + "string.prototype.trim": "^1.1.2" + }, + "dependencies": { + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=", + "dev": true + } } }, "enzyme-adapter-react-16": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.1.1.tgz", - "integrity": "sha512-kC8pAtU2Jk3OJ0EG8Y2813dg9Ol0TXi7UNxHzHiWs30Jo/hj7alc//G1YpKUsPP1oKl9X+Lkx+WlGJpPYA+nvw==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.6.0.tgz", + "integrity": "sha512-ay9eGFpChyUDnjTFMMJHzrb681LF3hPWJLEA7RoLFG9jSWAdAm2V50pGmFV9dYGJgh5HfdiqM+MNvle41Yf/PA==", "dev": true, "requires": { - "enzyme-adapter-utils": "^1.3.0", - "lodash": "^4.17.4", - "object.assign": "^4.0.4", + "enzyme-adapter-utils": "^1.8.0", + "function.prototype.name": "^1.1.0", + "object.assign": "^4.1.0", "object.values": "^1.0.4", - "prop-types": "^15.6.0", - "react-reconciler": "^0.7.0", + "prop-types": "^15.6.2", + "react-is": "^16.5.2", "react-test-renderer": "^16.0.0-0" }, "dependencies": { @@ -6982,17 +6993,24 @@ "loose-envify": "^1.3.1", "object-assign": "^4.1.1" } + }, + "react-is": { + "version": "16.6.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.6.0.tgz", + "integrity": "sha512-q8U7k0Fi7oxF1HvQgyBjPwDXeMplEsArnKt2iYhuIF86+GBbgLHdAmokL3XUFjTd7Q363OSNG55FOGUdONVn1g==", + "dev": true } } }, "enzyme-adapter-utils": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/enzyme-adapter-utils/-/enzyme-adapter-utils-1.4.0.tgz", - "integrity": "sha512-ajvyXQYbmCoKCX/FaraNzBgXDXJBltCd0GdXfKc0DdRPYgCLaZfS6Ts576IFt8aX2GU9ajZv2g5jfcJ+Nttejw==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/enzyme-adapter-utils/-/enzyme-adapter-utils-1.8.1.tgz", + "integrity": "sha512-s3QB3xQAowaDS2sHhmEqrT13GJC4+n5bG015ZkLv60n9k5vhxxHTQRIneZmQ4hmdCZEBrvUJ89PG6fRI5OEeuQ==", "dev": true, "requires": { + "function.prototype.name": "^1.1.0", "object.assign": "^4.1.0", - "prop-types": "^15.6.0" + "prop-types": "^15.6.2" }, "dependencies": { "prop-types": { @@ -13457,6 +13475,12 @@ "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=", "dev": true }, + "lodash.escape": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-4.0.1.tgz", + "integrity": "sha1-yQRGkMIeBClL6qUXcS/e0fqI3pg=", + "dev": true + }, "lodash.flattendeep": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", @@ -14180,6 +14204,12 @@ "moment": ">= 2.9.0" } }, + "moo": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/moo/-/moo-0.4.3.tgz", + "integrity": "sha512-gFD2xGCl8YFgGHsqJ9NKRVdwlioeW3mI1iqfLNYQOv0+6JRwG58Zk9DIGQgyIaffSYaO1xsKnMaYzzNr1KyIAw==", + "dev": true + }, "mousetrap": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/mousetrap/-/mousetrap-1.6.2.tgz", @@ -14255,11 +14285,12 @@ "dev": true }, "nearley": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.13.0.tgz", - "integrity": "sha512-ioYYogSaZhFlCpRizQgY3UT3G1qFXmHGY/5ozoFE3dMfiCRAeJfh+IPE3/eh9gCZvqLhPCWb4bLt7Bqzo+1mLQ==", + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.15.1.tgz", + "integrity": "sha512-8IUY/rUrKz2mIynUGh8k+tul1awMKEjeHHC5G3FHvvyAW6oq4mQfNp2c0BMea+sYZJvYcrrM6GmZVIle/GRXGw==", "dev": true, "requires": { + "moo": "^0.4.3", "nomnom": "~1.6.2", "railroad-diagrams": "^1.0.0", "randexp": "0.4.6", @@ -16825,9 +16856,9 @@ "dev": true }, "raf": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.0.tgz", - "integrity": "sha512-pDP/NMRAXoTfrhCfyfSEwJAKLaxBU9eApMeBPB1TkDouZmvPerIClV8lTAd+uF8ZiTaVl69e1FCxQrAd/VTjGw==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", "dev": true, "requires": { "performance-now": "^2.1.0" @@ -17089,30 +17120,6 @@ "prop-types": "^15.5.8" } }, - "react-reconciler": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.7.0.tgz", - "integrity": "sha512-50JwZ3yNyMS8fchN+jjWEJOH3Oze7UmhxeoJLn2j6f3NjpfCRbcmih83XTWmzqtar/ivd5f7tvQhvvhism2fgg==", - "dev": true, - "requires": { - "fbjs": "^0.8.16", - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.0" - }, - "dependencies": { - "prop-types": { - "version": "15.6.2", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz", - "integrity": "sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==", - "dev": true, - "requires": { - "loose-envify": "^1.3.1", - "object-assign": "^4.1.1" - } - } - } - }, "react-test-renderer": { "version": "16.4.1", "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.4.1.tgz", @@ -18945,6 +18952,17 @@ "strip-ansi": "^4.0.0" } }, + "string.prototype.trim": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.1.2.tgz", + "integrity": "sha1-0E3iyJ4Tf019IG8Ia17S+ua+jOo=", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "es-abstract": "^1.5.0", + "function-bind": "^1.0.2" + } + }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", diff --git a/packages/block-library/package.json b/packages/block-library/package.json index 9a4c619156759..844b958b3fb86 100644 --- a/packages/block-library/package.json +++ b/packages/block-library/package.json @@ -45,7 +45,7 @@ }, "devDependencies": { "deep-freeze": "^0.0.1", - "enzyme": "^3.3.0", + "enzyme": "^3.7.0", "react-test-renderer": "^16.4.1" }, "publishConfig": { diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 10bb6486c84e2..2ceb51c459d3c 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -4,6 +4,7 @@ - Forward `ref` in the `PanelBody` component. - Tooltip are no longer removed when Button becomes disabled, it's left to the component rendering the Tooltip. +- Forward `ref` support in `TabbableContainer` and `NavigableMenu` components. ## 5.0.1 (2018-10-30) diff --git a/packages/components/package.json b/packages/components/package.json index cff58949ea314..a13d4c61b9aab 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -49,7 +49,7 @@ }, "devDependencies": { "@wordpress/token-list": "file:../token-list", - "enzyme": "^3.3.0", + "enzyme": "^3.7.0", "react-test-renderer": "^16.4.1" }, "publishConfig": { diff --git a/packages/components/src/menu-group/test/__snapshots__/index.js.snap b/packages/components/src/menu-group/test/__snapshots__/index.js.snap index b64233334e7f0..c33b2fcad917c 100644 --- a/packages/components/src/menu-group/test/__snapshots__/index.js.snap +++ b/packages/components/src/menu-group/test/__snapshots__/index.js.snap @@ -10,13 +10,13 @@ exports[`MenuGroup should match snapshot 1`] = ` > My group -

My item

-
+ `; diff --git a/packages/components/src/navigable-container/container.js b/packages/components/src/navigable-container/container.js new file mode 100644 index 0000000000000..7471243cd48f9 --- /dev/null +++ b/packages/components/src/navigable-container/container.js @@ -0,0 +1,131 @@ +/** + * External Dependencies + */ +import { omit, noop, isFunction } from 'lodash'; + +/** + * WordPress Dependencies + */ +import { Component, forwardRef } from '@wordpress/element'; +import { focus } from '@wordpress/dom'; + +function cycleValue( value, total, offset ) { + const nextValue = value + offset; + if ( nextValue < 0 ) { + return total + nextValue; + } else if ( nextValue >= total ) { + return nextValue - total; + } + + return nextValue; +} + +class NavigableContainer extends Component { + constructor() { + super( ...arguments ); + this.onKeyDown = this.onKeyDown.bind( this ); + this.bindContainer = this.bindContainer.bind( this ); + + this.getFocusableContext = this.getFocusableContext.bind( this ); + this.getFocusableIndex = this.getFocusableIndex.bind( this ); + } + + bindContainer( ref ) { + const { forwardedRef } = this.props; + this.container = ref; + + if ( isFunction( forwardedRef ) ) { + forwardedRef( ref ); + } else if ( forwardedRef && 'current' in forwardedRef ) { + forwardedRef.current = ref; + } + } + + getFocusableContext( target ) { + const { onlyBrowserTabstops } = this.props; + const finder = onlyBrowserTabstops ? focus.tabbable : focus.focusable; + const focusables = finder.find( this.container ); + + const index = this.getFocusableIndex( focusables, target ); + if ( index > -1 && target ) { + return { index, target, focusables }; + } + return null; + } + + getFocusableIndex( focusables, target ) { + const directIndex = focusables.indexOf( target ); + if ( directIndex !== -1 ) { + return directIndex; + } + } + + onKeyDown( event ) { + if ( this.props.onKeyDown ) { + this.props.onKeyDown( event ); + } + + const { getFocusableContext } = this; + const { cycle = true, eventToOffset, onNavigate = noop, stopNavigationEvents } = this.props; + + const offset = eventToOffset( event ); + + // eventToOffset returns undefined if the event is not handled by the component + if ( offset !== undefined && stopNavigationEvents ) { + // Prevents arrow key handlers bound to the document directly interfering + event.nativeEvent.stopImmediatePropagation(); + + // When navigating a collection of items, prevent scroll containers + // from scrolling. + if ( event.target.getAttribute( 'role' ) === 'menuitem' ) { + event.preventDefault(); + } + + event.stopPropagation(); + } + + if ( ! offset ) { + return; + } + + const context = getFocusableContext( document.activeElement ); + if ( ! context ) { + return; + } + + const { index, focusables } = context; + const nextIndex = cycle ? cycleValue( index, focusables.length, offset ) : index + offset; + if ( nextIndex >= 0 && nextIndex < focusables.length ) { + focusables[ nextIndex ].focus(); + onNavigate( nextIndex, focusables[ nextIndex ] ); + } + } + + render() { + const { children, ...props } = this.props; + // Disable reason: Assumed role is applied by parent via props spread. + /* eslint-disable jsx-a11y/no-static-element-interactions */ + return ( +
+ { children } +
+ ); + } +} + +const forwardedNavigableContainer = ( props, ref ) => { + return ; +}; +forwardedNavigableContainer.displayName = 'NavigableContainer'; + +export default forwardRef( forwardedNavigableContainer ); diff --git a/packages/components/src/navigable-container/index.js b/packages/components/src/navigable-container/index.js index ce77b5bb82974..e98f1b1236d7b 100644 --- a/packages/components/src/navigable-container/index.js +++ b/packages/components/src/navigable-container/index.js @@ -1,193 +1,5 @@ /** - * External Dependencies + * Internal Dependencies */ -import { omit, noop, includes } from 'lodash'; - -/** - * WordPress Dependencies - */ -import { Component } from '@wordpress/element'; -import { focus } from '@wordpress/dom'; -import { UP, DOWN, LEFT, RIGHT, TAB } from '@wordpress/keycodes'; - -function cycleValue( value, total, offset ) { - const nextValue = value + offset; - if ( nextValue < 0 ) { - return total + nextValue; - } else if ( nextValue >= total ) { - return nextValue - total; - } - - return nextValue; -} - -class NavigableContainer extends Component { - constructor() { - super( ...arguments ); - this.bindContainer = this.bindContainer.bind( this ); - this.onKeyDown = this.onKeyDown.bind( this ); - - this.getFocusableContext = this.getFocusableContext.bind( this ); - this.getFocusableIndex = this.getFocusableIndex.bind( this ); - } - - bindContainer( ref ) { - this.container = ref; - } - - getFocusableContext( target ) { - const { onlyBrowserTabstops } = this.props; - const finder = onlyBrowserTabstops ? focus.tabbable : focus.focusable; - const focusables = finder.find( this.container ); - - const index = this.getFocusableIndex( focusables, target ); - if ( index > -1 && target ) { - return { index, target, focusables }; - } - return null; - } - - getFocusableIndex( focusables, target ) { - const directIndex = focusables.indexOf( target ); - if ( directIndex !== -1 ) { - return directIndex; - } - } - - onKeyDown( event ) { - if ( this.props.onKeyDown ) { - this.props.onKeyDown( event ); - } - - const { getFocusableContext } = this; - const { cycle = true, eventToOffset, onNavigate = noop, stopNavigationEvents } = this.props; - - const offset = eventToOffset( event ); - - // eventToOffset returns undefined if the event is not handled by the component - if ( offset !== undefined && stopNavigationEvents ) { - // Prevents arrow key handlers bound to the document directly interfering - event.nativeEvent.stopImmediatePropagation(); - - // When navigating a collection of items, prevent scroll containers - // from scrolling. - if ( event.target.getAttribute( 'role' ) === 'menuitem' ) { - event.preventDefault(); - } - - event.stopPropagation(); - } - - if ( ! offset ) { - return; - } - - const context = getFocusableContext( document.activeElement ); - if ( ! context ) { - return; - } - - const { index, focusables } = context; - const nextIndex = cycle ? cycleValue( index, focusables.length, offset ) : index + offset; - if ( nextIndex >= 0 && nextIndex < focusables.length ) { - focusables[ nextIndex ].focus(); - onNavigate( nextIndex, focusables[ nextIndex ] ); - } - } - - render() { - const { children, ...props } = this.props; - - // Disable reason: Assumed role is applied by parent via props spread. - /* eslint-disable jsx-a11y/no-static-element-interactions */ - return ( -
- { children } -
- ); - } -} - -export class NavigableMenu extends Component { - render() { - const { role = 'menu', orientation = 'vertical', ...rest } = this.props; - - const eventToOffset = ( evt ) => { - const { keyCode } = evt; - - let next = [ DOWN ]; - let previous = [ UP ]; - - if ( orientation === 'horizontal' ) { - next = [ RIGHT ]; - previous = [ LEFT ]; - } - - if ( orientation === 'both' ) { - next = [ RIGHT, DOWN ]; - previous = [ LEFT, UP ]; - } - - if ( includes( next, keyCode ) ) { - return 1; - } else if ( includes( previous, keyCode ) ) { - return -1; - } - }; - - return ( - - ); - } -} - -export class TabbableContainer extends Component { - render() { - const eventToOffset = ( evt ) => { - const { keyCode, shiftKey } = evt; - if ( TAB === keyCode ) { - return shiftKey ? -1 : 1; - } - - // Allow custom handling of keys besides Tab. - // - // By default, TabbableContainer will move focus forward on Tab and - // backward on Shift+Tab. The handler below will be used for all other - // events. The semantics for `this.props.eventToOffset`'s return - // values are the following: - // - // - +1: move focus forward - // - -1: move focus backward - // - 0: don't move focus, but acknowledge event and thus stop it - // - undefined: do nothing, let the event propagate - if ( this.props.eventToOffset ) { - return this.props.eventToOffset( evt ); - } - }; - - return ( - - ); - } -} +export { default as NavigableMenu } from './menu'; +export { default as TabbableContainer } from './tabbable'; diff --git a/packages/components/src/navigable-container/menu.js b/packages/components/src/navigable-container/menu.js new file mode 100644 index 0000000000000..869bde1706407 --- /dev/null +++ b/packages/components/src/navigable-container/menu.js @@ -0,0 +1,58 @@ +/** + * External Dependencies + */ +import { includes } from 'lodash'; + +/** + * WordPress Dependencies + */ +import { forwardRef } from '@wordpress/element'; +import { UP, DOWN, LEFT, RIGHT } from '@wordpress/keycodes'; + +/** + * Internal dependencies + */ +import NavigableContainer from './container'; + +export function NavigableMenu( { + role = 'menu', + orientation = 'vertical', + ...rest +}, ref ) { + const eventToOffset = ( evt ) => { + const { keyCode } = evt; + + let next = [ DOWN ]; + let previous = [ UP ]; + + if ( orientation === 'horizontal' ) { + next = [ RIGHT ]; + previous = [ LEFT ]; + } + + if ( orientation === 'both' ) { + next = [ RIGHT, DOWN ]; + previous = [ LEFT, UP ]; + } + + if ( includes( next, keyCode ) ) { + return 1; + } else if ( includes( previous, keyCode ) ) { + return -1; + } + }; + + return ( + + ); +} + +export default forwardRef( NavigableMenu ); diff --git a/packages/components/src/navigable-container/tabbable.js b/packages/components/src/navigable-container/tabbable.js new file mode 100644 index 0000000000000..3e1191d998e2d --- /dev/null +++ b/packages/components/src/navigable-container/tabbable.js @@ -0,0 +1,46 @@ +/** + * WordPress Dependencies + */ +import { forwardRef } from '@wordpress/element'; +import { TAB } from '@wordpress/keycodes'; + +/** + * Internal dependencies + */ +import NavigableContainer from './container'; + +export function TabbableContainer( { eventToOffset, ...props }, ref ) { + const innerEventToOffset = ( evt ) => { + const { keyCode, shiftKey } = evt; + if ( TAB === keyCode ) { + return shiftKey ? -1 : 1; + } + + // Allow custom handling of keys besides Tab. + // + // By default, TabbableContainer will move focus forward on Tab and + // backward on Shift+Tab. The handler below will be used for all other + // events. The semantics for `eventToOffset`'s return + // values are the following: + // + // - +1: move focus forward + // - -1: move focus backward + // - 0: don't move focus, but acknowledge event and thus stop it + // - undefined: do nothing, let the event propagate + if ( eventToOffset ) { + return eventToOffset( evt ); + } + }; + + return ( + + ); +} + +export default forwardRef( TabbableContainer ); diff --git a/packages/components/src/navigable-container/test/index.js b/packages/components/src/navigable-container/test/menu.js similarity index 70% rename from packages/components/src/navigable-container/test/index.js rename to packages/components/src/navigable-container/test/menu.js index 946ea1929d4f0..7dff49f321a13 100644 --- a/packages/components/src/navigable-container/test/index.js +++ b/packages/components/src/navigable-container/test/menu.js @@ -7,12 +7,12 @@ import { each } from 'lodash'; /** * WordPress dependencies */ -import { UP, DOWN, TAB, LEFT, RIGHT, SPACE } from '@wordpress/keycodes'; +import { UP, DOWN, LEFT, RIGHT, SPACE } from '@wordpress/keycodes'; /** * Internal dependencies */ -import { TabbableContainer, NavigableMenu } from '../'; +import { NavigableMenu } from '../menu'; function simulateVisible( wrapper, selector ) { const elements = wrapper.getDOMNode().querySelectorAll( selector ); @@ -251,84 +251,3 @@ describe( 'NavigableMenu', () => { assertKeyDown( SPACE, 0, false ); } ); } ); - -describe( 'TabbableContainer', () => { - it( 'should navigate by keypresses', () => { - let currentIndex = 0; - const wrapper = mount( ( - /* - Disabled because of our rule restricting literal IDs, preferring - `withInstanceId`. In this case, it's fine to use literal IDs. - */ - /* eslint-disable no-restricted-syntax */ - currentIndex = index }> -
Section One
-
Section Two
-
-
Section to not skip
-
-
Section Three
-
- /* eslint-enable no-restricted-syntax */ - ) ); - - simulateVisible( wrapper, '*' ); - - const container = wrapper.find( 'div.wrapper' ); - wrapper.getDOMNode().querySelector( '#section1' ).focus(); - - // Navigate options - function assertKeyDown( keyCode, shiftKey, expectedActiveIndex, expectedStop ) { - const interaction = fireKeyDown( container, keyCode, shiftKey ); - expect( currentIndex ).toBe( expectedActiveIndex ); - expect( interaction.stopped ).toBe( expectedStop ); - } - - assertKeyDown( TAB, false, 1, true ); - assertKeyDown( TAB, false, 2, true ); - assertKeyDown( TAB, false, 3, true ); - assertKeyDown( TAB, false, 0, true ); - assertKeyDown( TAB, true, 3, true ); - assertKeyDown( TAB, true, 2, true ); - assertKeyDown( TAB, true, 1, true ); - assertKeyDown( TAB, true, 0, true ); - assertKeyDown( SPACE, false, 0, false ); - } ); - - it( 'should navigate by keypresses and stop at edges', () => { - let currentIndex = 0; - const wrapper = mount( ( - /* - Disabled because of our rule restricting literal IDs, preferring - `withInstanceId`. In this case, it's fine to use literal IDs. - */ - /* eslint-disable no-restricted-syntax */ - currentIndex = index }> -
Section One
-
Section Two
-
Section Three
-
- /* eslint-enable no-restricted-syntax */ - ) ); - - simulateVisible( wrapper, '*' ); - - const container = wrapper.find( 'div.wrapper' ); - wrapper.getDOMNode().querySelector( '#section1' ).focus(); - - // Navigate options - function assertKeyDown( keyCode, shiftKey, expectedActiveIndex, expectedStop ) { - const interaction = fireKeyDown( container, keyCode, shiftKey ); - expect( currentIndex ).toBe( expectedActiveIndex ); - expect( interaction.stopped ).toBe( expectedStop ); - } - - assertKeyDown( TAB, false, 1, true ); - assertKeyDown( TAB, false, 2, true ); - assertKeyDown( TAB, false, 2, true ); - assertKeyDown( TAB, true, 1, true ); - assertKeyDown( TAB, true, 0, true ); - assertKeyDown( TAB, true, 0, true ); - assertKeyDown( SPACE, false, 0, false ); - } ); -} ); diff --git a/packages/components/src/navigable-container/test/tabbable.js b/packages/components/src/navigable-container/test/tabbable.js new file mode 100644 index 0000000000000..b56421df41bb3 --- /dev/null +++ b/packages/components/src/navigable-container/test/tabbable.js @@ -0,0 +1,123 @@ +/** + * External dependencies + */ +import { mount } from 'enzyme'; +import { each } from 'lodash'; + +/** + * WordPress dependencies + */ +import { TAB, SPACE } from '@wordpress/keycodes'; + +/** + * Internal dependencies + */ +import { TabbableContainer } from '../tabbable'; + +function simulateVisible( wrapper, selector ) { + const elements = wrapper.getDOMNode().querySelectorAll( selector ); + each( elements, ( elem ) => { + elem.getClientRects = () => [ 'trick-jsdom-into-having-size-for-element-rect' ]; + } ); +} + +function fireKeyDown( container, keyCode, shiftKey ) { + const interaction = { + stopped: false, + }; + + container.simulate( 'keydown', { + stopPropagation: () => { + interaction.stopped = true; + }, + preventDefault: () => {}, + nativeEvent: { + stopImmediatePropagation: () => {}, + }, + keyCode, + shiftKey, + } ); + + return interaction; +} + +describe( 'TabbableContainer', () => { + it( 'should navigate by keypresses', () => { + let currentIndex = 0; + const wrapper = mount( ( + /* + Disabled because of our rule restricting literal IDs, preferring + `withInstanceId`. In this case, it's fine to use literal IDs. + */ + /* eslint-disable no-restricted-syntax */ + currentIndex = index }> +
Section One
+
Section Two
+
+
Section to not skip
+
+
Section Three
+
+ /* eslint-enable no-restricted-syntax */ + ) ); + + simulateVisible( wrapper, '*' ); + + const container = wrapper.find( 'div.wrapper' ); + wrapper.getDOMNode().querySelector( '#section1' ).focus(); + + // Navigate options + function assertKeyDown( keyCode, shiftKey, expectedActiveIndex, expectedStop ) { + const interaction = fireKeyDown( container, keyCode, shiftKey ); + expect( currentIndex ).toBe( expectedActiveIndex ); + expect( interaction.stopped ).toBe( expectedStop ); + } + + assertKeyDown( TAB, false, 1, true ); + assertKeyDown( TAB, false, 2, true ); + assertKeyDown( TAB, false, 3, true ); + assertKeyDown( TAB, false, 0, true ); + assertKeyDown( TAB, true, 3, true ); + assertKeyDown( TAB, true, 2, true ); + assertKeyDown( TAB, true, 1, true ); + assertKeyDown( TAB, true, 0, true ); + assertKeyDown( SPACE, false, 0, false ); + } ); + + it( 'should navigate by keypresses and stop at edges', () => { + let currentIndex = 0; + const wrapper = mount( ( + /* + Disabled because of our rule restricting literal IDs, preferring + `withInstanceId`. In this case, it's fine to use literal IDs. + */ + /* eslint-disable no-restricted-syntax */ + currentIndex = index }> +
Section One
+
Section Two
+
Section Three
+
+ /* eslint-enable no-restricted-syntax */ + ) ); + + simulateVisible( wrapper, '*' ); + + const container = wrapper.find( 'div.wrapper' ); + wrapper.getDOMNode().querySelector( '#section1' ).focus(); + + // Navigate options + function assertKeyDown( keyCode, shiftKey, expectedActiveIndex, expectedStop ) { + const interaction = fireKeyDown( container, keyCode, shiftKey ); + expect( currentIndex ).toBe( expectedActiveIndex ); + expect( interaction.stopped ).toBe( expectedStop ); + } + + assertKeyDown( TAB, false, 1, true ); + assertKeyDown( TAB, false, 2, true ); + assertKeyDown( TAB, false, 2, true ); + assertKeyDown( TAB, true, 1, true ); + assertKeyDown( TAB, true, 0, true ); + assertKeyDown( TAB, true, 0, true ); + assertKeyDown( SPACE, false, 0, false ); + } ); +} ); diff --git a/packages/components/src/panel/test/__snapshots__/color.js.snap b/packages/components/src/panel/test/__snapshots__/color.js.snap index 81e55e37d94b4..96c1ee2e38f76 100644 --- a/packages/components/src/panel/test/__snapshots__/color.js.snap +++ b/packages/components/src/panel/test/__snapshots__/color.js.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`PanelColor should match snapshot when title is provided 1`] = ` -<[object Object] + { afterEach( () => { testDocument = null; - if ( wrapper ) { + if ( wrapper && wrapper.length ) { wrapper.unmount(); wrapper = null; } diff --git a/packages/compose/package.json b/packages/compose/package.json index 1d8cb3a6d2b12..237ff1af8dd1e 100644 --- a/packages/compose/package.json +++ b/packages/compose/package.json @@ -28,7 +28,7 @@ "lodash": "^4.17.10" }, "devDependencies": { - "enzyme": "^3.3.0", + "enzyme": "^3.7.0", "react-dom": "^16.4.1", "react-test-renderer": "^16.4.1" }, diff --git a/packages/data/package.json b/packages/data/package.json index b1c9624c3054a..49ab7df773ca0 100644 --- a/packages/data/package.json +++ b/packages/data/package.json @@ -35,7 +35,7 @@ }, "devDependencies": { "deep-freeze": "^0.0.1", - "enzyme": "^3.3.0", + "enzyme": "^3.7.0", "react-test-renderer": "^16.4.1" }, "publishConfig": { diff --git a/packages/edit-post/package.json b/packages/edit-post/package.json index 0044be9c1e8a4..7d26bc0de2cd9 100644 --- a/packages/edit-post/package.json +++ b/packages/edit-post/package.json @@ -45,7 +45,7 @@ }, "devDependencies": { "deep-freeze": "^0.0.1", - "enzyme": "^3.3.0" + "enzyme": "^3.7.0" }, "publishConfig": { "access": "public" 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 346d861e4581f..65d9ef3a52aaf 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 @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`KeyboardShortcutHelpModal should match snapshot when the modal is active 1`] = ` - + - + `; exports[`KeyboardShortcutHelpModal should match snapshot when the modal is not active 1`] = ` - + - + `; diff --git a/packages/editor/CHANGELOG.md b/packages/editor/CHANGELOG.md index b02d02f46ca0a..64475184f0431 100644 --- a/packages/editor/CHANGELOG.md +++ b/packages/editor/CHANGELOG.md @@ -4,6 +4,7 @@ - Remove `findDOMNode` usage from the `Inserter` component. - Remove `findDOMNode` usage from the `Block` component. +- Remove `findDOMNode` usage from the `NavigableToolbar` component. ## 6.1.0 (2018-10-30) diff --git a/packages/editor/package.json b/packages/editor/package.json index 82b583ba46091..a56ad455c1f75 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -61,7 +61,7 @@ }, "devDependencies": { "deep-freeze": "^0.0.1", - "enzyme": "^3.3.0", + "enzyme": "^3.7.0", "react-dom": "^16.4.1", "react-test-renderer": "^16.4.1" }, diff --git a/packages/editor/src/components/block-compare/test/__snapshots__/block-view.js.snap b/packages/editor/src/components/block-compare/test/__snapshots__/block-view.js.snap index f9cb16182490f..087e5187b1fa1 100644 --- a/packages/editor/src/components/block-compare/test/__snapshots__/block-view.js.snap +++ b/packages/editor/src/components/block-compare/test/__snapshots__/block-view.js.snap @@ -26,13 +26,13 @@ exports[`BlockView should match snapshot 1`] = `
- <[object Object] + action - +
`; diff --git a/packages/editor/src/components/navigable-toolbar/index.js b/packages/editor/src/components/navigable-toolbar/index.js index f631f17b3c830..803b9c1b4715d 100644 --- a/packages/editor/src/components/navigable-toolbar/index.js +++ b/packages/editor/src/components/navigable-toolbar/index.js @@ -7,7 +7,7 @@ import { cond, matchesProperty } from 'lodash'; * WordPress dependencies */ import { NavigableMenu, KeyboardShortcuts } from '@wordpress/components'; -import { Component, findDOMNode } from '@wordpress/element'; +import { Component, createRef } from '@wordpress/element'; import { focus } from '@wordpress/dom'; import { ESCAPE } from '@wordpress/keycodes'; @@ -21,24 +21,17 @@ class NavigableToolbar extends Component { constructor() { super( ...arguments ); - this.bindNode = this.bindNode.bind( this ); this.focusToolbar = this.focusToolbar.bind( this ); this.focusSelection = this.focusSelection.bind( this ); this.switchOnKeyDown = cond( [ [ matchesProperty( [ 'keyCode' ], ESCAPE ), this.focusSelection ], ] ); - } - - bindNode( ref ) { - // Disable reason: Need DOM node for finding first focusable element - // on keyboard interaction to shift to toolbar. - // eslint-disable-next-line react/no-find-dom-node - this.toolbar = findDOMNode( ref ); + this.toolbar = createRef(); } focusToolbar() { - const tabbables = focus.tabbable.find( this.toolbar ); + const tabbables = focus.tabbable.find( this.toolbar.current ); if ( tabbables.length ) { tabbables[ 0 ].focus(); } @@ -74,7 +67,7 @@ class NavigableToolbar extends Component { 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 7564cd036d689..fa697d260d195 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`] = ` -<[object Object] + - + `; 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`] = ` -<[object Object] + - + `; 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`] = ` -<[object Object] + - + `; diff --git a/packages/editor/src/components/post-saved-state/test/__snapshots__/index.js.snap b/packages/editor/src/components/post-saved-state/test/__snapshots__/index.js.snap index a36dbb35124ef..c881259580d2c 100644 --- a/packages/editor/src/components/post-saved-state/test/__snapshots__/index.js.snap +++ b/packages/editor/src/components/post-saved-state/test/__snapshots__/index.js.snap @@ -3,12 +3,12 @@ exports[`PostSavedState returns a switch to draft link if the post is published 1`] = ``; exports[`PostSavedState should return Save button if edits to be saved 1`] = ` -<[object Object] + Save Draft - + `; diff --git a/packages/editor/src/components/post-taxonomies/test/index.js b/packages/editor/src/components/post-taxonomies/test/index.js index e43f3d4f068a0..b7a941eb65801 100644 --- a/packages/editor/src/components/post-taxonomies/test/index.js +++ b/packages/editor/src/components/post-taxonomies/test/index.js @@ -48,7 +48,7 @@ describe( 'PostTaxonomies', () => { /> ); - expect( wrapperOne.at( 0 ) ).toHaveLength( 1 ); + expect( wrapperOne ).toHaveLength( 1 ); const wrapperTwo = shallow( { /> ); - expect( wrapperTwo.at( 0 ) ).toHaveLength( 2 ); + expect( wrapperTwo ).toHaveLength( 2 ); } ); it( 'should not render taxonomy components that hide their ui', () => { diff --git a/packages/element/package.json b/packages/element/package.json index 4d36e1a5418f9..efdf718f6370b 100644 --- a/packages/element/package.json +++ b/packages/element/package.json @@ -28,7 +28,7 @@ "react-dom": "^16.4.1" }, "devDependencies": { - "enzyme": "^3.3.0" + "enzyme": "^3.7.0" }, "publishConfig": { "access": "public" diff --git a/packages/jest-preset-default/package.json b/packages/jest-preset-default/package.json index 4befd16058b41..fb797c500079b 100644 --- a/packages/jest-preset-default/package.json +++ b/packages/jest-preset-default/package.json @@ -28,8 +28,8 @@ "dependencies": { "@wordpress/jest-console": "file:../jest-console", "babel-jest": "^23.4.2", - "enzyme": "^3.3.0", - "enzyme-adapter-react-16": "^1.1.1", + "enzyme": "^3.7.0", + "enzyme-adapter-react-16": "^1.6.0", "jest-enzyme": "^6.0.2" }, "peerDependencies": { diff --git a/packages/nux/package.json b/packages/nux/package.json index 015ed82e90a32..c7e52cf9efd4a 100644 --- a/packages/nux/package.json +++ b/packages/nux/package.json @@ -31,7 +31,7 @@ "rememo": "^3.0.0" }, "devDependencies": { - "enzyme": "^3.3.0" + "enzyme": "^3.7.0" }, "publishConfig": { "access": "public" From a1b647dccabc1f12a2aec8c0352e44c5c468fc8e Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Tue, 6 Nov 2018 09:45:35 +0100 Subject: [PATCH 006/106] Remove moment from the public API of the date module (#11418) * Remove moment from the public API of the date module * Remove the offset argument and leave the computation to the caller --- docs/reference/deprecated.md | 1 + packages/date/CHANGELOG.md | 8 +++++++- packages/date/src/index.js | 20 ++++++++++++++++++++ packages/editor/src/store/selectors.js | 13 +++++++------ packages/editor/src/store/test/selectors.js | 5 +++-- 5 files changed, 38 insertions(+), 9 deletions(-) diff --git a/docs/reference/deprecated.md b/docs/reference/deprecated.md index 0bf9117f287c3..282d8c20940d7 100644 --- a/docs/reference/deprecated.md +++ b/docs/reference/deprecated.md @@ -11,6 +11,7 @@ Gutenberg's deprecation policy is intended to support backwards-compatibility fo - `wp.data` `registry.registerSelectors` has been deprecated. Use `registry.registerStore` instead. - `wp.data` `registry.registerActions` has been deprecated. Use `registry.registerStore` instead. - `wp.data` `registry.registerResolvers` has been deprecated. Use `registry.registerStore` instead. +- `moment` has been removed from the public API for the date module. ## 4.3.0 diff --git a/packages/date/CHANGELOG.md b/packages/date/CHANGELOG.md index abad6aacd56b9..82c11f6a046cb 100644 --- a/packages/date/CHANGELOG.md +++ b/packages/date/CHANGELOG.md @@ -1,8 +1,14 @@ +## 2.2.0 (Unreleased) + +### Deprecations + +- Remove `moment` from the public API for the date module. + ## 2.1.0 (2018-10-29) ### Breaking Change -- Marked getSettings as experimental +- Marked getSettings as experimental ## 2.0.3 (2018-09-26) diff --git a/packages/date/src/index.js b/packages/date/src/index.js index 124f7238d4608..bcc1e4d32be6d 100644 --- a/packages/date/src/index.js +++ b/packages/date/src/index.js @@ -119,6 +119,12 @@ function setupWPTimezone() { // the attached timezone, instead of setting a default timezone on // the global moment object. export const moment = ( ...args ) => { + deprecated( 'wp.date.moment', { + version: '4.4', + alternative: 'the moment script as a dependency', + plugin: 'Gutenberg', + } ); + return momentLib.tz( ...args, 'WP' ); }; @@ -386,4 +392,18 @@ export function dateI18n( dateFormat, dateValue = new Date(), gmt = false ) { return format( dateFormat, dateMoment ); } +/** + * Check whether a date is considered in the future according to the WordPress settings. + * + * @param {(Date|string)} dateValue Date object or string. + * + * @return {boolean} Is in the future. + */ +export function isInTheFuture( dateValue ) { + const now = momentLib.tz( 'WP' ); + const momentObject = momentLib.tz( dateValue, 'WP' ); + + return momentObject.isAfter( now ); +} + setupWPTimezone(); diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index 1a868297469c2..b9964d28c1ce1 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -32,7 +32,7 @@ import { getFreeformContentHandlerName, isUnmodifiedDefaultBlock, } from '@wordpress/blocks'; -import { moment } from '@wordpress/date'; +import { isInTheFuture } from '@wordpress/date'; import { removep } from '@wordpress/autop'; import { select } from '@wordpress/data'; import deprecated from '@wordpress/deprecated'; @@ -54,6 +54,7 @@ export const INSERTER_UTILITY_NONE = 0; const MILLISECONDS_PER_HOUR = 3600 * 1000; const MILLISECONDS_PER_DAY = 24 * 3600 * 1000; const MILLISECONDS_PER_WEEK = 7 * 24 * 3600 * 1000; +const ONE_MINUTE_IN_MS = 60 * 1000; /** * Shared reference to an empty array for cases where it is important to avoid @@ -323,7 +324,7 @@ export function isCurrentPostPublished( state ) { const post = getCurrentPost( state ); return [ 'publish', 'private' ].indexOf( post.status ) !== -1 || - ( post.status === 'future' && moment( post.date ).isBefore( moment() ) ); + ( post.status === 'future' && ! isInTheFuture( new Date( Number( new Date( post.date ) ) + ONE_MINUTE_IN_MS ) ) ); } /** @@ -481,11 +482,11 @@ export function hasAutosave( state ) { * @return {boolean} Whether the post has been published. */ export function isEditedPostBeingScheduled( state ) { - const date = moment( getEditedPostAttribute( state, 'date' ) ); - // Adding 1 minute as an error threshold between the server and the client dates. - const now = moment().add( 1, 'minute' ); + const date = getEditedPostAttribute( state, 'date' ); + // Offset the date by one minute (network latency) + const checkedDate = new Date( Number( new Date( date ) ) + ONE_MINUTE_IN_MS ); - return date.isAfter( now ); + return isInTheFuture( checkedDate ); } /** diff --git a/packages/editor/src/store/test/selectors.js b/packages/editor/src/store/test/selectors.js index 2b516d5684328..bdaecf9327286 100644 --- a/packages/editor/src/store/test/selectors.js +++ b/packages/editor/src/store/test/selectors.js @@ -15,7 +15,6 @@ import { setDefaultBlockName, setFreeformContentHandlerName, } from '@wordpress/blocks'; -import { moment } from '@wordpress/date'; import { RawHTML } from '@wordpress/element'; /** @@ -1513,10 +1512,12 @@ describe( 'selectors', () => { describe( 'isEditedPostBeingScheduled', () => { it( 'should return true for posts with a future date', () => { + const time = Date.now() + ( 1000 * 3600 * 24 * 7 ); // 7 days in the future + const date = new Date( time ); const state = { editor: { present: { - edits: { date: moment().add( 7, 'days' ).format( '' ) }, + edits: { date: date.toUTCString() }, }, }, }; From ead88a14d64f24a7e98723a5f4d34e0935236c64 Mon Sep 17 00:00:00 2001 From: John Date: Tue, 6 Nov 2018 09:31:59 +0000 Subject: [PATCH 007/106] =?UTF-8?q?Don=E2=80=99t=20auto-select=20a=20pullq?= =?UTF-8?q?uote=20text=20colour=20for=20regular=20style=20(#10792)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Don’t auto-select a pullquote text colour for regular style Regular style uses the theme background, but the auto-selected colour assumes the main colour is the background. This can often lead to a very poor colour choice. * Only auto-set colour if setting the main colour This prevents clearing of the main colour from setting the text colour * Stop switch from solid to regular style still changing text colour If you picked the solid style and set a main colour and then switched to regular style and picked a main colour it would continue to change the text colour. This moves the wasTextColorAutomaticallyComputed check so this doesn’t happen --- packages/block-library/src/pullquote/edit.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/block-library/src/pullquote/edit.js b/packages/block-library/src/pullquote/edit.js index 2e16d06d48125..71d5dca4ce15d 100644 --- a/packages/block-library/src/pullquote/edit.js +++ b/packages/block-library/src/pullquote/edit.js @@ -33,9 +33,13 @@ class PullQuoteEdit extends Component { } pullQuoteMainColorSetter( colorValue ) { - const { colorUtils, textColor, setTextColor, setMainColor } = this.props; + const { colorUtils, textColor, setTextColor, setMainColor, className } = this.props; + const isSolidColorStyle = includes( className, SOLID_COLOR_CLASS ); + const needTextColor = ! textColor.color || this.wasTextColorAutomaticallyComputed; + const shouldSetTextColor = isSolidColorStyle && needTextColor && colorValue; + setMainColor( colorValue ); - if ( ! textColor.color || this.wasTextColorAutomaticallyComputed ) { + if ( shouldSetTextColor ) { this.wasTextColorAutomaticallyComputed = true; setTextColor( colorUtils.getMostReadableColor( colorValue ) ); } From 180ad3029fd0e8896663ff95eff87b0975266ca4 Mon Sep 17 00:00:00 2001 From: Matthew Riley MacPherson Date: Tue, 6 Nov 2018 10:05:25 +0000 Subject: [PATCH 008/106] feat: Add multi-block selection announcement (#11422) * feat: Add multi-block selection announcement Fixes #3696 * chore: Add test for multi-select speak() --- packages/editor/src/store/effects.js | 8 ++++++++ test/e2e/specs/multi-block-selection.test.js | 21 ++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/packages/editor/src/store/effects.js b/packages/editor/src/store/effects.js index dd4d8798ed198..42ffd3dd857a9 100644 --- a/packages/editor/src/store/effects.js +++ b/packages/editor/src/store/effects.js @@ -6,6 +6,7 @@ import { compact, last } from 'lodash'; /** * WordPress dependencies */ +import { speak } from '@wordpress/a11y'; import { parse, getBlockType, @@ -13,6 +14,7 @@ import { doBlocksMatchTemplate, synchronizeBlocksWithTemplate, } from '@wordpress/blocks'; +import { __, sprintf } from '@wordpress/i18n'; /** * Internal dependencies @@ -32,6 +34,7 @@ import { getBlockCount, getPreviousBlockClientId, getSelectedBlock, + getSelectedBlockCount, getTemplate, getTemplateLock, isValidTemplate, @@ -258,4 +261,9 @@ export default { REPLACE_BLOCKS: [ ensureDefaultBlock, ], + MULTI_SELECT: ( action, { getState } ) => { + const blockCount = getSelectedBlockCount( getState() ); + + speak( sprintf( __( '%s blocks selected.' ), blockCount ), 'assertive' ); + }, }; diff --git a/test/e2e/specs/multi-block-selection.test.js b/test/e2e/specs/multi-block-selection.test.js index 866b9465834a8..bd07dc8b099f8 100644 --- a/test/e2e/specs/multi-block-selection.test.js +++ b/test/e2e/specs/multi-block-selection.test.js @@ -117,4 +117,25 @@ describe( 'Multi-block selection', () => { // Verify selection await expectMultiSelected( blocks, true ); } ); + + it( 'should speak() number of blocks selected with multi-block selection', async () => { + await clickBlockAppender(); + await page.keyboard.type( 'First Paragraph' ); + await insertBlock( 'Paragraph' ); + await page.keyboard.type( 'Second Paragraph' ); + await insertBlock( 'Paragraph' ); + await page.keyboard.type( 'Third Paragraph' ); + + // Multiselect via keyboard. + await pressWithModifier( META_KEY, 'a' ); + await pressWithModifier( META_KEY, 'a' ); + + // TODO: It would be great to do this test by spying on `wp.a11y.speak`, + // but it's very difficult to do that because `wp.a11y` has + // DOM-dependant side-effect setup code and doesn't seem straightforward + // to mock. Instead, we check for the DOM node that `wp.a11y.speak()` + // inserts text into. + const speakTextContent = await page.$eval( '#a11y-speak-assertive', ( element ) => element.textContent ); + expect( speakTextContent.trim() ).toEqual( '3 blocks selected.' ); + } ); } ); From 08e3154cf72423afd5f26b5edd2818b23f3a1343 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Tue, 6 Nov 2018 11:14:49 +0100 Subject: [PATCH 009/106] Remove findDomNode from withHoverAreas by making a render prop component (#11407) * Remove findDomNode from withHoverAreas by making a render prop component * Small refactoring per review --- .../editor/src/components/block-list/block.js | 426 +++++++++--------- .../src/components/block-list/hover-area.js | 82 ++++ .../components/block-list/with-hover-areas.js | 69 --- 3 files changed, 300 insertions(+), 277 deletions(-) create mode 100644 packages/editor/src/components/block-list/hover-area.js delete mode 100644 packages/editor/src/components/block-list/with-hover-areas.js diff --git a/packages/editor/src/components/block-list/block.js b/packages/editor/src/components/block-list/block.js index 1d8561de8d913..8b656f0616944 100644 --- a/packages/editor/src/components/block-list/block.js +++ b/packages/editor/src/components/block-list/block.js @@ -46,7 +46,7 @@ import BlockInsertionPoint from './insertion-point'; import IgnoreNestedEvents from '../ignore-nested-events'; import InserterWithShortcuts from '../inserter-with-shortcuts'; import Inserter from '../inserter'; -import withHoverAreas from './with-hover-areas'; +import HoverArea from './hover-area'; import { isInsideRootBlock } from '../../utils/dom'; export class BlockListBlock extends Component { @@ -104,6 +104,11 @@ export class BlockListBlock extends Component { setBlockListRef( node ) { this.wrapperNode = node; this.props.blockRef( node, this.props.clientId ); + + // We need to rerender to trigger a rerendering of HoverArea + // it depents on this.wrapperNode but we can't keep this.wrapperNode in state + // Because we need it to be immediately availeble for `focusableTabbable` to work. + this.forceUpdate(); } bindBlockNode( node ) { @@ -353,219 +358,225 @@ export class BlockListBlock extends Component { } render() { - const { - block, - order, - mode, - isFocusMode, - hasFixedToolbar, - isLocked, - isFirst, - isLast, - clientId, - rootClientId, - isSelected, - isPartOfMultiSelection, - isFirstMultiSelected, - isTypingWithinBlock, - isCaretWithinFormattedText, - isMultiSelecting, - hoverArea, - isEmptyDefaultBlock, - isMovable, - isPreviousBlockADefaultEmptyBlock, - isParentOfSelectedBlock, - isDraggable, - } = this.props; - const isHovered = this.state.isHovered && ! isMultiSelecting; - const { name: blockName, isValid } = block; - const blockType = getBlockType( blockName ); - // translators: %s: Type of block (i.e. Text, Image etc) - const blockLabel = sprintf( __( 'Block: %s' ), blockType.title ); - // The block as rendered in the editor is composed of general block UI - // (mover, toolbar, wrapper) and the display of the block content. - - const isUnregisteredBlock = block.name === getUnregisteredTypeHandlerName(); - - // If the block is selected and we're typing the block should not appear. - // Empty paragraph blocks should always show up as unselected. - const showEmptyBlockSideInserter = ( isSelected || isHovered ) && isEmptyDefaultBlock && isValid; - const showSideInserter = ( isSelected || isHovered ) && isEmptyDefaultBlock; - const shouldAppearSelected = ! isFocusMode && ! hasFixedToolbar && ! showSideInserter && isSelected && ! isTypingWithinBlock; - const shouldAppearHovered = ! isFocusMode && ! hasFixedToolbar && isHovered && ! isEmptyDefaultBlock; - // We render block movers and block settings to keep them tabbale even if hidden - const shouldRenderMovers = ! isFocusMode && ( isSelected || hoverArea === 'left' ) && ! showEmptyBlockSideInserter && ! isMultiSelecting && ! isPartOfMultiSelection && ! isTypingWithinBlock; - const shouldShowBreadcrumb = ! isFocusMode && isHovered && ! isEmptyDefaultBlock; - const shouldShowContextualToolbar = ! hasFixedToolbar && ! showSideInserter && ( ( isSelected && ( ! isTypingWithinBlock || isCaretWithinFormattedText ) ) || isFirstMultiSelected ); - const shouldShowMobileToolbar = shouldAppearSelected; - const { error, dragging } = this.state; - - // 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. - // Generate the wrapper class names handling the different states of the block. - const wrapperClassName = classnames( 'wp-block editor-block-list__block', { - 'has-warning': ! isValid || !! error || isUnregisteredBlock, - 'is-selected': shouldAppearSelected, - 'is-multi-selected': isPartOfMultiSelection, - 'is-hovered': shouldAppearHovered, - 'is-reusable': isReusableBlock( blockType ), - 'is-dragging': dragging, - 'is-typing': isTypingWithinBlock, - 'is-focused': isFocusMode && ( isSelected || isParentOfSelectedBlock ), - 'is-focus-mode': isFocusMode, - } ); - - const { onReplace } = this.props; - - // Determine whether the block has props to apply to the wrapper. - let wrapperProps = this.props.wrapperProps; - if ( blockType.getEditWrapperProps ) { - wrapperProps = { - ...wrapperProps, - ...blockType.getEditWrapperProps( block.attributes ), - }; - } - const blockElementId = `block-${ clientId }`; - - // We wrap the BlockEdit component in a div that hides it when editing in - // HTML mode. This allows us to render all of the ancillary pieces - // (InspectorControls, etc.) which are inside `BlockEdit` but not - // `BlockHTML`, even in HTML mode. - let blockEdit = ( - - ); - if ( mode !== 'visual' ) { - blockEdit =
{ blockEdit }
; - } - - // Disable reasons: - // - // jsx-a11y/mouse-events-have-key-events: - // - onMouseOver is explicitly handling hover effects - // - // jsx-a11y/no-static-element-interactions: - // - Each block can be selected by clicking on it - - /* eslint-disable jsx-a11y/mouse-events-have-key-events, jsx-a11y/no-static-element-interactions, jsx-a11y/onclick-has-role, jsx-a11y/click-events-have-key-events */ return ( - - { shouldShowInsertionPoint && ( - - ) } - - { shouldRenderMovers && ( - - ) } - { shouldShowBreadcrumb && ( - - ) } - { shouldShowContextualToolbar && } - { isFirstMultiSelected && ( - - ) } - - - { isValid && blockEdit } - { isValid && mode === 'html' && ( - - ) } - { ! isValid && [ - , -
- { getSaveElement( blockType, block.attributes ) } -
, - ] } -
- { shouldShowMobileToolbar && ( - + { ( { hoverArea } ) => { + const { + block, + order, + mode, + isFocusMode, + hasFixedToolbar, + isLocked, + isFirst, + isLast, + clientId, + rootClientId, + isSelected, + isPartOfMultiSelection, + isFirstMultiSelected, + isTypingWithinBlock, + isCaretWithinFormattedText, + isMultiSelecting, + isEmptyDefaultBlock, + isMovable, + isPreviousBlockADefaultEmptyBlock, + isParentOfSelectedBlock, + isDraggable, + } = this.props; + const isHovered = this.state.isHovered && ! isMultiSelecting; + const { name: blockName, isValid } = block; + const blockType = getBlockType( blockName ); + // translators: %s: Type of block (i.e. Text, Image etc) + const blockLabel = sprintf( __( 'Block: %s' ), blockType.title ); + // The block as rendered in the editor is composed of general block UI + // (mover, toolbar, wrapper) and the display of the block content. + + const isUnregisteredBlock = block.name === getUnregisteredTypeHandlerName(); + + // If the block is selected and we're typing the block should not appear. + // Empty paragraph blocks should always show up as unselected. + const showEmptyBlockSideInserter = ( isSelected || isHovered ) && isEmptyDefaultBlock && isValid; + const showSideInserter = ( isSelected || isHovered ) && isEmptyDefaultBlock; + const shouldAppearSelected = ! isFocusMode && ! hasFixedToolbar && ! showSideInserter && isSelected && ! isTypingWithinBlock; + const shouldAppearHovered = ! isFocusMode && ! hasFixedToolbar && isHovered && ! isEmptyDefaultBlock; + // We render block movers and block settings to keep them tabbale even if hidden + const shouldRenderMovers = ! isFocusMode && ( isSelected || hoverArea === 'left' ) && ! showEmptyBlockSideInserter && ! isMultiSelecting && ! isPartOfMultiSelection && ! isTypingWithinBlock; + const shouldShowBreadcrumb = ! isFocusMode && isHovered && ! isEmptyDefaultBlock; + const shouldShowContextualToolbar = ! hasFixedToolbar && ! showSideInserter && ( ( isSelected && ( ! isTypingWithinBlock || isCaretWithinFormattedText ) ) || isFirstMultiSelected ); + const shouldShowMobileToolbar = shouldAppearSelected; + const { error, dragging } = this.state; + + // 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. + // Generate the wrapper class names handling the different states of the block. + const wrapperClassName = classnames( 'wp-block editor-block-list__block', { + 'has-warning': ! isValid || !! error || isUnregisteredBlock, + 'is-selected': shouldAppearSelected, + 'is-multi-selected': isPartOfMultiSelection, + 'is-hovered': shouldAppearHovered, + 'is-reusable': isReusableBlock( blockType ), + 'is-dragging': dragging, + 'is-typing': isTypingWithinBlock, + 'is-focused': isFocusMode && ( isSelected || isParentOfSelectedBlock ), + 'is-focus-mode': isFocusMode, + } ); + + const { onReplace } = this.props; + + // Determine whether the block has props to apply to the wrapper. + let wrapperProps = this.props.wrapperProps; + if ( blockType.getEditWrapperProps ) { + wrapperProps = { + ...wrapperProps, + ...blockType.getEditWrapperProps( block.attributes ), + }; + } + const blockElementId = `block-${ clientId }`; + + // We wrap the BlockEdit component in a div that hides it when editing in + // HTML mode. This allows us to render all of the ancillary pieces + // (InspectorControls, etc.) which are inside `BlockEdit` but not + // `BlockHTML`, even in HTML mode. + let blockEdit = ( + - ) } - { !! error && } -
- { showEmptyBlockSideInserter && ( - -
- { blockEdit }
; + } + + // Disable reasons: + // + // jsx-a11y/mouse-events-have-key-events: + // - onMouseOver is explicitly handling hover effects + // + // jsx-a11y/no-static-element-interactions: + // - Each block can be selected by clicking on it + + /* eslint-disable jsx-a11y/mouse-events-have-key-events, jsx-a11y/no-static-element-interactions, jsx-a11y/onclick-has-role, jsx-a11y/click-events-have-key-events */ + + return ( + + { shouldShowInsertionPoint && ( + + ) } + - -
- -
-
- ) } -
+ { shouldRenderMovers && ( + + ) } + { shouldShowBreadcrumb && ( + + ) } + { shouldShowContextualToolbar && } + { isFirstMultiSelected && ( + + ) } + + + { isValid && blockEdit } + { isValid && mode === 'html' && ( + + ) } + { ! isValid && [ + , +
+ { getSaveElement( blockType, block.attributes ) } +
, + ] } +
+ { shouldShowMobileToolbar && ( + + ) } + { !! error && } +
+ { showEmptyBlockSideInserter && ( + +
+ +
+
+ +
+
+ ) } + + ); + /* eslint-enable jsx-a11y/no-static-element-interactions, jsx-a11y/onclick-has-role, jsx-a11y/click-events-have-key-events */ + } } + ); - /* eslint-enable jsx-a11y/no-static-element-interactions, jsx-a11y/onclick-has-role, jsx-a11y/click-events-have-key-events */ } } @@ -676,5 +687,4 @@ export default compose( applyWithSelect, applyWithDispatch, withFilters( 'editor.BlockListBlock' ), - withHoverAreas, )( BlockListBlock ); diff --git a/packages/editor/src/components/block-list/hover-area.js b/packages/editor/src/components/block-list/hover-area.js new file mode 100644 index 0000000000000..ff0a16144b4f0 --- /dev/null +++ b/packages/editor/src/components/block-list/hover-area.js @@ -0,0 +1,82 @@ +/** + * WordPress dependencies + */ +import { Component } from '@wordpress/element'; +import { withSelect } from '@wordpress/data'; + +class HoverArea extends Component { + constructor() { + super( ...arguments ); + this.state = { + hoverArea: null, + }; + this.onMouseLeave = this.onMouseLeave.bind( this ); + this.onMouseMove = this.onMouseMove.bind( this ); + } + + componentWillUnmount() { + if ( this.props.container ) { + this.toggleListeners( this.props.container, false ); + } + } + + componentDidMount() { + if ( this.props.container ) { + this.toggleListeners( this.props.container ); + } + } + + componentDidUpdate( prevProps ) { + if ( prevProps.container === this.props.container ) { + return; + } + if ( prevProps.container ) { + this.toggleListeners( prevProps.container, false ); + } + if ( this.props.container ) { + this.toggleListeners( this.props.container, true ); + } + } + + toggleListeners( container, shouldListnerToEvents = true ) { + const method = shouldListnerToEvents ? 'addEventListener' : 'removeEventListener'; + container[ method ]( 'mousemove', this.onMouseMove ); + container[ method ]( 'mouseleave', this.onMouseLeave ); + } + + onMouseLeave() { + if ( this.state.hoverArea ) { + this.setState( { hoverArea: null } ); + } + } + + onMouseMove( event ) { + const { isRTL, container } = this.props; + const { width, left, right } = container.getBoundingClientRect(); + + let hoverArea = null; + if ( ( event.clientX - left ) < width / 3 ) { + hoverArea = isRTL ? 'right' : 'left'; + } else if ( ( right - event.clientX ) < width / 3 ) { + hoverArea = isRTL ? 'left' : 'right'; + } + + if ( hoverArea !== this.state.hoverArea ) { + this.setState( { hoverArea } ); + } + } + + render() { + const { hoverArea } = this.state; + const { children } = this.props; + + return children( { hoverArea } ); + } +} + +export default withSelect( ( select ) => { + return { + isRTL: select( 'core/editor' ).getEditorSettings().isRTL, + }; +} )( HoverArea ); + diff --git a/packages/editor/src/components/block-list/with-hover-areas.js b/packages/editor/src/components/block-list/with-hover-areas.js deleted file mode 100644 index d5f51f6e1bfd8..0000000000000 --- a/packages/editor/src/components/block-list/with-hover-areas.js +++ /dev/null @@ -1,69 +0,0 @@ -/** - * WordPress dependencies - */ -import { Component, findDOMNode } from '@wordpress/element'; -import { withSelect } from '@wordpress/data'; -import { createHigherOrderComponent } from '@wordpress/compose'; - -const withHoverAreas = createHigherOrderComponent( ( WrappedComponent ) => { - class WithHoverAreasComponent extends Component { - constructor() { - super( ...arguments ); - this.state = { - hoverArea: null, - }; - this.onMouseLeave = this.onMouseLeave.bind( this ); - this.onMouseMove = this.onMouseMove.bind( this ); - } - - componentDidMount() { - // Disable reason: We use findDOMNode to avoid unnecessary extra dom Nodes - // eslint-disable-next-line react/no-find-dom-node - this.container = findDOMNode( this ); - this.container.addEventListener( 'mousemove', this.onMouseMove ); - this.container.addEventListener( 'mouseleave', this.onMouseLeave ); - } - - componentWillUnmount() { - this.container.removeEventListener( 'mousemove', this.onMouseMove ); - this.container.removeEventListener( 'mouseleave', this.onMouseLeave ); - } - - onMouseLeave() { - if ( this.state.hoverArea ) { - this.setState( { hoverArea: null } ); - } - } - - onMouseMove( event ) { - const { isRTL } = this.props; - const { width, left, right } = this.container.getBoundingClientRect(); - - let hoverArea = null; - if ( ( event.clientX - left ) < width / 3 ) { - hoverArea = isRTL ? 'right' : 'left'; - } else if ( ( right - event.clientX ) < width / 3 ) { - hoverArea = isRTL ? 'left' : 'right'; - } - - if ( hoverArea !== this.state.hoverArea ) { - this.setState( { hoverArea } ); - } - } - - render() { - const { hoverArea } = this.state; - return ( - - ); - } - } - - return withSelect( ( select ) => { - return { - isRTL: select( 'core/editor' ).getEditorSettings().isRTL, - }; - } )( WithHoverAreasComponent ); -} ); - -export default withHoverAreas; From 0829025dbab0aa3eb89152f822d7780b6d372610 Mon Sep 17 00:00:00 2001 From: Joen Asmussen Date: Tue, 6 Nov 2018 11:25:41 +0100 Subject: [PATCH 010/106] Disable fixed cover attachment on iOS (#11480) This fixes #4133. Sadly, Mobile Safari does not support properly the fixed attachment background. Interestingly, the implementation on Android is also buggy. This PR leaves Android alone, hoping it'll one day work, but explicitly disables it on Mobile Safari. --- packages/block-library/src/cover/style.scss | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/block-library/src/cover/style.scss b/packages/block-library/src/cover/style.scss index 00a3ab812ed25..294118094ed84 100644 --- a/packages/block-library/src/cover/style.scss +++ b/packages/block-library/src/cover/style.scss @@ -55,6 +55,13 @@ &.has-parallax { background-attachment: fixed; + + // Mobile Safari does not support fixed background attachment properly. + // See also https://stackoverflow.com/questions/24154666/background-size-cover-not-working-on-ios + // Chrome on Android does not appear to support the attachment at all: https://issuetracker.google.com/issues/36908439 + @supports (-webkit-overflow-scrolling: touch) { + background-attachment: scroll; + } } &.has-background-dim::before { From 1f40e1e1e7b20651ac09ebc74dbb672b039e1521 Mon Sep 17 00:00:00 2001 From: Jorge Date: Tue, 6 Nov 2018 10:38:18 +0000 Subject: [PATCH 011/106] Fix: Translator comments are not included in gutenberg-translations file for quote and pullquote placeholders. (#11514) --- packages/block-library/src/pullquote/edit.js | 12 ++++++++---- packages/block-library/src/quote/index.js | 12 ++++++++---- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/packages/block-library/src/pullquote/edit.js b/packages/block-library/src/pullquote/edit.js index 71d5dca4ce15d..eb7b2b4e0bbd8 100644 --- a/packages/block-library/src/pullquote/edit.js +++ b/packages/block-library/src/pullquote/edit.js @@ -88,15 +88,19 @@ class PullQuoteEdit extends Component { value: nextValue, } ) } - /* translators: placeholder text used for the quote */ - placeholder={ __( 'Write quote…' ) } + placeholder={ + // translators: placeholder text used for the quote + __( 'Write quote…' ) + } wrapperClassName="block-library-pullquote__content" /> { ( ! RichText.isEmpty( citation ) || isSelected ) && ( setAttributes( { citation: nextCitation, diff --git a/packages/block-library/src/quote/index.js b/packages/block-library/src/quote/index.js index f99f725b245d6..67cf5cc67692a 100644 --- a/packages/block-library/src/quote/index.js +++ b/packages/block-library/src/quote/index.js @@ -215,8 +215,10 @@ export const settings = { onReplace( [] ); } } } - /* translators: placeholder text used for the quote */ - placeholder={ __( 'Write quote…' ) } + placeholder={ + // translators: placeholder text used for the quote + __( 'Write quote…' ) + } /> { ( ! RichText.isEmpty( citation ) || isSelected ) && ( ) } From db8ef74f8bcd0c87d09925defbaf7fd7309a4ea4 Mon Sep 17 00:00:00 2001 From: Jorge Date: Tue, 6 Nov 2018 10:40:32 +0000 Subject: [PATCH 012/106] Fix: Show generic upload errors coming from the server in the image block (#11435) The image block was not showing generic upload errors. This issue is a regression introduced when the edition state was added. When an error happens we should go back to the edition state otherwise the error is not shown. This PR makes sure when an upload error happens the image block goes back to the edition state, and the error is shown to the user. This problem was not affecting the other blocks I tested (audio, video, gallery, cover). ## How has this been tested? I created a post and added an image block. I added a PHP error (appended a random string) in the index.php of WordPress to make the return error 500 on all uploads. I uploaded an image, and I checked we correctly display an error for the user. ## Screenshots Before: screenshot 2018-11-02 at 19 22 14 After: screenshot 2018-11-02 at 19 21 05 --- packages/block-library/src/image/edit.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/block-library/src/image/edit.js b/packages/block-library/src/image/edit.js index fa675533d775d..4d0e4c8dd3d2c 100644 --- a/packages/block-library/src/image/edit.js +++ b/packages/block-library/src/image/edit.js @@ -100,6 +100,7 @@ class ImageEdit extends Component { this.onSetCustomHref = this.onSetCustomHref.bind( this ); this.onSetLinkDestination = this.onSetLinkDestination.bind( this ); this.toggleIsEditing = this.toggleIsEditing.bind( this ); + this.onUploadError = this.onUploadError.bind( this ); this.state = { captionFocused: false, @@ -141,6 +142,14 @@ class ImageEdit extends Component { } } + onUploadError( message ) { + const { noticeOperations } = this.props; + noticeOperations.createErrorNotice( message ); + this.setState( { + isEditing: true, + } ); + } + onSelectImage( media ) { if ( ! media || ! media.url ) { this.props.setAttributes( { @@ -274,7 +283,6 @@ class ImageEdit extends Component { isSelected, className, maxWidth, - noticeOperations, noticeUI, toggleSelection, isRTL, @@ -339,7 +347,7 @@ class ImageEdit extends Component { onSelect={ this.onSelectImage } onSelectURL={ this.onSelectURL } notices={ noticeUI } - onError={ noticeOperations.createErrorNotice } + onError={ this.onUploadError } accept="image/*" allowedTypes={ ALLOWED_MEDIA_TYPES } value={ { id, src } } From 394be086fa83e205a5bce7385d588745967bf5a5 Mon Sep 17 00:00:00 2001 From: Nicola Heald Date: Tue, 6 Nov 2018 11:25:54 +0000 Subject: [PATCH 013/106] Request mocking helpers for e2e tests (#11250) --- test/e2e/specs/demo.test.js | 60 ++++---------- test/e2e/specs/embedding.test.js | 67 ++++++++-------- test/e2e/support/utils.js | 129 +++++++++++++++++++++++++++++++ 3 files changed, 176 insertions(+), 80 deletions(-) diff --git a/test/e2e/specs/demo.test.js b/test/e2e/specs/demo.test.js index eb2b7ec3e2014..a7d5f3dee95a5 100644 --- a/test/e2e/specs/demo.test.js +++ b/test/e2e/specs/demo.test.js @@ -1,12 +1,7 @@ -/** - * External dependencies - */ -import fetch from 'node-fetch'; - /** * Internal dependencies */ -import { visitAdmin } from '../support/utils'; +import { visitAdmin, matchURL, setUpResponseMocking, mockOrTransform } from '../support/utils'; const MOCK_VIMEO_RESPONSE = { url: 'https://vimeo.com/22439234', @@ -17,47 +12,22 @@ const MOCK_VIMEO_RESPONSE = { version: '1.0', }; +const couldNotBePreviewed = ( embedObject ) => { + return 'Embed Handler' === embedObject.provider_name; +}; + +const stripIframeFromEmbed = ( embedObject ) => { + return { ...embedObject, html: embedObject.html.replace( /src=[^\s]+/, '' ) }; +}; + describe( 'new editor state', () => { beforeAll( async () => { - // Intercept embed requests so that scripts loaded from third parties - // cannot leave errors in the console and cause the test to fail. - await page.setRequestInterception( true ); - page.on( 'request', async ( request ) => { - if ( request.url().indexOf( 'oembed%2F1.0%2Fproxy' ) !== -1 ) { - // Because we can't get the responses to requests and modify them on the fly, - // we have to make our own request, get the response, modify it, then use the - // modified values to respond to the request. - const response = await fetch( - request.url(), - { - headers: request.headers(), - method: request.method(), - body: request.postData(), - } - ); - const preview = await response.json(); - let responseBody; - if ( 'Embed Handler' === preview.provider_name ) { - // If Vimeo is down, that would make this test fail. So if we get a - // response from 'Embed Handler' instead of 'Vimeo', respond with a mock - // response, so we don't hold up development while Vimeo is down. - responseBody = MOCK_VIMEO_RESPONSE; - } else { - // Remove the first src attribute. This stops the Vimeo iframe loading the actual - // embedded content, but the height and width are preserved so layout related - // attributes, like aspect ratio CSS classes, remain the same. - preview.html = preview.html.replace( /src=[^\s]+/, '' ); - responseBody = preview; - } - request.respond( { - content: 'application/json', - body: JSON.stringify( responseBody ), - } ); - } else { - request.continue(); - } - } ); - + setUpResponseMocking( [ + { + match: matchURL( 'oembed%2F1.0%2Fproxy' ), + onRequestMatch: mockOrTransform( couldNotBePreviewed, MOCK_VIMEO_RESPONSE, stripIframeFromEmbed ), + }, + ] ); await visitAdmin( 'post-new.php', 'gutenberg-demo' ); } ); diff --git a/test/e2e/specs/embedding.test.js b/test/e2e/specs/embedding.test.js index 0284580d0c3aa..79f7d27707b0b 100644 --- a/test/e2e/specs/embedding.test.js +++ b/test/e2e/specs/embedding.test.js @@ -1,7 +1,7 @@ /** * Internal dependencies */ -import { clickBlockAppender, newPost } from '../support/utils'; +import { clickBlockAppender, newPost, isEmbedding, setUpResponseMocking, JSONResponse } from '../support/utils'; const MOCK_EMBED_WORDPRESS_SUCCESS_RESPONSE = { url: 'https://wordpress.org/gutenberg/handbook/block-api/attributes/', @@ -51,39 +51,36 @@ const MOCK_BAD_WORDPRESS_RESPONSE = { html: false, }; -const MOCK_RESPONSES = { - 'https://wordpress.org/gutenberg/handbook/': MOCK_BAD_WORDPRESS_RESPONSE, - 'https://wordpress.org/gutenberg/handbook/block-api/attributes/': MOCK_EMBED_WORDPRESS_SUCCESS_RESPONSE, - 'https://www.youtube.com/watch?v=lXMskKTw3Bc': MOCK_EMBED_VIDEO_SUCCESS_RESPONSE, - 'https://cloudup.com/cQFlxqtY4ob': MOCK_EMBED_RICH_SUCCESS_RESPONSE, - 'https://twitter.com/notnownikki': MOCK_EMBED_RICH_SUCCESS_RESPONSE, - 'https://twitter.com/thatbunty': MOCK_BAD_EMBED_PROVIDER_RESPONSE, - 'https://twitter.com/wooyaygutenberg123454312': MOCK_CANT_EMBED_RESPONSE, -}; - -const setupEmbedRequestInterception = async () => { - // Intercept successful embed requests so that scripts loaded from third parties - // cannot leave errors in the console and cause the test to fail. - await page.setRequestInterception( true ); - page.on( 'request', async ( request ) => { - const requestUrl = request.url(); - const isEmbeddingUrl = -1 !== requestUrl.indexOf( 'oembed%2F1.0%2Fproxy' ); - if ( isEmbeddingUrl ) { - const embedUrl = decodeURIComponent( /.*url=([^&]+).*/.exec( requestUrl )[ 1 ] ); - const mockResponse = MOCK_RESPONSES[ embedUrl ]; - if ( undefined !== mockResponse ) { - request.respond( { - content: 'application/json', - body: JSON.stringify( mockResponse ), - } ); - } else { - request.continue(); - } - } else { - request.continue(); - } - } ); -}; +const MOCK_RESPONSES = [ + { + match: isEmbedding( 'https://wordpress.org/gutenberg/handbook/' ), + onRequestMatch: JSONResponse( MOCK_BAD_WORDPRESS_RESPONSE ), + }, + { + match: isEmbedding( 'https://wordpress.org/gutenberg/handbook/block-api/attributes/' ), + onRequestMatch: JSONResponse( MOCK_EMBED_WORDPRESS_SUCCESS_RESPONSE ), + }, + { + match: isEmbedding( 'https://www.youtube.com/watch?v=lXMskKTw3Bc' ), + onRequestMatch: JSONResponse( MOCK_EMBED_VIDEO_SUCCESS_RESPONSE ), + }, + { + match: isEmbedding( 'https://cloudup.com/cQFlxqtY4ob' ), + onRequestMatch: JSONResponse( MOCK_EMBED_RICH_SUCCESS_RESPONSE ), + }, + { + match: isEmbedding( 'https://twitter.com/notnownikki' ), + onRequestMatch: JSONResponse( MOCK_EMBED_RICH_SUCCESS_RESPONSE ), + }, + { + match: isEmbedding( 'https://twitter.com/thatbunty' ), + onRequestMatch: JSONResponse( MOCK_BAD_EMBED_PROVIDER_RESPONSE ), + }, + { + match: isEmbedding( 'https://twitter.com/wooyaygutenberg123454312' ), + onRequestMatch: JSONResponse( MOCK_CANT_EMBED_RESPONSE ), + }, +]; const addEmbeds = async () => { await newPost(); @@ -139,7 +136,7 @@ const addEmbeds = async () => { }; const setUp = async () => { - await setupEmbedRequestInterception(); + await setUpResponseMocking( MOCK_RESPONSES ); await addEmbeds(); }; diff --git a/test/e2e/support/utils.js b/test/e2e/support/utils.js index ee7380de6cf19..a6ae59f1a2221 100644 --- a/test/e2e/support/utils.js +++ b/test/e2e/support/utils.js @@ -14,6 +14,7 @@ const { WP_USERNAME = 'admin', WP_PASSWORD = 'password', } = process.env; +import fetch from 'node-fetch'; /** * Platform-specific meta key. @@ -444,3 +445,131 @@ export function observeFocusLoss() { } ); } ); } + +/** + * Creates a function to determine if a request is embedding a certain URL. + * + * @param {string} url The URL to check against a request. + * @return {function} Function that determines if a request is for the embed API, embedding a specific URL. + */ +export function isEmbedding( url ) { + return ( request ) => matchURL( 'oembed%2F1.0%2Fproxy' )( request ) && parameterEquals( 'url', url )( request ); +} + +/** + * Respond to a request with a JSON response. + * + * @param {string} mockResponse The mock object to wrap in a JSON response. + * @return {Promise} Promise that responds to a request with the mock JSON response. + */ +export function JSONResponse( mockResponse ) { + return async ( request ) => request.respond( getJSONResponse( mockResponse ) ); +} + +/** + * Creates a function to determine if a request is calling a URL with the substring present. + * + * @param {string} substring The substring to check for. + * @return {function} Function that determines if a request's URL contains substring. + */ +export function matchURL( substring ) { + return ( request ) => -1 !== request.url().indexOf( substring ); +} + +/** + * Creates a function to determine if a request has a parameter with a certain value. + * + * @param {string} parameterName The query parameter to check. + * @param {string} value The value to check for. + * @return {function} Function that determines if a request's query parameter is the specified value. + */ +export function parameterEquals( parameterName, value ) { + return ( request ) => { + const url = request.url(); + const match = new RegExp( `.*${ parameterName }=([^&]+).*` ).exec( url ); + if ( ! match ) { + return false; + } + return value === decodeURIComponent( match[ 1 ] ); + }; +} + +/** + * Get a JSON response for the passed in object, for use with `request.respond`. + * + * @param {Object} obj Object to seralise for response. + * @return {Object} Response for use with `request.respond`. + */ +export function getJSONResponse( obj ) { + return { + content: 'application/json', + body: JSON.stringify( obj ), + }; +} + +/** + * Mocks a request with the supplied mock object, or allows it to run with an optional transform, based on the + * deserialised JSON response for the request. + * + * @param {function} mockCheck function that returns true if the request should be mocked. + * @param {Object} mock A mock object to wrap in a JSON response, if the request should be mocked. + * @param {function|undefined} responseObjectTransform An optional function that transforms the response's object before the response is used. + * @return {Promise} Promise that uses `mockCheck` to see if a request should be mocked with `mock`, and optionally transforms the response with `responseObjectTransform`. + */ +export function mockOrTransform( mockCheck, mock, responseObjectTransform = ( obj ) => obj ) { + return async ( request ) => { + // Because we can't get the responses to requests and modify them on the fly, + // we have to make our own request and get the response, then apply the + // optional transform to the json encoded object. + const response = await fetch( + request.url(), + { + headers: request.headers(), + method: request.method(), + body: request.postData(), + } + ); + const responseObject = await response.json(); + if ( mockCheck( responseObject ) ) { + request.respond( getJSONResponse( mock ) ); + } else { + request.respond( getJSONResponse( responseObjectTransform( responseObject ) ) ); + } + }; +} + +/** + * Sets up mock checks and responses. Accepts a list of mock settings with the following properties: + * - match: function to check if a request should be mocked. + * - onRequestMatch: async function to respond to the request. + * + * Example: + * const MOCK_RESPONSES = [ + * { + * match: isEmbedding( 'https://wordpress.org/gutenberg/handbook/' ), + * onRequestMatch: JSONResponse( MOCK_BAD_WORDPRESS_RESPONSE ), + * }, + * { + * match: isEmbedding( 'https://wordpress.org/gutenberg/handbook/block-api/attributes/' ), + * onRequestMatch: JSONResponse( MOCK_EMBED_WORDPRESS_SUCCESS_RESPONSE ), + * } + * ]; + * setUpResponseMocking( MOCK_RESPONSES ); + * + * If none of the mock settings match the request, the request is allowed to continue. + * + * @param {Array} mocks Array of mock settings. + */ +export async function setUpResponseMocking( mocks ) { + await page.setRequestInterception( true ); + page.on( 'request', async ( request ) => { + for ( let i = 0; i < mocks.length; i++ ) { + const mock = mocks[ i ]; + if ( mock.match( request ) ) { + await mock.onRequestMatch( request ); + return; + } + } + request.continue(); + } ); +} From 8ab9fa67b503be4809c1b1f030a6ca6ace16447b Mon Sep 17 00:00:00 2001 From: Jorge Date: Tue, 6 Nov 2018 11:31:05 +0000 Subject: [PATCH 014/106] Fix: Embeds: Clicking on a WordPress embed does not select the block. (#11217) --- .../block-library/src/embed/embed-preview.js | 10 ++-- .../src/embed/wp-embed-preview.js | 53 +++++++++++++++++++ 2 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 packages/block-library/src/embed/wp-embed-preview.js diff --git a/packages/block-library/src/embed/embed-preview.js b/packages/block-library/src/embed/embed-preview.js index b3da26ce75752..2282ed04809a2 100644 --- a/packages/block-library/src/embed/embed-preview.js +++ b/packages/block-library/src/embed/embed-preview.js @@ -18,6 +18,11 @@ import { __, sprintf } from '@wordpress/i18n'; import { Placeholder, SandBox } from '@wordpress/components'; import { RichText, BlockIcon } from '@wordpress/editor'; +/** + * Internal dependencies + */ +import WpEmbedPreview from './wp-embed-preview'; + const EmbedPreview = ( props ) => { const { preview, url, type, caption, onCaptionChange, isSelected, className, icon, label } = props; const { scripts } = preview; @@ -30,9 +35,8 @@ const EmbedPreview = ( props ) => { const sandboxClassnames = classnames( type, className, 'wp-block-embed__wrapper' ); const embedWrapper = 'wp-embed' === type ? ( -
) : (
diff --git a/packages/block-library/src/embed/wp-embed-preview.js b/packages/block-library/src/embed/wp-embed-preview.js new file mode 100644 index 0000000000000..ccfb0449819b8 --- /dev/null +++ b/packages/block-library/src/embed/wp-embed-preview.js @@ -0,0 +1,53 @@ +/** + * WordPress dependencies + */ +import { Component, createRef } from '@wordpress/element'; +import { withGlobalEvents } from '@wordpress/compose'; + +/** + * Browser dependencies + */ + +const { FocusEvent } = window; + +class WpEmbedPreview extends Component { + constructor() { + super( ...arguments ); + + this.checkFocus = this.checkFocus.bind( this ); + this.node = createRef(); + } + + /** + * Checks whether the wp embed iframe is the activeElement, + * if it is dispatch a focus event. + */ + checkFocus() { + const { activeElement } = document; + + if ( + activeElement.tagName !== 'IFRAME' || + activeElement.parentNode !== this.node.current + ) { + return; + } + + const focusEvent = new FocusEvent( 'focus', { bubbles: true } ); + activeElement.dispatchEvent( focusEvent ); + } + + render() { + const { html } = this.props; + return ( +
+ ); + } +} + +export default withGlobalEvents( { + blur: 'checkFocus', +} )( WpEmbedPreview ); From e6fb93e7b5ef7f23050dd1aeb533176d10b663db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s?= Date: Tue, 6 Nov 2018 13:10:08 +0100 Subject: [PATCH 015/106] Post publish panel: focus the post link (#11489) --- .../src/components/post-publish-panel/index.js | 12 +++++++----- .../post-publish-panel/postpublish.js | 11 +++++++++-- .../test/__snapshots__/index.js.snap | 8 ++++++-- test/e2e/specs/publish-panel.test.js | 18 +++++++++++++++++- 4 files changed, 39 insertions(+), 10 deletions(-) diff --git a/packages/editor/src/components/post-publish-panel/index.js b/packages/editor/src/components/post-publish-panel/index.js index 4d41e872bc723..e6d324624fab0 100644 --- a/packages/editor/src/components/post-publish-panel/index.js +++ b/packages/editor/src/components/post-publish-panel/index.js @@ -60,12 +60,14 @@ export class PostPublishPanel extends Component { PrePublishExtension, ...additionalProps } = this.props; - const isPublishedOrScheduled = isPublished || ( isScheduled && isBeingScheduled ); const propsForPanel = omit( additionalProps, [ 'hasPublishAction', 'isDirty' ] ); + const isPublishedOrScheduled = isPublished || ( isScheduled && isBeingScheduled ); + const isPrePublish = ! isPublishedOrScheduled && ! isSaving; + const isPostPublish = isPublishedOrScheduled && ! isSaving; return (
- { isPublishedOrScheduled && ! isSaving ? ( + { isPostPublish ? (
{ isScheduled ? __( 'Scheduled' ) : __( 'Published' ) }
@@ -83,13 +85,13 @@ export class PostPublishPanel extends Component { />
- { ! isSaving && ! isPublishedOrScheduled && ( + { isPrePublish && ( { PrePublishExtension && } ) } - { ! isSaving && isPublishedOrScheduled && ( - + { isPostPublish && ( + { PostPublishExtension && } ) } diff --git a/packages/editor/src/components/post-publish-panel/postpublish.js b/packages/editor/src/components/post-publish-panel/postpublish.js index 24b26e1439cad..0e0cc7f81862b 100644 --- a/packages/editor/src/components/post-publish-panel/postpublish.js +++ b/packages/editor/src/components/post-publish-panel/postpublish.js @@ -8,7 +8,7 @@ import { get } from 'lodash'; */ import { PanelBody, Button, ClipboardButton, TextControl } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; -import { Component, Fragment } from '@wordpress/element'; +import { Component, Fragment, createRef } from '@wordpress/element'; import { withSelect } from '@wordpress/data'; /** @@ -24,6 +24,13 @@ class PostPublishPanelPostpublish extends Component { }; this.onCopy = this.onCopy.bind( this ); this.onSelectInput = this.onSelectInput.bind( this ); + this.postLink = createRef(); + } + + componentDidMount() { + if ( this.props.focusOnMount ) { + this.postLink.current.focus(); + } } componentWillUnmount() { @@ -59,7 +66,7 @@ class PostPublishPanelPostpublish extends Component { return (
- { post.title || __( '(no title)' ) } { postPublishNonLinkHeader } + { post.title || __( '(no title)' ) } { postPublishNonLinkHeader }

diff --git a/packages/editor/src/components/post-publish-panel/test/__snapshots__/index.js.snap b/packages/editor/src/components/post-publish-panel/test/__snapshots__/index.js.snap index 81a53981d74d7..574787b931a8d 100644 --- a/packages/editor/src/components/post-publish-panel/test/__snapshots__/index.js.snap +++ b/packages/editor/src/components/post-publish-panel/test/__snapshots__/index.js.snap @@ -21,7 +21,9 @@ exports[`PostPublishPanel should render the post-publish panel if the post is pu

- +
- +
{ @@ -23,7 +24,7 @@ describe( 'PostPublishPanel', () => { } } ); - it( 'publish button should have focus', async () => { + it( 'PrePublish: publish button should have the focus', async () => { await page.type( '.editor-post-title__input', 'E2E Test Post' ); await openPublishPanel(); @@ -33,6 +34,21 @@ describe( 'PostPublishPanel', () => { expect( focusedElementClassList ).toContain( 'editor-post-publish-button' ); } ); + it( 'PostPublish: post link should have the focus', async () => { + const postTitle = 'E2E Test Post'; + await page.type( '.editor-post-title__input', postTitle ); + await publishPost(); + + const focusedElementTag = await page.$eval( ':focus', ( focusedElement ) => { + return focusedElement.tagName.toLowerCase(); + } ); + const focusedElementText = await page.$eval( ':focus', ( focusedElement ) => { + return focusedElement.text; + } ); + expect( focusedElementTag ).toBe( 'a' ); + expect( focusedElementText ).toBe( postTitle ); + } ); + it( 'should retain focus within the panel', async () => { await page.type( '.editor-post-title__input', 'E2E Test Post' ); await openPublishPanel(); From 7041e49031cf4b2091df45703b8129a52faac973 Mon Sep 17 00:00:00 2001 From: Sofia Sousa Date: Tue, 6 Nov 2018 12:25:04 +0000 Subject: [PATCH 016/106] fix: Media on the right option in MediaText block (#11534) What happens is: When we select 'Show media on right' option, the editor shows it correctly. ![screen shot 2018-11-06 at 12 09 23](https://user-images.githubusercontent.com/15556410/48063445-fb3d4d00-e1bc-11e8-9dd3-dced72878984.png) But on the front end, media is shown on the left. ![screen shot 2018-11-06 at 12 09 41](https://user-images.githubusercontent.com/15556410/48063466-0b552c80-e1bd-11e8-9ec2-8c21bad017b1.png) This PR fixes the css rule. --- packages/block-library/src/media-text/style.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-library/src/media-text/style.scss b/packages/block-library/src/media-text/style.scss index b6a87cb6cd066..a2c8ed6375221 100644 --- a/packages/block-library/src/media-text/style.scss +++ b/packages/block-library/src/media-text/style.scss @@ -7,7 +7,7 @@ align-items: center; grid-template-areas: "media-text-media media-text-content"; grid-template-columns: 50% auto; - .has-media-on-the-right { + &.has-media-on-the-right { grid-template-areas: "media-text-content media-text-media"; grid-template-columns: auto 50%; } From 95f064039165c881e0f9e2821027a7fca1de2847 Mon Sep 17 00:00:00 2001 From: Dion Hulse Date: Tue, 6 Nov 2018 22:30:38 +1000 Subject: [PATCH 017/106] Fetch all terms instead of just the first 100. Relies upon the fetch-all middleware. (#11524) --- .../editor/src/components/post-taxonomies/flat-term-selector.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 7e2a322d04284..c42e7e15ffbd2 100644 --- a/packages/editor/src/components/post-taxonomies/flat-term-selector.js +++ b/packages/editor/src/components/post-taxonomies/flat-term-selector.js @@ -44,7 +44,7 @@ class FlatTermSelector extends Component { this.setState( { loading: false } ); this.initRequest = this.fetchTerms( { include: this.props.terms.join( ',' ), - per_page: 100, + per_page: -1, } ); this.initRequest.then( () => { From 9195550b60e074b29984c0b630f860c62dca4db6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20Van=C2=A0Dorpe?= Date: Tue, 6 Nov 2018 16:55:12 +0100 Subject: [PATCH 018/106] Import Format API components instead of passing as props (#11545) * Import Format API components instead of passing as props * Include button title in inserter search * Simplify isResult * RichTextInserterListItem => RichTextInserterItem --- packages/editor/src/components/index.js | 7 +- .../src/components/rich-text/format-edit.js | 89 +------------------ .../editor/src/components/rich-text/index.js | 3 + .../rich-text/inserter-list-item.js | 38 ++++++++ .../src/components/rich-text/shortcut.js | 32 +++++++ .../components/rich-text/toolbar-button.js | 27 ++++++ packages/format-library/src/bold/index.js | 7 +- packages/format-library/src/code/index.js | 16 ++-- packages/format-library/src/image/index.js | 7 +- packages/format-library/src/italic/index.js | 7 +- packages/format-library/src/link/index.js | 15 ++-- .../format-library/src/strikethrough/index.js | 7 +- 12 files changed, 139 insertions(+), 116 deletions(-) create mode 100644 packages/editor/src/components/rich-text/inserter-list-item.js create mode 100644 packages/editor/src/components/rich-text/shortcut.js create mode 100644 packages/editor/src/components/rich-text/toolbar-button.js diff --git a/packages/editor/src/components/index.js b/packages/editor/src/components/index.js index 73684795468ae..3cce6ea3cd87a 100644 --- a/packages/editor/src/components/index.js +++ b/packages/editor/src/components/index.js @@ -19,7 +19,12 @@ export { default as InspectorControls } from './inspector-controls'; export { default as PanelColor } from './panel-color'; export { default as PanelColorSettings } from './panel-color-settings'; export { default as PlainText } from './plain-text'; -export { default as RichText } from './rich-text'; +export { + default as RichText, + RichTextShortcut, + RichTextToolbarButton, + RichTextInserterItem, +} from './rich-text'; export { default as ServerSideRender } from './server-side-render'; export { default as MediaPlaceholder } from './media-placeholder'; export { default as MediaUpload } from './media-upload'; diff --git a/packages/editor/src/components/rich-text/format-edit.js b/packages/editor/src/components/rich-text/format-edit.js index a0c021e60e259..7505ed1f81a3c 100644 --- a/packages/editor/src/components/rich-text/format-edit.js +++ b/packages/editor/src/components/rich-text/format-edit.js @@ -1,91 +1,14 @@ /** * WordPress dependencies */ -import { normalizeIconObject } from '@wordpress/blocks'; import { withSelect } from '@wordpress/data'; -import { Component, Fragment } from '@wordpress/element'; +import { Fragment } from '@wordpress/element'; import { getActiveFormat } from '@wordpress/rich-text'; -import { Fill, KeyboardShortcuts, ToolbarButton } from '@wordpress/components'; -import { rawShortcut, displayShortcut } from '@wordpress/keycodes'; - -/** - * Internal dependencies - */ -import InserterListItem from '../inserter-list-item'; -import { normalizeTerm } from '../inserter/menu'; - -function isResult( { title, keywords = [] }, filterValue ) { - const normalizedSearchTerm = normalizeTerm( filterValue ); - const matchSearch = ( string ) => normalizeTerm( string ).indexOf( normalizedSearchTerm ) !== -1; - return matchSearch( title ) || keywords.some( matchSearch ); -} - -function FillToolbarButton( { name, shortcutType, shortcutCharacter, ...props } ) { - let shortcut; - let fillName = 'RichText.ToolbarControls'; - - if ( name ) { - fillName += `.${ name }`; - } - - if ( shortcutType && shortcutCharacter ) { - shortcut = displayShortcut[ shortcutType ]( shortcutCharacter ); - } - - return ( - - - - ); -} - -function FillInserterListItem( props ) { - return ( - - { ( { filterValue } ) => { - if ( filterValue && ! isResult( props, filterValue ) ) { - return null; - } - - return ; - } } - - ); -} - -class Shortcut extends Component { - constructor() { - super( ...arguments ); - - this.onUse = this.onUse.bind( this ); - } - - onUse() { - this.props.onUse(); - return false; - } - - render() { - const { character, type } = this.props; - - return ( - - ); - } -} const FormatEdit = ( { formatTypes, onChange, value } ) => { return ( - { formatTypes.map( ( { name, edit: Edit, keywords } ) => { + { formatTypes.map( ( { name, edit: Edit } ) => { if ( ! Edit ) { return null; } @@ -101,14 +24,6 @@ const FormatEdit = ( { formatTypes, onChange, value } ) => { activeAttributes={ activeAttributes } value={ value } onChange={ onChange } - ToolbarButton={ FillToolbarButton } - InserterListItem={ ( props ) => - - } - Shortcut={ Shortcut } /> ); } ) } diff --git a/packages/editor/src/components/rich-text/index.js b/packages/editor/src/components/rich-text/index.js index 04837e079e93f..26e3b97a2b93b 100644 --- a/packages/editor/src/components/rich-text/index.js +++ b/packages/editor/src/components/rich-text/index.js @@ -1016,3 +1016,6 @@ RichTextContainer.Content.defaultProps = { }; export default RichTextContainer; +export { RichTextShortcut } from './shortcut'; +export { RichTextToolbarButton } from './toolbar-button'; +export { RichTextInserterItem } from './inserter-list-item'; diff --git a/packages/editor/src/components/rich-text/inserter-list-item.js b/packages/editor/src/components/rich-text/inserter-list-item.js new file mode 100644 index 0000000000000..82ff6d8b8b446 --- /dev/null +++ b/packages/editor/src/components/rich-text/inserter-list-item.js @@ -0,0 +1,38 @@ +/** + * WordPress dependencies + */ +import { normalizeIconObject } from '@wordpress/blocks'; +import { Fill } from '@wordpress/components'; +import { withSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import InserterListItem from '../inserter-list-item'; +import { normalizeTerm } from '../inserter/menu'; + +function isResult( keywords, filterValue ) { + return keywords.some( ( string ) => + normalizeTerm( string ).indexOf( normalizeTerm( filterValue ) ) !== -1 + ); +} + +export const RichTextInserterItem = withSelect( ( select, { name } ) => ( { + formatType: select( 'core/rich-text' ).getFormatType( name ), +} ) )( ( props ) => { + return ( + + { ( { filterValue } ) => { + const { keywords = [], title } = props.formatType; + + keywords.push( title, props.title ); + + if ( filterValue && ! isResult( keywords, filterValue ) ) { + return null; + } + + return ; + } } + + ); +} ); diff --git a/packages/editor/src/components/rich-text/shortcut.js b/packages/editor/src/components/rich-text/shortcut.js new file mode 100644 index 0000000000000..ade272214b6ba --- /dev/null +++ b/packages/editor/src/components/rich-text/shortcut.js @@ -0,0 +1,32 @@ +/** + * WordPress dependencies + */ +import { Component } from '@wordpress/element'; +import { KeyboardShortcuts } from '@wordpress/components'; +import { rawShortcut } from '@wordpress/keycodes'; + +export class RichTextShortcut extends Component { + constructor() { + super( ...arguments ); + + this.onUse = this.onUse.bind( this ); + } + + onUse() { + this.props.onUse(); + return false; + } + + render() { + const { character, type } = this.props; + + return ( + + ); + } +} diff --git a/packages/editor/src/components/rich-text/toolbar-button.js b/packages/editor/src/components/rich-text/toolbar-button.js new file mode 100644 index 0000000000000..2febd82e717c5 --- /dev/null +++ b/packages/editor/src/components/rich-text/toolbar-button.js @@ -0,0 +1,27 @@ +/** + * WordPress dependencies + */ +import { Fill, ToolbarButton } from '@wordpress/components'; +import { displayShortcut } from '@wordpress/keycodes'; + +export function RichTextToolbarButton( { name, shortcutType, shortcutCharacter, ...props } ) { + let shortcut; + let fillName = 'RichText.ToolbarControls'; + + if ( name ) { + fillName += `.${ name }`; + } + + if ( shortcutType && shortcutCharacter ) { + shortcut = displayShortcut[ shortcutType ]( shortcutCharacter ); + } + + return ( + + + + ); +} diff --git a/packages/format-library/src/bold/index.js b/packages/format-library/src/bold/index.js index cf86b264fa5b7..72c4133f4591d 100644 --- a/packages/format-library/src/bold/index.js +++ b/packages/format-library/src/bold/index.js @@ -4,6 +4,7 @@ import { __ } from '@wordpress/i18n'; import { Fragment } from '@wordpress/element'; import { toggleFormat } from '@wordpress/rich-text'; +import { RichTextToolbarButton, RichTextShortcut } from '@wordpress/editor'; const name = 'core/bold'; @@ -13,17 +14,17 @@ export const bold = { match: { tagName: 'strong', }, - edit( { isActive, value, onChange, ToolbarButton, Shortcut } ) { + edit( { isActive, value, onChange } ) { const onToggle = () => onChange( toggleFormat( value, { type: name } ) ); return ( - - onChange( toggleFormat( value, { type: name } ) ); return ( - - - + ); }, }; diff --git a/packages/format-library/src/image/index.js b/packages/format-library/src/image/index.js index aad07328b5c2c..01016cf7ac8ff 100644 --- a/packages/format-library/src/image/index.js +++ b/packages/format-library/src/image/index.js @@ -5,7 +5,7 @@ import { Path, SVG } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { Fragment, Component } from '@wordpress/element'; import { insertObject } from '@wordpress/rich-text'; -import { MediaUpload } from '@wordpress/editor'; +import { MediaUpload, RichTextInserterItem } from '@wordpress/editor'; const ALLOWED_MEDIA_TYPES = [ 'image' ]; @@ -44,11 +44,12 @@ export const image = { } render() { - const { value, onChange, InserterListItem } = this.props; + const { value, onChange } = this.props; return ( - } title={ __( 'Inline Image' ) } onClick={ this.openModal } diff --git a/packages/format-library/src/italic/index.js b/packages/format-library/src/italic/index.js index 1da241988fadf..6b8bd50dd4240 100644 --- a/packages/format-library/src/italic/index.js +++ b/packages/format-library/src/italic/index.js @@ -4,6 +4,7 @@ import { __ } from '@wordpress/i18n'; import { Fragment } from '@wordpress/element'; import { toggleFormat } from '@wordpress/rich-text'; +import { RichTextToolbarButton, RichTextShortcut } from '@wordpress/editor'; const name = 'core/italic'; @@ -13,17 +14,17 @@ export const italic = { match: { tagName: 'em', }, - edit( { isActive, value, onChange, ToolbarButton, Shortcut } ) { + edit( { isActive, value, onChange } ) { const onToggle = () => onChange( toggleFormat( value, { type: name } ) ); return ( - - - - - - - { isActive && } - { ! isActive && onChange( toggleFormat( value, { type: name } ) ); return ( - - Date: Tue, 6 Nov 2018 18:16:42 +0200 Subject: [PATCH 019/106] RNmobile: Trigger media library picker (#11511) * Trigger parent app to launch media library picker * Update Image url via an inline callback * Add RichText.isEmpty check for caption to avoid crash on an undefined value The caption field is optional so it is valid to have it undefined due to GB web. --- packages/block-library/src/image/edit.native.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/block-library/src/image/edit.native.js b/packages/block-library/src/image/edit.native.js index 5f517f72b678e..d339d077eb572 100644 --- a/packages/block-library/src/image/edit.native.js +++ b/packages/block-library/src/image/edit.native.js @@ -2,11 +2,12 @@ * External dependencies */ import { View, Image, TextInput } from 'react-native'; +import RNReactNativeGutenbergBridge from 'react-native-gutenberg-bridge'; /** * Internal dependencies */ -import { MediaPlaceholder } from '@wordpress/editor'; +import { MediaPlaceholder, RichText } from '@wordpress/editor'; export default function ImageEdit( props ) { const { attributes, isSelected, setAttributes } = props; @@ -19,9 +20,9 @@ export default function ImageEdit( props ) { }; const onMediaLibraryPress = () => { - // This method should present an image picker from - // the WordPress media library. - //TODO: Implement media library method. + // Call onMediaLibraryPress from the Native<->RN bridge. It should trigger an image picker from + // the WordPress media library and call the provided callback to set the image URL. + RNReactNativeGutenbergBridge.onMediaLibraryPress( ( mediaUrl ) => setAttributes( { url: mediaUrl } ) ); }; if ( ! url ) { @@ -40,7 +41,7 @@ export default function ImageEdit( props ) { resizeMethod="scale" source={ { uri: url } } /> - { ( caption.length > 0 || isSelected ) && ( + { ( ! RichText.isEmpty( caption ) > 0 || isSelected ) && ( Date: Tue, 6 Nov 2018 19:07:18 +0100 Subject: [PATCH 020/106] Rich Text: Fix Format Type Assignment During Parsing (#11488) --- packages/format-library/src/bold/index.js | 5 +- packages/format-library/src/code/index.js | 5 +- packages/format-library/src/image/index.js | 5 +- packages/format-library/src/italic/index.js | 5 +- packages/format-library/src/link/index.js | 5 +- .../format-library/src/strikethrough/index.js | 5 +- packages/rich-text/src/create.js | 27 +++-- .../rich-text/src/register-format-type.js | 49 ++++++++ packages/rich-text/src/store/selectors.js | 34 ++++++ packages/rich-text/src/test/create.js | 26 +++- packages/rich-text/src/test/helpers/index.js | 77 ++++++++++++ .../src/test/register-format-type.js | 112 ++++++++++++++++++ packages/rich-text/src/test/to-html-string.js | 25 ++++ packages/rich-text/src/to-tree.js | 26 ++-- 14 files changed, 367 insertions(+), 39 deletions(-) create mode 100644 packages/rich-text/src/test/register-format-type.js diff --git a/packages/format-library/src/bold/index.js b/packages/format-library/src/bold/index.js index 72c4133f4591d..dd236edf66798 100644 --- a/packages/format-library/src/bold/index.js +++ b/packages/format-library/src/bold/index.js @@ -11,9 +11,8 @@ const name = 'core/bold'; export const bold = { name, title: __( 'Bold' ), - match: { - tagName: 'strong', - }, + tagName: 'strong', + className: null, edit( { isActive, value, onChange } ) { const onToggle = () => onChange( toggleFormat( value, { type: name } ) ); diff --git a/packages/format-library/src/code/index.js b/packages/format-library/src/code/index.js index a0e5fb2f891e0..d5e0b2705f245 100644 --- a/packages/format-library/src/code/index.js +++ b/packages/format-library/src/code/index.js @@ -10,9 +10,8 @@ const name = 'core/code'; export const code = { name, title: __( 'Code' ), - match: { - tagName: 'code', - }, + tagName: 'code', + className: null, edit( { value, onChange } ) { const onToggle = () => onChange( toggleFormat( value, { type: name } ) ); diff --git a/packages/format-library/src/image/index.js b/packages/format-library/src/image/index.js index 01016cf7ac8ff..7deae883d3b3b 100644 --- a/packages/format-library/src/image/index.js +++ b/packages/format-library/src/image/index.js @@ -16,9 +16,8 @@ export const image = { title: __( 'Image' ), keywords: [ __( 'photo' ), __( 'media' ) ], object: true, - match: { - tagName: 'img', - }, + tagName: 'img', + className: null, attributes: { className: 'class', style: 'style', diff --git a/packages/format-library/src/italic/index.js b/packages/format-library/src/italic/index.js index 6b8bd50dd4240..3677c6e82acc9 100644 --- a/packages/format-library/src/italic/index.js +++ b/packages/format-library/src/italic/index.js @@ -11,9 +11,8 @@ const name = 'core/italic'; export const italic = { name, title: __( 'Italic' ), - match: { - tagName: 'em', - }, + tagName: 'em', + className: null, edit( { isActive, value, onChange } ) { const onToggle = () => onChange( toggleFormat( value, { type: name } ) ); diff --git a/packages/format-library/src/link/index.js b/packages/format-library/src/link/index.js index a5ab91c076bbb..a3ffc2c6b27f0 100644 --- a/packages/format-library/src/link/index.js +++ b/packages/format-library/src/link/index.js @@ -23,9 +23,8 @@ const name = 'core/link'; export const link = { name, title: __( 'Link' ), - match: { - tagName: 'a', - }, + tagName: 'a', + className: null, attributes: { url: 'href', target: 'target', diff --git a/packages/format-library/src/strikethrough/index.js b/packages/format-library/src/strikethrough/index.js index 3d3310434a21a..eba22e7a64f32 100644 --- a/packages/format-library/src/strikethrough/index.js +++ b/packages/format-library/src/strikethrough/index.js @@ -11,9 +11,8 @@ const name = 'core/strikethrough'; export const strikethrough = { name, title: __( 'Strikethrough' ), - match: { - tagName: 'del', - }, + tagName: 'del', + className: null, edit( { isActive, value, onChange } ) { const onToggle = () => onChange( toggleFormat( value, { type: name } ) ); diff --git a/packages/rich-text/src/create.js b/packages/rich-text/src/create.js index 39028ddc2ff8d..ec5a7eb175938 100644 --- a/packages/rich-text/src/create.js +++ b/packages/rich-text/src/create.js @@ -1,8 +1,7 @@ /** - * External dependencies + * WordPress dependencies */ - -import { find } from 'lodash'; +import { select } from '@wordpress/data'; /** * Internal dependencies @@ -11,7 +10,6 @@ import { find } from 'lodash'; 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, @@ -36,9 +34,24 @@ function simpleFindKey( object, value ) { } function toFormat( { type, attributes } ) { - const formatType = find( getFormatTypes(), ( { match } ) => - type === match.tagName - ); + let formatType; + + if ( attributes && attributes.class ) { + formatType = select( 'core/rich-text' ).getFormatTypeForClassName( attributes.class ); + + if ( formatType ) { + // Preserve any additional classes. + attributes.class = ` ${ attributes.class } `.replace( ` ${ formatType.className } `, ' ' ).trim(); + + if ( ! attributes.class ) { + delete attributes.class; + } + } + } + + if ( ! formatType ) { + formatType = select( 'core/rich-text' ).getFormatTypeForBareElement( type ); + } if ( ! formatType ) { return attributes ? { type, attributes } : { type }; diff --git a/packages/rich-text/src/register-format-type.js b/packages/rich-text/src/register-format-type.js index 1760f8023ddf8..1b475bf5d89d9 100644 --- a/packages/rich-text/src/register-format-type.js +++ b/packages/rich-text/src/register-format-type.js @@ -52,6 +52,55 @@ export function registerFormatType( name, settings ) { return; } + if ( + typeof settings.tagName !== 'string' || + settings.tagName === '' + ) { + window.console.error( + 'Format tag names must be a string.' + ); + return; + } + + if ( + ( typeof settings.className !== 'string' || settings.className === '' ) && + settings.className !== null + ) { + window.console.error( + 'Format class names must be a string, or null to handle bare elements.' + ); + return; + } + + if ( ! /^[_a-zA-Z]+[a-zA-Z0-9-]*$/.test( settings.className ) ) { + window.console.error( + 'A class name must begin with a letter, followed by any number of hyphens, letters, or numbers.' + ); + return; + } + + if ( settings.className === null ) { + const formatTypeForBareElement = select( 'core/rich-text' ) + .getFormatTypeForBareElement( settings.tagName ); + + if ( formatTypeForBareElement ) { + window.console.error( + `Format "${ formatTypeForBareElement.name }" is already registered to handle bare tag name "${ settings.tagName }".` + ); + return; + } + } else { + const formatTypeForClassName = select( 'core/rich-text' ) + .getFormatTypeForClassName( settings.className ); + + if ( formatTypeForClassName ) { + window.console.error( + `Format "${ formatTypeForClassName.name }" is already registered to handle class name "${ settings.className }".` + ); + return; + } + } + if ( ! ( 'title' in settings ) || settings.title === '' ) { window.console.error( 'The format "' + settings.name + '" must have a title.' diff --git a/packages/rich-text/src/store/selectors.js b/packages/rich-text/src/store/selectors.js index c5a28baf0abc2..518bb7d90800a 100644 --- a/packages/rich-text/src/store/selectors.js +++ b/packages/rich-text/src/store/selectors.js @@ -2,6 +2,7 @@ * External dependencies */ import createSelector from 'rememo'; +import { find } from 'lodash'; /** * Returns all the available format types. @@ -28,3 +29,36 @@ export const getFormatTypes = createSelector( export function getFormatType( state, name ) { return state.formatTypes[ name ]; } + +/** + * Gets the format type, if any, that can handle a bare element (without a + * data-format-type attribute), given the tag name of this element. + * + * @param {Object} state Data state. + * @param {string} bareElementTagName The tag name of the element to find a + * format type for. + * @return {?Object} Format type. + */ +export function getFormatTypeForBareElement( state, bareElementTagName ) { + return find( getFormatTypes( state ), ( { tagName } ) => { + return bareElementTagName === tagName; + } ); +} + +/** + * Gets the format type, if any, that can handle an element, given its classes. + * + * @param {Object} state Data state. + * @param {string} elementClassName The classes of the element to find a format + * type for. + * @return {?Object} Format type. + */ +export function getFormatTypeForClassName( state, elementClassName ) { + return find( getFormatTypes( state ), ( { className } ) => { + if ( className === null ) { + return false; + } + + return ` ${ elementClassName } `.indexOf( ` ${ className } ` ) >= 0; + } ); +} diff --git a/packages/rich-text/src/test/create.js b/packages/rich-text/src/test/create.js index 79d11f23134f5..e08f6b82be6b4 100644 --- a/packages/rich-text/src/test/create.js +++ b/packages/rich-text/src/test/create.js @@ -9,7 +9,9 @@ import { JSDOM } from 'jsdom'; */ import { create } from '../create'; import { createElement } from '../create-element'; -import { getSparseArrayLength, spec } from './helpers'; +import { registerFormatType } from '../register-format-type'; +import { unregisterFormatType } from '../unregister-format-type'; +import { getSparseArrayLength, spec, specWithRegistration } from './helpers'; const { window } = new JSDOM(); const { document } = window; @@ -54,6 +56,28 @@ describe( 'create', () => { } ); } ); + specWithRegistration.forEach( ( { + description, + formatName, + formatType, + html, + value: expectedValue, + } ) => { + it( description, () => { + if ( formatName ) { + registerFormatType( formatName, formatType ); + } + + const result = create( { html } ); + + if ( formatName ) { + unregisterFormatType( formatName ); + } + + expect( result ).toEqual( expectedValue ); + } ); + } ); + it( 'should reference formats', () => { const value = create( { html: 'test' } ); diff --git a/packages/rich-text/src/test/helpers/index.js b/packages/rich-text/src/test/helpers/index.js index a220d4ac70561..0b4a7b6e64b70 100644 --- a/packages/rich-text/src/test/helpers/index.js +++ b/packages/rich-text/src/test/helpers/index.js @@ -709,3 +709,80 @@ export const spec = [ }, }, ]; + +export const specWithRegistration = [ + { + description: 'should create format by matching the class', + formatName: 'my-plugin/link', + formatType: { + title: 'Custom Link', + tagName: 'a', + className: 'custom-format', + edit() {}, + }, + html: 'a', + value: { + formats: [ [ { + type: 'my-plugin/link', + attributes: {}, + unregisteredAttributes: {}, + } ] ], + text: 'a', + }, + }, + { + description: 'should retain class names', + formatName: 'my-plugin/link', + formatType: { + title: 'Custom Link', + tagName: 'a', + className: 'custom-format', + edit() {}, + }, + html: 'a', + value: { + formats: [ [ { + type: 'my-plugin/link', + attributes: {}, + unregisteredAttributes: { + class: 'test', + }, + } ] ], + text: 'a', + }, + }, + { + description: 'should create base format', + formatName: 'core/link', + formatType: { + title: 'Link', + tagName: 'a', + className: null, + edit() {}, + }, + html: 'a', + value: { + formats: [ [ { + type: 'core/link', + attributes: {}, + unregisteredAttributes: { + class: 'custom-format', + }, + } ] ], + text: 'a', + }, + }, + { + description: 'should create fallback format', + html: 'a', + value: { + formats: [ [ { + type: 'a', + attributes: { + class: 'custom-format', + }, + } ] ], + text: 'a', + }, + }, +]; diff --git a/packages/rich-text/src/test/register-format-type.js b/packages/rich-text/src/test/register-format-type.js new file mode 100644 index 0000000000000..5dadffb050abf --- /dev/null +++ b/packages/rich-text/src/test/register-format-type.js @@ -0,0 +1,112 @@ +/** + * WordPress dependencies + */ +import { select } from '@wordpress/data'; + +/** + * Internal dependencies + */ + +import { registerFormatType } from '../register-format-type'; +import { unregisterFormatType } from '../unregister-format-type'; + +describe( 'registerFormatType', () => { + beforeAll( () => { + // Initialize the rich-text store. + require( '../store' ); + } ); + + afterEach( () => { + select( 'core/rich-text' ).getFormatTypes().forEach( ( { name } ) => { + unregisterFormatType( name ); + } ); + } ); + + const validName = 'plugin/test'; + const validSettings = { + tagName: 'test', + className: null, + title: 'Test', + edit() {}, + }; + + it( 'should register format', () => { + const settings = registerFormatType( validName, validSettings ); + expect( settings ).toEqual( { ...validSettings, name: validName } ); + expect( console ).not.toHaveErrored(); + } ); + + it( 'should error without arguments', () => { + registerFormatType(); + expect( console ).toHaveErroredWith( 'Format names must be strings.' ); + } ); + + it( 'should error on invalid name', () => { + registerFormatType( 'test', validSettings ); + expect( console ).toHaveErroredWith( 'Format names must contain a namespace prefix, include only lowercase alphanumeric characters or dashes, and start with a letter. Example: my-plugin/my-custom-format' ); + } ); + + it( 'should error on already registered name', () => { + registerFormatType( validName, validSettings ); + registerFormatType( validName, validSettings ); + expect( console ).toHaveErroredWith( 'Format "plugin/test" is already registered.' ); + } ); + + it( 'should error on undefined edit property', () => { + registerFormatType( 'plugin/test', { + ...validSettings, + edit: undefined, + } ); + expect( console ).toHaveErroredWith( 'The "edit" property must be specified and must be a valid function.' ); + } ); + + it( 'should error on empty tagName property', () => { + registerFormatType( validName, { + ...validSettings, + tagName: '', + } ); + expect( console ).toHaveErroredWith( 'Format tag names must be a string.' ); + } ); + + it( 'should error on invalid empty className property', () => { + registerFormatType( validName, { + ...validSettings, + className: '', + } ); + expect( console ).toHaveErroredWith( 'Format class names must be a string, or null to handle bare elements.' ); + } ); + + it( 'should error on invalid className property', () => { + registerFormatType( validName, { + ...validSettings, + className: 'invalid class name', + } ); + expect( console ).toHaveErroredWith( 'A class name must begin with a letter, followed by any number of hyphens, letters, or numbers.' ); + } ); + + it( 'should error on already registered tagName', () => { + registerFormatType( validName, validSettings ); + registerFormatType( 'plugin/second', validSettings ); + expect( console ).toHaveErroredWith( 'Format "plugin/test" is already registered to handle bare tag name "test".' ); + } ); + + it( 'should error on already registered className', () => { + registerFormatType( validName, { + ...validSettings, + className: 'test', + } ); + registerFormatType( 'plugin/second', { + ...validSettings, + className: 'test', + } ); + expect( console ).toHaveErroredWith( 'Format "plugin/test" is already registered to handle class name "test".' ); + } ); + + it( 'should error on empty title property', () => { + registerFormatType( validName, { + ...validSettings, + title: '', + } ); + expect( console ).toHaveErroredWith( 'The format "plugin/test" must have a title.' ); + } ); +} ); diff --git a/packages/rich-text/src/test/to-html-string.js b/packages/rich-text/src/test/to-html-string.js index 975663acdadbb..cdb491b21dedf 100644 --- a/packages/rich-text/src/test/to-html-string.js +++ b/packages/rich-text/src/test/to-html-string.js @@ -10,6 +10,9 @@ import { JSDOM } from 'jsdom'; import { create } from '../create'; import { toHTMLString } from '../to-html-string'; +import { registerFormatType } from '../register-format-type'; +import { unregisterFormatType } from '../unregister-format-type'; +import { specWithRegistration } from './helpers'; const { window } = new JSDOM(); const { document } = window; @@ -26,6 +29,28 @@ describe( 'toHTMLString', () => { require( '../store' ); } ); + specWithRegistration.forEach( ( { + description, + formatName, + formatType, + html, + value, + } ) => { + it( description, () => { + if ( formatName ) { + registerFormatType( formatName, formatType ); + } + + const result = toHTMLString( { value } ); + + if ( formatName ) { + unregisterFormatType( formatName ); + } + + expect( result ).toEqual( html ); + } ); + } ); + it( 'should extract recreate HTML 1', () => { const HTML = 'one two 🍒 three'; const element = createNode( `

${ HTML }

` ); diff --git a/packages/rich-text/src/to-tree.js b/packages/rich-text/src/to-tree.js index fa8810b8e17ea..be23219851a96 100644 --- a/packages/rich-text/src/to-tree.js +++ b/packages/rich-text/src/to-tree.js @@ -9,21 +9,14 @@ import { ZERO_WIDTH_NO_BREAK_SPACE, } from './special-characters'; -function fromFormat( { type, attributes, object } ) { +function fromFormat( { type, attributes, unregisteredAttributes, object } ) { const formatType = getFormatType( type ); if ( ! formatType ) { return { type, attributes, object }; } - if ( ! attributes ) { - return { - type: formatType.match.tagName, - object: formatType.object, - }; - } - - const elementAttributes = {}; + const elementAttributes = { ...unregisteredAttributes }; for ( const name in attributes ) { const key = formatType.attributes[ name ]; @@ -35,8 +28,16 @@ function fromFormat( { type, attributes, object } ) { } } + if ( formatType.className ) { + if ( elementAttributes.class ) { + elementAttributes.class = `${ formatType.className } ${ elementAttributes.class }`; + } else { + elementAttributes.class = formatType.className; + } + } + return { - type: formatType.match.tagName, + type: formatType.tagName, object: formatType.object, attributes: elementAttributes, }; @@ -145,15 +146,14 @@ export function toTree( { return; } - const { type, attributes, object } = format; const parent = getParent( pointer ); - const newNode = append( parent, fromFormat( { type, attributes, object } ) ); + const newNode = append( parent, fromFormat( format ) ); if ( isText( pointer ) && getText( pointer ).length === 0 ) { remove( pointer ); } - pointer = append( object ? parent : newNode, '' ); + pointer = append( format.object ? parent : newNode, '' ); } ); } From 2dddcc95f22e879c989b9fe53dcbf50cef184107 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Tue, 6 Nov 2018 20:10:58 +0100 Subject: [PATCH 021/106] Update plugin version to 4.2.0 (#11549) --- 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 09ed86af7aa59..37215edb4a89d 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.2.0-rc.1 + * Version: 4.2.0 * Author: Gutenberg Team * * @package gutenberg diff --git a/package-lock.json b/package-lock.json index 3a02588023f2a..4353dd993bee9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "4.2.0-rc.1", + "version": "4.2.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index b5ce8b03ad7a0..0f8afe18111e6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "4.2.0-rc.1", + "version": "4.2.0", "private": true, "description": "A new WordPress editor experience", "repository": "git+https://github.com/WordPress/gutenberg.git", From 8cbfb3cca07ff7e517cd908315cfb75ab92a4eb7 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Tue, 6 Nov 2018 15:38:06 -0500 Subject: [PATCH 022/106] Editor: Factor initial edits to own state (#11267) * Editor: Factor initial edits to own state * Testing: Verify immediate saveability of demo post * URL: Test undefined values as ignored * URL: Test space encoding * Testing: Verify saveability, non-dirtiness of initial edits --- docs/data/data-core-editor.md | 11 +- lib/client-assets.php | 20 +- packages/edit-post/src/editor.js | 5 +- packages/edit-post/src/index.js | 49 +++-- .../editor/src/components/provider/index.js | 2 +- packages/editor/src/store/actions.js | 22 ++- packages/editor/src/store/defaults.js | 7 + packages/editor/src/store/effects.js | 25 +-- packages/editor/src/store/reducer.js | 93 +++++++-- packages/editor/src/store/selectors.js | 21 +- packages/editor/src/store/test/reducer.js | 100 ++++++++-- packages/editor/src/store/test/selectors.js | 183 ++++++++++++++---- packages/url/src/test/index.test.js | 14 ++ test/e2e/specs/change-detection.test.js | 12 +- test/e2e/specs/demo.test.js | 4 + test/e2e/specs/new-post.test.js | 7 + test/e2e/support/utils.js | 23 ++- 17 files changed, 462 insertions(+), 136 deletions(-) diff --git a/docs/data/data-core-editor.md b/docs/data/data-core-editor.md index 052292319f8ed..267184d78113e 100644 --- a/docs/data/data-core-editor.md +++ b/docs/data/data-core-editor.md @@ -1391,6 +1391,7 @@ the specified post object and editor settings. *Parameters* * post: Post object. + * edits: Initial edited attributes object. ### resetPost @@ -1427,7 +1428,6 @@ Returns an action object used to setup the editor state when first opening an ed * post: Post object. * blocks: Array of blocks. - * edits: Initial edited attributes object. ### resetBlocks @@ -1574,6 +1574,15 @@ Returns an action object resetting the template validity. Returns an action object synchronize the template with the list of blocks +### editPost + +Returns an action object used in signalling that attributes of the post have +been edited. + +*Parameters* + + * edits: Post attributes to edit. + ### savePost Returns an action object to save the post. diff --git a/lib/client-assets.php b/lib/client-assets.php index 94c92193f0ab3..44eb49b7594f0 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -1467,26 +1467,16 @@ function gutenberg_editor_scripts_and_styles( $hook ) { $demo_content = ob_get_clean(); $initial_edits = array( - 'title' => array( - 'raw' => __( 'Welcome to the Gutenberg Editor', 'gutenberg' ), - ), - 'content' => array( - 'raw' => $demo_content, - ), + 'title' => __( 'Welcome to the Gutenberg Editor', 'gutenberg' ), + 'content' => $demo_content, ); } elseif ( $is_new_post ) { // Override "(Auto Draft)" new post default title with empty string, // or filtered value. $initial_edits = array( - 'title' => array( - 'raw' => $post->post_title, - ), - 'content' => array( - 'raw' => $post->post_content, - ), - 'excerpt' => array( - 'raw' => $post->post_excerpt, - ), + 'title' => $post->post_title, + 'content' => $post->post_content, + 'excerpt' => $post->post_excerpt, ); } else { $initial_edits = null; diff --git a/packages/edit-post/src/editor.js b/packages/edit-post/src/editor.js index 35680ab884baf..31a51bffcaa95 100644 --- a/packages/edit-post/src/editor.js +++ b/packages/edit-post/src/editor.js @@ -14,7 +14,7 @@ function Editor( { hasFixedToolbar, focusMode, post, - overridePost, + initialEdits, onError, ...props } ) { @@ -32,7 +32,8 @@ function Editor( { diff --git a/packages/edit-post/src/index.js b/packages/edit-post/src/index.js index 70c8cf4d38fec..0832cbd141f69 100644 --- a/packages/edit-post/src/index.js +++ b/packages/edit-post/src/index.js @@ -22,18 +22,27 @@ import Editor from './editor'; * an unhandled error occurs, replacing previously mounted editor element using * an initial state from prior to the crash. * - * @param {Object} postType Post type of the post to edit. - * @param {Object} postId ID of the post to edit. - * @param {Element} target DOM node in which editor is rendered. - * @param {?Object} settings Editor settings object. - * @param {Object} overridePost Post properties to override. + * @param {Object} postType Post type of the post to edit. + * @param {Object} postId ID of the post to edit. + * @param {Element} target DOM node in which editor is rendered. + * @param {?Object} settings Editor settings object. + * @param {Object} initialEdits Programmatic edits to apply initially, to be + * considered as non-user-initiated (bypass for + * unsaved changes prompt). */ -export function reinitializeEditor( postType, postId, target, settings, overridePost ) { +export function reinitializeEditor( postType, postId, target, settings, initialEdits ) { unmountComponentAtNode( target ); - const reboot = reinitializeEditor.bind( null, postType, postId, target, settings, overridePost ); + const reboot = reinitializeEditor.bind( null, postType, postId, target, settings, initialEdits ); render( - , + , target ); } @@ -44,15 +53,17 @@ export function reinitializeEditor( postType, postId, target, settings, override * The return value of this function is not necessary if we change where we * call initializeEditor(). This is due to metaBox timing. * - * @param {string} id Unique identifier for editor instance. - * @param {Object} postType Post type of the post to edit. - * @param {Object} postId ID of the post to edit. - * @param {?Object} settings Editor settings object. - * @param {Object} overridePost Post properties to override. + * @param {string} id Unique identifier for editor instance. + * @param {Object} postType Post type of the post to edit. + * @param {Object} postId ID of the post to edit. + * @param {?Object} settings Editor settings object. + * @param {Object} initialEdits Programmatic edits to apply initially, to be + * considered as non-user-initiated (bypass for + * unsaved changes prompt). */ -export function initializeEditor( id, postType, postId, settings, overridePost ) { +export function initializeEditor( id, postType, postId, settings, initialEdits ) { const target = document.getElementById( id ); - const reboot = reinitializeEditor.bind( null, postType, postId, target, settings, overridePost ); + const reboot = reinitializeEditor.bind( null, postType, postId, target, settings, initialEdits ); registerCoreBlocks(); @@ -64,7 +75,13 @@ export function initializeEditor( id, postType, postId, settings, overridePost ) ] ); render( - , + , target ); } diff --git a/packages/editor/src/components/provider/index.js b/packages/editor/src/components/provider/index.js index 49cfc550d39c4..ebe4edf43e653 100644 --- a/packages/editor/src/components/provider/index.js +++ b/packages/editor/src/components/provider/index.js @@ -28,7 +28,7 @@ class EditorProvider extends Component { props.updateEditorSettings( props.settings ); props.updatePostLock( props.settings.postLock ); - props.setupEditor( props.post ); + props.setupEditor( props.post, props.initialEdits ); if ( props.settings.autosave ) { props.createWarningNotice( diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index 67864935bcea5..7d5c0d71ee2d1 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -17,14 +17,16 @@ import { dispatch } from '@wordpress/data'; * Returns an action object used in signalling that editor has initialized with * the specified post object and editor settings. * - * @param {Object} post Post object. + * @param {Object} post Post object. + * @param {Object} edits Initial edited attributes object. * * @return {Object} Action object. */ -export function setupEditor( post ) { +export function setupEditor( post, edits ) { return { type: 'SETUP_EDITOR', post, + edits, }; } @@ -76,18 +78,16 @@ export function updatePost( edits ) { /** * Returns an action object used to setup the editor state when first opening an editor. * - * @param {Object} post Post object. - * @param {Array} blocks Array of blocks. - * @param {Object} edits Initial edited attributes object. + * @param {Object} post Post object. + * @param {Array} blocks Array of blocks. * * @return {Object} Action object. */ -export function setupEditorState( post, blocks, edits ) { +export function setupEditorState( post, blocks ) { return { type: 'SETUP_EDITOR_STATE', post, blocks, - edits, }; } @@ -381,6 +381,14 @@ export function synchronizeTemplate() { }; } +/** + * Returns an action object used in signalling that attributes of the post have + * been edited. + * + * @param {Object} edits Post attributes to edit. + * + * @return {Object} Action object. + */ export function editPost( edits ) { return { type: 'EDIT_POST', diff --git a/packages/editor/src/store/defaults.js b/packages/editor/src/store/defaults.js index 908b18880e0f6..9776836510645 100644 --- a/packages/editor/src/store/defaults.js +++ b/packages/editor/src/store/defaults.js @@ -127,3 +127,10 @@ export const EDITOR_SETTINGS_DEFAULTS = { // List of allowed mime types and file extensions. allowedMimeTypes: null, }; + +/** + * Default initial edits state. + * + * @type {Object} + */ +export const INITIAL_EDITS_DEFAULTS = {}; diff --git a/packages/editor/src/store/effects.js b/packages/editor/src/store/effects.js index 42ffd3dd857a9..3afa0f1e055a1 100644 --- a/packages/editor/src/store/effects.js +++ b/packages/editor/src/store/effects.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { compact, last } from 'lodash'; +import { compact, last, has } from 'lodash'; /** * WordPress dependencies @@ -200,11 +200,20 @@ export default { ) ); }, SETUP_EDITOR( action, store ) { - const { post } = action; + const { post, edits } = action; const state = store.getState(); - // Parse content as blocks - let blocks = parse( post.content.raw ); + // In order to ensure maximum of a single parse during setup, edits are + // included as part of editor setup action. Assume edited content as + // canonical if provided, falling back to post. + let content; + if ( has( edits, [ 'content' ] ) ) { + content = edits.content; + } else { + content = post.content.raw; + } + + let blocks = parse( content ); // Apply a template for new posts only, if exists. const isNewPost = post.status === 'auto-draft'; @@ -213,13 +222,7 @@ export default { blocks = synchronizeBlocksWithTemplate( blocks, template ); } - // Include auto draft title in edits while not flagging post as dirty - const edits = {}; - if ( isNewPost ) { - edits.title = post.title.raw; - } - - const setupAction = setupEditorState( post, blocks, edits ); + const setupAction = setupEditorState( post, blocks ); return compact( [ setupAction, diff --git a/packages/editor/src/store/reducer.js b/packages/editor/src/store/reducer.js index dca04e51edd8d..e95b010dc94d9 100644 --- a/packages/editor/src/store/reducer.js +++ b/packages/editor/src/store/reducer.js @@ -28,7 +28,11 @@ import { combineReducers } from '@wordpress/data'; */ import withHistory from '../utils/with-history'; import withChangeDetection from '../utils/with-change-detection'; -import { PREFERENCES_DEFAULTS, EDITOR_SETTINGS_DEFAULTS } from './defaults'; +import { + PREFERENCES_DEFAULTS, + EDITOR_SETTINGS_DEFAULTS, + INITIAL_EDITS_DEFAULTS, +} from './defaults'; import { insertAt, moveTo } from './array'; /** @@ -97,6 +101,23 @@ function getFlattenedBlocks( blocks ) { return flattenedBlocks; } +/** + * Returns an object against which it is safe to perform mutating operations, + * given the original object and its current working copy. + * + * @param {Object} original Original object. + * @param {Object} working Working object. + * + * @return {Object} Mutation-safe object. + */ +function getMutateSafeObject( original, working ) { + if ( original === working ) { + return { ...original }; + } + + return working; +} + /** * Returns true if the two object arguments have the same keys, or false * otherwise. @@ -226,16 +247,10 @@ export const editor = flow( [ edits( state = {}, action ) { switch ( action.type ) { case 'EDIT_POST': - case 'SETUP_EDITOR_STATE': return reduce( action.edits, ( result, value, key ) => { // Only assign into result if not already same value if ( value !== state[ key ] ) { - // Avoid mutating original state by creating shallow - // clone. Should only occur once per reduce. - if ( result === state ) { - result = { ...state }; - } - + result = getMutateSafeObject( state, result ); result[ key ] = value; } @@ -263,10 +278,7 @@ export const editor = flow( [ return result; } - if ( state === result ) { - result = { ...state }; - } - + result = getMutateSafeObject( state, result ); delete result[ key ]; return result; }, state ); @@ -297,11 +309,7 @@ export const editor = flow( [ // 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 = getMutateSafeObject( state[ action.clientId ].attributes, result ); result[ key ] = value; } @@ -515,6 +523,51 @@ export const editor = flow( [ } ), } ); +/** + * Reducer returning the initial edits state. With matching shape to that of + * `editor.edits`, the initial edits are those applied programmatically, are + * not considered in prmopting the user for unsaved changes, and are included + * in (and reset by) the next save payload. + * + * @param {Object} state Current state. + * @param {Object} action Action object. + * + * @return {Object} Next state. + */ +export function initialEdits( state = INITIAL_EDITS_DEFAULTS, action ) { + switch ( action.type ) { + case 'SETUP_EDITOR': + if ( ! action.edits ) { + break; + } + + return action.edits; + + case 'SETUP_EDITOR_STATE': + if ( 'content' in state ) { + return omit( state, 'content' ); + } + + return state; + + case 'UPDATE_POST': + return reduce( action.edits, ( result, value, key ) => { + if ( ! result.hasOwnProperty( key ) ) { + return result; + } + + result = getMutateSafeObject( state, result ); + delete result[ key ]; + return result; + }, state ); + + case 'RESET_POST': + return INITIAL_EDITS_DEFAULTS; + } + + return state; +} + /** * Reducer returning the last-known state of the current post, in the format * returned by the WP REST API. @@ -924,10 +977,7 @@ export const reusableBlocks = combineReducers( { const value = { clientId, title }; if ( ! isEqual( nextState[ id ], value ) ) { - if ( nextState === state ) { - nextState = { ...nextState }; - } - + nextState = getMutateSafeObject( state, nextState ); nextState[ id ] = value; } @@ -1096,6 +1146,7 @@ export function autosave( state = null, action ) { export default optimist( combineReducers( { editor, + initialEdits, currentPost, isTyping, isCaretWithinFormattedText, diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index b9964d28c1ce1..f405c90cbe272 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -193,9 +193,18 @@ export function getCurrentPostLastRevisionId( state ) { * * @return {Object} Object of key value pairs comprising unsaved edits. */ -export function getPostEdits( state ) { - return state.editor.present.edits; -} +export const getPostEdits = createSelector( + ( state ) => { + return { + ...state.initialEdits, + ...state.editor.present.edits, + }; + }, + ( state ) => [ + state.editor.present.edits, + state.initialEdits, + ] +); /** * Returns a new reference when edited values have changed. This is useful in @@ -597,6 +606,7 @@ export const getBlock = createSelector( state.editor.present.blocks.byClientId[ clientId ], getBlockDependantsCacheBust( state, clientId ), state.editor.present.edits.meta, + state.initialEdits.meta, state.currentPost.meta, ] ); @@ -704,6 +714,7 @@ export const getBlocksByClientId = createSelector( ), ( state ) => [ state.editor.present.edits.meta, + state.initialEdits.meta, state.currentPost.meta, state.editor.present.blocks, ] @@ -1022,6 +1033,7 @@ export const getMultiSelectedBlocks = createSelector( state.blockSelection.end, state.editor.present.blocks.byClientId, state.editor.present.edits.meta, + state.initialEdits.meta, state.currentPost.meta, ] ); @@ -1538,8 +1550,9 @@ export const getEditedPostContent = createSelector( return content; }, ( state ) => [ - state.editor.present.edits.content, state.editor.present.blocks, + state.editor.present.edits.content, + state.initialEdits.content, ], ); diff --git a/packages/editor/src/store/test/reducer.js b/packages/editor/src/store/test/reducer.js index 60a9cbc256a62..7d75ce2c55b4f 100644 --- a/packages/editor/src/store/test/reducer.js +++ b/packages/editor/src/store/test/reducer.js @@ -23,6 +23,7 @@ import { shouldOverwriteState, getPostRawValue, editor, + initialEdits, currentPost, isTyping, isCaretWithinFormattedText, @@ -37,6 +38,7 @@ import { autosave, postSavingLock, } from '../reducer'; +import { INITIAL_EDITS_DEFAULTS } from '../defaults'; describe( 'state', () => { describe( 'hasSameKeys()', () => { @@ -1038,22 +1040,6 @@ describe( 'state', () => { } ); } ); - it( 'should save initial post state', () => { - const state = editor( undefined, { - type: 'SETUP_EDITOR_STATE', - edits: { - status: 'draft', - title: 'post title', - }, - blocks: [], - } ); - - expect( state.present.edits ).toEqual( { - status: 'draft', - title: 'post title', - } ); - } ); - it( 'should omit content when resetting', () => { // Use case: When editing in Text mode, we defer to content on // the property, but we reset blocks by parse when switching @@ -1243,6 +1229,88 @@ describe( 'state', () => { } ); } ); + describe( 'initialEdits', () => { + it( 'should default to initial edits', () => { + const state = initialEdits( undefined, {} ); + + expect( state ).toBe( INITIAL_EDITS_DEFAULTS ); + } ); + + it( 'should return initial edits on post reset', () => { + const state = initialEdits( undefined, { + type: 'RESET_POST', + } ); + + expect( state ).toBe( INITIAL_EDITS_DEFAULTS ); + } ); + + it( 'should return referentially equal state if setup includes no edits', () => { + const original = initialEdits( undefined, {} ); + const state = initialEdits( deepFreeze( original ), { + type: 'SETUP_EDITOR', + } ); + + expect( state ).toBe( original ); + } ); + + it( 'should return referentially equal state if reset while having made no edits', () => { + const original = initialEdits( undefined, {} ); + const state = initialEdits( deepFreeze( original ), { + type: 'RESET_POST', + } ); + + expect( state ).toBe( original ); + } ); + + it( 'should return setup edits', () => { + const original = initialEdits( undefined, {} ); + const state = initialEdits( deepFreeze( original ), { + type: 'SETUP_EDITOR', + edits: { + title: '', + content: '', + }, + } ); + + expect( state ).toEqual( { + title: '', + content: '', + } ); + } ); + + it( 'should unset content on editor setup', () => { + const original = initialEdits( undefined, { + type: 'SETUP_EDITOR', + edits: { + title: '', + content: '', + }, + } ); + const state = initialEdits( deepFreeze( original ), { + type: 'SETUP_EDITOR_STATE', + } ); + + expect( state ).toEqual( { title: '' } ); + } ); + + it( 'should unset values on post update', () => { + const original = initialEdits( undefined, { + type: 'SETUP_EDITOR', + edits: { + title: '', + }, + } ); + const state = initialEdits( deepFreeze( original ), { + type: 'UPDATE_POST', + edits: { + title: '', + }, + } ); + + expect( state ).toEqual( {} ); + } ); + } ); + describe( 'currentPost()', () => { it( 'should reset a post object', () => { const original = deepFreeze( { title: 'unmodified' } ); diff --git a/packages/editor/src/store/test/selectors.js b/packages/editor/src/store/test/selectors.js index bdaecf9327286..2474cc1e2565f 100644 --- a/packages/editor/src/store/test/selectors.js +++ b/packages/editor/src/store/test/selectors.js @@ -246,6 +246,7 @@ describe( 'selectors', () => { }, }, }, + initialEdits: {}, }; expect( isEditedPostNew( state ) ).toBe( true ); @@ -263,6 +264,7 @@ describe( 'selectors', () => { }, }, }, + initialEdits: {}, }; expect( isEditedPostNew( state ) ).toBe( false ); @@ -434,6 +436,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, }; expect( getEditedPostAttribute( state, 'slug' ) ).toBe( 'post slug' ); @@ -449,6 +452,7 @@ describe( 'selectors', () => { }, }, }, + initialEdits: {}, }; expect( getEditedPostAttribute( state, 'slug' ) ).toBe( 'new slug' ); @@ -464,6 +468,7 @@ describe( 'selectors', () => { edits: { status: 'private' }, }, }, + initialEdits: {}, }; expect( getEditedPostAttribute( state, 'title' ) ).toBe( 'sassel' ); @@ -479,6 +484,7 @@ describe( 'selectors', () => { edits: { title: 'youcha' }, }, }, + initialEdits: {}, }; expect( getEditedPostAttribute( state, 'title' ) ).toBe( 'youcha' ); @@ -492,6 +498,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, }; expect( getEditedPostAttribute( state, 'valueOf' ) ).toBeUndefined(); @@ -606,10 +613,37 @@ describe( 'selectors', () => { edits: { title: 'terga' }, }, }, + initialEdits: {}, }; expect( getPostEdits( state ) ).toEqual( { title: 'terga' } ); } ); + + it( 'should return value from initial edits', () => { + const state = { + editor: { + present: { + edits: {}, + }, + }, + initialEdits: { title: 'terga' }, + }; + + expect( getPostEdits( state ) ).toEqual( { title: 'terga' } ); + } ); + + it( 'should prefer value from edits over initial edits', () => { + const state = { + editor: { + present: { + edits: { title: 'werga' }, + }, + }, + initialEdits: { title: 'terga' }, + }; + + expect( getPostEdits( state ) ).toEqual( { title: 'werga' } ); + } ); } ); describe( 'getReferenceByDistinctEdits', () => { @@ -638,6 +672,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, }; expect( getEditedPostVisibility( state ) ).toBe( 'public' ); @@ -653,6 +688,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, }; expect( getEditedPostVisibility( state ) ).toBe( 'private' ); @@ -669,6 +705,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, }; expect( getEditedPostVisibility( state ) ).toBe( 'password' ); @@ -688,6 +725,7 @@ describe( 'selectors', () => { }, }, }, + initialEdits: {}, }; expect( getEditedPostVisibility( state ) ).toBe( 'private' ); @@ -957,6 +995,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, currentPost: {}, saving: {}, }; @@ -975,6 +1014,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, currentPost: { title: 'sassel', }, @@ -997,6 +1037,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, currentPost: { title: 'sassel', }, @@ -1017,6 +1058,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, currentPost: { excerpt: 'sassel', }, @@ -1048,6 +1090,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, currentPost: {}, saving: {}, }; @@ -1076,6 +1119,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, currentPost: {}, saving: {}, }; @@ -1105,6 +1149,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, currentPost: { title: 'sassel', }, @@ -1127,6 +1172,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, currentPost: { title: 'sassel', }, @@ -1152,6 +1198,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, currentPost: { title: 'sassel', }, @@ -1175,6 +1222,7 @@ describe( 'selectors', () => { }, }, }, + initialEdits: {}, currentPost: { title: 'foo', content: 'foo', @@ -1206,6 +1254,7 @@ describe( 'selectors', () => { }, }, }, + initialEdits: {}, currentPost: { title: 'foo', content: 'foo', @@ -1279,6 +1328,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, currentPost: {}, }; @@ -1307,6 +1357,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, currentPost: {}, }; @@ -1337,6 +1388,7 @@ describe( 'selectors', () => { }, }, }, + initialEdits: {}, currentPost: { content: '', }, @@ -1356,6 +1408,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, currentPost: { content: '', }, @@ -1377,6 +1430,7 @@ describe( 'selectors', () => { }, }, }, + initialEdits: {}, currentPost: {}, }; @@ -1405,6 +1459,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, currentPost: {}, }; @@ -1433,6 +1488,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, currentPost: { content: '', }, @@ -1463,6 +1519,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, currentPost: { content: 'Test Data', }, @@ -1501,6 +1558,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, currentPost: { content: '\n\n', }, @@ -1520,6 +1578,7 @@ describe( 'selectors', () => { edits: { date: date.toUTCString() }, }, }, + initialEdits: {}, }; expect( isEditedPostBeingScheduled( state ) ).toBe( true ); @@ -1532,6 +1591,7 @@ describe( 'selectors', () => { edits: { date: '2016-05-30T17:21:39' }, }, }, + initialEdits: {}, }; expect( isEditedPostBeingScheduled( state ) ).toBe( false ); @@ -1539,16 +1599,6 @@ describe( 'selectors', () => { } ); describe( 'isEditedPostDateFloating', () => { - let editor; - - beforeEach( () => { - editor = { - present: { - edits: {}, - }, - }; - } ); - it( 'should return true for draft posts where the date matches the modified date', () => { const state = { currentPost: { @@ -1556,7 +1606,12 @@ describe( 'selectors', () => { modified: '2018-09-27T01:23:45.678Z', status: 'draft', }, - editor, + editor: { + present: { + edits: {}, + }, + }, + initialEdits: {}, }; expect( isEditedPostDateFloating( state ) ).toBe( true ); @@ -1569,7 +1624,12 @@ describe( 'selectors', () => { modified: '2018-09-27T01:23:45.678Z', status: 'auto-draft', }, - editor, + editor: { + present: { + edits: {}, + }, + }, + initialEdits: {}, }; expect( isEditedPostDateFloating( state ) ).toBe( true ); @@ -1582,7 +1642,12 @@ describe( 'selectors', () => { modified: '1970-01-01T00:00:00.000Z', status: 'draft', }, - editor, + editor: { + present: { + edits: {}, + }, + }, + initialEdits: {}, }; expect( isEditedPostDateFloating( state ) ).toBe( false ); @@ -1595,7 +1660,12 @@ describe( 'selectors', () => { modified: '1970-01-01T00:00:00.000Z', status: 'auto-draft', }, - editor, + editor: { + present: { + edits: {}, + }, + }, + initialEdits: {}, }; expect( isEditedPostDateFloating( state ) ).toBe( false ); @@ -1608,7 +1678,12 @@ describe( 'selectors', () => { modified: '2018-09-27T01:23:45.678Z', status: 'publish', }, - editor, + editor: { + present: { + edits: {}, + }, + }, + initialEdits: {}, }; expect( isEditedPostDateFloating( state ) ).toBe( false ); @@ -1638,6 +1713,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, }; const nextState = { @@ -1656,6 +1732,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, }; expect( @@ -1680,6 +1757,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, }; const nextState = { @@ -1700,6 +1778,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, }; expect( @@ -1730,6 +1809,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, }; const nextState = { @@ -1750,6 +1830,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, }; expect( @@ -1779,6 +1860,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, }; const nextState = { @@ -1799,6 +1881,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, }; expect( @@ -1832,6 +1915,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, }; const nextState = { @@ -1854,6 +1938,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, }; expect( @@ -1875,6 +1960,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, }; const name = getBlockName( state, 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1' ); @@ -1903,6 +1989,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, }; const name = getBlockName( state, 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1' ); @@ -1929,6 +2016,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, }; expect( getBlock( state, 123 ) ).toEqual( { @@ -1951,6 +2039,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, }; expect( getBlock( state, 123 ) ).toBe( null ); @@ -1975,6 +2064,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, }; expect( getBlock( state, 123 ) ).toEqual( { @@ -2024,6 +2114,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, }; expect( getBlock( state, 123 ) ).toEqual( { @@ -2057,6 +2148,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, }; expect( getBlocks( state ) ).toEqual( [ @@ -2111,6 +2203,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, }; expect( getClientIdsOfDescendants( state, [ 'uuid-10' ] ) ).toEqual( [ 'uuid-12', @@ -2170,6 +2263,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, }; expect( getClientIdsWithDescendants( state ) ).toEqual( [ 'uuid-6', @@ -2365,6 +2459,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, blockSelection: { start: null, end: null }, }; @@ -2390,6 +2485,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, blockSelection: { start: 23, end: 123 }, }; @@ -2415,6 +2511,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, blockSelection: { start: 23, end: 23 }, }; @@ -2577,6 +2674,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, blockSelection: { start: null, end: null }, currentPost: {}, }; @@ -3178,6 +3276,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, insertionPoint: { rootClientId: undefined, index: 0, @@ -3212,6 +3311,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, insertionPoint: null, }; @@ -3245,6 +3345,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, insertionPoint: null, }; @@ -3278,6 +3379,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, insertionPoint: null, }; @@ -3311,6 +3413,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, insertionPoint: null, }; @@ -3420,6 +3523,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, currentPost: {}, }; @@ -3442,6 +3546,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, currentPost: {}, }; @@ -3463,6 +3568,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, currentPost: {}, }; @@ -3484,6 +3590,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, currentPost: {}, }; @@ -3505,6 +3612,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, currentPost: {}, }; @@ -3527,6 +3635,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, currentPost: {}, }; @@ -3580,6 +3689,7 @@ describe( 'selectors', () => { }, }, }, + initialEdits: {}, currentPost: {}, }; @@ -3605,6 +3715,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, currentPost: {}, }; @@ -3631,6 +3742,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, currentPost: {}, }; @@ -3661,6 +3773,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, currentPost: {}, }; @@ -3685,6 +3798,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, currentPost: {}, }; @@ -3715,6 +3829,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, currentPost: {}, }; @@ -3918,6 +4033,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, reusableBlocks: { data: { 1: { clientId: 'block1', title: 'Reusable Block 1' }, @@ -3978,6 +4094,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, reusableBlocks: { data: { 1: { clientId: 'block1', title: 'Reusable Block 1' }, @@ -4018,6 +4135,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, reusableBlocks: { data: { 1: { clientId: 'block1', title: 'Reusable Block 1' }, @@ -4081,6 +4199,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, reusableBlocks: { data: {}, }, @@ -4107,6 +4226,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, reusableBlocks: { data: {}, }, @@ -4133,6 +4253,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, reusableBlocks: { data: {}, }, @@ -4166,6 +4287,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, reusableBlocks: { data: {}, }, @@ -4397,9 +4519,6 @@ describe( 'selectors', () => { saving: { requesting: false, }, - editor: { - edits: {}, - }, currentPost: { status: 'publish', }, @@ -4414,9 +4533,6 @@ describe( 'selectors', () => { saving: { requesting: true, }, - editor: { - edits: {}, - }, currentPost: { status: 'draft', }, @@ -4431,9 +4547,6 @@ describe( 'selectors', () => { saving: { requesting: true, }, - editor: { - edits: {}, - }, currentPost: { status: 'publish', }, @@ -4450,9 +4563,6 @@ describe( 'selectors', () => { saving: { requesting: false, }, - editor: { - edits: {}, - }, currentPost: { status: 'publish', }, @@ -4467,9 +4577,6 @@ describe( 'selectors', () => { saving: { requesting: true, }, - editor: { - edits: {}, - }, currentPost: { status: 'publish', }, @@ -4486,11 +4593,6 @@ describe( 'selectors', () => { saving: { requesting: false, }, - editor: { - edits: { - status: 'publish', - }, - }, currentPost: { status: 'draft', }, @@ -4505,9 +4607,6 @@ describe( 'selectors', () => { saving: { requesting: true, }, - editor: { - edits: {}, - }, currentPost: { status: 'publish', }, @@ -4604,6 +4703,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, }; expect( isPermalinkEditable( state ) ).toBe( false ); @@ -4617,6 +4717,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, }; expect( isPermalinkEditable( state ) ).toBe( false ); @@ -4630,6 +4731,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, }; expect( isPermalinkEditable( state ) ).toBe( true ); @@ -4643,6 +4745,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, }; expect( isPermalinkEditable( state ) ).toBe( true ); @@ -4659,6 +4762,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, }; expect( getPermalink( state ) ).toBe( url ); @@ -4675,6 +4779,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, }; expect( getPermalink( state ) ).toBe( 'http://foo.test/bar/baz/' ); @@ -4698,6 +4803,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, }; expect( getPermalinkParts( state ) ).toEqual( parts ); @@ -4718,6 +4824,7 @@ describe( 'selectors', () => { edits: {}, }, }, + initialEdits: {}, }; expect( getPermalinkParts( state ) ).toEqual( parts ); diff --git a/packages/url/src/test/index.test.js b/packages/url/src/test/index.test.js index 20d6c3628e752..86363a4f6bd40 100644 --- a/packages/url/src/test/index.test.js +++ b/packages/url/src/test/index.test.js @@ -71,6 +71,20 @@ describe( 'addQueryArgs', () => { expect( safeDecodeURI( addQueryArgs( url, args ) ) ).toBe( 'https://andalouses.example/beach?time[0]=10&time[1]=11&beach[0]=sand&beach[1]=rock' ); } ); + + it( 'should disregard keys with undefined values', () => { + const url = 'https://andalouses.example/beach'; + const args = { sun: 'true', sand: undefined }; + + expect( addQueryArgs( url, args ) ).toBe( 'https://andalouses.example/beach?sun=true' ); + } ); + + it( 'should encodes spaces by RFC 3986', () => { + const url = 'https://andalouses.example/beach'; + const args = { activity: 'fun in the sun' }; + + expect( addQueryArgs( url, args ) ).toBe( 'https://andalouses.example/beach?activity=fun%20in%20the%20sun' ); + } ); } ); describe( 'getQueryArg', () => { diff --git a/test/e2e/specs/change-detection.test.js b/test/e2e/specs/change-detection.test.js index ab92f4ac76c4d..a03e06cc3dd0f 100644 --- a/test/e2e/specs/change-detection.test.js +++ b/test/e2e/specs/change-detection.test.js @@ -130,7 +130,17 @@ describe( 'Change detection', () => { await assertIsDirty( true ); } ); - it( 'Should not prompt to confirm unsaved changes', async () => { + it( 'Should not prompt to confirm unsaved changes for new post', async () => { + await assertIsDirty( false ); + } ); + + it( 'Should not prompt to confirm unsaved changes for new post with initial edits', async () => { + await newPost( { + title: 'My New Post', + content: 'My content', + excerpt: 'My excerpt', + } ); + await assertIsDirty( false ); } ); diff --git a/test/e2e/specs/demo.test.js b/test/e2e/specs/demo.test.js index a7d5f3dee95a5..bcf0987ae8771 100644 --- a/test/e2e/specs/demo.test.js +++ b/test/e2e/specs/demo.test.js @@ -38,4 +38,8 @@ describe( 'new editor state', () => { } ); expect( isDirty ).toBeFalsy(); } ); + + it( 'should be immediately saveable', async () => { + await page.$( 'button.editor-post-save-draft' ); + } ); } ); diff --git a/test/e2e/specs/new-post.test.js b/test/e2e/specs/new-post.test.js index aae59dc54f2a3..da171371330db 100644 --- a/test/e2e/specs/new-post.test.js +++ b/test/e2e/specs/new-post.test.js @@ -73,4 +73,11 @@ describe( 'new editor state', () => { // focused by default when a post already has a title. expect( activeElementTagName ).toEqual( 'body' ); } ); + + it( 'should be saveable with sufficient initial edits', async () => { + await newPost( { title: 'Here is the title' } ); + + // Verify saveable by presence of the Save Draft button. + await page.$( 'button.editor-post-save-draft' ); + } ); } ); diff --git a/test/e2e/support/utils.js b/test/e2e/support/utils.js index a6ae59f1a2221..20f07f4705df0 100644 --- a/test/e2e/support/utils.js +++ b/test/e2e/support/utils.js @@ -8,13 +8,18 @@ import { URL } from 'url'; * External dependencies */ import { times, castArray } from 'lodash'; +import fetch from 'node-fetch'; + +/** + * WordPress dependencies + */ +import { addQueryArgs } from '@wordpress/url'; const { WP_BASE_URL = 'http://localhost:8889', WP_USERNAME = 'admin', WP_PASSWORD = 'password', } = process.env; -import fetch from 'node-fetch'; /** * Platform-specific meta key. @@ -96,8 +101,20 @@ export async function visitAdmin( adminPath, query ) { } } -export async function newPost( { postType, enableTips = false } = {} ) { - await visitAdmin( 'post-new.php', postType ? 'post_type=' + postType : '' ); +export async function newPost( { + postType, + title, + content, + excerpt, + enableTips = false, +} = {} ) { + const query = addQueryArgs( '', { + post_type: postType, + post_title: title, + content, + excerpt, + } ).slice( 1 ); + await visitAdmin( 'post-new.php', query ); await page.evaluate( ( _enableTips ) => { const action = _enableTips ? 'enableTips' : 'disableTips'; From 101438904b3737a3d8858643d49abdc172c32411 Mon Sep 17 00:00:00 2001 From: Dennis Snell Date: Tue, 6 Nov 2018 19:58:42 -0600 Subject: [PATCH 023/106] Parser: Add new list of HTML fragments to parse output (#11334) Attempt three at including positional information from the parse to enable isomorphic reconstruction of the source `post_content` after parsing. See alternate attempts: #11082, #11309 Motivated by: #7247, #8760, automattic/jetpack#10256 Enables: #10463, #10108 ## Abstract Add new `innerContent` property to each block in parser output indicating where in the innerHTML each innerBlock was found. ## Status - will update fixtures after design review indicates this is the desired approach - all parsers passing new tests for fragment behavior ## Summary Inner blocks, or nested blocks, or blocks-within-blocks, can exist in Gutenberg posts. They are serialized in `post_content` in place as normal blocks which exist in between another block's comment delimiters. ```html Check out my and my other with its own content. ``` The way this gets parsed leaves us in a quandary: we cannot reconstruct the original `post_content` after parsing because we lose the origin location information for each inner block since they are only passed as an array of inner blocks. ```json { "blockName": "core/outerBlock", "attrs": {}, "innerBlocks": [ { "blockName": "core/voidInnerBlock", "attrs": {}, "innerBlocks": [], "innerHTML": "" }, { "blockName": "core/innerBlock", "attrs": {}, "innerBlocks": [], "innerHTML": "\nwith its own content.\n" } ], "innerHTML": "\nCheck out my\n\nand my other\n\n" } ``` At this point we have parsed the blocks and prepared them for attaching into the JavaScript block code that interprets them but we have lost our reverse transformation. In this PR I'd like to introduce a new mechanism which shouldn't break existing functionality but which will enable us to go back and forth isomorphically between the `post_content` and first stage of parsing. If we can tear apart a Gutenberg post and reassemble then it will let us to structurally-informed processing of the posts without needing to be aware of all the block JavaScript. The proposed mechanism is a new property as a **list of HTML fragments with `null` values interspersed between those fragments where the blocks were found**. ```json { "blockName": "core/outerBlock", "attrs": {}, "innerBlocks": [ { "blockName": "core/voidInnerBlock", "attrs": {}, "innerBlocks": [], "blockMarkers": [], "innerHTML": "" }, { "blockName": "core/innerBlock", "attrs": {}, "innerBlocks": [], "blockMarkers": [], "innerHTML": "\nwith its own content.\n" } ], "innerHTML": "\nCheck out my\n\nand my other\n\n", "innerContent": [ "\nCheck out my\n", null, "\n and my other\n", null, "\n" ], } ``` Doing this allows us to replace those `null` values with their associated block (sequentially) from `innerBlocks`. ## Questions - Why not use a string token instead of an array? - See #11309. The fundamental problem with the token is that it could be valid content input from a person and so there's a probability that we would fail to split the content accurately. - Why add the `null` instead of leaving basic array splits like `[ 'before', 'after' ]`? - By inspection we can see that without an explicit marker we don't know if the block came before or after or between array elements. We could add empty strings `''` and say that blocks exist only _between_ array elements but the parser code would have to be more complicated to make sure we appropriately add those empty strings. The empty strings are a bit odd anyway. - Why add a new property? - Code already depends on `innerHTML` and `innerBlocks`; I don't want to break any existing behaviors and adding is less risky than changing. --- lib/parser.php | 260 ++++++++++++---- .../parser.php | 45 ++- .../src/index.js | 39 +-- .../test/__snapshots__/index.js.snap | 6 + .../grammar.pegjs | 79 +++-- .../block-serialization-spec-parser/parser.js | 287 ++++++++++++++---- .../shared-tests.js | 27 ++ .../test/__snapshots__/index.js.snap | 6 + ...ore__4-invalid-starting-letter.parsed.json | 5 +- .../fixtures/core__archives.parsed.json | 3 +- ...core__archives__showPostCounts.parsed.json | 3 +- .../fixtures/core__audio.parsed.json | 6 +- .../fixtures/core__block.parsed.json | 6 +- .../fixtures/core__button__center.parsed.json | 6 +- .../fixtures/core__categories.parsed.json | 6 +- .../fixtures/core__code.parsed.json | 6 +- .../fixtures/core__column.parsed.json | 12 +- .../fixtures/core__columns.parsed.json | 52 +++- .../core__columns__deprecated.parsed.json | 38 ++- .../fixtures/core__cover.parsed.json | 10 +- .../core__cover__video-overlay.parsed.json | 10 +- .../fixtures/core__cover__video.parsed.json | 10 +- .../fixtures/core__embed.parsed.json | 10 +- .../core__file__new-window.parsed.json | 10 +- ...core__file__no-download-button.parsed.json | 10 +- .../core__file__no-text-link.parsed.json | 10 +- .../fixtures/core__freeform.parsed.json | 10 +- .../core__freeform__undelimited.parsed.json | 5 +- .../fixtures/core__gallery.parsed.json | 10 +- .../core__gallery__columns.parsed.json | 10 +- .../fixtures/core__heading__h2-em.parsed.json | 10 +- .../fixtures/core__heading__h2.parsed.json | 10 +- .../fixtures/core__html.parsed.json | 10 +- .../fixtures/core__image.parsed.json | 10 +- .../core__image__attachment-link.parsed.json | 10 +- .../core__image__center-caption.parsed.json | 10 +- .../core__image__custom-link.parsed.json | 10 +- .../core__image__media-link.parsed.json | 10 +- .../core__invalid-Capitals.parsed.json | 5 +- .../core__invalid-special.parsed.json | 5 +- .../core__latest-comments.parsed.json | 8 +- .../fixtures/core__latest-posts.parsed.json | 8 +- ..._latest-posts__displayPostDate.parsed.json | 8 +- .../fixtures/core__list__ul.parsed.json | 10 +- .../fixtures/core__media-text.parsed.json | 17 +- ...media-text__image-alt-no-align.parsed.json | 17 +- ...dia-text__is-stacked-on-mobile.parsed.json | 17 +- ...text__media-right-custom-width.parsed.json | 17 +- .../core__media-text__video.parsed.json | 17 +- .../fixtures/core__missing.parsed.json | 6 +- .../fixtures/core__more.parsed.json | 6 +- ...core__more__custom-text-teaser.parsed.json | 6 +- .../fixtures/core__nextpage.parsed.json | 6 +- .../core__paragraph__align-right.parsed.json | 6 +- .../core__paragraph__deprecated.parsed.json | 6 +- .../fixtures/core__preformatted.parsed.json | 6 +- .../fixtures/core__pullquote.parsed.json | 6 +- ...re__pullquote__multi-paragraph.parsed.json | 6 +- .../fixtures/core__quote__style-1.parsed.json | 6 +- .../fixtures/core__quote__style-2.parsed.json | 6 +- .../fixtures/core__separator.parsed.json | 6 +- .../fixtures/core__shortcode.parsed.json | 6 +- .../fixtures/core__spacer.parsed.json | 6 +- .../fixtures/core__subhead.parsed.json | 6 +- .../fixtures/core__table.parsed.json | 6 +- .../fixtures/core__text-columns.parsed.json | 6 +- ...e__text__converts-to-paragraph.parsed.json | 6 +- .../fixtures/core__verse.parsed.json | 6 +- .../fixtures/core__video.parsed.json | 6 +- 69 files changed, 1017 insertions(+), 309 deletions(-) diff --git a/lib/parser.php b/lib/parser.php index 330249196861a..85ca8213c3e60 100644 --- a/lib/parser.php +++ b/lib/parser.php @@ -259,20 +259,22 @@ private function peg_f1($pre, $bs, $post) { return peg_join_blocks( $pre, $bs, $ private function peg_f2($blockName, $a) { return $a; } private function peg_f3($blockName, $attrs) { return array( - 'blockName' => $blockName, - 'attrs' => isset( $attrs ) ? $attrs : array(), - 'innerBlocks' => array(), - 'innerHTML' => '', + 'blockName' => $blockName, + 'attrs' => isset( $attrs ) ? $attrs : array(), + 'innerBlocks' => array(), + 'innerHTML' => '', + 'innerContent' => array(), ); } private function peg_f4($s, $children, $e) { - list( $innerHTML, $innerBlocks ) = peg_array_partition( $children, 'is_string' ); + list( $innerHTML, $innerBlocks, $innerContent ) = peg_process_inner_content( $children ); return array( 'blockName' => $s['blockName'], 'attrs' => $s['attrs'], 'innerBlocks' => $innerBlocks, - 'innerHTML' => implode( '', $innerHTML ), + 'innerHTML' => $innerHTML, + 'innerContent' => $innerContent, ); } private function peg_f5($blockName, $attrs) { @@ -711,36 +713,106 @@ private function peg_parseBlock_Balanced() { $s3 = $this->peg_parseBlock(); if ($s3 === $this->peg_FAILED) { $s3 = $this->peg_currPos; - $s4 = $this->peg_currPos; + $s4 = array(); $s5 = $this->peg_currPos; + $s6 = $this->peg_currPos; $this->peg_silentFails++; - $s6 = $this->peg_parseBlock_End(); + $s7 = $this->peg_parseBlock(); $this->peg_silentFails--; - if ($s6 === $this->peg_FAILED) { - $s5 = null; + if ($s7 === $this->peg_FAILED) { + $s6 = null; + } else { + $this->peg_currPos = $s6; + $s6 = $this->peg_FAILED; + } + if ($s6 !== $this->peg_FAILED) { + $s7 = $this->peg_currPos; + $this->peg_silentFails++; + $s8 = $this->peg_parseBlock_End(); + $this->peg_silentFails--; + if ($s8 === $this->peg_FAILED) { + $s7 = null; + } else { + $this->peg_currPos = $s7; + $s7 = $this->peg_FAILED; + } + if ($s7 !== $this->peg_FAILED) { + if ($this->input_length > $this->peg_currPos) { + $s8 = $this->input_substr($this->peg_currPos, 1); + $this->peg_currPos++; + } else { + $s8 = $this->peg_FAILED; + if ($this->peg_silentFails === 0) { + $this->peg_fail($this->peg_c0); + } + } + if ($s8 !== $this->peg_FAILED) { + $s6 = array($s6, $s7, $s8); + $s5 = $s6; + } else { + $this->peg_currPos = $s5; + $s5 = $this->peg_FAILED; + } + } else { + $this->peg_currPos = $s5; + $s5 = $this->peg_FAILED; + } } else { $this->peg_currPos = $s5; $s5 = $this->peg_FAILED; } if ($s5 !== $this->peg_FAILED) { - if ($this->input_length > $this->peg_currPos) { - $s6 = $this->input_substr($this->peg_currPos, 1); - $this->peg_currPos++; - } else { - $s6 = $this->peg_FAILED; - if ($this->peg_silentFails === 0) { - $this->peg_fail($this->peg_c0); + while ($s5 !== $this->peg_FAILED) { + $s4[] = $s5; + $s5 = $this->peg_currPos; + $s6 = $this->peg_currPos; + $this->peg_silentFails++; + $s7 = $this->peg_parseBlock(); + $this->peg_silentFails--; + if ($s7 === $this->peg_FAILED) { + $s6 = null; + } else { + $this->peg_currPos = $s6; + $s6 = $this->peg_FAILED; + } + if ($s6 !== $this->peg_FAILED) { + $s7 = $this->peg_currPos; + $this->peg_silentFails++; + $s8 = $this->peg_parseBlock_End(); + $this->peg_silentFails--; + if ($s8 === $this->peg_FAILED) { + $s7 = null; + } else { + $this->peg_currPos = $s7; + $s7 = $this->peg_FAILED; + } + if ($s7 !== $this->peg_FAILED) { + if ($this->input_length > $this->peg_currPos) { + $s8 = $this->input_substr($this->peg_currPos, 1); + $this->peg_currPos++; + } else { + $s8 = $this->peg_FAILED; + if ($this->peg_silentFails === 0) { + $this->peg_fail($this->peg_c0); + } + } + if ($s8 !== $this->peg_FAILED) { + $s6 = array($s6, $s7, $s8); + $s5 = $s6; + } else { + $this->peg_currPos = $s5; + $s5 = $this->peg_FAILED; + } + } else { + $this->peg_currPos = $s5; + $s5 = $this->peg_FAILED; + } + } else { + $this->peg_currPos = $s5; + $s5 = $this->peg_FAILED; } - } - if ($s6 !== $this->peg_FAILED) { - $s5 = array($s5, $s6); - $s4 = $s5; - } else { - $this->peg_currPos = $s4; - $s4 = $this->peg_FAILED; } } else { - $this->peg_currPos = $s4; $s4 = $this->peg_FAILED; } if ($s4 !== $this->peg_FAILED) { @@ -754,36 +826,106 @@ private function peg_parseBlock_Balanced() { $s3 = $this->peg_parseBlock(); if ($s3 === $this->peg_FAILED) { $s3 = $this->peg_currPos; - $s4 = $this->peg_currPos; + $s4 = array(); $s5 = $this->peg_currPos; + $s6 = $this->peg_currPos; $this->peg_silentFails++; - $s6 = $this->peg_parseBlock_End(); + $s7 = $this->peg_parseBlock(); $this->peg_silentFails--; - if ($s6 === $this->peg_FAILED) { - $s5 = null; + if ($s7 === $this->peg_FAILED) { + $s6 = null; + } else { + $this->peg_currPos = $s6; + $s6 = $this->peg_FAILED; + } + if ($s6 !== $this->peg_FAILED) { + $s7 = $this->peg_currPos; + $this->peg_silentFails++; + $s8 = $this->peg_parseBlock_End(); + $this->peg_silentFails--; + if ($s8 === $this->peg_FAILED) { + $s7 = null; + } else { + $this->peg_currPos = $s7; + $s7 = $this->peg_FAILED; + } + if ($s7 !== $this->peg_FAILED) { + if ($this->input_length > $this->peg_currPos) { + $s8 = $this->input_substr($this->peg_currPos, 1); + $this->peg_currPos++; + } else { + $s8 = $this->peg_FAILED; + if ($this->peg_silentFails === 0) { + $this->peg_fail($this->peg_c0); + } + } + if ($s8 !== $this->peg_FAILED) { + $s6 = array($s6, $s7, $s8); + $s5 = $s6; + } else { + $this->peg_currPos = $s5; + $s5 = $this->peg_FAILED; + } + } else { + $this->peg_currPos = $s5; + $s5 = $this->peg_FAILED; + } } else { $this->peg_currPos = $s5; $s5 = $this->peg_FAILED; } if ($s5 !== $this->peg_FAILED) { - if ($this->input_length > $this->peg_currPos) { - $s6 = $this->input_substr($this->peg_currPos, 1); - $this->peg_currPos++; - } else { - $s6 = $this->peg_FAILED; - if ($this->peg_silentFails === 0) { - $this->peg_fail($this->peg_c0); + while ($s5 !== $this->peg_FAILED) { + $s4[] = $s5; + $s5 = $this->peg_currPos; + $s6 = $this->peg_currPos; + $this->peg_silentFails++; + $s7 = $this->peg_parseBlock(); + $this->peg_silentFails--; + if ($s7 === $this->peg_FAILED) { + $s6 = null; + } else { + $this->peg_currPos = $s6; + $s6 = $this->peg_FAILED; + } + if ($s6 !== $this->peg_FAILED) { + $s7 = $this->peg_currPos; + $this->peg_silentFails++; + $s8 = $this->peg_parseBlock_End(); + $this->peg_silentFails--; + if ($s8 === $this->peg_FAILED) { + $s7 = null; + } else { + $this->peg_currPos = $s7; + $s7 = $this->peg_FAILED; + } + if ($s7 !== $this->peg_FAILED) { + if ($this->input_length > $this->peg_currPos) { + $s8 = $this->input_substr($this->peg_currPos, 1); + $this->peg_currPos++; + } else { + $s8 = $this->peg_FAILED; + if ($this->peg_silentFails === 0) { + $this->peg_fail($this->peg_c0); + } + } + if ($s8 !== $this->peg_FAILED) { + $s6 = array($s6, $s7, $s8); + $s5 = $s6; + } else { + $this->peg_currPos = $s5; + $s5 = $this->peg_FAILED; + } + } else { + $this->peg_currPos = $s5; + $s5 = $this->peg_FAILED; + } + } else { + $this->peg_currPos = $s5; + $s5 = $this->peg_FAILED; } - } - if ($s6 !== $this->peg_FAILED) { - $s5 = array($s5, $s6); - $s4 = $s5; - } else { - $this->peg_currPos = $s4; - $s4 = $this->peg_FAILED; } } else { - $this->peg_currPos = $s4; $s4 = $this->peg_FAILED; } if ($s4 !== $this->peg_FAILED) { @@ -1441,18 +1583,23 @@ public function parse($input) { // are the same as `json_decode` // array arguments are backwards because of PHP - if ( ! function_exists( 'peg_array_partition' ) ) { - function peg_array_partition( $array, $predicate ) { - $truthy = array(); - $falsey = array(); + if ( ! function_exists( 'peg_process_inner_content' ) ) { + function peg_process_inner_content( $array ) { + $html = ''; + $blocks = array(); + $content = array(); foreach ( $array as $item ) { - call_user_func( $predicate, $item ) - ? $truthy[] = $item - : $falsey[] = $item; + if ( is_string( $item ) ) { + $html .= $item; + $content[] = $item; + } else { + $blocks[] = $item; + $content[] = null; + } } - return array( $truthy, $falsey ); + return array( $html, $blocks, $content ); } } @@ -1465,7 +1612,8 @@ function peg_join_blocks( $pre, $tokens, $post ) { 'blockName' => null, 'attrs' => array(), 'innerBlocks' => array(), - 'innerHTML' => $pre + 'innerHTML' => $pre, + 'innerContent' => array( $pre ), ); } @@ -1479,7 +1627,8 @@ function peg_join_blocks( $pre, $tokens, $post ) { 'blockName' => null, 'attrs' => array(), 'innerBlocks' => array(), - 'innerHTML' => $html + 'innerHTML' => $html, + 'innerContent' => array( $html ), ); } } @@ -1489,7 +1638,8 @@ function peg_join_blocks( $pre, $tokens, $post ) { 'blockName' => null, 'attrs' => array(), 'innerBlocks' => array(), - 'innerHTML' => $post + 'innerHTML' => $post, + 'innerContent' => array( $post ), ); } diff --git a/packages/block-serialization-default-parser/parser.php b/packages/block-serialization-default-parser/parser.php index 9eb8aae492b72..2211cf0c19226 100644 --- a/packages/block-serialization-default-parser/parser.php +++ b/packages/block-serialization-default-parser/parser.php @@ -48,11 +48,26 @@ class WP_Block_Parser_Block { */ public $innerHTML; - function __construct( $name, $attrs, $innerBlocks, $innerHTML ) { + /** + * List of string fragments and null markers where inner blocks were found + * + * @example array( + * 'innerHTML' => 'BeforeInnerAfter', + * 'innerBlocks' => array( block, block ), + * 'innerContent' => array( 'Before', null, 'Inner', null, 'After' ), + * ) + * + * @since 4.2.0 + * @var array + */ + public $innerContent; + + function __construct( $name, $attrs, $innerBlocks, $innerHTML, $innerContent ) { $this->blockName = $name; $this->attrs = $attrs; $this->innerBlocks = $innerBlocks; $this->innerHTML = $innerHTML; + $this->innerContent = $innerContent; } } @@ -252,14 +267,14 @@ function proceed() { ) ); } - $this->output[] = (array) new WP_Block_Parser_Block( $block_name, $attrs, array(), '' ); + $this->output[] = (array) new WP_Block_Parser_Block( $block_name, $attrs, array(), '', array() ); $this->offset = $start_offset + $token_length; return true; } // otherwise we found an inner block $this->add_inner_block( - new WP_Block_Parser_Block( $block_name, $attrs, array(), '' ), + new WP_Block_Parser_Block( $block_name, $attrs, array(), '', array() ), $start_offset, $token_length ); @@ -269,7 +284,7 @@ function proceed() { case 'block-opener': // track all newly-opened blocks on the stack array_push( $this->stack, new WP_Block_Parser_Frame( - new WP_Block_Parser_Block( $block_name, $attrs, array(), '' ), + new WP_Block_Parser_Block( $block_name, $attrs, array(), '', array() ), $start_offset, $token_length, $start_offset + $token_length, @@ -306,7 +321,9 @@ function proceed() { * block and add it as a new innerBlock to the parent */ $stack_top = array_pop( $this->stack ); - $stack_top->block->innerHTML .= substr( $this->document, $stack_top->prev_offset, $start_offset - $stack_top->prev_offset ); + $html = substr( $this->document, $stack_top->prev_offset, $start_offset - $stack_top->prev_offset ); + $stack_top->block->innerHTML .= $html; + $stack_top->block->innerContent[] = $html; $stack_top->prev_offset = $start_offset + $token_length; $this->add_inner_block( @@ -406,7 +423,7 @@ function next_token() { * @return WP_Block_Parser_Block freeform block object */ static function freeform( $innerHTML ) { - return new WP_Block_Parser_Block( null, array(), array(), $innerHTML ); + return new WP_Block_Parser_Block( null, array(), array(), $innerHTML, array( $innerHTML ) ); } /** @@ -441,7 +458,14 @@ function add_freeform( $length = null ) { function add_inner_block( WP_Block_Parser_Block $block, $token_start, $token_length, $last_offset = null ) { $parent = $this->stack[ count( $this->stack ) - 1 ]; $parent->block->innerBlocks[] = $block; - $parent->block->innerHTML .= substr( $this->document, $parent->prev_offset, $token_start - $parent->prev_offset ); + $html = substr( $this->document, $parent->prev_offset, $token_start - $parent->prev_offset ); + + if ( ! empty( $html ) ) { + $parent->block->innerHTML .= $html; + $parent->block->innerContent[] = $html; + } + + $parent->block->innerContent[] = null; $parent->prev_offset = $last_offset ? $last_offset : $token_start + $token_length; } @@ -456,10 +480,15 @@ function add_block_from_stack( $end_offset = null ) { $stack_top = array_pop( $this->stack ); $prev_offset = $stack_top->prev_offset; - $stack_top->block->innerHTML .= isset( $end_offset ) + $html = isset( $end_offset ) ? substr( $this->document, $prev_offset, $end_offset - $prev_offset ) : substr( $this->document, $prev_offset ); + if ( ! empty( $html ) ) { + $stack_top->block->innerHTML .= $html; + $stack_top->block->innerContent[] = $html; + } + if ( isset( $stack_top->leading_html_start ) ) { $this->output[] = (array) self::freeform( substr( $this->document, diff --git a/packages/block-serialization-default-parser/src/index.js b/packages/block-serialization-default-parser/src/index.js index 27dd93bb9fb0f..577284fc1e539 100644 --- a/packages/block-serialization-default-parser/src/index.js +++ b/packages/block-serialization-default-parser/src/index.js @@ -4,17 +4,18 @@ let output; let stack; const tokenizer = /)[^])+?}\s+)?(\/)?-->/g; -function Block( blockName, attrs, innerBlocks, innerHTML ) { +function Block( blockName, attrs, innerBlocks, innerHTML, innerContent ) { return { blockName, attrs, innerBlocks, innerHTML, + innerContent, }; } function Freeform( innerHTML ) { - return Block( null, {}, [], innerHTML ); + return Block( null, {}, [], innerHTML, [ innerHTML ] ); } function Frame( block, tokenStart, tokenLength, prevOffset, leadingHtmlStart ) { @@ -84,14 +85,14 @@ function proceed() { if ( null !== leadingHtmlStart ) { output.push( Freeform( document.substr( leadingHtmlStart, startOffset - leadingHtmlStart ) ) ); } - output.push( Block( blockName, attrs, [], '' ) ); + output.push( Block( blockName, attrs, [], '', [] ) ); offset = startOffset + tokenLength; return true; } // otherwise we found an inner block addInnerBlock( - Block( blockName, attrs, [], '' ), + Block( blockName, attrs, [], '', [] ), startOffset, tokenLength, ); @@ -102,7 +103,7 @@ function proceed() { // track all newly-opened blocks on the stack stack.push( Frame( - Block( blockName, attrs, [], '' ), + Block( blockName, attrs, [], '', [] ), startOffset, tokenLength, startOffset + tokenLength, @@ -134,10 +135,9 @@ function proceed() { // otherwise we're nested and we have to close out the current // block and add it as a innerBlock to the parent const stackTop = stack.pop(); - stackTop.block.innerHTML += document.substr( - stackTop.prevOffset, - startOffset - stackTop.prevOffset, - ); + const html = document.substr( stackTop.prevOffset, startOffset - stackTop.prevOffset ); + stackTop.block.innerHTML += html; + stackTop.block.innerContent.push( html ); stackTop.prevOffset = startOffset + tokenLength; addInnerBlock( @@ -230,20 +230,25 @@ function addFreeform( rawLength ) { function addInnerBlock( block, tokenStart, tokenLength, lastOffset ) { const parent = stack[ stack.length - 1 ]; parent.block.innerBlocks.push( block ); - parent.block.innerHTML += document.substr( - parent.prevOffset, - tokenStart - parent.prevOffset, - ); + const html = document.substr( parent.prevOffset, tokenStart - parent.prevOffset ); + + if ( html ) { + parent.block.innerHTML += html; + parent.block.innerContent.push( html ); + } + + parent.block.innerContent.push( null ); parent.prevOffset = lastOffset ? lastOffset : tokenStart + tokenLength; } function addBlockFromStack( endOffset ) { const { block, leadingHtmlStart, prevOffset, tokenStart } = stack.pop(); - if ( endOffset ) { - block.innerHTML += document.substr( prevOffset, endOffset - prevOffset ); - } else { - block.innerHTML += document.substr( prevOffset ); + const html = endOffset ? document.substr( prevOffset, endOffset - prevOffset ) : document.substr( prevOffset ); + + if ( html ) { + block.innerHTML += html; + block.innerContent.push( html ); } if ( null !== leadingHtmlStart ) { diff --git a/packages/block-serialization-default-parser/test/__snapshots__/index.js.snap b/packages/block-serialization-default-parser/test/__snapshots__/index.js.snap index 5b5abb61a3d2e..4a6910fa35f61 100644 --- a/packages/block-serialization-default-parser/test/__snapshots__/index.js.snap +++ b/packages/block-serialization-default-parser/test/__snapshots__/index.js.snap @@ -6,6 +6,9 @@ Array [ "attrs": Object {}, "blockName": "core/more", "innerBlocks": Array [], + "innerContent": Array [ + "", + ], "innerHTML": "", }, ] @@ -17,6 +20,9 @@ Array [ "attrs": Object {}, "blockName": "core/more", "innerBlocks": Array [], + "innerContent": Array [ + "", + ], "innerHTML": "", }, ] diff --git a/packages/block-serialization-spec-parser/grammar.pegjs b/packages/block-serialization-spec-parser/grammar.pegjs index 00d1fec211413..4bf80d2f6755a 100644 --- a/packages/block-serialization-spec-parser/grammar.pegjs +++ b/packages/block-serialization-spec-parser/grammar.pegjs @@ -51,18 +51,23 @@ // are the same as `json_decode` // array arguments are backwards because of PHP -if ( ! function_exists( 'peg_array_partition' ) ) { - function peg_array_partition( $array, $predicate ) { - $truthy = array(); - $falsey = array(); +if ( ! function_exists( 'peg_process_inner_content' ) ) { + function peg_process_inner_content( $array ) { + $html = ''; + $blocks = array(); + $content = array(); foreach ( $array as $item ) { - call_user_func( $predicate, $item ) - ? $truthy[] = $item - : $falsey[] = $item; + if ( is_string( $item ) ) { + $html .= $item; + $content[] = $item; + } else { + $blocks[] = $item; + $content[] = null; + } } - return array( $truthy, $falsey ); + return array( $html, $blocks, $content ); } } @@ -75,7 +80,8 @@ if ( ! function_exists( 'peg_join_blocks' ) ) { 'blockName' => null, 'attrs' => array(), 'innerBlocks' => array(), - 'innerHTML' => $pre + 'innerHTML' => $pre, + 'innerContent' => array( $pre ), ); } @@ -89,7 +95,8 @@ if ( ! function_exists( 'peg_join_blocks' ) ) { 'blockName' => null, 'attrs' => array(), 'innerBlocks' => array(), - 'innerHTML' => $html + 'innerHTML' => $html, + 'innerContent' => array( $html ), ); } } @@ -99,7 +106,8 @@ if ( ! function_exists( 'peg_join_blocks' ) ) { 'blockName' => null, 'attrs' => array(), 'innerBlocks' => array(), - 'innerHTML' => $post + 'innerHTML' => $post, + 'innerContent' => array( $post ), ); } @@ -115,6 +123,7 @@ function freeform( s ) { attrs: {}, innerBlocks: [], innerHTML: s, + innerContent: [ s ], }; } @@ -151,22 +160,27 @@ function maybeJSON( s ) { } } -function partition( predicate, list ) { +function processInnerContent( list ) { var i, l, item; - var truthy = []; - var falsey = []; + var html = ''; + var blocks = []; + var content = []; // 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 ) + if ( 'string' === typeof item ) { + html += item; + content.push( item ); + } else { + blocks.push( item ); + content.push( null ); + } }; - return [ truthy, falsey ]; + return [ html, blocks, content ]; } } @@ -197,10 +211,11 @@ Block_Void { /** $blockName, - 'attrs' => isset( $attrs ) ? $attrs : array(), - 'innerBlocks' => array(), - 'innerHTML' => '', + 'blockName' => $blockName, + 'attrs' => isset( $attrs ) ? $attrs : array(), + 'innerBlocks' => array(), + 'innerHTML' => '', + 'innerContent' => array(), ); ?> **/ @@ -208,33 +223,37 @@ Block_Void blockName: blockName, attrs: attrs || {}, innerBlocks: [], - innerHTML: '' + innerHTML: '', + innerContent: [] }; } Block_Balanced - = s:Block_Start children:(Block / $(!Block_End .))* e:Block_End + = s:Block_Start children:(Block / $((!Block !Block_End .)+))* e:Block_End { /** $s['blockName'], 'attrs' => $s['attrs'], 'innerBlocks' => $innerBlocks, - 'innerHTML' => implode( '', $innerHTML ), + 'innerHTML' => $innerHTML, + 'innerContent' => $innerContent, ); ?> **/ - var innerContent = partition( function( a ) { return 'string' === typeof a }, children ); - var innerHTML = innerContent[ 0 ]; - var innerBlocks = innerContent[ 1 ]; + var innerParts = processInnerContent( children ); + var innerHTML = innerParts[ 0 ]; + var innerBlocks = innerParts[ 1 ]; + var innerContent = innerParts[ 2 ]; return { blockName: s.blockName, attrs: s.attrs, innerBlocks: innerBlocks, - innerHTML: innerHTML.join( '' ) + innerHTML: innerHTML, + innerContent: innerContent, }; } diff --git a/packages/block-serialization-spec-parser/parser.js b/packages/block-serialization-spec-parser/parser.js index e716efb9e3e8d..85fdc6ec9b5d6 100644 --- a/packages/block-serialization-spec-parser/parser.js +++ b/packages/block-serialization-spec-parser/parser.js @@ -165,10 +165,11 @@ peg$c10 = function(blockName, attrs) { /** $blockName, - 'attrs' => isset( $attrs ) ? $attrs : array(), - 'innerBlocks' => array(), - 'innerHTML' => '', + 'blockName' => $blockName, + 'attrs' => isset( $attrs ) ? $attrs : array(), + 'innerBlocks' => array(), + 'innerHTML' => '', + 'innerContent' => array(), ); ?> **/ @@ -176,30 +177,34 @@ blockName: blockName, attrs: attrs || {}, innerBlocks: [], - innerHTML: '' + innerHTML: '', + innerContent: [] }; }, peg$c11 = function(s, children, e) { /** $s['blockName'], 'attrs' => $s['attrs'], 'innerBlocks' => $innerBlocks, - 'innerHTML' => implode( '', $innerHTML ), + 'innerHTML' => $innerHTML, + 'innerContent' => $innerContent, ); ?> **/ - var innerContent = partition( function( a ) { return 'string' === typeof a }, children ); - var innerHTML = innerContent[ 0 ]; - var innerBlocks = innerContent[ 1 ]; + var innerParts = processInnerContent( children ); + var innerHTML = innerParts[ 0 ]; + var innerBlocks = innerParts[ 1 ]; + var innerContent = innerParts[ 2 ]; return { blockName: s.blockName, attrs: s.attrs, innerBlocks: innerBlocks, - innerHTML: innerHTML.join( '' ) + innerHTML: innerHTML, + innerContent: innerContent, }; }, peg$c12 = "-->", @@ -784,7 +789,7 @@ } function peg$parseBlock_Balanced() { - var s0, s1, s2, s3, s4, s5, s6; + var s0, s1, s2, s3, s4, s5, s6, s7, s8; s0 = peg$currPos; s1 = peg$parseBlock_Start(); @@ -793,34 +798,102 @@ s3 = peg$parseBlock(); if (s3 === peg$FAILED) { s3 = peg$currPos; - s4 = peg$currPos; + s4 = []; s5 = peg$currPos; + s6 = peg$currPos; peg$silentFails++; - s6 = peg$parseBlock_End(); + s7 = peg$parseBlock(); peg$silentFails--; - if (s6 === peg$FAILED) { - s5 = void 0; + if (s7 === peg$FAILED) { + s6 = void 0; } else { - peg$currPos = s5; - s5 = peg$FAILED; + peg$currPos = s6; + s6 = peg$FAILED; } - if (s5 !== peg$FAILED) { - if (input.length > peg$currPos) { - s6 = input.charAt(peg$currPos); - peg$currPos++; + if (s6 !== peg$FAILED) { + s7 = peg$currPos; + peg$silentFails++; + s8 = peg$parseBlock_End(); + peg$silentFails--; + if (s8 === peg$FAILED) { + s7 = void 0; } else { - s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c0); } + peg$currPos = s7; + s7 = peg$FAILED; } - if (s6 !== peg$FAILED) { - s5 = [s5, s6]; - s4 = s5; + if (s7 !== peg$FAILED) { + if (input.length > peg$currPos) { + s8 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s8 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c0); } + } + if (s8 !== peg$FAILED) { + s6 = [s6, s7, s8]; + s5 = s6; + } else { + peg$currPos = s5; + s5 = peg$FAILED; + } } else { - peg$currPos = s4; - s4 = peg$FAILED; + peg$currPos = s5; + s5 = peg$FAILED; + } + } else { + peg$currPos = s5; + s5 = peg$FAILED; + } + if (s5 !== peg$FAILED) { + while (s5 !== peg$FAILED) { + s4.push(s5); + s5 = peg$currPos; + s6 = peg$currPos; + peg$silentFails++; + s7 = peg$parseBlock(); + peg$silentFails--; + if (s7 === peg$FAILED) { + s6 = void 0; + } else { + peg$currPos = s6; + s6 = peg$FAILED; + } + if (s6 !== peg$FAILED) { + s7 = peg$currPos; + peg$silentFails++; + s8 = peg$parseBlock_End(); + peg$silentFails--; + if (s8 === peg$FAILED) { + s7 = void 0; + } else { + peg$currPos = s7; + s7 = peg$FAILED; + } + if (s7 !== peg$FAILED) { + if (input.length > peg$currPos) { + s8 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s8 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c0); } + } + if (s8 !== peg$FAILED) { + s6 = [s6, s7, s8]; + s5 = s6; + } else { + peg$currPos = s5; + s5 = peg$FAILED; + } + } else { + peg$currPos = s5; + s5 = peg$FAILED; + } + } else { + peg$currPos = s5; + s5 = peg$FAILED; + } } } else { - peg$currPos = s4; s4 = peg$FAILED; } if (s4 !== peg$FAILED) { @@ -834,34 +907,102 @@ s3 = peg$parseBlock(); if (s3 === peg$FAILED) { s3 = peg$currPos; - s4 = peg$currPos; + s4 = []; s5 = peg$currPos; + s6 = peg$currPos; peg$silentFails++; - s6 = peg$parseBlock_End(); + s7 = peg$parseBlock(); peg$silentFails--; - if (s6 === peg$FAILED) { - s5 = void 0; + if (s7 === peg$FAILED) { + s6 = void 0; } else { - peg$currPos = s5; - s5 = peg$FAILED; + peg$currPos = s6; + s6 = peg$FAILED; } - if (s5 !== peg$FAILED) { - if (input.length > peg$currPos) { - s6 = input.charAt(peg$currPos); - peg$currPos++; + if (s6 !== peg$FAILED) { + s7 = peg$currPos; + peg$silentFails++; + s8 = peg$parseBlock_End(); + peg$silentFails--; + if (s8 === peg$FAILED) { + s7 = void 0; } else { - s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c0); } + peg$currPos = s7; + s7 = peg$FAILED; } - if (s6 !== peg$FAILED) { - s5 = [s5, s6]; - s4 = s5; + if (s7 !== peg$FAILED) { + if (input.length > peg$currPos) { + s8 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s8 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c0); } + } + if (s8 !== peg$FAILED) { + s6 = [s6, s7, s8]; + s5 = s6; + } else { + peg$currPos = s5; + s5 = peg$FAILED; + } } else { - peg$currPos = s4; - s4 = peg$FAILED; + peg$currPos = s5; + s5 = peg$FAILED; + } + } else { + peg$currPos = s5; + s5 = peg$FAILED; + } + if (s5 !== peg$FAILED) { + while (s5 !== peg$FAILED) { + s4.push(s5); + s5 = peg$currPos; + s6 = peg$currPos; + peg$silentFails++; + s7 = peg$parseBlock(); + peg$silentFails--; + if (s7 === peg$FAILED) { + s6 = void 0; + } else { + peg$currPos = s6; + s6 = peg$FAILED; + } + if (s6 !== peg$FAILED) { + s7 = peg$currPos; + peg$silentFails++; + s8 = peg$parseBlock_End(); + peg$silentFails--; + if (s8 === peg$FAILED) { + s7 = void 0; + } else { + peg$currPos = s7; + s7 = peg$FAILED; + } + if (s7 !== peg$FAILED) { + if (input.length > peg$currPos) { + s8 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s8 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c0); } + } + if (s8 !== peg$FAILED) { + s6 = [s6, s7, s8]; + s5 = s6; + } else { + peg$currPos = s5; + s5 = peg$FAILED; + } + } else { + peg$currPos = s5; + s5 = peg$FAILED; + } + } else { + peg$currPos = s5; + s5 = peg$FAILED; + } } } else { - peg$currPos = s4; s4 = peg$FAILED; } if (s4 !== peg$FAILED) { @@ -1478,18 +1619,23 @@ // are the same as `json_decode` // array arguments are backwards because of PHP - if ( ! function_exists( 'peg_array_partition' ) ) { - function peg_array_partition( $array, $predicate ) { - $truthy = array(); - $falsey = array(); + if ( ! function_exists( 'peg_process_inner_content' ) ) { + function peg_process_inner_content( $array ) { + $html = ''; + $blocks = array(); + $content = array(); foreach ( $array as $item ) { - call_user_func( $predicate, $item ) - ? $truthy[] = $item - : $falsey[] = $item; + if ( is_string( $item ) ) { + $html .= $item; + $content[] = $item; + } else { + $blocks[] = $item; + $content[] = null; + } } - return array( $truthy, $falsey ); + return array( $html, $blocks, $content ); } } @@ -1502,7 +1648,8 @@ 'blockName' => null, 'attrs' => array(), 'innerBlocks' => array(), - 'innerHTML' => $pre + 'innerHTML' => $pre, + 'innerContent' => array( $pre ), ); } @@ -1516,7 +1663,8 @@ 'blockName' => null, 'attrs' => array(), 'innerBlocks' => array(), - 'innerHTML' => $html + 'innerHTML' => $html, + 'innerContent' => array( $html ), ); } } @@ -1526,7 +1674,8 @@ 'blockName' => null, 'attrs' => array(), 'innerBlocks' => array(), - 'innerHTML' => $post + 'innerHTML' => $post, + 'innerContent' => array( $post ), ); } @@ -1542,6 +1691,7 @@ attrs: {}, innerBlocks: [], innerHTML: s, + innerContent: [ s ], }; } @@ -1578,22 +1728,27 @@ } } - function partition( predicate, list ) { + function processInnerContent( list ) { var i, l, item; - var truthy = []; - var falsey = []; + var html = ''; + var blocks = []; + var content = []; // 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 ) + if ( 'string' === typeof item ) { + html += item; + content.push( item ); + } else { + blocks.push( item ); + content.push( null ); + } }; - return [ truthy, falsey ]; + return [ html, blocks, content ]; } diff --git a/packages/block-serialization-spec-parser/shared-tests.js b/packages/block-serialization-spec-parser/shared-tests.js index abfee9be4b521..54137636d3afd 100644 --- a/packages/block-serialization-spec-parser/shared-tests.js +++ b/packages/block-serialization-spec-parser/shared-tests.js @@ -62,6 +62,33 @@ export const jsTester = ( parse ) => () => { ] ) ) ); } ); + describe( 'innerBlock placemarkers', () => { + test( 'innerContent exists', () => { + expect( parse( 'test' )[ 0 ] ).toHaveProperty( 'innerContent', [ 'test' ] ); + expect( parse( '' )[ 0 ] ).toHaveProperty( 'innerContent', [] ); + } ); + + test( 'innerContent contains innerHTML', () => { + expect( parse( 'Inner' )[ 0 ] ).toHaveProperty( 'innerContent', [ 'Inner' ] ); + } ); + + test( 'block locations become null', () => { + expect( parse( '' )[ 0 ] ).toHaveProperty( 'innerContent', [ null ] ); + } ); + + test( 'HTML soup appears after blocks', () => { + expect( parse( 'After' )[ 0 ] ).toHaveProperty( 'innerContent', [ null, 'After' ] ); + } ); + + test( 'HTML soup appears before blocks', () => { + expect( parse( 'Before' )[ 0 ] ).toHaveProperty( 'innerContent', [ 'Before', null ] ); + } ); + + test( 'blocks follow each other', () => { + expect( parse( '' )[ 0 ] ).toHaveProperty( 'innerContent', [ null, null ] ); + } ); + } ); + describe( 'attack vectors', () => { test( 'really long JSON attribute sections', () => { const length = 100000; 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 1a014545d9874..480cde5428505 100644 --- a/packages/block-serialization-spec-parser/test/__snapshots__/index.js.snap +++ b/packages/block-serialization-spec-parser/test/__snapshots__/index.js.snap @@ -6,6 +6,9 @@ Array [ "attrs": Object {}, "blockName": "core/more", "innerBlocks": Array [], + "innerContent": Array [ + "", + ], "innerHTML": "", }, ] @@ -17,6 +20,9 @@ Array [ "attrs": Array [], "blockName": "core/more", "innerBlocks": Array [], + "innerContent": Array [ + "", + ], "innerHTML": "", }, ] diff --git a/test/integration/full-content/fixtures/core__4-invalid-starting-letter.parsed.json b/test/integration/full-content/fixtures/core__4-invalid-starting-letter.parsed.json index 2dfce101b41c9..b3c0c79ad5cf3 100644 --- a/test/integration/full-content/fixtures/core__4-invalid-starting-letter.parsed.json +++ b/test/integration/full-content/fixtures/core__4-invalid-starting-letter.parsed.json @@ -3,6 +3,9 @@ "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ + "\n" + ] } ] diff --git a/test/integration/full-content/fixtures/core__archives.parsed.json b/test/integration/full-content/fixtures/core__archives.parsed.json index fab690ff93afd..f974a7a167ab3 100644 --- a/test/integration/full-content/fixtures/core__archives.parsed.json +++ b/test/integration/full-content/fixtures/core__archives.parsed.json @@ -6,6 +6,7 @@ "showPostCounts": false }, "innerBlocks": [], - "innerHTML": "" + "innerHTML": "", + "innerContent": [] } ] diff --git a/test/integration/full-content/fixtures/core__archives__showPostCounts.parsed.json b/test/integration/full-content/fixtures/core__archives__showPostCounts.parsed.json index cd58cbf96924d..88439c3fc9ff0 100644 --- a/test/integration/full-content/fixtures/core__archives__showPostCounts.parsed.json +++ b/test/integration/full-content/fixtures/core__archives__showPostCounts.parsed.json @@ -6,6 +6,7 @@ "showPostCounts": true }, "innerBlocks": [], - "innerHTML": "" + "innerHTML": "", + "innerContent": [] } ] diff --git a/test/integration/full-content/fixtures/core__audio.parsed.json b/test/integration/full-content/fixtures/core__audio.parsed.json index 33926c0b889b5..6b0acbd0c4a1f 100644 --- a/test/integration/full-content/fixtures/core__audio.parsed.json +++ b/test/integration/full-content/fixtures/core__audio.parsed.json @@ -5,12 +5,14 @@ "align": "right" }, "innerBlocks": [], - "innerHTML": "\n
\n \n
\n" + "innerHTML": "\n
\n \n
\n", + "innerContent": [ "\n
\n \n
\n" ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ "\n" ] } ] diff --git a/test/integration/full-content/fixtures/core__block.parsed.json b/test/integration/full-content/fixtures/core__block.parsed.json index 61b65e317a8e1..33c4d86f6c6c7 100644 --- a/test/integration/full-content/fixtures/core__block.parsed.json +++ b/test/integration/full-content/fixtures/core__block.parsed.json @@ -5,12 +5,14 @@ "ref": 123 }, "innerBlocks": [], - "innerHTML": "" + "innerHTML": "", + "innerContent": [] }, { "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ "\n" ] } ] diff --git a/test/integration/full-content/fixtures/core__button__center.parsed.json b/test/integration/full-content/fixtures/core__button__center.parsed.json index 0094e8f264073..352767e1c1a8f 100644 --- a/test/integration/full-content/fixtures/core__button__center.parsed.json +++ b/test/integration/full-content/fixtures/core__button__center.parsed.json @@ -5,12 +5,14 @@ "align": "center" }, "innerBlocks": [], - "innerHTML": "\n\n" + "innerHTML": "\n\n", + "innerContent": [ "\n\n" ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ "\n" ] } ] diff --git a/test/integration/full-content/fixtures/core__categories.parsed.json b/test/integration/full-content/fixtures/core__categories.parsed.json index df39a8078db37..60d03d7bc5062 100644 --- a/test/integration/full-content/fixtures/core__categories.parsed.json +++ b/test/integration/full-content/fixtures/core__categories.parsed.json @@ -7,12 +7,14 @@ "showHierarchy": false }, "innerBlocks": [], - "innerHTML": "" + "innerHTML": "", + "innerContent": [] }, { "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ "\n" ] } ] diff --git a/test/integration/full-content/fixtures/core__code.parsed.json b/test/integration/full-content/fixtures/core__code.parsed.json index 5aea1d9a14a01..d9bf0a215e82b 100644 --- a/test/integration/full-content/fixtures/core__code.parsed.json +++ b/test/integration/full-content/fixtures/core__code.parsed.json @@ -3,12 +3,14 @@ "blockName": "core/code", "attrs": {}, "innerBlocks": [], - "innerHTML": "\n
export default function MyButton() {\n\treturn <Button>Click Me!</Button>;\n}
\n" + "innerHTML": "\n
export default function MyButton() {\n\treturn <Button>Click Me!</Button>;\n}
\n", + "innerContent": [ "\n
export default function MyButton() {\n\treturn <Button>Click Me!</Button>;\n}
\n" ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ "\n" ] } ] diff --git a/test/integration/full-content/fixtures/core__column.parsed.json b/test/integration/full-content/fixtures/core__column.parsed.json index 74b53bdf6ae96..10f1e1a07cf05 100644 --- a/test/integration/full-content/fixtures/core__column.parsed.json +++ b/test/integration/full-content/fixtures/core__column.parsed.json @@ -7,21 +7,25 @@ "blockName": "core/paragraph", "attrs": {}, "innerBlocks": [], - "innerHTML": "\n\t

Column One, Paragraph One

\n\t" + "innerHTML": "\n\t

Column One, Paragraph One

\n\t", + "innerContent": [ "\n\t

Column One, Paragraph One

\n\t" ] }, { "blockName": "core/paragraph", "attrs": {}, "innerBlocks": [], - "innerHTML": "\n\t

Column One, Paragraph Two

\n\t" + "innerHTML": "\n\t

Column One, Paragraph Two

\n\t", + "innerContent": [ "\n\t

Column One, Paragraph Two

\n\t" ] } ], - "innerHTML": "\n
\n\t\n\t\n
\n" + "innerHTML": "\n
\n\t\n\t\n
\n", + "innerContent": [ "\n
\n\t", null, "\n\t", null, "\n
\n" ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ "\n" ] } ] diff --git a/test/integration/full-content/fixtures/core__columns.parsed.json b/test/integration/full-content/fixtures/core__columns.parsed.json index 9ec5cef66ad23..7dbde278bc08c 100644 --- a/test/integration/full-content/fixtures/core__columns.parsed.json +++ b/test/integration/full-content/fixtures/core__columns.parsed.json @@ -13,16 +13,29 @@ "blockName": "core/paragraph", "attrs": {}, "innerBlocks": [], - "innerHTML": "\n\t\t

Column One, Paragraph One

\n\t\t" + "innerHTML": "\n\t\t

Column One, Paragraph One

\n\t\t", + "innerContent": [ + "\n\t\t

Column One, Paragraph One

\n\t\t" + ] }, { "blockName": "core/paragraph", "attrs": {}, "innerBlocks": [], - "innerHTML": "\n\t\t

Column One, Paragraph Two

\n\t\t" + "innerHTML": "\n\t\t

Column One, Paragraph Two

\n\t\t", + "innerContent": [ + "\n\t\t

Column One, Paragraph Two

\n\t\t" + ] } ], - "innerHTML": "\n\t
\n\t\t\n\t\t\n\t
\n\t" + "innerHTML": "\n\t
\n\t\t\n\t\t\n\t
\n\t", + "innerContent": [ + "\n\t
\n\t\t", + null, + "\n\t\t", + null, + "\n\t
\n\t" + ] }, { "blockName": "core/column", @@ -32,24 +45,47 @@ "blockName": "core/paragraph", "attrs": {}, "innerBlocks": [], - "innerHTML": "\n\t\t

Column Two, Paragraph One

\n\t\t" + "innerHTML": "\n\t\t

Column Two, Paragraph One

\n\t\t", + "innerContent": [ + "\n\t\t

Column Two, Paragraph One

\n\t\t" + ] }, { "blockName": "core/paragraph", "attrs": {}, "innerBlocks": [], - "innerHTML": "\n\t\t

Column Three, Paragraph One

\n\t\t" + "innerHTML": "\n\t\t

Column Three, Paragraph One

\n\t\t", + "innerContent": [ + "\n\t\t

Column Three, Paragraph One

\n\t\t" + ] } ], - "innerHTML": "\n\t
\n\t\t\n\t\t\n\t
\n\t" + "innerHTML": "\n\t
\n\t\t\n\t\t\n\t
\n\t", + "innerContent": [ + "\n\t
\n\t\t", + null, + "\n\t\t", + null, + "\n\t
\n\t" + ] } ], - "innerHTML": "\n
\n\t\n\t\n
\n" + "innerHTML": "\n
\n\t\n\t\n
\n", + "innerContent": [ + "\n
\n\t", + null, + "\n\t", + null, + "\n
\n" + ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ + "\n" + ] } ] diff --git a/test/integration/full-content/fixtures/core__columns__deprecated.parsed.json b/test/integration/full-content/fixtures/core__columns__deprecated.parsed.json index 2d973027f4a5a..6bb604b40ecfb 100644 --- a/test/integration/full-content/fixtures/core__columns__deprecated.parsed.json +++ b/test/integration/full-content/fixtures/core__columns__deprecated.parsed.json @@ -11,7 +11,10 @@ "layout": "column-1" }, "innerBlocks": [], - "innerHTML": "\n\t

Column One, Paragraph One

\n\t" + "innerHTML": "\n\t

Column One, Paragraph One

\n\t", + "innerContent": [ + "\n\t

Column One, Paragraph One

\n\t" + ] }, { "blockName": "core/paragraph", @@ -19,7 +22,10 @@ "layout": "column-1" }, "innerBlocks": [], - "innerHTML": "\n\t

Column One, Paragraph Two

\n\t" + "innerHTML": "\n\t

Column One, Paragraph Two

\n\t", + "innerContent": [ + "\n\t

Column One, Paragraph Two

\n\t" + ] }, { "blockName": "core/paragraph", @@ -27,7 +33,10 @@ "layout": "column-2" }, "innerBlocks": [], - "innerHTML": "\n\t

Column Two, Paragraph One

\n\t" + "innerHTML": "\n\t

Column Two, Paragraph One

\n\t", + "innerContent": [ + "\n\t

Column Two, Paragraph One

\n\t" + ] }, { "blockName": "core/paragraph", @@ -35,15 +44,32 @@ "layout": "column-3" }, "innerBlocks": [], - "innerHTML": "\n\t

Column Three, Paragraph One

\n\t" + "innerHTML": "\n\t

Column Three, Paragraph One

\n\t", + "innerContent": [ + "\n\t

Column Three, Paragraph One

\n\t" + ] } ], - "innerHTML": "\n
\n\t\n\t\n\t\n\t\n
\n" + "innerHTML": "\n
\n\t\n\t\n\t\n\t\n
\n", + "innerContent": [ + "\n
\n\t", + null, + "\n\t", + null, + "\n\t", + null, + "\n\t", + null, + "\n
\n" + ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ + "\n" + ] } ] diff --git a/test/integration/full-content/fixtures/core__cover.parsed.json b/test/integration/full-content/fixtures/core__cover.parsed.json index ba371b9c5960f..14f37b617067b 100644 --- a/test/integration/full-content/fixtures/core__cover.parsed.json +++ b/test/integration/full-content/fixtures/core__cover.parsed.json @@ -6,12 +6,18 @@ "dimRatio": 40 }, "innerBlocks": [], - "innerHTML": "\n
\n

Guten Berg!

\n
\n" + "innerHTML": "\n
\n

Guten Berg!

\n
\n", + "innerContent": [ + "\n
\n

Guten Berg!

\n
\n" + ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ + "\n" + ] } ] diff --git a/test/integration/full-content/fixtures/core__cover__video-overlay.parsed.json b/test/integration/full-content/fixtures/core__cover__video-overlay.parsed.json index 71dd6c0a0294a..a2a3ccc94a713 100644 --- a/test/integration/full-content/fixtures/core__cover__video-overlay.parsed.json +++ b/test/integration/full-content/fixtures/core__cover__video-overlay.parsed.json @@ -8,12 +8,18 @@ "backgroundType": "video" }, "innerBlocks": [], - "innerHTML": "\n
\n\t\n\t

Guten Berg!

\n
\n" + "innerHTML": "\n
\n\t\n\t

Guten Berg!

\n
\n", + "innerContent": [ + "\n
\n\t\n\t

Guten Berg!

\n
\n" + ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ + "\n" + ] } ] diff --git a/test/integration/full-content/fixtures/core__cover__video.parsed.json b/test/integration/full-content/fixtures/core__cover__video.parsed.json index 218846a689b1f..a864b5fea344e 100644 --- a/test/integration/full-content/fixtures/core__cover__video.parsed.json +++ b/test/integration/full-content/fixtures/core__cover__video.parsed.json @@ -7,12 +7,18 @@ "backgroundType": "video" }, "innerBlocks": [], - "innerHTML": "\n
\n\t\n\t

Guten Berg!

\n
\n" + "innerHTML": "\n
\n\t\n\t

Guten Berg!

\n
\n", + "innerContent": [ + "\n
\n\t\n\t

Guten Berg!

\n
\n" + ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ + "\n" + ] } ] diff --git a/test/integration/full-content/fixtures/core__embed.parsed.json b/test/integration/full-content/fixtures/core__embed.parsed.json index 5efa23bb5f83c..43bbfac823cd0 100644 --- a/test/integration/full-content/fixtures/core__embed.parsed.json +++ b/test/integration/full-content/fixtures/core__embed.parsed.json @@ -5,12 +5,18 @@ "url": "https://example.com/" }, "innerBlocks": [], - "innerHTML": "\n
\n
\n https://example.com/\n
\n
Embedded content from an example URL
\n
\n" + "innerHTML": "\n
\n
\n https://example.com/\n
\n
Embedded content from an example URL
\n
\n", + "innerContent": [ + "\n
\n
\n https://example.com/\n
\n
Embedded content from an example URL
\n
\n" + ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ + "\n" + ] } ] diff --git a/test/integration/full-content/fixtures/core__file__new-window.parsed.json b/test/integration/full-content/fixtures/core__file__new-window.parsed.json index c674f669bc26f..a29bea33c5b2b 100644 --- a/test/integration/full-content/fixtures/core__file__new-window.parsed.json +++ b/test/integration/full-content/fixtures/core__file__new-window.parsed.json @@ -7,12 +7,18 @@ "id": 176 }, "innerBlocks": [], - "innerHTML": "\n\n" + "innerHTML": "\n\n", + "innerContent": [ + "\n\n" + ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ + "\n" + ] } ] diff --git a/test/integration/full-content/fixtures/core__file__no-download-button.parsed.json b/test/integration/full-content/fixtures/core__file__no-download-button.parsed.json index 65a2cffa5ec9a..ac019b969423c 100644 --- a/test/integration/full-content/fixtures/core__file__no-download-button.parsed.json +++ b/test/integration/full-content/fixtures/core__file__no-download-button.parsed.json @@ -7,12 +7,18 @@ "id": 176 }, "innerBlocks": [], - "innerHTML": "\n\n" + "innerHTML": "\n\n", + "innerContent": [ + "\n\n" + ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ + "\n" + ] } ] diff --git a/test/integration/full-content/fixtures/core__file__no-text-link.parsed.json b/test/integration/full-content/fixtures/core__file__no-text-link.parsed.json index 6a891d4ea839c..d70321c12c06d 100644 --- a/test/integration/full-content/fixtures/core__file__no-text-link.parsed.json +++ b/test/integration/full-content/fixtures/core__file__no-text-link.parsed.json @@ -7,12 +7,18 @@ "id": 176 }, "innerBlocks": [], - "innerHTML": "\n\n" + "innerHTML": "\n\n", + "innerContent": [ + "\n\n" + ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ + "\n" + ] } ] diff --git a/test/integration/full-content/fixtures/core__freeform.parsed.json b/test/integration/full-content/fixtures/core__freeform.parsed.json index 5cf330c0e9e99..1ec86e571b009 100644 --- a/test/integration/full-content/fixtures/core__freeform.parsed.json +++ b/test/integration/full-content/fixtures/core__freeform.parsed.json @@ -3,12 +3,18 @@ "blockName": "core/freeform", "attrs": {}, "innerBlocks": [], - "innerHTML": "\nTesting freeform block with some\n
\n\tHTML content\n
\n" + "innerHTML": "\nTesting freeform block with some\n
\n\tHTML content\n
\n", + "innerContent": [ + "\nTesting freeform block with some\n
\n\tHTML content\n
\n" + ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ + "\n" + ] } ] diff --git a/test/integration/full-content/fixtures/core__freeform__undelimited.parsed.json b/test/integration/full-content/fixtures/core__freeform__undelimited.parsed.json index 02999f7b830c4..4aca91e2f3a22 100644 --- a/test/integration/full-content/fixtures/core__freeform__undelimited.parsed.json +++ b/test/integration/full-content/fixtures/core__freeform__undelimited.parsed.json @@ -3,6 +3,9 @@ "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "Testing freeform block with some\n
\n\tHTML content\n
\n" + "innerHTML": "Testing freeform block with some\n
\n\tHTML content\n
\n", + "innerContent": [ + "Testing freeform block with some\n
\n\tHTML content\n
\n" + ] } ] diff --git a/test/integration/full-content/fixtures/core__gallery.parsed.json b/test/integration/full-content/fixtures/core__gallery.parsed.json index 3d80657c7f204..fc5c9e17d6890 100644 --- a/test/integration/full-content/fixtures/core__gallery.parsed.json +++ b/test/integration/full-content/fixtures/core__gallery.parsed.json @@ -3,12 +3,18 @@ "blockName": "core/gallery", "attrs": {}, "innerBlocks": [], - "innerHTML": "\n\n" + "innerHTML": "\n\n", + "innerContent": [ + "\n\n" + ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ + "\n" + ] } ] diff --git a/test/integration/full-content/fixtures/core__gallery__columns.parsed.json b/test/integration/full-content/fixtures/core__gallery__columns.parsed.json index 44faed907a8fc..6f6e4b856d7ec 100644 --- a/test/integration/full-content/fixtures/core__gallery__columns.parsed.json +++ b/test/integration/full-content/fixtures/core__gallery__columns.parsed.json @@ -5,12 +5,18 @@ "columns": 1 }, "innerBlocks": [], - "innerHTML": "\n\n" + "innerHTML": "\n\n", + "innerContent": [ + "\n\n" + ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ + "\n" + ] } ] diff --git a/test/integration/full-content/fixtures/core__heading__h2-em.parsed.json b/test/integration/full-content/fixtures/core__heading__h2-em.parsed.json index 76d235d42c896..e10209f2270c9 100644 --- a/test/integration/full-content/fixtures/core__heading__h2-em.parsed.json +++ b/test/integration/full-content/fixtures/core__heading__h2-em.parsed.json @@ -3,12 +3,18 @@ "blockName": "core/heading", "attrs": {}, "innerBlocks": [], - "innerHTML": "\n

The Inserter Tool

\n" + "innerHTML": "\n

The Inserter Tool

\n", + "innerContent": [ + "\n

The Inserter Tool

\n" + ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ + "\n" + ] } ] diff --git a/test/integration/full-content/fixtures/core__heading__h2.parsed.json b/test/integration/full-content/fixtures/core__heading__h2.parsed.json index 700f5f941e3fd..be6bc63b316de 100644 --- a/test/integration/full-content/fixtures/core__heading__h2.parsed.json +++ b/test/integration/full-content/fixtures/core__heading__h2.parsed.json @@ -3,12 +3,18 @@ "blockName": "core/heading", "attrs": {}, "innerBlocks": [], - "innerHTML": "\n

A picture is worth a thousand words, or so the saying goes

\n" + "innerHTML": "\n

A picture is worth a thousand words, or so the saying goes

\n", + "innerContent": [ + "\n

A picture is worth a thousand words, or so the saying goes

\n" + ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ + "\n" + ] } ] diff --git a/test/integration/full-content/fixtures/core__html.parsed.json b/test/integration/full-content/fixtures/core__html.parsed.json index 2bfdc658667ae..adb0cd44b3367 100644 --- a/test/integration/full-content/fixtures/core__html.parsed.json +++ b/test/integration/full-content/fixtures/core__html.parsed.json @@ -3,12 +3,18 @@ "blockName": "core/html", "attrs": {}, "innerBlocks": [], - "innerHTML": "\n

Some HTML code

\nThis text will scroll from right to left\n" + "innerHTML": "\n

Some HTML code

\nThis text will scroll from right to left\n", + "innerContent": [ + "\n

Some HTML code

\nThis text will scroll from right to left\n" + ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ + "\n" + ] } ] diff --git a/test/integration/full-content/fixtures/core__image.parsed.json b/test/integration/full-content/fixtures/core__image.parsed.json index fff414d9b01ba..d7e16a440ddef 100644 --- a/test/integration/full-content/fixtures/core__image.parsed.json +++ b/test/integration/full-content/fixtures/core__image.parsed.json @@ -3,12 +3,18 @@ "blockName": "core/image", "attrs": {}, "innerBlocks": [], - "innerHTML": "\n
\"\"
\n" + "innerHTML": "\n
\"\"
\n", + "innerContent": [ + "\n
\"\"
\n" + ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ + "\n" + ] } ] diff --git a/test/integration/full-content/fixtures/core__image__attachment-link.parsed.json b/test/integration/full-content/fixtures/core__image__attachment-link.parsed.json index e5da07242d556..fae6604516028 100644 --- a/test/integration/full-content/fixtures/core__image__attachment-link.parsed.json +++ b/test/integration/full-content/fixtures/core__image__attachment-link.parsed.json @@ -5,12 +5,18 @@ "linkDestination": "attachment" }, "innerBlocks": [], - "innerHTML": "\n
\"\"
\n" + "innerHTML": "\n
\"\"
\n", + "innerContent": [ + "\n
\"\"
\n" + ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ + "\n" + ] } ] diff --git a/test/integration/full-content/fixtures/core__image__center-caption.parsed.json b/test/integration/full-content/fixtures/core__image__center-caption.parsed.json index 290057c2876a7..02ff95e3ae8cb 100644 --- a/test/integration/full-content/fixtures/core__image__center-caption.parsed.json +++ b/test/integration/full-content/fixtures/core__image__center-caption.parsed.json @@ -5,12 +5,18 @@ "align": "center" }, "innerBlocks": [], - "innerHTML": "\n
\"\"
Give it a try. Press the "really wide" button on the image toolbar.
\n" + "innerHTML": "\n
\"\"
Give it a try. Press the "really wide" button on the image toolbar.
\n", + "innerContent": [ + "\n
\"\"
Give it a try. Press the "really wide" button on the image toolbar.
\n" + ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ + "\n" + ] } ] diff --git a/test/integration/full-content/fixtures/core__image__custom-link.parsed.json b/test/integration/full-content/fixtures/core__image__custom-link.parsed.json index d814737aaedf2..a3625c6a1f683 100644 --- a/test/integration/full-content/fixtures/core__image__custom-link.parsed.json +++ b/test/integration/full-content/fixtures/core__image__custom-link.parsed.json @@ -5,12 +5,18 @@ "linkDestination": "custom" }, "innerBlocks": [], - "innerHTML": "\n
\"\"
\n" + "innerHTML": "\n
\"\"
\n", + "innerContent": [ + "\n
\"\"
\n" + ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ + "\n" + ] } ] diff --git a/test/integration/full-content/fixtures/core__image__media-link.parsed.json b/test/integration/full-content/fixtures/core__image__media-link.parsed.json index 345c5d4e258ed..46458a7eec3d7 100644 --- a/test/integration/full-content/fixtures/core__image__media-link.parsed.json +++ b/test/integration/full-content/fixtures/core__image__media-link.parsed.json @@ -5,12 +5,18 @@ "linkDestination": "media" }, "innerBlocks": [], - "innerHTML": "\n
\"\"
\n" + "innerHTML": "\n
\"\"
\n", + "innerContent": [ + "\n
\"\"
\n" + ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ + "\n" + ] } ] diff --git a/test/integration/full-content/fixtures/core__invalid-Capitals.parsed.json b/test/integration/full-content/fixtures/core__invalid-Capitals.parsed.json index c17c81b398cf3..cbfafc27db9e9 100644 --- a/test/integration/full-content/fixtures/core__invalid-Capitals.parsed.json +++ b/test/integration/full-content/fixtures/core__invalid-Capitals.parsed.json @@ -3,6 +3,9 @@ "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ + "\n" + ] } ] diff --git a/test/integration/full-content/fixtures/core__invalid-special.parsed.json b/test/integration/full-content/fixtures/core__invalid-special.parsed.json index 3198b43be513a..05b96f6deb2fc 100644 --- a/test/integration/full-content/fixtures/core__invalid-special.parsed.json +++ b/test/integration/full-content/fixtures/core__invalid-special.parsed.json @@ -3,6 +3,9 @@ "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ + "\n" + ] } ] diff --git a/test/integration/full-content/fixtures/core__latest-comments.parsed.json b/test/integration/full-content/fixtures/core__latest-comments.parsed.json index 7e8bb541adc0c..b7a0e614220d8 100644 --- a/test/integration/full-content/fixtures/core__latest-comments.parsed.json +++ b/test/integration/full-content/fixtures/core__latest-comments.parsed.json @@ -7,12 +7,16 @@ "displayTimestamp": true }, "innerBlocks": [], - "innerHTML": "" + "innerHTML": "", + "innerContent": [] }, { "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ + "\n" + ] } ] diff --git a/test/integration/full-content/fixtures/core__latest-posts.parsed.json b/test/integration/full-content/fixtures/core__latest-posts.parsed.json index f1c800291d9bc..4279957a0f5c1 100644 --- a/test/integration/full-content/fixtures/core__latest-posts.parsed.json +++ b/test/integration/full-content/fixtures/core__latest-posts.parsed.json @@ -6,12 +6,16 @@ "displayPostDate": false }, "innerBlocks": [], - "innerHTML": "" + "innerHTML": "", + "innerContent": [] }, { "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ + "\n" + ] } ] diff --git a/test/integration/full-content/fixtures/core__latest-posts__displayPostDate.parsed.json b/test/integration/full-content/fixtures/core__latest-posts__displayPostDate.parsed.json index a92b392679099..57a36f5dce8b8 100644 --- a/test/integration/full-content/fixtures/core__latest-posts__displayPostDate.parsed.json +++ b/test/integration/full-content/fixtures/core__latest-posts__displayPostDate.parsed.json @@ -6,12 +6,16 @@ "displayPostDate": true }, "innerBlocks": [], - "innerHTML": "" + "innerHTML": "", + "innerContent": [] }, { "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ + "\n" + ] } ] diff --git a/test/integration/full-content/fixtures/core__list__ul.parsed.json b/test/integration/full-content/fixtures/core__list__ul.parsed.json index c42181ba553ac..83cb9ff7fc24d 100644 --- a/test/integration/full-content/fixtures/core__list__ul.parsed.json +++ b/test/integration/full-content/fixtures/core__list__ul.parsed.json @@ -3,12 +3,18 @@ "blockName": "core/list", "attrs": {}, "innerBlocks": [], - "innerHTML": "\n
  • Text & Headings
  • Images & Videos
  • Galleries
  • Embeds, like YouTube, Tweets, or other WordPress posts.
  • Layout blocks, like Buttons, Hero Images, Separators, etc.
  • And Lists like this one of course :)
\n" + "innerHTML": "\n
  • Text & Headings
  • Images & Videos
  • Galleries
  • Embeds, like YouTube, Tweets, or other WordPress posts.
  • Layout blocks, like Buttons, Hero Images, Separators, etc.
  • And Lists like this one of course :)
\n", + "innerContent": [ + "\n
  • Text & Headings
  • Images & Videos
  • Galleries
  • Embeds, like YouTube, Tweets, or other WordPress posts.
  • Layout blocks, like Buttons, Hero Images, Separators, etc.
  • And Lists like this one of course :)
\n" + ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ + "\n" + ] } ] diff --git a/test/integration/full-content/fixtures/core__media-text.parsed.json b/test/integration/full-content/fixtures/core__media-text.parsed.json index 82df709985669..ec0bf62e61317 100644 --- a/test/integration/full-content/fixtures/core__media-text.parsed.json +++ b/test/integration/full-content/fixtures/core__media-text.parsed.json @@ -13,15 +13,26 @@ "fontSize": "large" }, "innerBlocks": [], - "innerHTML": "\n\t\t

My Content

\n\t\t" + "innerHTML": "\n\t\t

My Content

\n\t\t", + "innerContent": [ + "\n\t\t

My Content

\n\t\t" + ] } ], - "innerHTML": "\n
\n\t
\n\t\t\"\"/\n\t
\n\t
\n\t\t\n\t
\n
\n" + "innerHTML": "\n
\n\t
\n\t\t\"\"/\n\t
\n\t
\n\t\t\n\t
\n
\n", + "innerContent": [ + "\n
\n\t
\n\t\t\"\"/\n\t
\n\t
\n\t\t", + null, + "\n\t
\n
\n" + ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ + "\n" + ] } ] diff --git a/test/integration/full-content/fixtures/core__media-text__image-alt-no-align.parsed.json b/test/integration/full-content/fixtures/core__media-text__image-alt-no-align.parsed.json index 5e9fc5b3f5e1e..e078f9c6ae589 100644 --- a/test/integration/full-content/fixtures/core__media-text__image-alt-no-align.parsed.json +++ b/test/integration/full-content/fixtures/core__media-text__image-alt-no-align.parsed.json @@ -14,15 +14,26 @@ "fontSize": "large" }, "innerBlocks": [], - "innerHTML": "\n\t\t

Content

\n\t\t" + "innerHTML": "\n\t\t

Content

\n\t\t", + "innerContent": [ + "\n\t\t

Content

\n\t\t" + ] } ], - "innerHTML": "\n
\n\t
\n\t\t\"my\n\t
\n\t
\n\t\t\n\t
\n
\n" + "innerHTML": "\n
\n\t
\n\t\t\"my\n\t
\n\t
\n\t\t\n\t
\n
\n", + "innerContent": [ + "\n
\n\t
\n\t\t\"my\n\t
\n\t
\n\t\t", + null, + "\n\t
\n
\n" + ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ + "\n" + ] } ] diff --git a/test/integration/full-content/fixtures/core__media-text__is-stacked-on-mobile.parsed.json b/test/integration/full-content/fixtures/core__media-text__is-stacked-on-mobile.parsed.json index ae97bce200994..3451267ab6897 100644 --- a/test/integration/full-content/fixtures/core__media-text__is-stacked-on-mobile.parsed.json +++ b/test/integration/full-content/fixtures/core__media-text__is-stacked-on-mobile.parsed.json @@ -14,15 +14,26 @@ "fontSize": "large" }, "innerBlocks": [], - "innerHTML": "\n\t\t

My Content

\n\t\t" + "innerHTML": "\n\t\t

My Content

\n\t\t", + "innerContent": [ + "\n\t\t

My Content

\n\t\t" + ] } ], - "innerHTML": "\n
\n\t
\n\t\t\n\t
\n\t
\n\t\t\n\t
\n
\n" + "innerHTML": "\n
\n\t
\n\t\t\n\t
\n\t
\n\t\t\n\t
\n
\n", + "innerContent": [ + "\n
\n\t
\n\t\t\n\t
\n\t
\n\t\t", + null, + "\n\t
\n
\n" + ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ + "\n" + ] } ] diff --git a/test/integration/full-content/fixtures/core__media-text__media-right-custom-width.parsed.json b/test/integration/full-content/fixtures/core__media-text__media-right-custom-width.parsed.json index d344565dadc83..37484502190c7 100644 --- a/test/integration/full-content/fixtures/core__media-text__media-right-custom-width.parsed.json +++ b/test/integration/full-content/fixtures/core__media-text__media-right-custom-width.parsed.json @@ -17,15 +17,26 @@ "fontSize": "large" }, "innerBlocks": [], - "innerHTML": "\n\t\t

My video

\n\t\t" + "innerHTML": "\n\t\t

My video

\n\t\t", + "innerContent": [ + "\n\t\t

My video

\n\t\t" + ] } ], - "innerHTML": "\n
\n\t
\n\t\t\n\t
\n\t
\n\t\t\n\t
\n
\n" + "innerHTML": "\n
\n\t
\n\t\t\n\t
\n\t
\n\t\t\n\t
\n
\n", + "innerContent": [ + "\n
\n\t
\n\t\t\n\t
\n\t
\n\t\t", + null, + "\n\t
\n
\n" + ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ + "\n" + ] } ] diff --git a/test/integration/full-content/fixtures/core__media-text__video.parsed.json b/test/integration/full-content/fixtures/core__media-text__video.parsed.json index ff58f6ab6c5fd..4e06eb858a520 100644 --- a/test/integration/full-content/fixtures/core__media-text__video.parsed.json +++ b/test/integration/full-content/fixtures/core__media-text__video.parsed.json @@ -13,15 +13,26 @@ "fontSize": "large" }, "innerBlocks": [], - "innerHTML": "\n\t\t

My Content

\n\t\t" + "innerHTML": "\n\t\t

My Content

\n\t\t", + "innerContent": [ + "\n\t\t

My Content

\n\t\t" + ] } ], - "innerHTML": "\n
\n\t
\n\t\t\n\t
\n\t
\n\t\t\n\t
\n
\n" + "innerHTML": "\n
\n\t
\n\t\t\n\t
\n\t
\n\t\t\n\t
\n
\n", + "innerContent": [ + "\n
\n\t
\n\t\t\n\t
\n\t
\n\t\t", + null, + "\n\t
\n
\n" + ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ + "\n" + ] } ] diff --git a/test/integration/full-content/fixtures/core__missing.parsed.json b/test/integration/full-content/fixtures/core__missing.parsed.json index 3a559918b6619..85ab1542c72a3 100644 --- a/test/integration/full-content/fixtures/core__missing.parsed.json +++ b/test/integration/full-content/fixtures/core__missing.parsed.json @@ -6,12 +6,14 @@ "attr2": "Two" }, "innerBlocks": [], - "innerHTML": "\n

Testing missing block with some

\n
\n\tHTML content\n
\n" + "innerHTML": "\n

Testing missing block with some

\n
\n\tHTML content\n
\n", + "innerContent": [ "\n

Testing missing block with some

\n
\n\tHTML content\n
\n" ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ "\n" ] } ] diff --git a/test/integration/full-content/fixtures/core__more.parsed.json b/test/integration/full-content/fixtures/core__more.parsed.json index 176e7fe7f7c16..b806b0c0af8db 100644 --- a/test/integration/full-content/fixtures/core__more.parsed.json +++ b/test/integration/full-content/fixtures/core__more.parsed.json @@ -3,12 +3,14 @@ "blockName": "core/more", "attrs": {}, "innerBlocks": [], - "innerHTML": "\n\n" + "innerHTML": "\n\n", + "innerContent": [ "\n\n" ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ "\n" ] } ] diff --git a/test/integration/full-content/fixtures/core__more__custom-text-teaser.parsed.json b/test/integration/full-content/fixtures/core__more__custom-text-teaser.parsed.json index a533e3c09c471..e3096b9d7cdbb 100644 --- a/test/integration/full-content/fixtures/core__more__custom-text-teaser.parsed.json +++ b/test/integration/full-content/fixtures/core__more__custom-text-teaser.parsed.json @@ -6,12 +6,14 @@ "noTeaser": true }, "innerBlocks": [], - "innerHTML": "\n\n\n" + "innerHTML": "\n\n\n", + "innerContent": [ "\n\n\n" ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ "\n" ] } ] diff --git a/test/integration/full-content/fixtures/core__nextpage.parsed.json b/test/integration/full-content/fixtures/core__nextpage.parsed.json index e4dd2534c821e..ff3f2703bf69a 100644 --- a/test/integration/full-content/fixtures/core__nextpage.parsed.json +++ b/test/integration/full-content/fixtures/core__nextpage.parsed.json @@ -3,12 +3,14 @@ "blockName": "core/nextpage", "attrs": {}, "innerBlocks": [], - "innerHTML": "\n\n" + "innerHTML": "\n\n", + "innerContent": [ "\n\n" ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ "\n" ] } ] diff --git a/test/integration/full-content/fixtures/core__paragraph__align-right.parsed.json b/test/integration/full-content/fixtures/core__paragraph__align-right.parsed.json index 060d40a38de5a..e0c2ab7be237b 100644 --- a/test/integration/full-content/fixtures/core__paragraph__align-right.parsed.json +++ b/test/integration/full-content/fixtures/core__paragraph__align-right.parsed.json @@ -5,12 +5,14 @@ "align": "right" }, "innerBlocks": [], - "innerHTML": "\n

... like this one, which is separate from the above and right aligned.

\n" + "innerHTML": "\n

... like this one, which is separate from the above and right aligned.

\n", + "innerContent": [ "\n

... like this one, which is separate from the above and right aligned.

\n" ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ "\n" ] } ] diff --git a/test/integration/full-content/fixtures/core__paragraph__deprecated.parsed.json b/test/integration/full-content/fixtures/core__paragraph__deprecated.parsed.json index ac11af1b3a92c..e6b914e24e111 100644 --- a/test/integration/full-content/fixtures/core__paragraph__deprecated.parsed.json +++ b/test/integration/full-content/fixtures/core__paragraph__deprecated.parsed.json @@ -3,12 +3,14 @@ "blockName": "core/paragraph", "attrs": {}, "innerBlocks": [], - "innerHTML": "\nUnwrapped is still valid.\n" + "innerHTML": "\nUnwrapped is still valid.\n", + "innerContent": [ "\nUnwrapped is still valid.\n" ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ "\n" ] } ] diff --git a/test/integration/full-content/fixtures/core__preformatted.parsed.json b/test/integration/full-content/fixtures/core__preformatted.parsed.json index 247fdda2e3eee..c78497076e90d 100644 --- a/test/integration/full-content/fixtures/core__preformatted.parsed.json +++ b/test/integration/full-content/fixtures/core__preformatted.parsed.json @@ -3,12 +3,14 @@ "blockName": "core/preformatted", "attrs": {}, "innerBlocks": [], - "innerHTML": "\n
Some preformatted text...
And more!
\n" + "innerHTML": "\n
Some preformatted text...
And more!
\n", + "innerContent": [ "\n
Some preformatted text...
And more!
\n" ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ "\n" ] } ] diff --git a/test/integration/full-content/fixtures/core__pullquote.parsed.json b/test/integration/full-content/fixtures/core__pullquote.parsed.json index 0e39eea4ec89c..033b311fa5a19 100644 --- a/test/integration/full-content/fixtures/core__pullquote.parsed.json +++ b/test/integration/full-content/fixtures/core__pullquote.parsed.json @@ -3,12 +3,14 @@ "blockName": "core/pullquote", "attrs": {}, "innerBlocks": [], - "innerHTML": "\n
\n
\n

Testing pullquote block...

...with a caption\n
\n
\n" + "innerHTML": "\n
\n
\n

Testing pullquote block...

...with a caption\n
\n
\n", + "innerContent": [ "\n
\n
\n

Testing pullquote block...

...with a caption\n
\n
\n" ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ "\n" ] } ] diff --git a/test/integration/full-content/fixtures/core__pullquote__multi-paragraph.parsed.json b/test/integration/full-content/fixtures/core__pullquote__multi-paragraph.parsed.json index 3b303a0d8d157..fe8abfce70a36 100644 --- a/test/integration/full-content/fixtures/core__pullquote__multi-paragraph.parsed.json +++ b/test/integration/full-content/fixtures/core__pullquote__multi-paragraph.parsed.json @@ -3,12 +3,14 @@ "blockName": "core/pullquote", "attrs": {}, "innerBlocks": [], - "innerHTML": "\n
\n
\n

Paragraph one

\n

Paragraph two

\n by whomever\n\t
\n
\n" + "innerHTML": "\n
\n
\n

Paragraph one

\n

Paragraph two

\n by whomever\n\t
\n
\n", + "innerContent": [ "\n
\n
\n

Paragraph one

\n

Paragraph two

\n by whomever\n\t
\n
\n" ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ "\n" ] } ] diff --git a/test/integration/full-content/fixtures/core__quote__style-1.parsed.json b/test/integration/full-content/fixtures/core__quote__style-1.parsed.json index 12f480ef313fe..6a873438f1731 100644 --- a/test/integration/full-content/fixtures/core__quote__style-1.parsed.json +++ b/test/integration/full-content/fixtures/core__quote__style-1.parsed.json @@ -3,12 +3,14 @@ "blockName": "core/quote", "attrs": {}, "innerBlocks": [], - "innerHTML": "\n

The editor will endeavour to create a new page and post building experience that makes writing rich posts effortless, and has “blocks” to make it easy what today might take shortcodes, custom HTML, or “mystery meat” embed discovery.

Matt Mullenweg, 2017
\n" + "innerHTML": "\n

The editor will endeavour to create a new page and post building experience that makes writing rich posts effortless, and has “blocks” to make it easy what today might take shortcodes, custom HTML, or “mystery meat” embed discovery.

Matt Mullenweg, 2017
\n", + "innerContent": [ "\n

The editor will endeavour to create a new page and post building experience that makes writing rich posts effortless, and has “blocks” to make it easy what today might take shortcodes, custom HTML, or “mystery meat” embed discovery.

Matt Mullenweg, 2017
\n" ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ "\n" ] } ] diff --git a/test/integration/full-content/fixtures/core__quote__style-2.parsed.json b/test/integration/full-content/fixtures/core__quote__style-2.parsed.json index 87a9377e2e9ed..6470afbc17a2e 100644 --- a/test/integration/full-content/fixtures/core__quote__style-2.parsed.json +++ b/test/integration/full-content/fixtures/core__quote__style-2.parsed.json @@ -5,12 +5,14 @@ "className": "is-style-large" }, "innerBlocks": [], - "innerHTML": "\n

There is no greater agony than bearing an untold story inside you.

Maya Angelou
\n" + "innerHTML": "\n

There is no greater agony than bearing an untold story inside you.

Maya Angelou
\n", + "innerContent": [ "\n

There is no greater agony than bearing an untold story inside you.

Maya Angelou
\n" ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ "\n" ] } ] diff --git a/test/integration/full-content/fixtures/core__separator.parsed.json b/test/integration/full-content/fixtures/core__separator.parsed.json index d01eb1eed1f59..48a8e742c35b0 100644 --- a/test/integration/full-content/fixtures/core__separator.parsed.json +++ b/test/integration/full-content/fixtures/core__separator.parsed.json @@ -3,12 +3,14 @@ "blockName": "core/separator", "attrs": {}, "innerBlocks": [], - "innerHTML": "\n
\n" + "innerHTML": "\n
\n", + "innerContent": [ "\n
\n" ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ "\n" ] } ] diff --git a/test/integration/full-content/fixtures/core__shortcode.parsed.json b/test/integration/full-content/fixtures/core__shortcode.parsed.json index 70d24dc276253..b875770f15a45 100644 --- a/test/integration/full-content/fixtures/core__shortcode.parsed.json +++ b/test/integration/full-content/fixtures/core__shortcode.parsed.json @@ -3,12 +3,14 @@ "blockName": "core/shortcode", "attrs": {}, "innerBlocks": [], - "innerHTML": "\n[gallery ids=\"238,338\"]\n" + "innerHTML": "\n[gallery ids=\"238,338\"]\n", + "innerContent": [ "\n[gallery ids=\"238,338\"]\n" ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ "\n" ] } ] diff --git a/test/integration/full-content/fixtures/core__spacer.parsed.json b/test/integration/full-content/fixtures/core__spacer.parsed.json index 8b8baa2a7fc61..c3c0938df5b9d 100644 --- a/test/integration/full-content/fixtures/core__spacer.parsed.json +++ b/test/integration/full-content/fixtures/core__spacer.parsed.json @@ -3,12 +3,14 @@ "blockName": "core/spacer", "attrs": {}, "innerBlocks": [], - "innerHTML": "\n
\n" + "innerHTML": "\n
\n", + "innerContent": [ "\n
\n" ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ "\n" ] } ] diff --git a/test/integration/full-content/fixtures/core__subhead.parsed.json b/test/integration/full-content/fixtures/core__subhead.parsed.json index 06ca46da1b88d..d88b9ee4c90b6 100644 --- a/test/integration/full-content/fixtures/core__subhead.parsed.json +++ b/test/integration/full-content/fixtures/core__subhead.parsed.json @@ -3,12 +3,14 @@ "blockName": "core/subhead", "attrs": {}, "innerBlocks": [], - "innerHTML": "\n

This is a subhead.

\n" + "innerHTML": "\n

This is a subhead.

\n", + "innerContent": [ "\n

This is a subhead.

\n" ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ "\n" ] } ] diff --git a/test/integration/full-content/fixtures/core__table.parsed.json b/test/integration/full-content/fixtures/core__table.parsed.json index 8c24b96113b94..7a2d91003f437 100644 --- a/test/integration/full-content/fixtures/core__table.parsed.json +++ b/test/integration/full-content/fixtures/core__table.parsed.json @@ -3,12 +3,14 @@ "blockName": "core/table", "attrs": {}, "innerBlocks": [], - "innerHTML": "\n
VersionMusicianDate
.70No musician chosen.May 27, 2003
1.0Miles DavisJanuary 3, 2004
Lots of versions skipped, see the full list
4.4Clifford BrownDecember 8, 2015
4.5Coleman HawkinsApril 12, 2016
4.6Pepper AdamsAugust 16, 2016
4.7Sarah VaughanDecember 6, 2016
\n" + "innerHTML": "\n
VersionMusicianDate
.70No musician chosen.May 27, 2003
1.0Miles DavisJanuary 3, 2004
Lots of versions skipped, see the full list
4.4Clifford BrownDecember 8, 2015
4.5Coleman HawkinsApril 12, 2016
4.6Pepper AdamsAugust 16, 2016
4.7Sarah VaughanDecember 6, 2016
\n", + "innerContent": [ "\n
VersionMusicianDate
.70No musician chosen.May 27, 2003
1.0Miles DavisJanuary 3, 2004
Lots of versions skipped, see the full list
4.4Clifford BrownDecember 8, 2015
4.5Coleman HawkinsApril 12, 2016
4.6Pepper AdamsAugust 16, 2016
4.7Sarah VaughanDecember 6, 2016
\n" ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ "\n" ] } ] diff --git a/test/integration/full-content/fixtures/core__text-columns.parsed.json b/test/integration/full-content/fixtures/core__text-columns.parsed.json index 293c930ac59e6..1a7db4e09ed3a 100644 --- a/test/integration/full-content/fixtures/core__text-columns.parsed.json +++ b/test/integration/full-content/fixtures/core__text-columns.parsed.json @@ -5,12 +5,14 @@ "width": "center" }, "innerBlocks": [], - "innerHTML": "\n
\n
\n

One

\n
\n
\n

Two

\n
\n
\n" + "innerHTML": "\n
\n
\n

One

\n
\n
\n

Two

\n
\n
\n", + "innerContent": [ "\n
\n
\n

One

\n
\n
\n

Two

\n
\n
\n" ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ "\n" ] } ] diff --git a/test/integration/full-content/fixtures/core__text__converts-to-paragraph.parsed.json b/test/integration/full-content/fixtures/core__text__converts-to-paragraph.parsed.json index 958d2a2921327..75a5ca1140907 100644 --- a/test/integration/full-content/fixtures/core__text__converts-to-paragraph.parsed.json +++ b/test/integration/full-content/fixtures/core__text__converts-to-paragraph.parsed.json @@ -3,12 +3,14 @@ "blockName": "core/text", "attrs": {}, "innerBlocks": [], - "innerHTML": "\n

This is an old-style text block. Changed to paragraph in #2135.

\n" + "innerHTML": "\n

This is an old-style text block. Changed to paragraph in #2135.

\n", + "innerContent": [ "\n

This is an old-style text block. Changed to paragraph in #2135.

\n" ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ "\n" ] } ] diff --git a/test/integration/full-content/fixtures/core__verse.parsed.json b/test/integration/full-content/fixtures/core__verse.parsed.json index 4f244cb551d50..4cccc9383a50c 100644 --- a/test/integration/full-content/fixtures/core__verse.parsed.json +++ b/test/integration/full-content/fixtures/core__verse.parsed.json @@ -3,12 +3,14 @@ "blockName": "core/verse", "attrs": {}, "innerBlocks": [], - "innerHTML": "\n
A verse
And more!
\n" + "innerHTML": "\n
A verse
And more!
\n", + "innerContent": [ "\n
A verse
And more!
\n" ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ "\n" ] } ] diff --git a/test/integration/full-content/fixtures/core__video.parsed.json b/test/integration/full-content/fixtures/core__video.parsed.json index 7b448d6f380d9..e9be9d8a2ea2c 100644 --- a/test/integration/full-content/fixtures/core__video.parsed.json +++ b/test/integration/full-content/fixtures/core__video.parsed.json @@ -3,12 +3,14 @@ "blockName": "core/video", "attrs": {}, "innerBlocks": [], - "innerHTML": "\n
\n" + "innerHTML": "\n
\n", + "innerContent": [ "\n
\n" ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], - "innerHTML": "\n" + "innerHTML": "\n", + "innerContent": [ "\n" ] } ] From 7a20afe72b5a3a6a5574a514dfeea3bd201947ab Mon Sep 17 00:00:00 2001 From: Danilo Ercoli Date: Wed, 7 Nov 2018 09:54:11 +0100 Subject: [PATCH 024/106] [RNMobile] Remove delayed call to `onChange` method (#11537) * Remove 1 sec timeout on `onChange` call. This is required to keep content in synch when mergin/splitting/removing blocks. * Remove all refs to the updater timer * Trivial change pushed to restart Travis --- .../src/components/rich-text/index.native.js | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/packages/editor/src/components/rich-text/index.native.js b/packages/editor/src/components/rich-text/index.native.js index 71cd3b85d2422..c0fb04867bde4 100644 --- a/packages/editor/src/components/rich-text/index.native.js +++ b/packages/editor/src/components/rich-text/index.native.js @@ -172,23 +172,16 @@ export class RichText extends Component { } /** - * Handles any case where the content of the AztecRN instance has changed. + * Handles any case where the content of the AztecRN instance has changed */ onChange( event ) { - // If we had a timer set to propagate a change, let's cancel it, because the user meanwhile typed something extra - if ( !! this.currentTimer ) { - clearTimeout( this.currentTimer ); - } this.lastEventCount = event.nativeEvent.eventCount; const contentWithoutRootTag = this.removeRootTagsProduceByAztec( event.nativeEvent.text ); this.lastContent = contentWithoutRootTag; - // Set a time to call the onChange prop if nothing changes in the next second - this.currentTimer = setTimeout( function() { - this.props.onChange( { - content: this.lastContent, - } ); - }.bind( this ), 1000 ); + this.props.onChange( { + content: this.lastContent, + } ); } /** From a61278d5821afccb0c9c2ad51bc6648b2ae7ced4 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Wed, 7 Nov 2018 10:18:32 +0100 Subject: [PATCH 025/106] Only show the image sizes with names (#11481) --- lib/client-assets.php | 8 ++---- packages/block-library/src/image/edit.js | 36 +++++++++++++----------- packages/editor/src/store/defaults.js | 18 ++++++------ 3 files changed, 32 insertions(+), 30 deletions(-) diff --git a/lib/client-assets.php b/lib/client-assets.php index 44eb49b7594f0..3cdfda9619901 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -1332,8 +1332,6 @@ function gutenberg_load_locale_data() { * @return array */ function gutenberg_get_available_image_sizes() { - $sizes = get_intermediate_image_sizes(); - $sizes[] = 'full'; $size_names = apply_filters( 'image_size_names_choose', array( @@ -1345,10 +1343,10 @@ function gutenberg_get_available_image_sizes() { ); $all_sizes = array(); - foreach ( $sizes as $size_slug ) { + foreach ( $size_names as $size_slug => $size_name ) { $all_sizes[] = array( 'slug' => $size_slug, - 'name' => isset( $size_names[ $size_slug ] ) ? $size_names[ $size_slug ] : $size_slug, + 'name' => $size_name, ); } @@ -1622,7 +1620,7 @@ function gutenberg_editor_scripts_and_styles( $hook ) { 'maxUploadFileSize' => $max_upload_size, 'allowedMimeTypes' => get_allowed_mime_types(), 'styles' => $styles, - 'availableImageSizes' => gutenberg_get_available_image_sizes(), + 'imageSizes' => gutenberg_get_available_image_sizes(), // Ideally, we'd remove this and rely on a REST API endpoint. 'postLock' => $lock_details, diff --git a/packages/block-library/src/image/edit.js b/packages/block-library/src/image/edit.js index 4d0e4c8dd3d2c..3d0c3706d14e4 100644 --- a/packages/block-library/src/image/edit.js +++ b/packages/block-library/src/image/edit.js @@ -7,8 +7,7 @@ import { isEmpty, map, pick, - startCase, - keyBy, + compact, } from 'lodash'; /** @@ -255,10 +254,6 @@ class ImageEdit extends Component { }; } - getImageSizes() { - return get( this.props.image, [ 'media_details', 'sizes' ], {} ); - } - getLinkDestinationOptions() { return [ { value: LINK_DESTINATION_NONE, label: __( 'None' ) }, @@ -274,6 +269,20 @@ class ImageEdit extends Component { } ); } + getImageSizeOptions() { + const { imageSizes, image } = this.props; + return compact( map( imageSizes, ( { name, slug } ) => { + const sizeUrl = get( image, [ 'media_details', 'sizes', slug, 'source_url' ] ); + if ( ! sizeUrl ) { + return null; + } + return { + value: sizeUrl, + label: name, + }; + } ) ); + } + render() { const { isEditing } = this.state; const { @@ -286,11 +295,10 @@ class ImageEdit extends Component { noticeUI, toggleSelection, isRTL, - availableImageSizes, } = this.props; const { url, alt, caption, align, id, href, linkDestination, width, height, linkTarget } = attributes; const isExternal = isExternalImage( id, url ); - const availableImageSizesBySlug = keyBy( availableImageSizes, 'slug' ); + const imageSizeOptions = this.getImageSizeOptions(); let toolbarEditButton; if ( url ) { @@ -362,7 +370,6 @@ class ImageEdit extends Component { 'is-focused': isSelected, } ); - const imageSizes = this.getImageSizes(); const isResizable = [ 'wide', 'full' ].indexOf( align ) === -1 && isLargeViewport; const isLinkURLInputDisabled = linkDestination !== LINK_DESTINATION_CUSTOM; @@ -375,14 +382,11 @@ class ImageEdit extends Component { onChange={ this.updateAlt } help={ __( 'Alternative text describes your image to people who can’t see it. Add a short description with its key details.' ) } /> - { ! isEmpty( imageSizes ) && ( + { ! isEmpty( imageSizeOptions ) && ( ( { - value: size.source_url, - label: availableImageSizesBySlug[ slug ] ? availableImageSizesBySlug[ slug ].name : startCase( slug ), - } ) ) } + options={ imageSizeOptions } onChange={ this.updateImageURL } /> ) } @@ -596,13 +600,13 @@ export default compose( [ const { getMedia } = select( 'core' ); const { getEditorSettings } = select( 'core/editor' ); const { id } = props.attributes; - const { maxWidth, isRTL, availableImageSizes } = getEditorSettings(); + const { maxWidth, isRTL, imageSizes } = getEditorSettings(); return { image: id ? getMedia( id ) : null, maxWidth, isRTL, - availableImageSizes, + imageSizes, }; } ), withViewportMatch( { isLargeViewport: 'medium' } ), diff --git a/packages/editor/src/store/defaults.js b/packages/editor/src/store/defaults.js index 9776836510645..78a05a7a315bf 100644 --- a/packages/editor/src/store/defaults.js +++ b/packages/editor/src/store/defaults.js @@ -11,14 +11,14 @@ export const PREFERENCES_DEFAULTS = { /** * The default editor settings * - * alignWide boolean Enable/Disable Wide/Full Alignments - * colors Array Palette colors - * fontSizes Array Available font sizes - * availableImageSizes Array Available image sizes - * maxWidth number Max width to constraint resizing - * blockTypes boolean|Array Allowed block types - * hasFixedToolbar boolean Whether or not the editor toolbar is fixed - * focusMode boolean Whether the focus mode is enabled or not + * alignWide boolean Enable/Disable Wide/Full Alignments + * colors Array Palette colors + * fontSizes Array Available font sizes + * imageSizes Array Available image sizes + * maxWidth number Max width to constraint resizing + * blockTypes boolean|Array Allowed block types + * hasFixedToolbar boolean Whether or not the editor toolbar is fixed + * focusMode boolean Whether the focus mode is enabled or not */ export const EDITOR_SETTINGS_DEFAULTS = { alignWide: false, @@ -107,7 +107,7 @@ export const EDITOR_SETTINGS_DEFAULTS = { }, ], - availableImageSizes: [ + imageSizes: [ { slug: 'thumbnail', label: __( 'Thumbnail' ) }, { slug: 'medium', label: __( 'Medium' ) }, { slug: 'large', label: __( 'Large' ) }, From 489eb792391a4d2bcb2bde701b52507f1d7334d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20Van=C2=A0Dorpe?= Date: Wed, 7 Nov 2018 10:49:55 +0100 Subject: [PATCH 026/106] Separate Paste Handler (#11539) * Separate paste handler * Add test for WP raw HTML conversion * Add docs --- packages/blocks/src/api/index.js | 2 +- packages/blocks/src/api/raw-handling/index.js | 128 +++++++++++++----- .../src/components/block-drop-zone/index.js | 4 +- .../block-list/block-invalid-warning.js | 1 - .../block-html-convert-button.js | 13 +- .../block-unknown-convert-button.js | 13 +- .../editor/src/components/rich-text/index.js | 6 +- .../blocks-raw-handling.spec.js.snap | 35 +++++ test/integration/blocks-raw-handling.spec.js | 38 ++++-- .../fixtures/wordpress-convert.html | 8 ++ 10 files changed, 174 insertions(+), 74 deletions(-) create mode 100644 test/integration/__snapshots__/blocks-raw-handling.spec.js.snap create mode 100644 test/integration/fixtures/wordpress-convert.html diff --git a/packages/blocks/src/api/index.js b/packages/blocks/src/api/index.js index d9fb8f5668964..0bdf1d01abd6b 100644 --- a/packages/blocks/src/api/index.js +++ b/packages/blocks/src/api/index.js @@ -11,7 +11,7 @@ export { getBlockAttributes, parseWithAttributeSchema, } from './parser'; -export { default as rawHandler, getPhrasingContentSchema } from './raw-handling'; +export { pasteHandler, rawHandler, getPhrasingContentSchema } from './raw-handling'; export { default as serialize, getBlockContent, diff --git a/packages/blocks/src/api/raw-handling/index.js b/packages/blocks/src/api/raw-handling/index.js index 82488e7038077..c38cdab46e0dc 100644 --- a/packages/blocks/src/api/raw-handling/index.js +++ b/packages/blocks/src/api/raw-handling/index.js @@ -65,6 +65,51 @@ function getRawTransformations() { } ); } +/** + * Converts HTML directly to blocks. Looks for a matching transform for each + * top-level tag. The HTML should be filtered to not have any text between + * top-level tags and formatted in a way that blocks can handle the HTML. + * + * @param {Object} $1 Named parameters. + * @param {string} $1.html HTML to convert. + * @param {Array} $1.rawTransforms Transforms that can be used. + * + * @return {Array} An array of blocks. + */ +function htmlToBlocks( { html, rawTransforms } ) { + const doc = document.implementation.createHTMLDocument( '' ); + + doc.body.innerHTML = html; + + return Array.from( doc.body.children ).map( ( node ) => { + const rawTransform = findTransform( rawTransforms, ( { isMatch } ) => isMatch( node ) ); + + if ( ! rawTransform ) { + console.warn( + 'A block registered a raw transformation schema for `' + node.nodeName + '` but did not match it. ' + + 'Make sure there is a `selector` or `isMatch` property that can match the schema.\n' + + 'Sanitized HTML: `' + node.outerHTML + '`' + ); + + return; + } + + const { transform, blockName } = rawTransform; + + if ( transform ) { + return transform( node ); + } + + return createBlock( + blockName, + getBlockAttributes( + getBlockType( blockName ), + node.outerHTML + ) + ); + } ); +} + /** * Converts an HTML string to known blocks. Strips everything else. * @@ -79,7 +124,7 @@ function getRawTransformations() { * * @return {Array|string} A list of blocks or a string, depending on `handlerMode`. */ -export default function rawHandler( { HTML = '', plainText = '', mode = 'AUTO', tagName, canUserUseUnfilteredHTML = false } ) { +export function pasteHandler( { HTML = '', plainText = '', mode = 'AUTO', tagName, canUserUseUnfilteredHTML = false } ) { // First of all, strip any meta tags. HTML = HTML.replace( /]+>/, '' ); @@ -137,9 +182,9 @@ export default function rawHandler( { HTML = '', plainText = '', mode = 'AUTO', return filterInlineHTML( HTML ); } - const rawTransformations = getRawTransformations(); + const rawTransforms = getRawTransformations(); const phrasingContentSchema = getPhrasingContentSchema(); - const blockContentSchema = getBlockContentSchema( rawTransformations ); + const blockContentSchema = getBlockContentSchema( rawTransforms ); const blocks = compact( flatMap( pieces, ( piece ) => { // Already a block from shortcode. @@ -176,37 +221,7 @@ export default function rawHandler( { HTML = '', plainText = '', mode = 'AUTO', // Allows us to ask for this information when we get a report. console.log( 'Processed HTML piece:\n\n', piece ); - const doc = document.implementation.createHTMLDocument( '' ); - - doc.body.innerHTML = piece; - - return Array.from( doc.body.children ).map( ( node ) => { - const rawTransformation = findTransform( rawTransformations, ( { isMatch } ) => isMatch( node ) ); - - if ( ! rawTransformation ) { - console.warn( - 'A block registered a raw transformation schema for `' + node.nodeName + '` but did not match it. ' + - 'Make sure there is a `selector` or `isMatch` property that can match the schema.\n' + - 'Sanitized HTML: `' + node.outerHTML + '`' - ); - - return; - } - - const { transform, blockName } = rawTransformation; - - if ( transform ) { - return transform( node ); - } - - return createBlock( - blockName, - getBlockAttributes( - getBlockType( blockName ), - node.outerHTML - ) - ); - } ); + return htmlToBlocks( { html: piece, rawTransforms } ); } ) ); // If we're allowed to return inline content and there is only one block @@ -225,3 +240,48 @@ export default function rawHandler( { HTML = '', plainText = '', mode = 'AUTO', return blocks; } + +/** + * Converts an HTML string to known blocks. + * + * @param {string} $1.HTML The HTML to convert. + * + * @return {Array} A list of blocks. + */ +export function rawHandler( { HTML = '' } ) { + // If we detect block delimiters, parse entirely as blocks. + if ( HTML.indexOf( ' +

Howdy

+ + + +
\\"\\"/
+ + + +

This is a paragraph.

+ + + +

Preserve me!

+ + + +

More tag

+ + + + + + + +

Shortcode

+ + + + +" +`; diff --git a/test/integration/blocks-raw-handling.spec.js b/test/integration/blocks-raw-handling.spec.js index 26bee315fc5a5..b0997d3b757d9 100644 --- a/test/integration/blocks-raw-handling.spec.js +++ b/test/integration/blocks-raw-handling.spec.js @@ -9,11 +9,16 @@ import path from 'path'; */ import { getBlockContent, + pasteHandler, rawHandler, serialize, } from '@wordpress/blocks'; import { registerCoreBlocks } from '@wordpress/block-library'; +function readFile( filePath ) { + return fs.existsSync( filePath ) ? fs.readFileSync( filePath, 'utf8' ).trim() : ''; +} + describe( 'Blocks raw handling', () => { beforeAll( () => { // Load all hooks that modify blocks @@ -22,7 +27,7 @@ describe( 'Blocks raw handling', () => { } ); it( 'should filter inline content', () => { - const filtered = rawHandler( { + const filtered = pasteHandler( { HTML: '

test

', mode: 'INLINE', } ); @@ -32,7 +37,7 @@ describe( 'Blocks raw handling', () => { } ); it( 'should parse Markdown', () => { - const filtered = rawHandler( { + const filtered = pasteHandler( { HTML: '* one
* two
* three', plainText: '* one\n* two\n* three', mode: 'AUTO', @@ -43,7 +48,7 @@ describe( 'Blocks raw handling', () => { } ); it( 'should parse inline Markdown', () => { - const filtered = rawHandler( { + const filtered = pasteHandler( { HTML: 'Some **bold** text.', plainText: 'Some **bold** text.', mode: 'AUTO', @@ -54,7 +59,7 @@ describe( 'Blocks raw handling', () => { } ); it( 'should parse HTML in plainText', () => { - const filtered = rawHandler( { + const filtered = pasteHandler( { HTML: '<p>Some <strong>bold</strong> text.</p>', plainText: '

Some bold text.

', mode: 'AUTO', @@ -65,7 +70,7 @@ describe( 'Blocks raw handling', () => { } ); it( 'should parse Markdown with HTML', () => { - const filtered = rawHandler( { + const filtered = pasteHandler( { HTML: '', plainText: '# Some heading\n\nA paragraph.', mode: 'AUTO', @@ -76,7 +81,7 @@ describe( 'Blocks raw handling', () => { } ); it( 'should break up forced inline content', () => { - const filtered = rawHandler( { + const filtered = pasteHandler( { HTML: '

test

test

', mode: 'INLINE', } ); @@ -86,7 +91,7 @@ describe( 'Blocks raw handling', () => { } ); it( 'should normalize decomposed characters', () => { - const filtered = rawHandler( { + const filtered = pasteHandler( { HTML: 'schön', mode: 'INLINE', } ); @@ -96,7 +101,7 @@ describe( 'Blocks raw handling', () => { } ); it( 'should treat single list item as inline text', () => { - const filtered = rawHandler( { + const filtered = pasteHandler( { HTML: '
  • Some bold text.
', plainText: 'Some bold text.\n', mode: 'AUTO', @@ -107,7 +112,7 @@ describe( 'Blocks raw handling', () => { } ); it( 'should treat multiple list items as a block', () => { - const filtered = rawHandler( { + const filtered = pasteHandler( { HTML: '
  • One
  • Two
  • Three
', plainText: 'One\nTwo\nThree\n', mode: 'AUTO', @@ -117,11 +122,7 @@ describe( 'Blocks raw handling', () => { expect( console ).toHaveLogged(); } ); - describe( 'serialize', () => { - function readFile( filePath ) { - return fs.existsSync( filePath ) ? fs.readFileSync( filePath, 'utf8' ).trim() : ''; - } - + describe( 'pasteHandler', () => { [ 'plain', 'classic', @@ -143,7 +144,7 @@ describe( 'Blocks raw handling', () => { const HTML = readFile( path.join( __dirname, `fixtures/${ type }-in.html` ) ); const plainText = readFile( path.join( __dirname, `fixtures/${ type }-in.txt` ) ); const output = readFile( path.join( __dirname, `fixtures/${ type }-out.html` ) ); - const converted = rawHandler( { HTML, plainText, canUserUseUnfilteredHTML: true } ); + const converted = pasteHandler( { HTML, plainText, canUserUseUnfilteredHTML: true } ); const serialized = typeof converted === 'string' ? converted : serialize( converted ); expect( serialized ).toBe( output ); @@ -154,4 +155,11 @@ describe( 'Blocks raw handling', () => { } ); } ); } ); + + describe( 'rawHandler', () => { + it( 'should convert HTML post to blocks with minimal content changes', () => { + const HTML = readFile( path.join( __dirname, 'fixtures/wordpress-convert.html' ) ); + expect( serialize( rawHandler( { HTML } ) ) ).toMatchSnapshot(); + } ); + } ); } ); diff --git a/test/integration/fixtures/wordpress-convert.html b/test/integration/fixtures/wordpress-convert.html new file mode 100644 index 0000000000000..1cb01568c3705 --- /dev/null +++ b/test/integration/fixtures/wordpress-convert.html @@ -0,0 +1,8 @@ +

Howdy

+ +

This is a paragraph.

+

Preserve me!

+

More tag

+

+

Shortcode

+

[gallery ids="1"]

From d6ba8c28c0e8384b8bde979826e63df49e9de0e8 Mon Sep 17 00:00:00 2001 From: Nicola Heald Date: Wed, 7 Nov 2018 09:59:14 +0000 Subject: [PATCH 027/106] Run the E2E tests as a user with the author role (#11359) --- .travis.yml | 6 ++- bin/install-wordpress.sh | 10 +++++ bin/run-e2e-tests.sh | 7 ++- .../block-library/src/latest-posts/edit.js | 42 +++++++++++++++--- test/e2e/specs/change-detection.test.js | 6 +-- test/e2e/specs/templates.test.js | 11 ++++- test/e2e/support/plugins.js | 10 ++++- test/e2e/support/setup-test-framework.js | 6 +++ test/e2e/support/utils.js | 43 ++++++++++++++++--- 9 files changed, 122 insertions(+), 19 deletions(-) diff --git a/.travis.yml b/.travis.yml index 51ae7a8eaa9e0..5dc3f1ee26230 100644 --- a/.travis.yml +++ b/.travis.yml @@ -67,5 +67,9 @@ jobs: - stage: test env: WP_VERSION=latest script: - - npm install || exit 1 + - ./bin/run-e2e-tests.sh || exit 1 + + - stage: test + env: WP_VERSION=latest E2E_ROLE=author + script: - ./bin/run-e2e-tests.sh || exit 1 diff --git a/bin/install-wordpress.sh b/bin/install-wordpress.sh index c2ddd14f30b6f..643c42b972f34 100755 --- a/bin/install-wordpress.sh +++ b/bin/install-wordpress.sh @@ -54,6 +54,13 @@ echo -e $(status_message "Installing WordPress...") # prevents permissions errors. See: https://github.com/WordPress/gutenberg/pull/8427#issuecomment-410232369 docker-compose $DOCKER_COMPOSE_FILE_OPTIONS run --rm -u 33 $CLI core install --title="$SITE_TITLE" --admin_user=admin --admin_password=password --admin_email=test@test.com --skip-email --url=http://localhost:$HOST_PORT >/dev/null +if [ "$E2E_ROLE" = "author" ]; then + # Create an additional author user for testsing. + docker-compose $DOCKER_COMPOSE_FILE_OPTIONS run --rm -u 33 $CLI user create author author@example.com --role=author --user_pass=authpass + # Assign the existing Hello World post to the author. + docker-compose $DOCKER_COMPOSE_FILE_OPTIONS run --rm -u 33 $CLI post update 1 --post_author=2 +fi + if [ "$WP_VERSION" == "latest" ]; then # Check for WordPress updates, to make sure we're running the very latest version. docker-compose $DOCKER_COMPOSE_FILE_OPTIONS run --rm -u 33 $CLI core update >/dev/null @@ -69,3 +76,6 @@ fi # Activate Gutenberg. echo -e $(status_message "Activating Gutenberg...") docker-compose $DOCKER_COMPOSE_FILE_OPTIONS run --rm $CLI plugin activate gutenberg >/dev/null + +# Install a dummy favicon to avoid 404 errors. +docker-compose $DOCKER_COMPOSE_FILE_OPTIONS run --rm $CONTAINER touch /var/www/html/favicon.ico diff --git a/bin/run-e2e-tests.sh b/bin/run-e2e-tests.sh index f0d70fe438861..98a6c17930972 100755 --- a/bin/run-e2e-tests.sh +++ b/bin/run-e2e-tests.sh @@ -7,5 +7,8 @@ cd "$(dirname "$0")/../" # Setup local environement ( ./bin/setup-local-env.sh ) -# Run the tests -npm run test-e2e +if [ "$E2E_ROLE" = "author" ]; then + WP_PASSWORD=authpass WP_USERNAME=author npm run test-e2e +else + npm run test-e2e +fi diff --git a/packages/block-library/src/latest-posts/edit.js b/packages/block-library/src/latest-posts/edit.js index 77f9d3eecac24..a9e66200ff5bb 100644 --- a/packages/block-library/src/latest-posts/edit.js +++ b/packages/block-library/src/latest-posts/edit.js @@ -17,6 +17,8 @@ import { ToggleControl, Toolbar, } from '@wordpress/components'; +import apiFetch from '@wordpress/api-fetch'; +import { addQueryArgs } from '@wordpress/url'; import { __ } from '@wordpress/i18n'; import { dateI18n, format, __experimentalGetSettings } from '@wordpress/date'; import { decodeEntities } from '@wordpress/html-entities'; @@ -27,15 +29,46 @@ import { } from '@wordpress/editor'; import { withSelect } from '@wordpress/data'; +/** + * Module Constants + */ +const CATEGORIES_LIST_QUERY = { + per_page: 100, +}; const MAX_POSTS_COLUMNS = 6; class LatestPostsEdit extends Component { constructor() { super( ...arguments ); - + this.state = { + categoriesList: [], + }; this.toggleDisplayPostDate = this.toggleDisplayPostDate.bind( this ); } + componentWillMount() { + this.isStillMounted = true; + this.fetchRequest = apiFetch( { + path: addQueryArgs( `/wp/v2/categories`, CATEGORIES_LIST_QUERY ), + } ).then( + ( categoriesList ) => { + if ( this.isStillMounted ) { + this.setState( { categoriesList } ); + } + } + ).catch( + () => { + if ( this.isStillMounted ) { + this.setState( { categoriesList: [] } ); + } + } + ); + } + + componentWillUnmount() { + this.isStillMounted = false; + } + toggleDisplayPostDate() { const { displayPostDate } = this.props.attributes; const { setAttributes } = this.props; @@ -44,7 +77,8 @@ class LatestPostsEdit extends Component { } render() { - const { attributes, categoriesList, setAttributes, latestPosts } = this.props; + const { attributes, setAttributes, latestPosts } = this.props; + const { categoriesList } = this.state; const { displayPostDate, align, postLayout, columns, order, orderBy, categories, postsToShow } = attributes; const inspectorControls = ( @@ -163,11 +197,7 @@ export default withSelect( ( select, props ) => { orderby: orderBy, per_page: postsToShow, }, ( value ) => ! isUndefined( value ) ); - const categoriesListQuery = { - per_page: 100, - }; return { latestPosts: getEntityRecords( 'postType', 'post', latestPostsQuery ), - categoriesList: getEntityRecords( 'taxonomy', 'category', categoriesListQuery ), }; } )( LatestPostsEdit ); diff --git a/test/e2e/specs/change-detection.test.js b/test/e2e/specs/change-detection.test.js index a03e06cc3dd0f..9ac12abbf5b09 100644 --- a/test/e2e/specs/change-detection.test.js +++ b/test/e2e/specs/change-detection.test.js @@ -91,11 +91,11 @@ describe( 'Change detection', () => { it( 'Should prompt to confirm unsaved changes for autosaved draft for non-content fields', async () => { await page.type( '.editor-post-title__input', 'Hello World' ); - // Toggle post as sticky (not persisted for autosave). + // Toggle post as needing review (not persisted for autosave). await ensureSidebarOpened(); - const postStickyToggleButton = ( await page.$x( "//label[contains(text(), 'Stick to the Front Page')]" ) )[ 0 ]; - await postStickyToggleButton.click( 'button' ); + const postPendingReviewButton = ( await page.$x( "//label[contains(text(), 'Pending Review')]" ) )[ 0 ]; + await postPendingReviewButton.click( 'button' ); // Force autosave to occur immediately. await Promise.all( [ diff --git a/test/e2e/specs/templates.test.js b/test/e2e/specs/templates.test.js index fd665db26ac3c..4becad767d74b 100644 --- a/test/e2e/specs/templates.test.js +++ b/test/e2e/specs/templates.test.js @@ -9,6 +9,8 @@ import { pressWithModifier, visitAdmin, clickBlockAppender, + switchToAdminUser, + switchToTestUser, } from '../support/utils'; import { activatePlugin, deactivatePlugin } from '../support/plugins'; @@ -60,12 +62,15 @@ describe( 'templates', () => { const STANDARD_FORMAT_VALUE = '0'; async function setPostFormat( format ) { + // To set the post format, we need to be the admin user. + await switchToAdminUser(); await visitAdmin( 'options-writing.php' ); await page.select( '#default_post_format', format ); - return Promise.all( [ + await Promise.all( [ page.waitForNavigation(), page.click( '#submit' ), ] ); + await switchToTestUser(); } beforeAll( async () => await setPostFormat( 'image' ) ); @@ -93,9 +98,13 @@ describe( 'templates', () => { } ); it( 'should not populate new page with default block for format', async () => { + // This test always needs to run as the admin user, because other roles can't create pages. + // It can't be skipped, because then it failed because of not testing the snapshot. + await switchToAdminUser(); await newPost( { postType: 'page' } ); expect( await getEditedPostContent() ).toMatchSnapshot(); + await switchToTestUser(); } ); } ); } ); diff --git a/test/e2e/support/plugins.js b/test/e2e/support/plugins.js index ade1decc57af9..da9edd3656860 100644 --- a/test/e2e/support/plugins.js +++ b/test/e2e/support/plugins.js @@ -1,7 +1,7 @@ /** * Node dependencies */ -import { visitAdmin } from './utils'; +import { visitAdmin, switchToAdminUser, switchToTestUser } from './utils'; /** * Install a plugin from the WP.org repository. @@ -10,9 +10,11 @@ import { visitAdmin } from './utils'; * @param {string?} searchTerm If the plugin is not findable by its slug use an alternative term to search. */ export async function installPlugin( slug, searchTerm ) { + await switchToAdminUser(); await visitAdmin( 'plugin-install.php?s=' + encodeURIComponent( searchTerm || slug ) + '&tab=search&type=term' ); await page.click( '.install-now[data-slug="' + slug + '"]' ); await page.waitForSelector( '.activate-now[data-slug="' + slug + '"]' ); + await switchToTestUser(); } /** @@ -21,9 +23,11 @@ export async function installPlugin( slug, searchTerm ) { * @param {string} slug Plugin slug. */ export async function activatePlugin( slug ) { + await switchToAdminUser(); await visitAdmin( 'plugins.php' ); await page.click( 'tr[data-slug="' + slug + '"] .activate a' ); await page.waitForSelector( 'tr[data-slug="' + slug + '"] .deactivate a' ); + await switchToTestUser(); } /** @@ -32,9 +36,11 @@ export async function activatePlugin( slug ) { * @param {string} slug Plugin slug. */ export async function deactivatePlugin( slug ) { + await switchToAdminUser(); await visitAdmin( 'plugins.php' ); await page.click( 'tr[data-slug="' + slug + '"] .deactivate a' ); await page.waitForSelector( 'tr[data-slug="' + slug + '"] .delete a' ); + await switchToTestUser(); } /** @@ -43,6 +49,7 @@ export async function deactivatePlugin( slug ) { * @param {string} slug Plugin slug. */ export async function uninstallPlugin( slug ) { + await switchToAdminUser(); await visitAdmin( 'plugins.php' ); const confirmPromise = new Promise( ( resolve ) => { page.once( 'dialog', () => resolve() ); @@ -52,4 +59,5 @@ export async function uninstallPlugin( slug ) { page.click( 'tr[data-slug="' + slug + '"] .delete a' ), ] ); await page.waitForSelector( 'tr[data-slug="' + slug + '"].deleted' ); + await switchToTestUser(); } diff --git a/test/e2e/support/setup-test-framework.js b/test/e2e/support/setup-test-framework.js index 20e05e418a29a..1b951ebc579fb 100644 --- a/test/e2e/support/setup-test-framework.js +++ b/test/e2e/support/setup-test-framework.js @@ -116,6 +116,12 @@ function observeConsoleLogging() { return; } + // Viewing posts on the front end can result in this error, which + // has nothing to do with Gutenberg. + if ( text.includes( 'net::ERR_UNKNOWN_URL_SCHEME' ) ) { + return; + } + const logFunction = OBSERVED_CONSOLE_MESSAGE_TYPES[ type ]; // Disable reason: We intentionally bubble up the console message diff --git a/test/e2e/support/utils.js b/test/e2e/support/utils.js index 20f07f4705df0..4b6fbefabcc36 100644 --- a/test/e2e/support/utils.js +++ b/test/e2e/support/utils.js @@ -15,10 +15,15 @@ import fetch from 'node-fetch'; */ import { addQueryArgs } from '@wordpress/url'; +const WP_ADMIN_USER = { + username: 'admin', + password: 'password', +}; + const { WP_BASE_URL = 'http://localhost:8889', - WP_USERNAME = 'admin', - WP_PASSWORD = 'password', + WP_USERNAME = WP_ADMIN_USER.username, + WP_PASSWORD = WP_ADMIN_USER.password, } = process.env; /** @@ -82,9 +87,13 @@ async function goToWPPath( WPPath, query ) { await page.goto( getUrl( WPPath, query ) ); } -async function login() { - await page.type( '#user_login', WP_USERNAME ); - await page.type( '#user_pass', WP_PASSWORD ); +async function login( username = WP_USERNAME, password = WP_PASSWORD ) { + await page.focus( '#user_login' ); + await pressWithModifier( META_KEY, 'a' ); + await page.type( '#user_login', username ); + await page.focus( '#user_pass' ); + await pressWithModifier( META_KEY, 'a' ); + await page.type( '#user_pass', password ); await Promise.all( [ page.waitForNavigation(), @@ -92,6 +101,30 @@ async function login() { ] ); } +/** + * Switches the current user to the admin user (if the user + * running the test is not already the admin user). + */ +export async function switchToAdminUser() { + if ( WP_USERNAME === WP_ADMIN_USER.username ) { + return; + } + await goToWPPath( 'wp-login.php' ); + await login( WP_ADMIN_USER.username, WP_ADMIN_USER.password ); +} + +/** + * Switches the current user to whichever user we should be + * running the tests as (if we're not already that user). + */ +export async function switchToTestUser() { + if ( WP_USERNAME === WP_ADMIN_USER.username ) { + return; + } + await goToWPPath( 'wp-login.php' ); + await login(); +} + export async function visitAdmin( adminPath, query ) { await goToWPPath( join( 'wp-admin', adminPath ), query ); From 57a7405a89dbd6ad7763aa04dcd2aabdd26b25b9 Mon Sep 17 00:00:00 2001 From: Tugdual de Kerviler Date: Wed, 7 Nov 2018 11:22:11 +0100 Subject: [PATCH 028/106] Expose @wordpress/editor to Gutenberg mobile (#11486) * Make the Editor component work with native * Define @wordpress/date entrypoint for react-native * Expose BlockEdit to gutenberg mobile * Add withFilter back and remove className omit in BlockEdit --- packages/blocks/src/api/index.native.js | 1 + packages/components/src/index.native.js | 3 +++ packages/date/package.json | 1 + .../src/components/block-edit/edit.native.js | 22 +++++++++++++++++++ .../editor/src/components/index.native.js | 1 + packages/editor/src/index.native.js | 14 ++++++++++++ packages/editor/src/store/index.native.js | 0 7 files changed, 42 insertions(+) create mode 100644 packages/editor/src/components/block-edit/edit.native.js create mode 100644 packages/editor/src/index.native.js delete mode 100644 packages/editor/src/store/index.native.js diff --git a/packages/blocks/src/api/index.native.js b/packages/blocks/src/api/index.native.js index e987acbdaaf4e..ac675f1f21870 100644 --- a/packages/blocks/src/api/index.native.js +++ b/packages/blocks/src/api/index.native.js @@ -19,6 +19,7 @@ export { getBlockType, getBlockTypes, hasBlockSupport, + isReusableBlock, } from './registration'; export { getPhrasingContentSchema } from './raw-handling'; export { default as children } from './children'; diff --git a/packages/components/src/index.native.js b/packages/components/src/index.native.js index fe55d594d3d73..e363c136f3628 100644 --- a/packages/components/src/index.native.js +++ b/packages/components/src/index.native.js @@ -3,3 +3,6 @@ export * from './primitives'; export { default as Dashicon } from './dashicon'; export { default as Toolbar } from './toolbar'; export { default as withSpokenMessages } from './higher-order/with-spoken-messages'; + +// Higher-Order Components +export { default as withFilters } from './higher-order/with-filters'; diff --git a/packages/date/package.json b/packages/date/package.json index 3301f25d5bdc2..9735a52fbed28 100644 --- a/packages/date/package.json +++ b/packages/date/package.json @@ -18,6 +18,7 @@ }, "main": "build/index.js", "module": "build-module/index.js", + "react-native": "src/index", "dependencies": { "@babel/runtime": "^7.0.0", "moment": "^2.22.1", diff --git a/packages/editor/src/components/block-edit/edit.native.js b/packages/editor/src/components/block-edit/edit.native.js new file mode 100644 index 0000000000000..8a4d0ad8350e9 --- /dev/null +++ b/packages/editor/src/components/block-edit/edit.native.js @@ -0,0 +1,22 @@ +/** + * WordPress dependencies + */ +import { withFilters } from '@wordpress/components'; +import { getBlockType } from '@wordpress/blocks'; + +export const Edit = ( props ) => { + const { name } = props; + const blockType = getBlockType( name ); + + if ( ! blockType ) { + return null; + } + + const Component = blockType.edit; + + return ( + + ); +}; + +export default withFilters( 'editor.BlockEdit' )( Edit ); diff --git a/packages/editor/src/components/index.native.js b/packages/editor/src/components/index.native.js index 0ddc4c9b38380..9cb399c034a6a 100644 --- a/packages/editor/src/components/index.native.js +++ b/packages/editor/src/components/index.native.js @@ -3,3 +3,4 @@ export * from './font-sizes'; export { default as PlainText } from './plain-text'; export { default as RichText } from './rich-text'; export { default as MediaPlaceholder } from './media-placeholder'; +export { default as BlockEdit } from './block-edit'; diff --git a/packages/editor/src/index.native.js b/packages/editor/src/index.native.js new file mode 100644 index 0000000000000..5577700ddf021 --- /dev/null +++ b/packages/editor/src/index.native.js @@ -0,0 +1,14 @@ +/** + * WordPress dependencies + */ +import '@wordpress/blocks'; +import '@wordpress/core-data'; + +/** + * Internal dependencies + */ +import './store'; +import './hooks'; + +export * from './components'; +export * from './utils'; diff --git a/packages/editor/src/store/index.native.js b/packages/editor/src/store/index.native.js deleted file mode 100644 index e69de29bb2d1d..0000000000000 From f23242b47ae5885537fa356995d69905900cf01b Mon Sep 17 00:00:00 2001 From: Jorge Date: Wed, 7 Nov 2018 10:43:30 +0000 Subject: [PATCH 029/106] Fix: "View as" link is not updated after the post is updated and the permalink is changed (#11262) --- packages/edit-post/src/store/effects.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/edit-post/src/store/effects.js b/packages/edit-post/src/store/effects.js index a03043bbe1f1f..f0b6d39a107aa 100644 --- a/packages/edit-post/src/store/effects.js +++ b/packages/edit-post/src/store/effects.js @@ -27,6 +27,8 @@ import { import { getMetaBoxContainer } from '../utils/meta-boxes'; import { onChangeListener } from './utils'; +const VIEW_AS_LINK_SELECTOR = '#wp-admin-bar-view a'; + const effects = { SET_META_BOXES_PER_LOCATIONS( action, store ) { // Allow toggling metaboxes panels @@ -154,6 +156,26 @@ const effects = { // Collapse sidebar when viewport shrinks. // Reopen sidebar it if viewport expands and it was closed because of a previous shrink. subscribe( onChangeListener( isMobileViewPort, adjustSidebar ) ); + + // Update View as link when currentPost link changes + const updateViewAsLink = ( newPermalink ) => { + if ( ! newPermalink ) { + return; + } + + const nodeToUpdate = document.querySelector( + VIEW_AS_LINK_SELECTOR + ); + if ( ! nodeToUpdate ) { + return; + } + nodeToUpdate.setAttribute( 'href', newPermalink ); + }; + + subscribe( onChangeListener( + () => select( 'core/editor' ).getCurrentPost().link, + updateViewAsLink + ) ); }, }; From 7c63d1095606ac069a288116f4ece054730ae30b Mon Sep 17 00:00:00 2001 From: Jorge Date: Wed, 7 Nov 2018 12:01:30 +0000 Subject: [PATCH 030/106] Allow a block to disable being converted into a reusable block; Fix: Column block (#11550) --- docs/block-api.md | 7 +++++ packages/block-library/src/classic/index.js | 3 ++ packages/block-library/src/columns/column.js | 1 + packages/block-library/src/missing/index.js | 1 + .../reusable-block-convert-button.js | 29 ++++++++----------- 5 files changed, 24 insertions(+), 17 deletions(-) diff --git a/docs/block-api.md b/docs/block-api.md index a3df73ffcb80e..01b2295b46de0 100644 --- a/docs/block-api.md +++ b/docs/block-api.md @@ -504,6 +504,13 @@ inserter: false, multiple: false, ``` +- `reusable` (default `true`): A block may want to disable the ability of being converted into a reusable block. +By default all blocks can be converted to a reusable block. If supports reusable is set to false, the option to convert the block into a reusable block will not appear. + +```js +// Don't allow the block to be converted into a reusable block. +reusable: false, +``` ## Edit and Save The `edit` and `save` functions define the editor interface with which a user would interact, and the markup to be serialized back when a post is saved. They are the heart of how a block operates, so they are [covered separately](../docs/block-api/block-edit-save.md). diff --git a/packages/block-library/src/classic/index.js b/packages/block-library/src/classic/index.js index e30d7fa75eada..bcc4f0a388010 100644 --- a/packages/block-library/src/classic/index.js +++ b/packages/block-library/src/classic/index.js @@ -31,6 +31,9 @@ export const settings = { supports: { className: false, customClassName: false, + // Hide 'Add to Reusable Blocks' on Classic blocks. Showing it causes a + // confusing UX, because of its similarity to the 'Convert to Blocks' button. + reusable: false, }, edit, diff --git a/packages/block-library/src/columns/column.js b/packages/block-library/src/columns/column.js index 89e22f1601d2c..a8f37dd7df7ec 100644 --- a/packages/block-library/src/columns/column.js +++ b/packages/block-library/src/columns/column.js @@ -20,6 +20,7 @@ export const settings = { supports: { inserter: false, + reusable: false, }, edit() { diff --git a/packages/block-library/src/missing/index.js b/packages/block-library/src/missing/index.js index 1ef9391b9264c..82de41b1d6eff 100644 --- a/packages/block-library/src/missing/index.js +++ b/packages/block-library/src/missing/index.js @@ -66,6 +66,7 @@ export const settings = { customClassName: false, inserter: false, html: false, + reusable: false, }, attributes: { 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 84530fb22bbef..6d39538aa5e5a 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 @@ -1,7 +1,7 @@ /** * External dependencies */ -import { noop, every, map } from 'lodash'; +import { noop, every } from 'lodash'; /** * WordPress dependencies @@ -9,7 +9,7 @@ import { noop, every, map } from 'lodash'; import { Fragment } from '@wordpress/element'; import { MenuItem } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; -import { isReusableBlock } from '@wordpress/blocks'; +import { hasBlockSupport, isReusableBlock } from '@wordpress/blocks'; import { withSelect, withDispatch } from '@wordpress/data'; import { compose } from '@wordpress/compose'; @@ -50,31 +50,26 @@ export function ReusableBlockConvertButton( { export default compose( [ withSelect( ( select, { clientIds } ) => { const { - getBlock, + getBlocksByClientId, canInsertBlockType, __experimentalGetReusableBlock: getReusableBlock, } = select( 'core/editor' ); - const { - getFreeformFallbackBlockName, - getUnregisteredFallbackBlockName, - } = select( 'core/blocks' ); - const blocks = map( clientIds, ( clientId ) => getBlock( clientId ) ); + const blocks = getBlocksByClientId( clientIds ); const isVisible = ( - // Guard against the case where a regular block has *just* been converted to a - // reusable block and doesn't yet exist in the editor store. - every( blocks, ( block ) => !! block ) && - // Hide 'Add to Reusable Blocks' when Reusable Blocks are disabled, i.e. when // core/block is not in the allowed_block_types filter. canInsertBlockType( 'core/block' ) && - // Hide 'Add to Reusable Blocks' on Classic blocks. Showing it causes a - // confusing UX, because of its similarity to the 'Convert to Blocks' button. - ( blocks.length !== 1 || ( - blocks[ 0 ].name !== getFreeformFallbackBlockName() && - blocks[ 0 ].name !== getUnregisteredFallbackBlockName() + every( blocks, ( block ) => ( + // Guard against the case where a regular block has *just* been converted to a + // reusable block and doesn't yet exist in the editor store. + !! block && + // Only show the option to covert to reusable blocks on valid blocks. + block.isValid && + // Make sure the block supports being converted into a reusable block (by default that is the case). + hasBlockSupport( block.name, 'reusable', true ) ) ) ); From 16a718a4bf359c53f0fb9c3626b08e2434a6fd7d Mon Sep 17 00:00:00 2001 From: Jorge Date: Wed, 7 Nov 2018 12:16:22 +0000 Subject: [PATCH 031/106] Add mechanism to avoid forced child selection on blocks with templates. (#10696) Currently, if a block contains a template when we insert that block the block that gets focused is one of the child blocks specified in the template. We don't offer a way to disable this behavior. This current behavior makes sense in most cases (e.g: columns) but for some cases, we may want to keep the focus on the parent. E.g. In PR #9416 @afercia pointed some a11y concerns in automatically selecting the child block on the InnerBlocks area. Blocks may have their own edition area and an InnerBlocks are at the end. Automatically selecting a block in the InnerBlocks area, has some accessible concerns has the parent block edition area may be unnoticeable for screen reader users. This PR adds a flag to the InnerBlocks component that allows blocks to disable the behavior of automatically selecting the children. In order for this flag to be possible another flag was added in insertBlock(s) action that allows for blocks to be inserted without a selection update happening. --- docs/data/data-core-editor.md | 8 +++---- .../src/components/inner-blocks/README.md | 7 ++++++ .../src/components/inner-blocks/index.js | 4 ++-- packages/editor/src/store/actions.js | 23 ++++++++++--------- packages/editor/src/store/reducer.js | 20 +++++++++------- packages/editor/src/store/test/actions.js | 2 ++ packages/editor/src/store/test/reducer.js | 19 +++++++++++++++ 7 files changed, 58 insertions(+), 25 deletions(-) diff --git a/docs/data/data-core-editor.md b/docs/data/data-core-editor.md index 267184d78113e..5b823d1893d0d 100644 --- a/docs/data/data-core-editor.md +++ b/docs/data/data-core-editor.md @@ -1532,8 +1532,8 @@ inserted, optionally at a specific index respective a root block list. * block: Block object to insert. * index: Index at which block should be inserted. - * rootClientId: Optional root client ID of block list on which - to insert. + * rootClientId: Optional root client ID of block list on which to insert. + * updateSelection: If true block selection will be updated. If false, block selection will not change. Defaults to true. ### insertBlocks @@ -1544,8 +1544,8 @@ 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 client ID of block list on - which to insert. + * rootClientId: Optional root cliente ID of block list on which to insert. + * updateSelection: If true block selection will be updated. If false, block selection will not change. Defaults to true. ### showInsertionPoint diff --git a/packages/editor/src/components/inner-blocks/README.md b/packages/editor/src/components/inner-blocks/README.md index 494fb750fb74c..040b0d463861c 100644 --- a/packages/editor/src/components/inner-blocks/README.md +++ b/packages/editor/src/components/inner-blocks/README.md @@ -90,6 +90,13 @@ const TEMPLATE = [ [ 'core/columns', {}, [ The previous example creates an InnerBlocks area containing two columns one with an image and the other with a paragraph. +### `templateInsertUpdatesSelection` +* **Type:** `Boolean` +* **Default:** `true` + +If true when child blocks in the template are inserted the selection is updated. +If false the selection should not be updated when child blocks specified in the template are inserted. + ### `templateLock` * **Type:** `String|Boolean` diff --git a/packages/editor/src/components/inner-blocks/index.js b/packages/editor/src/components/inner-blocks/index.js index c9252e912b5df..2183163221181 100644 --- a/packages/editor/src/components/inner-blocks/index.js +++ b/packages/editor/src/components/inner-blocks/index.js @@ -142,7 +142,7 @@ InnerBlocks = compose( [ insertBlocks, updateBlockListSettings, } = dispatch( 'core/editor' ); - const { block, clientId } = ownProps; + const { block, clientId, templateInsertUpdatesSelection = true } = ownProps; return { replaceInnerBlocks( blocks ) { @@ -150,7 +150,7 @@ InnerBlocks = compose( [ if ( clientIds.length ) { replaceBlocks( clientIds, blocks ); } else { - insertBlocks( blocks, undefined, clientId ); + insertBlocks( blocks, undefined, clientId, templateInsertUpdatesSelection ); } }, updateNestedSettings( settings ) { diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index 7d5c0d71ee2d1..a77b27add51c2 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -295,35 +295,36 @@ export function moveBlockToPosition( clientId, fromRootClientId, toRootClientId, * Returns an action object used in signalling that a single block should be * inserted, optionally at a specific index respective a root block list. * - * @param {Object} block Block object to insert. - * @param {?number} index Index at which block should be inserted. - * @param {?string} rootClientId Optional root client ID of block list on which - * to insert. + * @param {Object} block Block object to insert. + * @param {?number} index Index at which block should be inserted. + * @param {?string} rootClientId Optional root client ID of block list on which to insert. + * @param {?boolean} updateSelection If true block selection will be updated. If false, block selection will not change. Defaults to true. * * @return {Object} Action object. */ -export function insertBlock( block, index, rootClientId ) { - return insertBlocks( [ block ], index, rootClientId ); +export function insertBlock( block, index, rootClientId, updateSelection = true ) { + return insertBlocks( [ block ], index, rootClientId, updateSelection ); } /** * Returns an action object used in signalling that an array of blocks should * be inserted, optionally at a specific index respective a root block list. * - * @param {Object[]} blocks Block objects to insert. - * @param {?number} index Index at which block should be inserted. - * @param {?string} rootClientId Optional root client ID of block list on - * which to insert. + * @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 which to insert. + * @param {?boolean} updateSelection If true block selection will be updated. If false, block selection will not change. Defaults to true. * * @return {Object} Action object. */ -export function insertBlocks( blocks, index, rootClientId ) { +export function insertBlocks( blocks, index, rootClientId, updateSelection = true ) { return { type: 'INSERT_BLOCKS', blocks: castArray( blocks ), index, rootClientId, time: Date.now(), + updateSelection, }; } diff --git a/packages/editor/src/store/reducer.js b/packages/editor/src/store/reducer.js index e95b010dc94d9..9ea2259b2c791 100644 --- a/packages/editor/src/store/reducer.js +++ b/packages/editor/src/store/reducer.js @@ -705,14 +705,18 @@ export function blockSelection( state = { end: action.clientId, initialPosition: action.initialPosition, }; - case 'INSERT_BLOCKS': - return { - ...state, - start: action.blocks[ 0 ].clientId, - end: action.blocks[ 0 ].clientId, - initialPosition: null, - isMultiSelecting: false, - }; + case 'INSERT_BLOCKS': { + if ( action.updateSelection ) { + return { + ...state, + start: action.blocks[ 0 ].clientId, + end: action.blocks[ 0 ].clientId, + initialPosition: null, + isMultiSelecting: false, + }; + } + return state; + } case 'REMOVE_BLOCKS': if ( ! action.clientIds || ! action.clientIds.length || action.clientIds.indexOf( state.start ) === -1 ) { return state; diff --git a/packages/editor/src/store/test/actions.js b/packages/editor/src/store/test/actions.js index 4358054f8573b..b071dc78204fb 100644 --- a/packages/editor/src/store/test/actions.js +++ b/packages/editor/src/store/test/actions.js @@ -188,6 +188,7 @@ describe( 'actions', () => { index, rootClientId: 'testclientid', time: expect.any( Number ), + updateSelection: true, } ); } ); } ); @@ -204,6 +205,7 @@ describe( 'actions', () => { index, rootClientId: 'testclientid', time: expect.any( Number ), + updateSelection: true, } ); } ); } ); diff --git a/packages/editor/src/store/test/reducer.js b/packages/editor/src/store/test/reducer.js index 7d75ce2c55b4f..005fd9127690f 100644 --- a/packages/editor/src/store/test/reducer.js +++ b/packages/editor/src/store/test/reducer.js @@ -1567,6 +1567,7 @@ describe( 'state', () => { clientId: 'ribs', name: 'core/freeform', } ], + updateSelection: true, } ); expect( state3 ).toEqual( { @@ -1577,6 +1578,24 @@ describe( 'state', () => { } ); } ); + it( 'should not select inserted block if updateSelection flag is false', () => { + const original = deepFreeze( { start: 'a', end: 'b' } ); + + const state3 = blockSelection( original, { + type: 'INSERT_BLOCKS', + blocks: [ { + clientId: 'ribs', + name: 'core/freeform', + } ], + updateSelection: false, + } ); + + expect( state3 ).toEqual( { + start: 'a', + end: 'b', + } ); + } ); + it( 'should not update the state if the block moved is already selected', () => { const original = deepFreeze( { start: 'ribs', end: 'ribs' } ); const state = blockSelection( original, { From 26ccc159f373e5412d4fabf20fa00431d8fce7b6 Mon Sep 17 00:00:00 2001 From: Marko Savic Date: Wed, 7 Nov 2018 13:59:49 +0100 Subject: [PATCH 032/106] Slot/Fill pattern with Toolbar #199 (#11115) Added support for native toolbar component --- packages/block-library/src/code/edit.native.js | 7 ++++++- packages/block-library/src/code/theme.android.scss | 4 ++++ packages/block-library/src/code/theme.ios.scss | 5 +++++ packages/block-library/src/heading/edit.native.js | 6 ++++-- packages/block-library/src/paragraph/edit.native.js | 9 +++++++-- packages/block-library/src/paragraph/style.native.scss | 3 +++ packages/components/src/button/index.native.js | 4 ++-- packages/components/src/index.native.js | 1 + .../components/src/toolbar/toolbar-container.native.js | 2 +- packages/editor/src/components/index.native.js | 2 ++ packages/editor/src/components/rich-text/index.native.js | 5 +++-- 11 files changed, 38 insertions(+), 10 deletions(-) create mode 100644 packages/block-library/src/code/theme.android.scss create mode 100644 packages/block-library/src/code/theme.ios.scss create mode 100644 packages/block-library/src/paragraph/style.native.scss diff --git a/packages/block-library/src/code/edit.native.js b/packages/block-library/src/code/edit.native.js index 563c37dd4b13c..b6f4a8e541b37 100644 --- a/packages/block-library/src/code/edit.native.js +++ b/packages/block-library/src/code/edit.native.js @@ -13,6 +13,11 @@ import { __ } from '@wordpress/i18n'; */ import { PlainText } from '@wordpress/editor'; +/** + * Block code style + */ +import styles from './theme.scss'; + // Note: styling is applied directly to the (nested) PlainText component. Web-side components // apply it to the container 'div' but we don't have a proper proposal for cascading styling yet. export default function CodeEdit( { attributes, setAttributes, style } ) { @@ -20,7 +25,7 @@ export default function CodeEdit( { attributes, setAttributes, style } ) { setAttributes( { content } ) } diff --git a/packages/block-library/src/code/theme.android.scss b/packages/block-library/src/code/theme.android.scss new file mode 100644 index 0000000000000..18d9c4cdcf2dc --- /dev/null +++ b/packages/block-library/src/code/theme.android.scss @@ -0,0 +1,4 @@ +.blockCode { + font-family: monospace; +} + diff --git a/packages/block-library/src/code/theme.ios.scss b/packages/block-library/src/code/theme.ios.scss new file mode 100644 index 0000000000000..c553ba8e275f7 --- /dev/null +++ b/packages/block-library/src/code/theme.ios.scss @@ -0,0 +1,5 @@ +/* stylelint-disable font-family-no-missing-generic-family-keyword */ +.blockCode { + font-family: courier; +} + diff --git a/packages/block-library/src/heading/edit.native.js b/packages/block-library/src/heading/edit.native.js index 30cc236034e97..077d2701c84d3 100644 --- a/packages/block-library/src/heading/edit.native.js +++ b/packages/block-library/src/heading/edit.native.js @@ -13,7 +13,7 @@ import { View } from 'react-native'; */ import { __ } from '@wordpress/i18n'; import { Component } from '@wordpress/element'; -import { RichText } from '@wordpress/editor'; +import { RichText, BlockControls } from '@wordpress/editor'; import { parse, createBlock } from '@wordpress/blocks'; /** @@ -42,7 +42,9 @@ class HeadingEdit extends Component { return ( <View> - <HeadingToolbar minLevel={ 2 } maxLevel={ 5 } selectedLevel={ level } onChange={ ( newLevel ) => setAttributes( { level: newLevel } ) } /> + <BlockControls> + <HeadingToolbar minLevel={ 2 } maxLevel={ 5 } selectedLevel={ level } onChange={ ( newLevel ) => setAttributes( { level: newLevel } ) } /> + </BlockControls> <RichText tagName={ tagName } value={ content } diff --git a/packages/block-library/src/paragraph/edit.native.js b/packages/block-library/src/paragraph/edit.native.js index 0a0216d24865c..c77f28fd556d7 100644 --- a/packages/block-library/src/paragraph/edit.native.js +++ b/packages/block-library/src/paragraph/edit.native.js @@ -11,7 +11,10 @@ import { Component } from '@wordpress/element'; import { parse, createBlock } from '@wordpress/blocks'; import { RichText } from '@wordpress/editor'; -const minHeight = 50; +/** + * Import style + */ +import styles from './style.scss'; const name = 'core/paragraph'; @@ -68,8 +71,8 @@ class ParagraphEdit extends Component { const { attributes, setAttributes, - style, mergeBlocks, + style, } = this.props; const { @@ -77,6 +80,8 @@ class ParagraphEdit extends Component { content, } = attributes; + const minHeight = styles.blockText.minHeight; + return ( <View> <RichText diff --git a/packages/block-library/src/paragraph/style.native.scss b/packages/block-library/src/paragraph/style.native.scss new file mode 100644 index 0000000000000..8724a7ca468ca --- /dev/null +++ b/packages/block-library/src/paragraph/style.native.scss @@ -0,0 +1,3 @@ +.blockText { + min-height: 50; +} diff --git a/packages/components/src/button/index.native.js b/packages/components/src/button/index.native.js index f4d2f2b9542a4..be7f7caf4369b 100644 --- a/packages/components/src/button/index.native.js +++ b/packages/components/src/button/index.native.js @@ -12,9 +12,9 @@ export default function Button( props ) { onPress={ onClick } style={ { borderColor: ariaPressed ? 'black' : 'white', borderWidth: 1, borderRadius: 2 } } > - <View style={ { flex: 1, flexDirection: 'row' } }> + <View style={ { height: 44, width: 44, flexDirection: 'row', justifyContent: 'center', alignItems: 'center' } }> { children } - { subscript && ( <Text style={ { fontVariant: [ 'small-caps' ], textAlignVertical: 'bottom' } }>{ subscript }</Text> ) } + { subscript && ( <Text style={ { fontVariant: [ 'small-caps' ] } }>{ subscript }</Text> ) } </View> </TouchableOpacity> ); diff --git a/packages/components/src/index.native.js b/packages/components/src/index.native.js index e363c136f3628..2fd9eaa5e836e 100644 --- a/packages/components/src/index.native.js +++ b/packages/components/src/index.native.js @@ -3,6 +3,7 @@ export * from './primitives'; export { default as Dashicon } from './dashicon'; export { default as Toolbar } from './toolbar'; export { default as withSpokenMessages } from './higher-order/with-spoken-messages'; +export { createSlotFill, Slot, Fill, Provider as SlotFillProvider } from './slot-fill'; // Higher-Order Components export { default as withFilters } from './higher-order/with-filters'; diff --git a/packages/components/src/toolbar/toolbar-container.native.js b/packages/components/src/toolbar/toolbar-container.native.js index c3504fc40202c..08106a987f408 100644 --- a/packages/components/src/toolbar/toolbar-container.native.js +++ b/packages/components/src/toolbar/toolbar-container.native.js @@ -4,7 +4,7 @@ import { View } from 'react-native'; export default ( props ) => ( - <View style={ { flex: 1, flexDirection: 'row' } }> + <View style={ { flexDirection: 'row' } }> { props.children } </View> ); diff --git a/packages/editor/src/components/index.native.js b/packages/editor/src/components/index.native.js index 9cb399c034a6a..17943dee6b3ae 100644 --- a/packages/editor/src/components/index.native.js +++ b/packages/editor/src/components/index.native.js @@ -3,4 +3,6 @@ export * from './font-sizes'; export { default as PlainText } from './plain-text'; export { default as RichText } from './rich-text'; export { default as MediaPlaceholder } from './media-placeholder'; +export { default as BlockFormatControls } from './block-format-controls'; +export { default as BlockControls } from './block-controls'; export { default as BlockEdit } from './block-edit'; diff --git a/packages/editor/src/components/rich-text/index.native.js b/packages/editor/src/components/rich-text/index.native.js index c0fb04867bde4..7fe26564cae5e 100644 --- a/packages/editor/src/components/rich-text/index.native.js +++ b/packages/editor/src/components/rich-text/index.native.js @@ -14,6 +14,7 @@ import { import { Component, RawHTML } from '@wordpress/element'; import { withInstanceId, compose } from '@wordpress/compose'; import { Toolbar } from '@wordpress/components'; +import { BlockFormatControls } from '@wordpress/editor'; import { isEmpty, create, @@ -342,9 +343,9 @@ export class RichText extends Component { return ( <View> - <View style={ { flex: 1 } }> + <BlockFormatControls> <Toolbar controls={ toolbarControls } /> - </View> + </BlockFormatControls> <RCTAztecView ref={ ( ref ) => { this._editor = ref; From 1ac70e14a0986a9e9ff93386f3a76aaaa7ef53f8 Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Wed, 7 Nov 2018 16:54:55 +0100 Subject: [PATCH 033/106] Fix the isBeingScheduled Selector. (#11572) * Fix publish button label (schedule instead of publish) * Fix the isBeingScheduledSelector calculation * Tweak function comments * Clarify test comment Co-Authored-By: youknowriad <benguella@gmail.com> --- packages/date/src/index.js | 17 ++++++++++- packages/date/src/test/index.js | 40 ++++++++++++++++++++++++++ packages/editor/src/store/selectors.js | 6 ++-- 3 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 packages/date/src/test/index.js diff --git a/packages/date/src/index.js b/packages/date/src/index.js index bcc1e4d32be6d..8580f97062f27 100644 --- a/packages/date/src/index.js +++ b/packages/date/src/index.js @@ -395,7 +395,7 @@ export function dateI18n( dateFormat, dateValue = new Date(), gmt = false ) { /** * Check whether a date is considered in the future according to the WordPress settings. * - * @param {(Date|string)} dateValue Date object or string. + * @param {string} dateValue Date String or Date object in the Defined WP Timezone. * * @return {boolean} Is in the future. */ @@ -406,4 +406,19 @@ export function isInTheFuture( dateValue ) { return momentObject.isAfter( now ); } +/** + * Create and return a JavaScript Date Object from a date string in the WP timezone. + * + * @param {string?} dateString Date formatted in the WP timezone. + * + * @return {Date} Date + */ +export function getDate( dateString ) { + if ( ! dateString ) { + return momentLib.tz( 'WP' ).toDate(); + } + + return momentLib.tz( dateString, 'WP' ).toDate(); +} + setupWPTimezone(); diff --git a/packages/date/src/test/index.js b/packages/date/src/test/index.js new file mode 100644 index 0000000000000..f18e2b0915825 --- /dev/null +++ b/packages/date/src/test/index.js @@ -0,0 +1,40 @@ +/** + * Internal dependencies + */ +import { isInTheFuture, getDate, setSettings, __experimentalGetSettings } from '../'; + +describe( 'isInTheFuture', () => { + it( 'should return true if the date is in the future', () => { + // Create a Date object 1 minute in the future. + const date = new Date( Number( getDate() ) + ( 1000 * 60 ) ); + + expect( isInTheFuture( date ) ).toBe( true ); + } ); + + it( 'should return true if the date is in the past', () => { + // Create a Date object 1 minute in the past. + const date = new Date( Number( getDate() ) - ( 1000 * 60 ) ); + + expect( isInTheFuture( date ) ).toBe( false ); + } ); + + it( 'should ignore the timezone', () => { + const settings = __experimentalGetSettings(); + + // Set a timezone in the future + setSettings( { + ...settings, + timezone: { offset: '4', string: '' }, + } ); + // Create a Date object 1 minute in the past. + let date = new Date( Number( getDate() ) - ( 1000 * 60 ) ); + expect( isInTheFuture( date ) ).toBe( false ); + + // Create a Date object 1 minute in the future. + date = new Date( Number( getDate() ) + ( 1000 * 60 ) ); + expect( isInTheFuture( date ) ).toBe( true ); + + // Restore default settings + setSettings( settings ); + } ); +} ); diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index f405c90cbe272..a4e426507f816 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -32,7 +32,7 @@ import { getFreeformContentHandlerName, isUnmodifiedDefaultBlock, } from '@wordpress/blocks'; -import { isInTheFuture } from '@wordpress/date'; +import { isInTheFuture, getDate } from '@wordpress/date'; import { removep } from '@wordpress/autop'; import { select } from '@wordpress/data'; import deprecated from '@wordpress/deprecated'; @@ -333,7 +333,7 @@ export function isCurrentPostPublished( state ) { const post = getCurrentPost( state ); return [ 'publish', 'private' ].indexOf( post.status ) !== -1 || - ( post.status === 'future' && ! isInTheFuture( new Date( Number( new Date( post.date ) ) + ONE_MINUTE_IN_MS ) ) ); + ( post.status === 'future' && ! isInTheFuture( new Date( Number( getDate( post.date ) ) - ONE_MINUTE_IN_MS ) ) ); } /** @@ -493,7 +493,7 @@ export function hasAutosave( state ) { export function isEditedPostBeingScheduled( state ) { const date = getEditedPostAttribute( state, 'date' ); // Offset the date by one minute (network latency) - const checkedDate = new Date( Number( new Date( date ) ) + ONE_MINUTE_IN_MS ); + const checkedDate = new Date( Number( getDate( date ) ) - ONE_MINUTE_IN_MS ); return isInTheFuture( checkedDate ); } From 78dd21f2e8e21c4beb6ee8d81258eb3fd34f15ef Mon Sep 17 00:00:00 2001 From: Jorge <jorge.costa@developer.pt> Date: Thu, 8 Nov 2018 03:14:10 +0000 Subject: [PATCH 034/106] Fix: Contrast checker: Consider fontSize large when size >= 24px instead of >= 18px (#9268) --- packages/block-library/src/button/edit.js | 4 +++- packages/editor/src/components/contrast-checker/index.js | 4 ++-- .../contrast-checker/test/__snapshots__/index.js.snap | 4 ++-- .../editor/src/components/contrast-checker/test/index.js | 8 ++++---- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/block-library/src/button/edit.js b/packages/block-library/src/button/edit.js index b101c13c560d6..860d6a1fb64b6 100644 --- a/packages/block-library/src/button/edit.js +++ b/packages/block-library/src/button/edit.js @@ -114,7 +114,9 @@ class ButtonEdit extends Component { > <ContrastChecker { ...{ - isLargeText: true, + // Text is considered large if font size is greater or equal to 18pt or 24px, + // currently that's not the case for button. + isLargeText: false, textColor: textColor.color, backgroundColor: backgroundColor.color, fallbackBackgroundColor, diff --git a/packages/editor/src/components/contrast-checker/index.js b/packages/editor/src/components/contrast-checker/index.js index 36cc0617e65fc..a5cbb23257242 100644 --- a/packages/editor/src/components/contrast-checker/index.js +++ b/packages/editor/src/components/contrast-checker/index.js @@ -13,7 +13,7 @@ function ContrastChecker( { backgroundColor, fallbackBackgroundColor, fallbackTextColor, - fontSize, + fontSize, // font size value in pixels isLargeText, textColor, } ) { @@ -27,7 +27,7 @@ function ContrastChecker( { if ( hasTransparency || tinycolor.isReadable( tinyBackgroundColor, tinyTextColor, - { level: 'AA', size: ( isLargeText || ( isLargeText !== false && fontSize >= 18 ) ? 'large' : 'small' ) } + { level: 'AA', size: ( isLargeText || ( isLargeText !== false && fontSize >= 24 ) ? 'large' : 'small' ) } ) ) { return null; } diff --git a/packages/editor/src/components/contrast-checker/test/__snapshots__/index.js.snap b/packages/editor/src/components/contrast-checker/test/__snapshots__/index.js.snap index 6bb1dc8d4f5bf..1c56207c62bea 100644 --- a/packages/editor/src/components/contrast-checker/test/__snapshots__/index.js.snap +++ b/packages/editor/src/components/contrast-checker/test/__snapshots__/index.js.snap @@ -87,7 +87,7 @@ exports[`ContrastChecker should render messages when the textColor is valid, but exports[`ContrastChecker should take into consideration the font size passed 1`] = ` <ContrastChecker backgroundColor="#C44B4B" - fontSize={17} + fontSize={23} textColor="#000000" > <div @@ -141,7 +141,7 @@ exports[`ContrastChecker should take into consideration wherever text is large o exports[`ContrastChecker should use isLargeText to make decisions if both isLargeText and fontSize props are passed 1`] = ` <ContrastChecker backgroundColor="#C44B4B" - fontSize={18} + fontSize={24} isLargeText={false} textColor="#000000" > diff --git a/packages/editor/src/components/contrast-checker/test/index.js b/packages/editor/src/components/contrast-checker/test/index.js index ee7cdc8161343..eca63c0e7b97d 100644 --- a/packages/editor/src/components/contrast-checker/test/index.js +++ b/packages/editor/src/components/contrast-checker/test/index.js @@ -115,7 +115,7 @@ describe( 'ContrastChecker', () => { <ContrastChecker backgroundColor="#C44B4B" textColor="#000000" - fontSize={ 17 } + fontSize={ 23 } /> ); @@ -125,7 +125,7 @@ describe( 'ContrastChecker', () => { <ContrastChecker backgroundColor="#C44B4B" textColor="#000000" - fontSize={ 18 } + fontSize={ 24 } /> ); @@ -137,7 +137,7 @@ describe( 'ContrastChecker', () => { <ContrastChecker backgroundColor="#C44B4B" textColor="#000000" - fontSize={ 17 } + fontSize={ 23 } isLargeText={ true } /> ); @@ -148,7 +148,7 @@ describe( 'ContrastChecker', () => { <ContrastChecker backgroundColor="#C44B4B" textColor="#000000" - fontSize={ 18 } + fontSize={ 24 } isLargeText={ false } /> ); From 4e9d9b3d36841c264bce35d4bf1ba3d9ff75f659 Mon Sep 17 00:00:00 2001 From: Matthew Riley MacPherson <hi@tofumatt.com> Date: Thu, 8 Nov 2018 03:46:46 +0000 Subject: [PATCH 035/106] chore: Improve undo/redo no-op (#11428) * chore: Improve undo/redo no-op See: https://github.com/WordPress/gutenberg/pull/11379\#discussion_r230406108 * Do not wrap dispatch undo/redo --- .../editor/src/components/editor-history/redo.js | 16 ++++++---------- .../editor/src/components/editor-history/undo.js | 16 ++++++---------- 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/packages/editor/src/components/editor-history/redo.js b/packages/editor/src/components/editor-history/redo.js index 33cd412d2adf4..de6c3e46a26da 100644 --- a/packages/editor/src/components/editor-history/redo.js +++ b/packages/editor/src/components/editor-history/redo.js @@ -13,8 +13,11 @@ function EditorHistoryRedo( { hasRedo, redo } ) { icon="redo" label={ __( 'Redo' ) } shortcut={ displayShortcut.primaryShift( 'z' ) } + // If there are no redo levels we don't want to actually disable this + // button, because it will remove focus for keyboard users. + // See: https://github.com/WordPress/gutenberg/issues/3486 aria-disabled={ ! hasRedo } - onClick={ redo } + onClick={ hasRedo ? redo : undefined } className="editor-history__redo" /> ); @@ -24,14 +27,7 @@ export default compose( [ withSelect( ( select ) => ( { hasRedo: select( 'core/editor' ).hasEditorRedo(), } ) ), - withDispatch( ( dispatch, ownProps ) => ( { - redo: () => { - // If there are no redo levels this is a no-op, because we don't actually - // disable the button. - // See: https://github.com/WordPress/gutenberg/issues/3486 - if ( ownProps.hasRedo ) { - dispatch( 'core/editor' ).redo(); - } - }, + withDispatch( ( dispatch ) => ( { + redo: dispatch( 'core/editor' ).redo, } ) ), ] )( EditorHistoryRedo ); diff --git a/packages/editor/src/components/editor-history/undo.js b/packages/editor/src/components/editor-history/undo.js index 008654dd43400..6c3826cc7f1f4 100644 --- a/packages/editor/src/components/editor-history/undo.js +++ b/packages/editor/src/components/editor-history/undo.js @@ -13,8 +13,11 @@ function EditorHistoryUndo( { hasUndo, undo } ) { icon="undo" label={ __( 'Undo' ) } shortcut={ displayShortcut.primary( 'z' ) } + // If there are no undo levels we don't want to actually disable this + // button, because it will remove focus for keyboard users. + // See: https://github.com/WordPress/gutenberg/issues/3486 aria-disabled={ ! hasUndo } - onClick={ undo } + onClick={ hasUndo ? undo : undefined } className="editor-history__undo" /> ); @@ -24,14 +27,7 @@ export default compose( [ withSelect( ( select ) => ( { hasUndo: select( 'core/editor' ).hasEditorUndo(), } ) ), - withDispatch( ( dispatch, ownProps ) => ( { - undo: () => { - // If there are no undo levels this is a no-op, because we don't actually - // disable the button. - // See: https://github.com/WordPress/gutenberg/issues/3486 - if ( ownProps.hasUndo ) { - dispatch( 'core/editor' ).undo(); - } - }, + withDispatch( ( dispatch ) => ( { + undo: dispatch( 'core/editor' ).undo, } ) ), ] )( EditorHistoryUndo ); From a9b0742ea2cd5b2bd39ac7242ceacc10aa538565 Mon Sep 17 00:00:00 2001 From: Michael Savchuk <pixelrobin@hotmail.com> Date: Thu, 8 Nov 2018 00:26:58 -0800 Subject: [PATCH 036/106] Add missing tests for Format API code (#11562) * Add missing tests for Format API code * Add missing tests for Format API code * Rebase, fix, and apply feedback --- packages/rich-text/src/insert-object.js | 2 +- packages/rich-text/src/store/test/actions.js | 30 +++++ .../rich-text/src/store/test/selectors.js | 38 ++++++ .../rich-text/src/test/get-format-type.js | 43 ++++++ .../rich-text/src/test/get-format-types.js | 57 ++++++++ packages/rich-text/src/test/insert-object.js | 36 +++++ .../src/test/register-format-type.js | 125 ++++++++++++++++-- packages/rich-text/src/test/toggle-format.js | 56 ++++++++ .../src/test/unregister-format-type.js | 53 ++++++++ 9 files changed, 427 insertions(+), 13 deletions(-) create mode 100644 packages/rich-text/src/store/test/actions.js create mode 100644 packages/rich-text/src/store/test/selectors.js create mode 100644 packages/rich-text/src/test/get-format-type.js create mode 100644 packages/rich-text/src/test/get-format-types.js create mode 100644 packages/rich-text/src/test/insert-object.js create mode 100644 packages/rich-text/src/test/toggle-format.js create mode 100644 packages/rich-text/src/test/unregister-format-type.js diff --git a/packages/rich-text/src/insert-object.js b/packages/rich-text/src/insert-object.js index b3486bcec4ebb..bf67ac3913b73 100644 --- a/packages/rich-text/src/insert-object.js +++ b/packages/rich-text/src/insert-object.js @@ -12,7 +12,7 @@ const OBJECT_REPLACEMENT_CHARACTER = '\ufffc'; * removed. Indices are retrieved from the selection if none are provided. * * @param {Object} value Value to modify. - * @param {string} formatToInsert Format to insert as object. + * @param {Object} formatToInsert Format to insert as object. * @param {number} startIndex Start index. * @param {number} endIndex End index. * diff --git a/packages/rich-text/src/store/test/actions.js b/packages/rich-text/src/store/test/actions.js new file mode 100644 index 0000000000000..a01c64e18b541 --- /dev/null +++ b/packages/rich-text/src/store/test/actions.js @@ -0,0 +1,30 @@ +/** + * Internal dependencies + */ +import { addFormatTypes, removeFormatTypes } from '../actions'; + +describe( 'actions', () => { + describe( 'addFormatTypes', () => { + it( 'should cast format types as an array', () => { + const formatTypes = { name: 'core/test-format' }; + const expected = { + type: 'ADD_FORMAT_TYPES', + formatTypes: [ formatTypes ], + }; + + expect( addFormatTypes( formatTypes ) ).toEqual( expected ); + } ); + } ); + + describe( 'removeFormatTypes', () => { + it( 'should cast format types as an array', () => { + const names = 'core/test-format'; + const expected = { + type: 'REMOVE_FORMAT_TYPES', + names: [ names ], + }; + + expect( removeFormatTypes( names ) ).toEqual( expected ); + } ); + } ); +} ); diff --git a/packages/rich-text/src/store/test/selectors.js b/packages/rich-text/src/store/test/selectors.js new file mode 100644 index 0000000000000..ca49185a6e6b1 --- /dev/null +++ b/packages/rich-text/src/store/test/selectors.js @@ -0,0 +1,38 @@ +/** + * External dependencies + */ +import deepFreeze from 'deep-freeze'; + +/** + * Internal dependencies + */ +import { getFormatTypes, getFormatType } from '../selectors'; + +describe( 'selectors', () => { + const defaultState = deepFreeze( { + formatTypes: { + 'core/test-format': { name: 'core/test-format' }, + 'core/test-format-2': { name: 'core/test-format-2' }, + }, + } ); + + describe( 'getFormatTypes', () => { + it( 'should get format types', () => { + const expected = [ + { name: 'core/test-format' }, + { name: 'core/test-format-2' }, + ]; + + expect( getFormatTypes( defaultState ) ).toEqual( expected ); + } ); + } ); + + describe( 'getFormatType', () => { + it( 'should get a format type', () => { + const expected = { name: 'core/test-format' }; + const result = getFormatType( defaultState, 'core/test-format' ); + + expect( result ).toEqual( expected ); + } ); + } ); +} ); diff --git a/packages/rich-text/src/test/get-format-type.js b/packages/rich-text/src/test/get-format-type.js new file mode 100644 index 0000000000000..edbb3d719d4f7 --- /dev/null +++ b/packages/rich-text/src/test/get-format-type.js @@ -0,0 +1,43 @@ +/** + * External dependencies + */ +import { noop } from 'lodash'; + +/** + * Internal dependencies + */ +import { getFormatType } from '../get-format-type'; +import { unregisterFormatType } from '../unregister-format-type'; +import { registerFormatType } from '../register-format-type'; +import { getFormatTypes } from '../get-format-types'; + +describe( 'getFormatType', () => { + beforeAll( () => { + // Initialize the rich-text store. + require( '../store' ); + } ); + + afterEach( () => { + getFormatTypes().forEach( ( format ) => { + unregisterFormatType( format.name ); + } ); + } ); + + it( 'should return all format type elements', () => { + const formatType = { + edit: noop, + title: 'format title', + keywords: [ 'one', 'two', 'three' ], + formatTestSetting: 'settingTestValue', + tagName: 'test', + className: null, + }; + + registerFormatType( 'core/test-format-with-settings', formatType ); + + expect( getFormatType( 'core/test-format-with-settings' ) ).toEqual( { + name: 'core/test-format-with-settings', + ...formatType, + } ); + } ); +} ); diff --git a/packages/rich-text/src/test/get-format-types.js b/packages/rich-text/src/test/get-format-types.js new file mode 100644 index 0000000000000..1f7939198c825 --- /dev/null +++ b/packages/rich-text/src/test/get-format-types.js @@ -0,0 +1,57 @@ +/** + * External dependencies + */ +import { noop } from 'lodash'; + +/** + * Internal dependencies + */ +import { getFormatTypes } from '../get-format-types'; +import { unregisterFormatType } from '../unregister-format-type'; +import { registerFormatType } from '../register-format-type'; + +describe( 'getFormatTypes', () => { + beforeAll( () => { + // Initialize the rich-text store. + require( '../store' ); + } ); + + afterEach( () => { + getFormatTypes().forEach( ( format ) => { + unregisterFormatType( format.name ); + } ); + } ); + + it( 'should return an empty array at first', () => { + expect( getFormatTypes() ).toEqual( [] ); + } ); + + it( 'should return all registered formats', () => { + const testFormat = { + edit: noop, + title: 'format title', + tagName: 'test', + className: null, + }; + const testFormatWithSettings = { + edit: noop, + title: 'format title 2', + keywords: [ 'one', 'two', 'three' ], + tagName: 'test 2', + className: null, + formatTestSetting: 'settingTestValue', + }; + registerFormatType( 'core/test-format', testFormat ); + registerFormatType( 'core/test-format-with-settings', testFormatWithSettings ); + expect( getFormatTypes() ).toEqual( [ + { + name: 'core/test-format', + ...testFormat, + }, + { + name: 'core/test-format-with-settings', + ...testFormatWithSettings, + }, + ] ); + } ); +} ); diff --git a/packages/rich-text/src/test/insert-object.js b/packages/rich-text/src/test/insert-object.js new file mode 100644 index 0000000000000..15680372986a9 --- /dev/null +++ b/packages/rich-text/src/test/insert-object.js @@ -0,0 +1,36 @@ +/** + * External dependencies + */ +import deepFreeze from 'deep-freeze'; + +/** + * Internal dependencies + */ +import { insertObject } from '../insert-object'; +import { getSparseArrayLength } from './helpers'; +import { OBJECT_REPLACEMENT_CHARACTER } from '../special-characters'; + +describe( 'insert', () => { + const obj = { type: 'obj' }; + const em = { type: 'em' }; + + it( 'should delete and insert', () => { + const record = { + formats: [ , , , , [ em ], [ em ], [ em ], , , , , , , ], + text: 'one two three', + start: 6, + end: 6, + }; + const expected = { + formats: [ , , [ { ...obj, object: true } ], [ em ], , , , , , , ], + text: `on${ OBJECT_REPLACEMENT_CHARACTER }o three`, + start: 3, + end: 3, + }; + const result = insertObject( deepFreeze( record ), obj, 2, 6 ); + + expect( result ).toEqual( expected ); + expect( result ).not.toBe( record ); + expect( getSparseArrayLength( result.formats ) ).toBe( 2 ); + } ); +} ); diff --git a/packages/rich-text/src/test/register-format-type.js b/packages/rich-text/src/test/register-format-type.js index 5dadffb050abf..cde3c900e8b41 100644 --- a/packages/rich-text/src/test/register-format-type.js +++ b/packages/rich-text/src/test/register-format-type.js @@ -6,9 +6,9 @@ import { select } from '@wordpress/data'; /** * Internal dependencies */ - import { registerFormatType } from '../register-format-type'; import { unregisterFormatType } from '../unregister-format-type'; +import { getFormatType } from '../get-format-type'; describe( 'registerFormatType', () => { beforeAll( () => { @@ -37,57 +37,129 @@ describe( 'registerFormatType', () => { } ); it( 'should error without arguments', () => { - registerFormatType(); + const format = registerFormatType(); expect( console ).toHaveErroredWith( 'Format names must be strings.' ); + expect( format ).toBeUndefined(); + } ); + + it( 'should reject format types without a namespace', () => { + const format = registerFormatType( 'doing-it-wrong' ); + expect( console ).toHaveErroredWith( 'Format names must contain a namespace prefix, include only lowercase alphanumeric characters or dashes, and start with a letter. Example: my-plugin/my-custom-format' ); + expect( format ).toBeUndefined(); + } ); + + it( 'should reject format types with too many namespaces', () => { + const format = registerFormatType( 'doing/it/wrong' ); + expect( console ).toHaveErroredWith( 'Format names must contain a namespace prefix, include only lowercase alphanumeric characters or dashes, and start with a letter. Example: my-plugin/my-custom-format' ); + expect( format ).toBeUndefined(); + } ); + + it( 'should reject format types with invalid characters', () => { + const format = registerFormatType( 'still/_doing_it_wrong' ); + expect( console ).toHaveErroredWith( 'Format names must contain a namespace prefix, include only lowercase alphanumeric characters or dashes, and start with a letter. Example: my-plugin/my-custom-format' ); + expect( format ).toBeUndefined(); + } ); + + it( 'should reject format types with uppercase characters', () => { + const format = registerFormatType( 'Core/Bold' ); + expect( console ).toHaveErroredWith( 'Format names must contain a namespace prefix, include only lowercase alphanumeric characters or dashes, and start with a letter. Example: my-plugin/my-custom-format' ); + expect( format ).toBeUndefined(); } ); - it( 'should error on invalid name', () => { - registerFormatType( 'test', validSettings ); + it( 'should reject format types not starting with a letter', () => { + const format = registerFormatType( 'my-plugin/4-fancy-format' ); expect( console ).toHaveErroredWith( 'Format names must contain a namespace prefix, include only lowercase alphanumeric characters or dashes, and start with a letter. Example: my-plugin/my-custom-format' ); + expect( format ).toBeUndefined(); + } ); + + it( 'should reject numbers', () => { + const format = registerFormatType( 42 ); + expect( console ).toHaveErroredWith( 'Format names must be strings.' ); + expect( format ).toBeUndefined(); + } ); + + it( 'should accept valid format names', () => { + const format = registerFormatType( 'my-plugin/fancy-format-4', validSettings ); + expect( console ).not.toHaveErrored(); + expect( format ).toEqual( { + name: 'my-plugin/fancy-format-4', + ...validSettings, + } ); } ); it( 'should error on already registered name', () => { registerFormatType( validName, validSettings ); - registerFormatType( validName, validSettings ); + const duplicateFormat = registerFormatType( validName, validSettings ); expect( console ).toHaveErroredWith( 'Format "plugin/test" is already registered.' ); + expect( duplicateFormat ).toBeUndefined(); } ); it( 'should error on undefined edit property', () => { - registerFormatType( 'plugin/test', { + const format = registerFormatType( 'plugin/test', { ...validSettings, edit: undefined, } ); expect( console ).toHaveErroredWith( 'The "edit" property must be specified and must be a valid function.' ); + expect( format ).toBeUndefined(); + } ); + + it( 'should reject formats with an invalid edit function', () => { + const format = registerFormatType( validName, { + ...validSettings, + edit: 'not-a-function', + } ); + expect( console ).toHaveErroredWith( 'The "edit" property must be specified and must be a valid function.' ); + expect( format ).toBeUndefined(); + } ); + + it( 'should reject formats without tag name', () => { + const settings = { ...validSettings }; + delete settings.tagName; + const format = registerFormatType( validName, settings ); + expect( console ).toHaveErroredWith( 'Format tag names must be a string.' ); + expect( format ).toBeUndefined(); } ); it( 'should error on empty tagName property', () => { - registerFormatType( validName, { + const format = registerFormatType( validName, { ...validSettings, tagName: '', } ); expect( console ).toHaveErroredWith( 'Format tag names must be a string.' ); + expect( format ).toBeUndefined(); + } ); + + it( 'should reject formats without class name', () => { + const settings = { ...validSettings }; + delete settings.className; + const format = registerFormatType( validName, settings ); + expect( console ).toHaveErroredWith( 'Format class names must be a string, or null to handle bare elements.' ); + expect( format ).toBeUndefined(); } ); it( 'should error on invalid empty className property', () => { - registerFormatType( validName, { + const format = registerFormatType( validName, { ...validSettings, className: '', } ); expect( console ).toHaveErroredWith( 'Format class names must be a string, or null to handle bare elements.' ); + expect( format ).toBeUndefined(); } ); it( 'should error on invalid className property', () => { - registerFormatType( validName, { + const format = registerFormatType( validName, { ...validSettings, className: 'invalid class name', } ); expect( console ).toHaveErroredWith( 'A class name must begin with a letter, followed by any number of hyphens, letters, or numbers.' ); + expect( format ).toBeUndefined(); } ); it( 'should error on already registered tagName', () => { registerFormatType( validName, validSettings ); - registerFormatType( 'plugin/second', validSettings ); + const duplicateTagNameFormat = registerFormatType( 'plugin/second', validSettings ); expect( console ).toHaveErroredWith( 'Format "plugin/test" is already registered to handle bare tag name "test".' ); + expect( duplicateTagNameFormat ).toBeUndefined(); } ); it( 'should error on already registered className', () => { @@ -95,18 +167,47 @@ describe( 'registerFormatType', () => { ...validSettings, className: 'test', } ); - registerFormatType( 'plugin/second', { + const duplicateClassNameFormat = registerFormatType( 'plugin/second', { ...validSettings, className: 'test', } ); expect( console ).toHaveErroredWith( 'Format "plugin/test" is already registered to handle class name "test".' ); + expect( duplicateClassNameFormat ).toBeUndefined(); + } ); + + it( 'should reject formats without title', () => { + const settings = { ...validSettings }; + delete settings.title; + const format = registerFormatType( validName, settings ); + expect( console ).toHaveErroredWith( `The format "${ validName }" must have a title.` ); + expect( format ).toBeUndefined(); } ); it( 'should error on empty title property', () => { - registerFormatType( validName, { + const format = registerFormatType( validName, { ...validSettings, title: '', } ); expect( console ).toHaveErroredWith( 'The format "plugin/test" must have a title.' ); + expect( format ).toBeUndefined(); + } ); + + it( 'should reject titles which are not strings', () => { + const format = registerFormatType( validName, { + ...validSettings, + title: 1337, + } ); + expect( console ).toHaveErroredWith( 'Format titles must be strings.' ); + expect( format ).toBeUndefined(); + } ); + + it( 'should store a copy of the format type', () => { + const formatType = { ...validSettings }; + registerFormatType( validName, formatType ); + formatType.mutated = true; + expect( getFormatType( validName ) ).toEqual( { + name: validName, + ...validSettings, + } ); } ); } ); diff --git a/packages/rich-text/src/test/toggle-format.js b/packages/rich-text/src/test/toggle-format.js new file mode 100644 index 0000000000000..4a890a43bfae5 --- /dev/null +++ b/packages/rich-text/src/test/toggle-format.js @@ -0,0 +1,56 @@ +/** + * External dependencies + */ +import deepFreeze from 'deep-freeze'; + +/** + * Internal dependencies + */ + +import { toggleFormat } from '../toggle-format'; +import { getSparseArrayLength } from './helpers'; + +describe( 'toggleFormat', () => { + const strong = { type: 'strong' }; + const em = { type: 'em' }; + + it( 'should remove format if it exists at start of selection', () => { + const record = { + formats: [ , , , [ strong ], [ em, strong ], [ em ], [ em ], , , , , , , ], + text: 'one two three', + start: 3, + end: 6, + }; + const expected = { + formats: [ , , , , [ em ], [ em ], [ em ], , , , , , , ], + text: 'one two three', + start: 3, + end: 6, + }; + const result = toggleFormat( deepFreeze( record ), strong ); + + expect( result ).toEqual( expected ); + expect( result ).not.toBe( record ); + expect( getSparseArrayLength( result.formats ) ).toBe( 3 ); + } ); + + it( 'should apply format if it doesn\'t exist at start of selection', () => { + const record = { + formats: [ , , , , [ em, strong ], [ em ], [ em ], , , , , , , ], + text: 'one two three', + start: 3, + end: 6, + }; + const expected = { + formats: [ , , , [ strong ], [ em, strong ], [ em, strong ], [ em ], , , , , , , ], + text: 'one two three', + start: 3, + end: 6, + }; + const result = toggleFormat( deepFreeze( record ), strong ); + + expect( result ).toEqual( expected ); + expect( result ).not.toBe( record ); + expect( getSparseArrayLength( result.formats ) ).toBe( 4 ); + } ); +} ); diff --git a/packages/rich-text/src/test/unregister-format-type.js b/packages/rich-text/src/test/unregister-format-type.js new file mode 100644 index 0000000000000..82774f5395d9d --- /dev/null +++ b/packages/rich-text/src/test/unregister-format-type.js @@ -0,0 +1,53 @@ +/** + * External dependencies + */ +import { noop } from 'lodash'; + +/** + * Internal dependencies + */ +import { unregisterFormatType } from '../unregister-format-type'; +import { registerFormatType } from '../register-format-type'; +import { getFormatTypes } from '../get-format-types'; +import { getFormatType } from '../get-format-type'; + +describe( 'unregisterFormatType', () => { + const defaultFormatSettings = { + edit: noop, + title: 'format title', + tagName: 'test', + className: null, + }; + + beforeAll( () => { + // Initialize the rich-text store. + require( '../store' ); + } ); + + afterEach( () => { + getFormatTypes().forEach( ( format ) => { + unregisterFormatType( format.name ); + } ); + } ); + + it( 'should fail if the format is not registered', () => { + const oldFormat = unregisterFormatType( 'core/test-format' ); + expect( console ).toHaveErroredWith( 'Format core/test-format is not registered.' ); + expect( oldFormat ).toBeUndefined(); + } ); + + it( 'should unregister existing formats', () => { + registerFormatType( 'core/test-format', defaultFormatSettings ); + expect( getFormatType( 'core/test-format' ) ).toEqual( { + name: 'core/test-format', + ...defaultFormatSettings, + } ); + const oldFormat = unregisterFormatType( 'core/test-format' ); + expect( console ).not.toHaveErrored(); + expect( oldFormat ).toEqual( { + name: 'core/test-format', + ...defaultFormatSettings, + } ); + expect( getFormatTypes() ).toEqual( [] ); + } ); +} ); From f96678087b96675b6cea7c786855670742c6d570 Mon Sep 17 00:00:00 2001 From: William Earnhardt <wearnhardt@gmail.com> Date: Thu, 8 Nov 2018 03:37:56 -0500 Subject: [PATCH 037/106] Rename wp-polyfill-ecmascript (#11216) * Rename wp-polyfill-ecmascript * Make babel polyfill be script file for wp-polyfill * Fix suffix error * Stop using CDN directly * Register wp-polyfill-ecmascript to null for back compat * Ensure we use some grammar in our comments * Remove extraneous wp-polyfill-ecmascript include * Framework: Log deprecation notice for wp-polyfill-ecmascript --- docs/reference/scripts.md | 3 +-- lib/client-assets.php | 27 +++++++++++++++++---------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/docs/reference/scripts.md b/docs/reference/scripts.md index 4fba8a1cbea67..0bd0cf9892792 100644 --- a/docs/reference/scripts.md +++ b/docs/reference/scripts.md @@ -60,10 +60,9 @@ It is recommened to use the main `wp-polyfill` script handle which takes care of | Script Name | Handle | Description | |-------------|--------|-------------| -| polyfill | wp-polyfill | Main script to load all the below mentioned polyfills | +| [Babel Polyfill](https://babeljs.io/docs/en/babel-polyfill) | wp-polyfill | Emulate a full ES2015+ environment. Main script to load all the below mentioned additional polyfills | | [Fetch Polyfill](https://www.npmjs.com/package/whatwg-fetch) | wp-polyfill-fetch | Polyfill that implements a subset of the standard Fetch specification | | [Promise Polyfill](https://www.npmjs.com/package/promise-polyfill) | wp-polyfill-promise| Lightweight ES6 Promise polyfill for the browser and node | | [Formdata Polyfill](https://www.npmjs.com/package/formdata-polyfill) | wp-polyfill-formdata| Polyfill conditionally replaces the native implementation | | [Node Contains Polyfill](https://polyfill.io) | wp-polyfill-node-contains |Polyfill for Node.contains | | [Element Closest Polyfill](https://www.npmjs.com/package/element-closest) | wp-polyfill-element-closest| Return the closest element matching a selector up the DOM tree | -| [ECMAScript Polyfill](https://babeljs.io/docs/en/babel-polyfill) | wp-polyfill-ecmascript | Emulate a full ES2015+ environment | diff --git a/lib/client-assets.php b/lib/client-assets.php index 3cdfda9619901..a1e3181344092 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -144,13 +144,6 @@ function gutenberg_register_scripts_and_styles() { register_tinymce_scripts(); - gutenberg_override_script( - 'wp-polyfill', - null, - array( - 'wp-polyfill-ecmascript', - ) - ); wp_script_add_data( 'wp-polyfill', 'data', @@ -486,7 +479,7 @@ function gutenberg_register_scripts_and_styles() { 'lodash', 'wp-a11y', 'wp-data', - 'wp-polyfill-ecmascript', + 'wp-polyfill', ), filemtime( gutenberg_dir_path() . 'build/notices/index.js' ), true @@ -752,7 +745,7 @@ function gutenberg_register_scripts_and_styles() { 'wp-compose', 'wp-element', 'wp-i18n', - 'wp-polyfill-ecmascript', + 'wp-polyfill', ), filemtime( gutenberg_dir_path() . 'build/list-reusable-blocks/index.js' ), true @@ -980,9 +973,23 @@ function gutenberg_register_vendor_scripts() { 'https://unpkg.com/element-closest@2.0.2/element-closest.js' ); gutenberg_register_vendor_script( - 'wp-polyfill-ecmascript', + 'wp-polyfill', 'https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/7.0.0/polyfill' . $suffix . '.js' ); + // Ensure backwards compatibility after renaming to wp-polyfill. + gutenberg_override_script( + 'wp-polyfill-ecmascript', + null, + array( + 'wp-polyfill', + 'wp-deprecated', + ) + ); + wp_script_add_data( + 'wp-polyfill-ecmascript', + 'data', + 'wp.deprecated( "wp-polyfill-ecmascript script handle", { plugin: "Gutenberg", version: "4.5" } );' + ); } /** From 27b15298656576e0d83e9603555f3f62db7ad5ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20Van=C2=A0Dorpe?= <iseulde@automattic.com> Date: Thu, 8 Nov 2018 09:46:48 +0100 Subject: [PATCH 038/106] Paste: Google Docs: fix nested formatting, sub, sup and del (#11207) --- .../raw-handling/phrasing-content-reducer.js | 31 ++++++++++++++----- .../test/phrasing-content-reducer.js | 10 ++++-- packages/dom/src/dom.js | 16 ++++++++-- test/integration/fixtures/google-docs-in.html | 2 +- .../integration/fixtures/google-docs-out.html | 2 +- 5 files changed, 46 insertions(+), 15 deletions(-) diff --git a/packages/blocks/src/api/raw-handling/phrasing-content-reducer.js b/packages/blocks/src/api/raw-handling/phrasing-content-reducer.js index 0504bbb6e45b8..3bc6fa11fb239 100644 --- a/packages/blocks/src/api/raw-handling/phrasing-content-reducer.js +++ b/packages/blocks/src/api/raw-handling/phrasing-content-reducer.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { unwrap, replaceTag } from '@wordpress/dom'; +import { wrap, unwrap, replaceTag } from '@wordpress/dom'; /** * Internal dependencies @@ -14,17 +14,34 @@ function isBlockContent( node, schema = {} ) { export default function( node, doc, schema ) { if ( node.nodeName === 'SPAN' ) { - const { fontWeight, fontStyle } = node.style; + const { + fontWeight, + fontStyle, + textDecorationLine, + verticalAlign, + } = node.style; if ( fontWeight === 'bold' || fontWeight === '700' ) { - node = replaceTag( node, 'strong', doc ); - } else if ( fontStyle === 'italic' ) { - node = replaceTag( node, 'em', doc ); + wrap( doc.createElement( 'strong' ), node ); + } + + if ( fontStyle === 'italic' ) { + wrap( doc.createElement( 'em' ), node ); + } + + if ( textDecorationLine === 'line-through' ) { + wrap( doc.createElement( 'del' ), node ); + } + + if ( verticalAlign === 'super' ) { + wrap( doc.createElement( 'sup' ), node ); + } else if ( verticalAlign === 'sub' ) { + wrap( doc.createElement( 'sub' ), node ); } } else if ( node.nodeName === 'B' ) { - node = replaceTag( node, 'strong', doc ); + node = replaceTag( node, 'strong' ); } else if ( node.nodeName === 'I' ) { - node = replaceTag( node, 'em', doc ); + node = replaceTag( node, 'em' ); } else if ( node.nodeName === 'A' ) { if ( node.target.toLowerCase() === '_blank' ) { node.rel = 'noreferrer noopener'; diff --git a/packages/blocks/src/api/raw-handling/test/phrasing-content-reducer.js b/packages/blocks/src/api/raw-handling/test/phrasing-content-reducer.js index 8cca172b93d0c..254344e50b11d 100644 --- a/packages/blocks/src/api/raw-handling/test/phrasing-content-reducer.js +++ b/packages/blocks/src/api/raw-handling/test/phrasing-content-reducer.js @@ -6,15 +6,19 @@ import { deepFilterHTML } from '../utils'; describe( 'phrasingContentReducer', () => { it( 'should transform font weight', () => { - expect( deepFilterHTML( '<span style="font-weight:bold">test</span>', [ phrasingContentReducer ], {} ) ).toEqual( '<strong>test</strong>' ); + expect( deepFilterHTML( '<span style="font-weight:bold">test</span>', [ phrasingContentReducer ], {} ) ).toEqual( '<strong><span style="font-weight:bold">test</span></strong>' ); } ); it( 'should transform numeric font weight', () => { - expect( deepFilterHTML( '<span style="font-weight:700">test</span>', [ phrasingContentReducer ], {} ) ).toEqual( '<strong>test</strong>' ); + expect( deepFilterHTML( '<span style="font-weight:700">test</span>', [ phrasingContentReducer ], {} ) ).toEqual( '<strong><span style="font-weight:700">test</span></strong>' ); } ); it( 'should transform font style', () => { - expect( deepFilterHTML( '<span style="font-style:italic">test</span>', [ phrasingContentReducer ], {} ) ).toEqual( '<em>test</em>' ); + expect( deepFilterHTML( '<span style="font-style:italic">test</span>', [ phrasingContentReducer ], {} ) ).toEqual( '<em><span style="font-style:italic">test</span></em>' ); + } ); + + it( 'should transform nested formatting', () => { + expect( deepFilterHTML( '<span style="font-style:italic;font-weight:bold">test</span>', [ phrasingContentReducer ], {} ) ).toEqual( '<strong><em><span style="font-style:italic;font-weight:bold">test</span></em></strong>' ); } ); it( 'should remove invalid phrasing content', () => { diff --git a/packages/dom/src/dom.js b/packages/dom/src/dom.js index 36ee8ab80068b..9f65e47559ac3 100644 --- a/packages/dom/src/dom.js +++ b/packages/dom/src/dom.js @@ -620,12 +620,11 @@ export function unwrap( node ) { * * @param {Element} node The node to replace * @param {string} tagName The new tag name. - * @param {Document} doc The document of the node. * * @return {Element} The new node. */ -export function replaceTag( node, tagName, doc ) { - const newNode = doc.createElement( tagName ); +export function replaceTag( node, tagName ) { + const newNode = node.ownerDocument.createElement( tagName ); while ( node.firstChild ) { newNode.appendChild( node.firstChild ); @@ -635,3 +634,14 @@ export function replaceTag( node, tagName, doc ) { return newNode; } + +/** + * Wraps the given node with a new node with the given tag name. + * + * @param {Element} newNode The node to insert. + * @param {Element} referenceNode The node to wrap. + */ +export function wrap( newNode, referenceNode ) { + referenceNode.parentNode.insertBefore( newNode, referenceNode ); + newNode.appendChild( referenceNode ); +} diff --git a/test/integration/fixtures/google-docs-in.html b/test/integration/fixtures/google-docs-in.html index f69199a6d85fb..c667fddceab44 100644 --- a/test/integration/fixtures/google-docs-in.html +++ b/test/integration/fixtures/google-docs-in.html @@ -1 +1 @@ -<meta charset="utf-8"><b style="font-weight:normal;" id="docs-internal-guid-56c01d11-ec11-c354-b250-768cb93dcb1a"><p dir="ltr" style="line-height:1.38;margin-top:0pt;margin-bottom:3pt;"><span style="font-size:26pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre-wrap;">This is a </span><span style="font-size:26pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:700;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre-wrap;">title</span></p><br><h2 dir="ltr" style="line-height:1.38;margin-top:18pt;margin-bottom:6pt;"><span style="font-size:16pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre-wrap;">This is a </span><span style="font-size:16pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:italic;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre-wrap;">heading</span></h2><br><p dir="ltr" style="line-height:1.38;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre-wrap;">This is a </span><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:700;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre-wrap;">paragraph</span><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre-wrap;"> with a </span><a href="https://w.org" style="text-decoration:none;"><span style="font-size:11pt;font-family:Arial;color:#1155cc;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:underline;vertical-align:baseline;white-space:pre-wrap;">link</span></a><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre-wrap;">.</span></p><br><ul style="margin-top:0pt;margin-bottom:0pt;"><li dir="ltr" style="list-style-type:disc;font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;"><p dir="ltr" style="line-height:1.38;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre-wrap;">A</span></p></li><li dir="ltr" style="list-style-type:disc;font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;"><p dir="ltr" style="line-height:1.38;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre-wrap;">Bulleted</span></p></li><ul style="margin-top:0pt;margin-bottom:0pt;"><li dir="ltr" style="list-style-type:circle;font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;"><p dir="ltr" style="line-height:1.38;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre-wrap;">Indented</span></p></li></ul><li dir="ltr" style="list-style-type:disc;font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;"><p dir="ltr" style="line-height:1.38;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre-wrap;">List</span></p></li></ul><br><ol style="margin-top:0pt;margin-bottom:0pt;"><li dir="ltr" style="list-style-type:decimal;font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;"><p dir="ltr" style="line-height:1.38;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre-wrap;">One</span></p></li><li dir="ltr" style="list-style-type:decimal;font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;"><p dir="ltr" style="line-height:1.38;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre-wrap;">Two</span></p></li><li dir="ltr" style="list-style-type:decimal;font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;"><p dir="ltr" style="line-height:1.38;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre-wrap;">Three</span></p></li></ol><br><div dir="ltr" style="margin-left:0pt;"><table style="border:none;border-collapse:collapse;width:451.27559055118115pt"><colgroup><col width="*"><col width="*"><col width="*"></colgroup><tr style="height:0pt"><td style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre-wrap;">One</span></p></td><td style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre-wrap;">Two</span></p></td><td style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre-wrap;">Three</span></p></td></tr><tr style="height:0pt"><td style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre-wrap;">1</span></p></td><td style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre-wrap;">2</span></p></td><td style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre-wrap;">3</span></p></td></tr><tr style="height:0pt"><td style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre-wrap;">I</span></p></td><td style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre-wrap;">II</span></p></td><td style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre-wrap;">III</span></p></td></tr></table></div><br><p dir="ltr" style="line-height:1.38;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre-wrap;"><hr></span></p><br><p dir="ltr" style="line-height:1.38;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre-wrap;">An image:</span></p><br><p dir="ltr" style="line-height:1.38;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre-wrap;"><img src="https://lh4.googleusercontent.com/ID" width="602" height="451" style="border: none; transform: rotate(0.00rad); -webkit-transform: rotate(0.00rad);"/></span></p></b> +<b id="docs-internal-guid-23a0eab1-7fff-c4e2-3a94-7d2570556656" style="caret-color: rgb(0, 0, 0); color: rgb(0, 0, 0); font-family: -webkit-standard; font-style: normal; font-variant-caps: normal; letter-spacing: normal; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: auto; word-spacing: 0px; -webkit-text-size-adjust: auto; -webkit-text-stroke-width: 0px; text-decoration: none; font-weight: normal;"><p dir="ltr" style="line-height: 1.38; margin-top: 0pt; margin-bottom: 3pt;"><span style="font-size: 26pt; font-family: Arial; color: rgb(0, 0, 0); background-color: transparent; font-weight: 400; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-east-asian: normal; font-variant-position: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">This is a </span><span style="font-size: 26pt; font-family: Arial; color: rgb(0, 0, 0); background-color: transparent; font-weight: 700; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-east-asian: normal; font-variant-position: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">title</span></p><br><h2 dir="ltr" style="line-height: 1.38; margin-top: 18pt; margin-bottom: 4pt;"><span style="font-size: 17pt; font-family: Arial; color: rgb(0, 0, 0); background-color: transparent; font-weight: 400; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-east-asian: normal; font-variant-position: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">This is a </span><span style="font-size: 17pt; font-family: Arial; color: rgb(0, 0, 0); background-color: transparent; font-weight: 400; font-style: italic; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-east-asian: normal; font-variant-position: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">heading</span></h2><br><p dir="ltr" style="line-height: 1.38; margin-top: 0pt; margin-bottom: 0pt;"><span style="font-size: 11pt; font-family: Arial; color: rgb(0, 0, 0); background-color: transparent; font-weight: 400; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-east-asian: normal; font-variant-position: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Formatting test: </span><span style="font-size: 11pt; font-family: Arial; color: rgb(0, 0, 0); background-color: transparent; font-weight: 700; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-east-asian: normal; font-variant-position: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">bold</span><span style="font-size: 11pt; font-family: Arial; color: rgb(0, 0, 0); background-color: transparent; font-weight: 400; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-east-asian: normal; font-variant-position: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">, </span><span style="font-size: 11pt; font-family: Arial; color: rgb(0, 0, 0); background-color: transparent; font-weight: 400; font-style: italic; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-east-asian: normal; font-variant-position: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">italic</span><span style="font-size: 11pt; font-family: Arial; color: rgb(0, 0, 0); background-color: transparent; font-weight: 400; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-east-asian: normal; font-variant-position: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">, </span><a href="https://w.org/" style="text-decoration: none;"><span style="font-size: 11pt; font-family: Arial; color: rgb(17, 85, 204); background-color: transparent; font-weight: 400; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-east-asian: normal; font-variant-position: normal; text-decoration: underline; -webkit-text-decoration-skip: none; vertical-align: baseline; white-space: pre-wrap;">link</span></a><span style="font-size: 11pt; font-family: Arial; color: rgb(0, 0, 0); background-color: transparent; font-weight: 400; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-east-asian: normal; font-variant-position: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">, </span><span style="font-size: 11pt; font-family: Arial; color: rgb(0, 0, 0); background-color: transparent; font-weight: 400; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-east-asian: normal; font-variant-position: normal; text-decoration: line-through; -webkit-text-decoration-skip: none; vertical-align: baseline; white-space: pre-wrap;">strikethrough</span><span style="font-size: 11pt; font-family: Arial; color: rgb(0, 0, 0); background-color: transparent; font-weight: 400; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-east-asian: normal; font-variant-position: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">, </span><span style="font-size: 6.6pt; font-family: Arial; color: rgb(0, 0, 0); background-color: transparent; font-weight: 400; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-east-asian: normal; font-variant-position: normal; text-decoration: none; vertical-align: super; white-space: pre-wrap;">superscript</span><span style="font-size: 11pt; font-family: Arial; color: rgb(0, 0, 0); background-color: transparent; font-weight: 400; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-east-asian: normal; font-variant-position: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">, </span><span style="font-size: 6.6pt; font-family: Arial; color: rgb(0, 0, 0); background-color: transparent; font-weight: 400; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-east-asian: normal; font-variant-position: normal; text-decoration: none; vertical-align: sub; white-space: pre-wrap;">subscript</span><span style="font-size: 11pt; font-family: Arial; color: rgb(0, 0, 0); background-color: transparent; font-weight: 400; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-east-asian: normal; font-variant-position: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">, </span><span style="font-size: 11pt; font-family: Arial; color: rgb(0, 0, 0); background-color: transparent; font-weight: 700; font-style: italic; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-east-asian: normal; font-variant-position: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">nested</span><span style="font-size: 11pt; font-family: Arial; color: rgb(0, 0, 0); background-color: transparent; font-weight: 400; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-east-asian: normal; font-variant-position: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">.</span></p><br><ul style="margin-top: 0pt; margin-bottom: 0pt;"><li dir="ltr" style="list-style-type: disc; font-size: 11pt; font-family: Arial; color: rgb(0, 0, 0); background-color: transparent; font-weight: 400; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-east-asian: normal; font-variant-position: normal; text-decoration: none; vertical-align: baseline; white-space: pre;"><p dir="ltr" style="line-height: 1.38; margin-top: 0pt; margin-bottom: 0pt;"><span style="font-size: 11pt; font-family: Arial; color: rgb(0, 0, 0); background-color: transparent; font-weight: 400; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-east-asian: normal; font-variant-position: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">A</span></p></li><li dir="ltr" style="list-style-type: disc; font-size: 11pt; font-family: Arial; color: rgb(0, 0, 0); background-color: transparent; font-weight: 400; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-east-asian: normal; font-variant-position: normal; text-decoration: none; vertical-align: baseline; white-space: pre;"><p dir="ltr" style="line-height: 1.38; margin-top: 0pt; margin-bottom: 0pt;"><span style="font-size: 11pt; font-family: Arial; color: rgb(0, 0, 0); background-color: transparent; font-weight: 400; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-east-asian: normal; font-variant-position: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Bulleted</span></p></li><ul style="margin-top: 0pt; margin-bottom: 0pt;"><li dir="ltr" style="list-style-type: circle; font-size: 11pt; font-family: Arial; color: rgb(0, 0, 0); background-color: transparent; font-weight: 400; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-east-asian: normal; font-variant-position: normal; text-decoration: none; vertical-align: baseline; white-space: pre;"><p dir="ltr" style="line-height: 1.38; margin-top: 0pt; margin-bottom: 0pt;"><span style="font-size: 11pt; font-family: Arial; color: rgb(0, 0, 0); background-color: transparent; font-weight: 400; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-east-asian: normal; font-variant-position: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Indented</span></p></li></ul><li dir="ltr" style="list-style-type: disc; font-size: 11pt; font-family: Arial; color: rgb(0, 0, 0); background-color: transparent; font-weight: 400; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-east-asian: normal; font-variant-position: normal; text-decoration: none; vertical-align: baseline; white-space: pre;"><p dir="ltr" style="line-height: 1.38; margin-top: 0pt; margin-bottom: 0pt;"><span style="font-size: 11pt; font-family: Arial; color: rgb(0, 0, 0); background-color: transparent; font-weight: 400; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-east-asian: normal; font-variant-position: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">List</span></p></li></ul><br><ol style="margin-top: 0pt; margin-bottom: 0pt;"><li dir="ltr" style="list-style-type: decimal; font-size: 11pt; font-family: Arial; color: rgb(0, 0, 0); background-color: transparent; font-weight: 400; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-east-asian: normal; font-variant-position: normal; text-decoration: none; vertical-align: baseline; white-space: pre;"><p dir="ltr" style="line-height: 1.38; margin-top: 0pt; margin-bottom: 0pt;"><span style="font-size: 11pt; font-family: Arial; color: rgb(0, 0, 0); background-color: transparent; font-weight: 400; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-east-asian: normal; font-variant-position: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">One</span></p></li><li dir="ltr" style="list-style-type: decimal; font-size: 11pt; font-family: Arial; color: rgb(0, 0, 0); background-color: transparent; font-weight: 400; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-east-asian: normal; font-variant-position: normal; text-decoration: none; vertical-align: baseline; white-space: pre;"><p dir="ltr" style="line-height: 1.38; margin-top: 0pt; margin-bottom: 0pt;"><span style="font-size: 11pt; font-family: Arial; color: rgb(0, 0, 0); background-color: transparent; font-weight: 400; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-east-asian: normal; font-variant-position: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Two</span></p></li><li dir="ltr" style="list-style-type: decimal; font-size: 11pt; font-family: Arial; color: rgb(0, 0, 0); background-color: transparent; font-weight: 400; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-east-asian: normal; font-variant-position: normal; text-decoration: none; vertical-align: baseline; white-space: pre;"><p dir="ltr" style="line-height: 1.38; margin-top: 0pt; margin-bottom: 0pt;"><span style="font-size: 11pt; font-family: Arial; color: rgb(0, 0, 0); background-color: transparent; font-weight: 400; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-east-asian: normal; font-variant-position: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Three</span></p></li></ol><br><div dir="ltr" style="margin-left: 0pt;"><table style="border: none; border-collapse: collapse; width: 451.27559055118115pt;"><colgroup><col width="*"><col width="*"><col width="*"></colgroup><tbody><tr style="height: 0pt;"><td style="border: 1pt solid rgb(0, 0, 0); vertical-align: top; padding: 5pt;"><p dir="ltr" style="line-height: 1.2; margin-top: 0pt; margin-bottom: 0pt;"><span style="font-size: 11pt; font-family: Arial; color: rgb(0, 0, 0); background-color: transparent; font-weight: 400; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-east-asian: normal; font-variant-position: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">One</span></p></td><td style="border: 1pt solid rgb(0, 0, 0); vertical-align: top; padding: 5pt;"><p dir="ltr" style="line-height: 1.2; margin-top: 0pt; margin-bottom: 0pt;"><span style="font-size: 11pt; font-family: Arial; color: rgb(0, 0, 0); background-color: transparent; font-weight: 400; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-east-asian: normal; font-variant-position: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Two</span></p></td><td style="border: 1pt solid rgb(0, 0, 0); vertical-align: top; padding: 5pt;"><p dir="ltr" style="line-height: 1.2; margin-top: 0pt; margin-bottom: 0pt;"><span style="font-size: 11pt; font-family: Arial; color: rgb(0, 0, 0); background-color: transparent; font-weight: 400; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-east-asian: normal; font-variant-position: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Three</span></p></td></tr><tr style="height: 0pt;"><td style="border: 1pt solid rgb(0, 0, 0); vertical-align: top; padding: 5pt;"><p dir="ltr" style="line-height: 1.2; margin-top: 0pt; margin-bottom: 0pt;"><span style="font-size: 11pt; font-family: Arial; color: rgb(0, 0, 0); background-color: transparent; font-weight: 400; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-east-asian: normal; font-variant-position: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">1</span></p></td><td style="border: 1pt solid rgb(0, 0, 0); vertical-align: top; padding: 5pt;"><p dir="ltr" style="line-height: 1.2; margin-top: 0pt; margin-bottom: 0pt;"><span style="font-size: 11pt; font-family: Arial; color: rgb(0, 0, 0); background-color: transparent; font-weight: 400; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-east-asian: normal; font-variant-position: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">2</span></p></td><td style="border: 1pt solid rgb(0, 0, 0); vertical-align: top; padding: 5pt;"><p dir="ltr" style="line-height: 1.2; margin-top: 0pt; margin-bottom: 0pt;"><span style="font-size: 11pt; font-family: Arial; color: rgb(0, 0, 0); background-color: transparent; font-weight: 400; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-east-asian: normal; font-variant-position: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">3</span></p></td></tr><tr style="height: 0pt;"><td style="border: 1pt solid rgb(0, 0, 0); vertical-align: top; padding: 5pt;"><p dir="ltr" style="line-height: 1.2; margin-top: 0pt; margin-bottom: 0pt;"><span style="font-size: 11pt; font-family: Arial; color: rgb(0, 0, 0); background-color: transparent; font-weight: 400; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-east-asian: normal; font-variant-position: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">I</span></p></td><td style="border: 1pt solid rgb(0, 0, 0); vertical-align: top; padding: 5pt;"><p dir="ltr" style="line-height: 1.2; margin-top: 0pt; margin-bottom: 0pt;"><span style="font-size: 11pt; font-family: Arial; color: rgb(0, 0, 0); background-color: transparent; font-weight: 400; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-east-asian: normal; font-variant-position: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">II</span></p></td><td style="border: 1pt solid rgb(0, 0, 0); vertical-align: top; padding: 5pt;"><p dir="ltr" style="line-height: 1.2; margin-top: 0pt; margin-bottom: 0pt;"><span style="font-size: 11pt; font-family: Arial; color: rgb(0, 0, 0); background-color: transparent; font-weight: 400; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-east-asian: normal; font-variant-position: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">III</span></p></td></tr></tbody></table></div><br><p dir="ltr" style="line-height: 1.38; margin-top: 0pt; margin-bottom: 0pt;"></p><hr><p></p><br><p dir="ltr" style="line-height: 1.38; margin-top: 0pt; margin-bottom: 0pt;"><span style="font-size: 11pt; font-family: Arial; color: rgb(0, 0, 0); background-color: transparent; font-weight: 400; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-east-asian: normal; font-variant-position: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">An image:</span></p><br><p dir="ltr" style="line-height: 1.38; margin-top: 0pt; margin-bottom: 0pt;"><span style="font-size: 11pt; font-family: Arial; color: rgb(0, 0, 0); background-color: transparent; font-weight: 400; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-east-asian: normal; font-variant-position: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;"><img src="https://lh4.googleusercontent.com/ID" width="544" height="184" style="border: none; transform: rotate(0rad);"></span></p></b><br class="Apple-interchange-newline" style="caret-color: rgb(0, 0, 0); color: rgb(0, 0, 0); font-family: -webkit-standard; font-style: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: auto; word-spacing: 0px; -webkit-text-size-adjust: auto; -webkit-text-stroke-width: 0px; text-decoration: none;"> \ No newline at end of file diff --git a/test/integration/fixtures/google-docs-out.html b/test/integration/fixtures/google-docs-out.html index 541a75306b02c..7733ca660bdd0 100644 --- a/test/integration/fixtures/google-docs-out.html +++ b/test/integration/fixtures/google-docs-out.html @@ -7,7 +7,7 @@ <h2>This is a <em>heading</em></h2> <!-- /wp:heading --> <!-- wp:paragraph --> -<p>This is a <strong>paragraph</strong> with a <a href="https://w.org">link</a>.<br></p> +<p>Formatting test: <strong>bold</strong>, <em>italic</em>, <a href="https://w.org/">link</a>, strikethrough, <sup>superscript</sup>, <sub>subscript</sub>, <strong><em>nested</em></strong>.<br></p> <!-- /wp:paragraph --> <!-- wp:list --> From e12f2b4381d3f56b65ce350a3fd204cdfd4bdd70 Mon Sep 17 00:00:00 2001 From: Andrea Fercia <a.fercia@gmail.com> Date: Thu, 8 Nov 2018 10:04:04 +0100 Subject: [PATCH 039/106] Fix regressions and improve the insert link search field and suggestions list (#10838) * Fix link search field padding and spinner. * Select the highlighted link when tabbing away, set focus when clicking, add speak message. * Fix links suggestions popover position and size. * Avoid events, they're bad. * Revert custom getAnchorRect. --- .../editor/src/components/url-input/index.js | 24 ++++++++++++++++--- .../src/components/url-input/style.scss | 14 +++++------ packages/format-library/src/link/inline.js | 4 ++-- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/packages/editor/src/components/url-input/index.js b/packages/editor/src/components/url-input/index.js index 23762991cddcf..c841d1acf3599 100644 --- a/packages/editor/src/components/url-input/index.js +++ b/packages/editor/src/components/url-input/index.js @@ -11,7 +11,7 @@ import scrollIntoView from 'dom-scroll-into-view'; import { __, sprintf, _n } from '@wordpress/i18n'; import { Component, Fragment, createRef } from '@wordpress/element'; import { decodeEntities } from '@wordpress/html-entities'; -import { UP, DOWN, ENTER } from '@wordpress/keycodes'; +import { UP, DOWN, ENTER, TAB } from '@wordpress/keycodes'; import { Spinner, withSpokenMessages, Popover } from '@wordpress/components'; import { withInstanceId } from '@wordpress/compose'; import apiFetch from '@wordpress/api-fetch'; @@ -29,6 +29,7 @@ class URLInput extends Component { this.onChange = this.onChange.bind( this ); this.onKeyDown = this.onKeyDown.bind( this ); this.autocompleteRef = autocompleteRef || createRef(); + this.inputRef = createRef(); this.updateSuggestions = throttle( this.updateSuggestions.bind( this ), 200 ); this.suggestionNodes = []; @@ -140,6 +141,8 @@ class URLInput extends Component { return; } + const post = this.state.posts[ this.state.selectedSuggestion ]; + switch ( event.keyCode ) { case UP: { event.stopPropagation(); @@ -159,12 +162,20 @@ class URLInput extends Component { } ); break; } + case TAB: { + if ( this.state.selectedSuggestion !== null ) { + this.selectLink( post ); + // Announce a link has been selected when tabbing away from the input field. + this.props.speak( __( 'Link selected' ) ); + } + break; + } case ENTER: { if ( this.state.selectedSuggestion !== null ) { event.stopPropagation(); - const post = this.state.posts[ this.state.selectedSuggestion ]; this.selectLink( post ); } + break; } } } @@ -177,6 +188,12 @@ class URLInput extends Component { } ); } + handleOnClick( post ) { + this.selectLink( post ); + // Move focus to the input field when a link suggestion is clicked. + this.inputRef.current.focus(); + } + render() { const { value = '', autoFocus = true, instanceId } = this.props; const { showSuggestions, posts, selectedSuggestion, loading } = this.state; @@ -199,6 +216,7 @@ class URLInput extends Component { aria-autocomplete="list" aria-owns={ `editor-url-input-suggestions-${ instanceId }` } aria-activedescendant={ selectedSuggestion !== null ? `editor-url-input-suggestion-${ instanceId }-${ selectedSuggestion }` : undefined } + ref={ this.inputRef } /> { ( loading ) && <Spinner /> } @@ -222,7 +240,7 @@ class URLInput extends Component { className={ classnames( 'editor-url-input__suggestion', { 'is-selected': index === selectedSuggestion, } ) } - onClick={ () => this.selectLink( post ) } + onClick={ () => this.handleOnClick( post ) } aria-selected={ index === selectedSuggestion } > { decodeEntities( post.title ) || __( '(no title)' ) } diff --git a/packages/editor/src/components/url-input/style.scss b/packages/editor/src/components/url-input/style.scss index 3fe564178d683..0946802bcbd05 100644 --- a/packages/editor/src/components/url-input/style.scss +++ b/packages/editor/src/components/url-input/style.scss @@ -1,11 +1,10 @@ // Link input -$input-padding: 9px 8px; +$input-padding: 8px; $input-size: 300px; .editor-block-list__block .editor-url-input, .components-popover .editor-url-input, .editor-url-input { - width: 100%; flex-grow: 1; position: relative; padding: 1px; @@ -28,8 +27,8 @@ $input-size: 300px; .components-spinner { position: absolute; - right: 0; - top: $input-padding; + right: $input-padding; + top: $input-padding + 1; margin: 0; } } @@ -38,9 +37,10 @@ $input-size: 300px; .editor-url-input__suggestions { max-height: 200px; transition: all 0.15s ease-in-out; - list-style: none; padding: 4px 0; - width: $input-size + 2 * $icon-button-size; + // To match the url-input width: input width + padding + 2 buttons. + width: $input-size + 2 + 2 * $icon-button-size; + overflow-y: auto; } // Hide suggestions on mobile until we @todo find a better way to show them @@ -53,7 +53,7 @@ $input-size: 300px; } .editor-url-input__suggestion { - padding: 4px #{ $icon-button-size + $input-padding } 4px $input-padding; + padding: 4px $input-padding; color: $dark-gray-300; // lightest we can use for contrast display: block; font-size: $default-font-size; diff --git a/packages/format-library/src/link/inline.js b/packages/format-library/src/link/inline.js index 95fb1eb404525..ef3ec4513adb5 100644 --- a/packages/format-library/src/link/inline.js +++ b/packages/format-library/src/link/inline.js @@ -179,9 +179,9 @@ class InlineLinkUI extends Component { this.resetState(); if ( isActive ) { - speak( __( 'Link edited.' ), 'assertive' ); + speak( __( 'Link edited' ), 'assertive' ); } else { - speak( __( 'Link added.' ), 'assertive' ); + speak( __( 'Link inserted' ), 'assertive' ); } } From 69427ea3c0be7504d213b98e0fbcace7f5fc53ed Mon Sep 17 00:00:00 2001 From: Robert Anderson <robert@noisysocks.com> Date: Thu, 8 Nov 2018 20:32:43 +1100 Subject: [PATCH 040/106] Gallery: Upload images one at a time (#11565) * Gallery: Upload images one at a time Makes mediaUpload() upload image files one at a time. This matches the existing Media Library's behaviour. By waiting for each image to process before starting on the next, the PHP/CGI backend isn't overloaded with requests to resize multiple image files at the same time--something that always causes timeout errors for large images. * Ensure files that fail validation are skipped and trigger an error, but in the case of mulitple files, valid uploads continue to be processed * Media Upload: Process blobs before uploading each image * Media Upload: Improve tests Properly mock the modules that mediaUpload() uses so that we can more easily test calling mediaUpload(). --- .../src/utils/media-upload/media-upload.js | 78 +++--- .../utils/media-upload/test/media-upload.js | 248 +++++++++--------- 2 files changed, 167 insertions(+), 159 deletions(-) diff --git a/packages/editor/src/utils/media-upload/media-upload.js b/packages/editor/src/utils/media-upload/media-upload.js index 8ef98130190d7..47486bc5671e4 100644 --- a/packages/editor/src/utils/media-upload/media-upload.js +++ b/packages/editor/src/utils/media-upload/media-upload.js @@ -61,7 +61,7 @@ export function getMimeTypesArray( wpMimeTypesObject ) { * @param {Function} $0.onFileChange Function called each time a file or a temporary representation of the file is available. * @param {?Object} $0.wpAllowedMimeTypes List of allowed mime types and file extensions. */ -export function mediaUpload( { +export async function mediaUpload( { allowedTypes, additionalData = {}, filesList, @@ -115,7 +115,9 @@ export function mediaUpload( { onError( error ); }; - files.forEach( ( mediaFile, idx ) => { + const validFiles = []; + + for ( const mediaFile of files ) { // verify if user is allowed to upload this mime type if ( allowedMimeTypesForUser && ! isAllowedMimeTypeForUser( mediaFile.type ) ) { triggerError( { @@ -123,7 +125,7 @@ export function mediaUpload( { message: __( 'Sorry, this file type is not permitted for security reasons.' ), file: mediaFile, } ); - return; + continue; } // Check if the block supports this mime type @@ -133,7 +135,7 @@ export function mediaUpload( { message: __( 'Sorry, this file type is not supported here.' ), file: mediaFile, } ); - return; + continue; } // verify if file is greater than the maximum file upload size allowed for the site. @@ -143,7 +145,7 @@ export function mediaUpload( { message: __( 'This file exceeds the maximum upload size for this site.' ), file: mediaFile, } ); - return; + continue; } // Don't allow empty files to be uploaded. @@ -153,45 +155,49 @@ export function mediaUpload( { message: __( 'This file is empty.' ), file: mediaFile, } ); - return; + continue; } + validFiles.push( mediaFile ); + // Set temporary URL to create placeholder media file, this is replaced // with final file from media gallery when upload is `done` below filesSet.push( { url: createBlobURL( mediaFile ) } ); onFileChange( filesSet ); + } - return createMediaFromFile( mediaFile, additionalData ) - .then( ( savedMedia ) => { - const mediaObject = { - ...omit( savedMedia, [ 'alt_text', 'source_url' ] ), - alt: savedMedia.alt_text, - caption: get( savedMedia, [ 'caption', 'raw' ], '' ), - title: savedMedia.title.raw, - url: savedMedia.source_url, - }; - setAndUpdateFiles( idx, mediaObject ); - } ) - .catch( ( error ) => { - // Reset to empty on failure. - setAndUpdateFiles( idx, null ); - let message; - if ( has( error, [ 'message' ] ) ) { - message = get( error, [ 'message' ] ); - } else { - message = sprintf( - // translators: %s: file name - __( 'Error while uploading file %s to the media library.' ), - mediaFile.name - ); - } - onError( { - code: 'GENERAL', - message, - file: mediaFile, - } ); + for ( let idx = 0; idx < validFiles.length; ++idx ) { + const mediaFile = validFiles[ idx ]; + try { + const savedMedia = await createMediaFromFile( mediaFile, additionalData ); + const mediaObject = { + ...omit( savedMedia, [ 'alt_text', 'source_url' ] ), + alt: savedMedia.alt_text, + caption: get( savedMedia, [ 'caption', 'raw' ], '' ), + title: savedMedia.title.raw, + url: savedMedia.source_url, + }; + setAndUpdateFiles( idx, mediaObject ); + } catch ( error ) { + // Reset to empty on failure. + setAndUpdateFiles( idx, null ); + let message; + if ( has( error, [ 'message' ] ) ) { + message = get( error, [ 'message' ] ); + } else { + message = sprintf( + // translators: %s: file name + __( 'Error while uploading file %s to the media library.' ), + mediaFile.name + ); + } + onError( { + code: 'GENERAL', + message, + file: mediaFile, } ); - } ); + } + } } /** diff --git a/packages/editor/src/utils/media-upload/test/media-upload.js b/packages/editor/src/utils/media-upload/test/media-upload.js index 04a356677ff79..d9e18cf7bf280 100644 --- a/packages/editor/src/utils/media-upload/test/media-upload.js +++ b/packages/editor/src/utils/media-upload/test/media-upload.js @@ -1,168 +1,170 @@ +/** + * WordPress dependencies + */ +import { createBlobURL } from '@wordpress/blob'; +import apiFetch from '@wordpress/api-fetch'; + /** * Internal dependencies */ import { mediaUpload, getMimeTypesArray } from '../media-upload'; -const invalidMediaObj = { - url: 'https://cldup.com/uuUqE_dXzy.jpg', - type: 'text/xml', - name: 'test.xml', -}; +jest.mock( '@wordpress/blob', () => ( { + createBlobURL: jest.fn(), + revokeBlobURL: jest.fn(), +} ) ); +jest.mock( '@wordpress/api-fetch', () => jest.fn() ); -const validMediaObj = { - url: 'https://cldup.com/uuUqE_dXzy.jpg', - type: 'image/jpeg', - size: 1024, - name: 'test.jpeg', -}; +const xmlFile = new window.File( [ 'fake_file' ], 'test.xml', { type: 'text/xml' } ); +const imageFile = new window.File( [ 'fake_file' ], 'test.jpeg', { type: 'image/jpeg' } ); describe( 'mediaUpload', () => { - const onFileChangeSpy = jest.fn(); - const createObjectURL = jest.fn(); - window.URL.createObjectURL = createObjectURL; - - beforeEach( () => { - onFileChangeSpy.mockReset(); - createObjectURL.mockReset(); - } ); + it( 'should do nothing on no files', async () => { + const onError = jest.fn(); + const onFileChange = jest.fn(); + await mediaUpload( { + filesList: [], + onError, + onFileChange, + } ); - it( 'should do nothing on no files', () => { - mediaUpload( { filesList: [], onFileChange: onFileChangeSpy, allowedType: 'image' } ); - expect( onFileChangeSpy ).not.toHaveBeenCalled(); + expect( onError ).not.toHaveBeenCalled(); + expect( onFileChange ).not.toHaveBeenCalled(); } ); - it( 'should do nothing on invalid image type', () => { + it( 'should error if allowedTypes contains a partial mime type and the validation fails', async () => { const onError = jest.fn(); - mediaUpload( { - filesList: [ invalidMediaObj ], - onFileChange: onFileChangeSpy, + const onFileChange = jest.fn(); + await mediaUpload( { allowedTypes: [ 'image' ], + filesList: [ xmlFile ], onError, + onFileChange, } ); - expect( onFileChangeSpy ).not.toHaveBeenCalled(); - expect( onError ).toHaveBeenCalled(); - expect( onError.mock.calls[ 0 ][ 0 ].code ).toBe( 'MIME_TYPE_NOT_SUPPORTED' ); + + expect( onError ).toHaveBeenCalledWith( expect.objectContaining( { + code: 'MIME_TYPE_NOT_SUPPORTED', + } ) ); + expect( onFileChange ).not.toHaveBeenCalled(); } ); - it( 'should error if allowedTypes contains a complete mime type string and the validation fails', () => { + it( 'should error if allowedTypes contains a complete mime type and the validation fails', async () => { const onError = jest.fn(); - const testFile = { - url: 'https://cldup.com/uuUqE_dXzy.mp3', - type: 'image/jpeg', - name: 'myImage.jpeg', - }; - expect( () => { - mediaUpload( { - filesList: [ testFile ], - onFileChange: onFileChangeSpy, - allowedTypes: [ 'image/gif' ], - onError, - } ); - } ).not.toThrow(); - expect( createObjectURL ).not.toHaveBeenCalled(); - expect( onError ).toHaveBeenCalled(); - expect( onError.mock.calls[ 0 ][ 0 ].code ).toBe( 'MIME_TYPE_NOT_SUPPORTED' ); - - createObjectURL.mockReset(); + const onFileChange = jest.fn(); + await mediaUpload( { + allowedTypes: [ 'image/gif' ], + filesList: [ imageFile ], + onError, + onFileChange, + } ); + + expect( onError ).toHaveBeenCalledWith( expect.objectContaining( { + code: 'MIME_TYPE_NOT_SUPPORTED', + } ) ); + expect( onFileChange ).not.toHaveBeenCalled(); } ); - it( 'should work as expected if allowedTypes contains a complete mime type string and the validation has success', () => { - // When the upload has success we get errors because we don't have available on the test environment - // all the functions required by mediaUpload. - // We know that when a validation success the function tries to create a temporary url for the file - // so we use that function to test the validation success. + it( 'should work if allowedTypes contains a complete mime type and the validation succeeds', async () => { + createBlobURL.mockReturnValue( 'blob:fake_blob' ); + apiFetch.mockResolvedValue( { title: { raw: 'Test' } } ); + const onError = jest.fn(); - const testFile = { - url: 'https://cldup.com/uuUqE_dXzy.mp3', - type: 'image/jpeg', - name: 'myImage.jpeg', - }; - - expect( () => { - mediaUpload( { - filesList: [ testFile ], - onFileChange: onFileChangeSpy, - allowedTypes: [ 'image/jpeg' ], - onError, - } ); - } ).toThrow(); - expect( createObjectURL ).not.toHaveBeenCalled(); + const onFileChange = jest.fn(); + await mediaUpload( { + allowedTypes: [ 'image/jpeg' ], + filesList: [ imageFile ], + onError, + onFileChange, + } ); + expect( onError ).not.toHaveBeenCalled(); + expect( onFileChange ).toHaveBeenCalledTimes( 2 ); } ); - it( 'should error if allowedTypes contains multiple types and the validation fails', () => { + it( 'should error if allowedTypes contains multiple types and the validation fails', async () => { const onError = jest.fn(); - const testFile = { - url: 'https://cldup.com/uuUqE_dXzy.mp3', - type: 'audio/mp3', - name: 'mymusic.mp3', - }; - expect( () => { - mediaUpload( { - filesList: [ testFile ], - onFileChange: onFileChangeSpy, - allowedTypes: [ 'video', 'image' ], - onError, - } ); - } ).not.toThrow(); - expect( createObjectURL ).not.toHaveBeenCalled(); - expect( onError ).toHaveBeenCalled(); - expect( onError.mock.calls[ 0 ][ 0 ].code ).toBe( 'MIME_TYPE_NOT_SUPPORTED' ); - - createObjectURL.mockReset(); + const onFileChange = jest.fn(); + await mediaUpload( { + allowedTypes: [ 'video', 'image' ], + filesList: [ xmlFile ], + onError, + onFileChange, + } ); + + expect( onError ).toHaveBeenCalledWith( expect.objectContaining( { + code: 'MIME_TYPE_NOT_SUPPORTED', + } ) ); + expect( onFileChange ).not.toHaveBeenCalled(); } ); - it( 'should work as expected if allowedTypes contains multiple types and the validation has success', () => { - // When the upload has success we get errors because we don't have available on the test environment - // all the functions required by mediaUpload. - // We know that when a validation success the function tries to create a temporary url for the file - // so we use that function to test the validation success. + it( 'should work if allowedTypes contains multiple types and the validation succeeds', async () => { + createBlobURL.mockReturnValue( 'blob:fake_blob' ); + apiFetch.mockResolvedValue( { title: { raw: 'Test' } } ); + const onError = jest.fn(); - const testFile = { - url: 'https://cldup.com/uuUqE_dXzy.mp3', - type: 'audio/mp3', - name: 'mymusic.mp3', - }; - - expect( () => { - mediaUpload( { - filesList: [ testFile ], - onFileChange: onFileChangeSpy, - allowedTypes: [ 'image', 'audio' ], - onError, - } ); - } ).toThrow(); - expect( createObjectURL ).not.toHaveBeenCalled(); + const onFileChange = jest.fn(); + await mediaUpload( { + allowedTypes: [ 'video', 'image' ], + filesList: [ imageFile ], + onError, + onFileChange, + } ); + expect( onError ).not.toHaveBeenCalled(); + expect( onFileChange ).toHaveBeenCalledTimes( 2 ); } ); - it( 'should call error handler with the correct error object if file size is greater than the maximum', () => { - const onError = jest.fn(); + it( 'should only fail the invalid file and still allow others to succeed when uploading multiple files', async () => { + createBlobURL.mockReturnValue( 'blob:fake_blob' ); + apiFetch.mockResolvedValue( { title: { raw: 'Test' } } ); - mediaUpload( { + const onError = jest.fn(); + const onFileChange = jest.fn(); + await mediaUpload( { allowedTypes: [ 'image' ], - filesList: [ validMediaObj ], - onFileChange: onFileChangeSpy, - maxUploadFileSize: 512, + filesList: [ imageFile, xmlFile ], onError, + onFileChange, } ); - expect( onError ).toHaveBeenCalled(); - expect( onError.mock.calls[ 0 ][ 0 ].code ).toBe( 'SIZE_ABOVE_LIMIT' ); + + expect( onError ).toHaveBeenCalledWith( expect.objectContaining( { + code: 'MIME_TYPE_NOT_SUPPORTED', + file: xmlFile, + } ) ); + expect( onFileChange ).toHaveBeenCalledTimes( 2 ); } ); - it( 'should call error handler with the correct error object if file type is not allowed for user', () => { + it( 'should error if the file size is greater than the maximum', async () => { const onError = jest.fn(); - const wpAllowedMimeTypes = { aac: 'audio/aac' }; + const onFileChange = jest.fn(); + await mediaUpload( { + allowedTypes: [ 'image' ], + filesList: [ imageFile ], + maxUploadFileSize: 1, + onError, + onFileChange, + } ); - mediaUpload( { + expect( onError ).toHaveBeenCalledWith( expect.objectContaining( { + code: 'SIZE_ABOVE_LIMIT', + } ) ); + expect( onFileChange ).not.toHaveBeenCalled(); + } ); + + it( 'should call error handler with the correct error object if file type is not allowed for user', async () => { + const onError = jest.fn(); + const onFileChange = jest.fn(); + await mediaUpload( { allowedTypes: [ 'image' ], - filesList: [ validMediaObj ], - onFileChange: onFileChangeSpy, + filesList: [ imageFile ], onError, - wpAllowedMimeTypes, + wpAllowedMimeTypes: { aac: 'audio/aac' }, } ); - expect( onError ).toHaveBeenCalled(); - expect( onError.mock.calls[ 0 ][ 0 ].code ).toBe( 'MIME_TYPE_NOT_ALLOWED_FOR_USER' ); + + expect( onError ).toHaveBeenCalledWith( expect.objectContaining( { + code: 'MIME_TYPE_NOT_ALLOWED_FOR_USER', + } ) ); + expect( onFileChange ).not.toHaveBeenCalled(); } ); } ); From 5728b361428cb00301a5605c9aec525e29ff2db3 Mon Sep 17 00:00:00 2001 From: Dominik Schilling <dominikschilling+git@gmail.com> Date: Thu, 8 Nov 2018 10:33:01 +0100 Subject: [PATCH 041/106] Remove undefined className argument from save() methods. (#11605) --- docs/blocks/applying-styles-with-stylesheets.md | 8 ++++---- docs/blocks/block-controls-toolbars-and-inspector.md | 3 +-- docs/blocks/introducing-attributes-and-editable-fields.md | 3 +-- packages/block-library/src/cover/index.js | 6 ++---- packages/block-library/src/subhead/index.js | 3 +-- packages/block-library/src/verse/index.js | 3 +-- 6 files changed, 10 insertions(+), 16 deletions(-) diff --git a/docs/blocks/applying-styles-with-stylesheets.md b/docs/blocks/applying-styles-with-stylesheets.md index 756741b38253a..47d2f70876143 100644 --- a/docs/blocks/applying-styles-with-stylesheets.md +++ b/docs/blocks/applying-styles-with-stylesheets.md @@ -21,8 +21,8 @@ registerBlockType( 'gutenberg-boilerplate-es5/hello-world-step-02', { return el( 'p', { className: props.className }, 'Hello editor.' ); }, - save: function( props ) { - return el( 'p', { className: props.className }, 'Hello saved content.' ); + save: function() { + return el( 'p', {}, 'Hello saved content.' ); } } ); ``` @@ -41,8 +41,8 @@ registerBlockType( 'gutenberg-boilerplate-esnext/hello-world-step-02', { return <p className={ className }>Hello editor.</p>; }, - save( { className } ) { - return <p className={ className }>Hello saved content.</p>; + save() { + return <p>Hello saved content.</p>; } } ); ``` diff --git a/docs/blocks/block-controls-toolbars-and-inspector.md b/docs/blocks/block-controls-toolbars-and-inspector.md index 5b9f97a3e63cf..236e985619769 100644 --- a/docs/blocks/block-controls-toolbars-and-inspector.md +++ b/docs/blocks/block-controls-toolbars-and-inspector.md @@ -152,12 +152,11 @@ registerBlockType( 'gutenberg-boilerplate-esnext/hello-world-step-04', { ); }, - save( { attributes, className } ) { + save( { attributes } ) { const { content, alignment } = attributes; return ( <RichText.Content - className={ className } style={ { textAlign: alignment } } value={ content } tagName="p" diff --git a/docs/blocks/introducing-attributes-and-editable-fields.md b/docs/blocks/introducing-attributes-and-editable-fields.md index 59c207a0d454d..7f96bd7898679 100644 --- a/docs/blocks/introducing-attributes-and-editable-fields.md +++ b/docs/blocks/introducing-attributes-and-editable-fields.md @@ -96,13 +96,12 @@ registerBlockType( 'gutenberg-boilerplate-esnext/hello-world-step-03', { ); }, - save( { attributes, className } ) { + save( { attributes } ) { const { content } = attributes; return ( <RichText.Content tagName="p" - className={ className } value={ content } /> ); diff --git a/packages/block-library/src/cover/index.js b/packages/block-library/src/cover/index.js index b282c5c7aeb8e..881755f1dd1f5 100644 --- a/packages/block-library/src/cover/index.js +++ b/packages/block-library/src/cover/index.js @@ -375,7 +375,7 @@ export const settings = { } ), - save( { attributes, className } ) { + save( { attributes } ) { const { align, backgroundType, @@ -396,7 +396,6 @@ export const settings = { } const classes = classnames( - className, dimRatioToClass( dimRatio ), overlayColorClass, { @@ -470,11 +469,10 @@ export const settings = { }, }, - save( { attributes, className } ) { + save( { attributes } ) { const { url, title, hasParallax, dimRatio, align } = attributes; const style = backgroundImageStyles( url ); const classes = classnames( - className, dimRatioToClass( dimRatio ), { 'has-background-dim': dimRatio !== 0, diff --git a/packages/block-library/src/subhead/index.js b/packages/block-library/src/subhead/index.js index f1c03fcc5e9d3..82fa6536fc366 100644 --- a/packages/block-library/src/subhead/index.js +++ b/packages/block-library/src/subhead/index.js @@ -85,13 +85,12 @@ export const settings = { ); }, - save( { attributes, className } ) { + save( { attributes } ) { const { align, content } = attributes; return ( <RichText.Content tagName="p" - className={ className } style={ { textAlign: align } } value={ content } /> diff --git a/packages/block-library/src/verse/index.js b/packages/block-library/src/verse/index.js index e70bc44550735..46bfac96d2208 100644 --- a/packages/block-library/src/verse/index.js +++ b/packages/block-library/src/verse/index.js @@ -85,13 +85,12 @@ export const settings = { ); }, - save( { attributes, className } ) { + save( { attributes } ) { const { textAlign, content } = attributes; return ( <RichText.Content tagName="pre" - className={ className } style={ { textAlign: textAlign } } value={ content } /> From 996f0fe4302e470cbf0f8ff6a8c2e49609e0c60e Mon Sep 17 00:00:00 2001 From: Danilo Ercoli <ercoli@gmail.com> Date: Thu, 8 Nov 2018 11:09:52 +0100 Subject: [PATCH 042/106] [RNMobile] Make sure the native side is refreshed after blocks Merging (#11576) * Make sure the new content is correctly propagated to the native side after merge. * Fix lint --- .../src/components/rich-text/index.native.js | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/packages/editor/src/components/rich-text/index.native.js b/packages/editor/src/components/rich-text/index.native.js index 7fe26564cae5e..a64a3bd422b9f 100644 --- a/packages/editor/src/components/rich-text/index.native.js +++ b/packages/editor/src/components/rich-text/index.native.js @@ -221,12 +221,6 @@ 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 ); } @@ -274,14 +268,23 @@ export class RichText extends Component { this.lastContent = undefined; return true; } - // The check below allows us to avoid updating the content right after an `onChange` call - // first time the component is drawn with empty content `lastContent` is undefined - if ( nextProps.value && + // The check below allows us to avoid updating the content right after an `onChange` call. + // The first time the component is drawn `lastContent` and `lastEventCount ` are both undefined + if ( this.lastEventCount && + nextProps.value && this.lastContent && - this.lastEventCount ) { + nextProps.value === this.lastContent ) { return false; } + // If the component is changed React side (merging/splitting/custom text actions) we need to make sure + // the native is updated as well + if ( nextProps.value && + this.lastContent && + nextProps.value !== this.lastContent ) { + this.lastEventCount = undefined; // force a refresh on the native side + } + return true; } From 53209f4a029fe3fe2cb643a7956abb588e3491bb Mon Sep 17 00:00:00 2001 From: Edwin Cromley <cromleyedwin89@gmail.com> Date: Thu, 8 Nov 2018 06:00:52 -0500 Subject: [PATCH 043/106] Change aria-checked to only be used on matching ARIA roles for MenuItem. (#11459) * Fixes #11431. Adjust aria-checked attribute to only work on matching aria roles. https://www.w3.org/TR/wai-aria-1.1/#aria-checked * Modify BlockNavigationList to make proper use of aria roles. * Modify the editor CHANGELOG.md to reflect a11y changes. * Modify changelog for components package to reflect a11y changes. * Add additional test case for MenuItem when aria-checked should be used. * Test fix for missing function toBeTrue, use toBe( true ) instead * Update README.md as well as update MenuItem to better fit coding aesthetics. * Modify changelog to reflect new feature. --- packages/components/CHANGELOG.md | 6 +++++ packages/components/src/menu-item/README.md | 10 ++++++++- packages/components/src/menu-item/index.js | 3 ++- .../components/src/menu-item/test/index.js | 22 +++++++++++++++++++ packages/editor/CHANGELOG.md | 6 +++++ .../src/components/block-navigation/index.js | 1 + 6 files changed, 46 insertions(+), 2 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 2ceb51c459d3c..4e91e6f7869da 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -1,3 +1,9 @@ +## 5.1.0 (unreleased) + +### New Features + +- Adjust a11y roles for MenuItem component, so that aria-checked is used properly, related change in Editor/Components/BlockNavigationList ([#11431](https://github.com/WordPress/gutenberg/issues/11431)). + ## 5.0.2 (2018-11-03) ### Polish diff --git a/packages/components/src/menu-item/README.md b/packages/components/src/menu-item/README.md index c7f4777bd43b0..76cff639fce51 100644 --- a/packages/components/src/menu-item/README.md +++ b/packages/components/src/menu-item/README.md @@ -25,7 +25,7 @@ const MyMenuItem = withState( { MenuItem supports the following props. Any additional props are passed through to the underlying [Button](../button) or [IconButton](../icon-button) component. -### `children` +### `children` - Type: `WPElement` - Required: No @@ -65,3 +65,11 @@ Refer to documentation for [IconButton's `icon` prop](../icon-button/README.md#i - Required: No Refer to documentation for [Shortcut's `shortcut` prop](../shortcut/README.md#shortcut). + +### `role` + +- Type: `string` +- Require: No +- Default: `'menuitem'` + +[Aria Spec](https://www.w3.org/TR/wai-aria-1.1/#aria-checked). If you need to have selectable menu items use menuitemradio for single select, and menuitemcheckbox for multiselect. diff --git a/packages/components/src/menu-item/index.js b/packages/components/src/menu-item/index.js index e281c6bf0ea65..fa8dd6a85d24f 100644 --- a/packages/components/src/menu-item/index.js +++ b/packages/components/src/menu-item/index.js @@ -78,7 +78,8 @@ export function MenuItem( { tagName, { 'aria-label': label, - 'aria-checked': isSelected, + // Make sure aria-checked matches spec https://www.w3.org/TR/wai-aria-1.1/#aria-checked + 'aria-checked': ( role === 'menuitemcheckbox' || role === 'menuitemradio' ) ? isSelected : undefined, role, className, ...props, diff --git a/packages/components/src/menu-item/test/index.js b/packages/components/src/menu-item/test/index.js index 9fbd8b869ff47..f79f2da61be78 100644 --- a/packages/components/src/menu-item/test/index.js +++ b/packages/components/src/menu-item/test/index.js @@ -72,4 +72,26 @@ describe( 'MenuItem', () => { expect( wrapper.prop( 'aria-label' ) ).toBeUndefined(); } ); + + it( 'should avoid using aria-checked if only menuitem is set as aria-role', () => { + const wrapper = shallow( + <MenuItem role="menuitem" isSelected={ true }><div /></MenuItem> + ); + + expect( wrapper.prop( 'aria-checked' ) ).toBeUndefined(); + } ); + + it( 'should use aria-checked if menuitemradio or menuitemcheckbox is set as aria-role', () => { + let wrapper = shallow( + <MenuItem role="menuitemradio" isSelected={ true }><div /></MenuItem> + ); + + expect( wrapper.prop( 'aria-checked' ) ).toBe( true ); + + wrapper = shallow( + <MenuItem role="menuitemcheckbox" isSelected={ true }><div /></MenuItem> + ); + + expect( wrapper.prop( 'aria-checked' ) ).toBe( true ); + } ); } ); diff --git a/packages/editor/CHANGELOG.md b/packages/editor/CHANGELOG.md index 64475184f0431..6c3f5dd6a8d75 100644 --- a/packages/editor/CHANGELOG.md +++ b/packages/editor/CHANGELOG.md @@ -1,3 +1,9 @@ +## 6.2.0 (unreleased) + +### New Features + +- Adjust a11y roles for menu items, and make sure screen readers can properly use BlockNavigationList ([#11431](https://github.com/WordPress/gutenberg/issues/11431)). + ## 6.1.1 (2018-11-03) ### Polish diff --git a/packages/editor/src/components/block-navigation/index.js b/packages/editor/src/components/block-navigation/index.js index 39ff985a2cc92..d1c1b919ed719 100644 --- a/packages/editor/src/components/block-navigation/index.js +++ b/packages/editor/src/components/block-navigation/index.js @@ -37,6 +37,7 @@ function BlockNavigationList( { } ) } onClick={ () => selectBlock( block.clientId ) } isSelected={ block.clientId === selectedBlockClientId } + role="menuitemradio" > <BlockIcon icon={ blockType.icon } showColors /> { blockType.title } From f20c45353f9830df366aa6ad292c7b20d61b2caf Mon Sep 17 00:00:00 2001 From: Jorge <jorge.costa@developer.pt> Date: Thu, 8 Nov 2018 11:37:26 +0000 Subject: [PATCH 044/106] Fix: Cover image tooltips for alignment are duplicated (#11263) ## Description Fixes: https://github.com/WordPress/gutenberg/issues/6013 The same tooltip as used for Block Alignment and text Alignment. This PR makes sure we use different tooltips for the text alignment buttons. https://user-images.githubusercontent.com/253067/38373204-92586b58-38e8-11e8-8ec2-09e73368cacb.png ## How has this been tested? I added a cover block and I checked that the tooltips for text alignment are in the format "Text align left/center/right" and block alignment tooltips are in the format "Text left/center/right" ## Screenshots <!-- if applicable --> Before: ![align](https://user-images.githubusercontent.com/253067/38373204-92586b58-38e8-11e8-8ec2-09e73368cacb.png) ![align2](https://user-images.githubusercontent.com/253067/38373205-928a870a-38e8-11e8-8391-ce5135c7593a.png) After: ![image](https://user-images.githubusercontent.com/11271197/47747203-b62a8f80-dc7f-11e8-86fd-970f642296ae.png) --- .../src/components/alignment-toolbar/index.js | 14 +++--- .../test/__snapshots__/index.js.snap | 31 +++++++++++-- .../alignment-toolbar/test/index.js | 43 +++++++++++++++++++ 3 files changed, 78 insertions(+), 10 deletions(-) diff --git a/packages/editor/src/components/alignment-toolbar/index.js b/packages/editor/src/components/alignment-toolbar/index.js index 5aa2d61b0c083..e06ee34f4d7d6 100644 --- a/packages/editor/src/components/alignment-toolbar/index.js +++ b/packages/editor/src/components/alignment-toolbar/index.js @@ -17,37 +17,37 @@ import { compose } from '@wordpress/compose'; */ import { withBlockEditContext } from '../block-edit/context'; -const ALIGNMENT_CONTROLS = [ +const DEFAULT_ALIGNMENT_CONTROLS = [ { icon: 'editor-alignleft', - title: __( 'Align left' ), + title: __( 'Align text left' ), align: 'left', }, { icon: 'editor-aligncenter', - title: __( 'Align center' ), + title: __( 'Align text center' ), align: 'center', }, { icon: 'editor-alignright', - title: __( 'Align right' ), + title: __( 'Align text right' ), align: 'right', }, ]; -export function AlignmentToolbar( { isCollapsed, value, onChange } ) { +export function AlignmentToolbar( { isCollapsed, value, onChange, alignmentControls = DEFAULT_ALIGNMENT_CONTROLS } ) { function applyOrUnset( align ) { return () => onChange( value === align ? undefined : align ); } - const activeAlignment = find( ALIGNMENT_CONTROLS, ( control ) => control.align === value ); + const activeAlignment = find( alignmentControls, ( control ) => control.align === value ); return ( <Toolbar isCollapsed={ isCollapsed } icon={ activeAlignment ? activeAlignment.icon : 'editor-alignleft' } label={ __( 'Change Text Alignment' ) } - controls={ ALIGNMENT_CONTROLS.map( ( control ) => { + controls={ alignmentControls.map( ( control ) => { const { align } = control; const isActive = ( value === align ); diff --git a/packages/editor/src/components/alignment-toolbar/test/__snapshots__/index.js.snap b/packages/editor/src/components/alignment-toolbar/test/__snapshots__/index.js.snap index 2308f4cda66ae..34e440f5d76fe 100644 --- a/packages/editor/src/components/alignment-toolbar/test/__snapshots__/index.js.snap +++ b/packages/editor/src/components/alignment-toolbar/test/__snapshots__/index.js.snap @@ -1,5 +1,30 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`AlignmentToolbar should allow custom alignment controls to be specified 1`] = ` +<Toolbar + controls={ + Array [ + Object { + "align": "custom-left", + "icon": "editor-alignleft", + "isActive": false, + "onClick": [Function], + "title": "My custom left", + }, + Object { + "align": "custom-right", + "icon": "editor-aligncenter", + "isActive": true, + "onClick": [Function], + "title": "My custom right", + }, + ] + } + icon="editor-aligncenter" + label="Change Text Alignment" +/> +`; + exports[`AlignmentToolbar should match snapshot 1`] = ` <Toolbar controls={ @@ -9,21 +34,21 @@ exports[`AlignmentToolbar should match snapshot 1`] = ` "icon": "editor-alignleft", "isActive": true, "onClick": [Function], - "title": "Align left", + "title": "Align text left", }, Object { "align": "center", "icon": "editor-aligncenter", "isActive": false, "onClick": [Function], - "title": "Align center", + "title": "Align text center", }, Object { "align": "right", "icon": "editor-alignright", "isActive": false, "onClick": [Function], - "title": "Align right", + "title": "Align text right", }, ] } diff --git a/packages/editor/src/components/alignment-toolbar/test/index.js b/packages/editor/src/components/alignment-toolbar/test/index.js index 803a5a56e6a82..03662cfdabe81 100644 --- a/packages/editor/src/components/alignment-toolbar/test/index.js +++ b/packages/editor/src/components/alignment-toolbar/test/index.js @@ -41,4 +41,47 @@ describe( 'AlignmentToolbar', () => { expect( onChangeSpy ).toHaveBeenCalledTimes( 1 ); expect( onChangeSpy ).toHaveBeenCalledWith( 'center' ); } ); + + test( 'should allow custom alignment controls to be specified', () => { + const wrapperCustomControls = shallow( + <AlignmentToolbar + value={ 'custom-right' } + onChange={ onChangeSpy } + alignmentControls={ [ + { + icon: 'editor-alignleft', + title: 'My custom left', + align: 'custom-left', + }, + { + icon: 'editor-aligncenter', + title: 'My custom right', + align: 'custom-right', + }, + ] } + /> + ); + expect( wrapperCustomControls ).toMatchSnapshot(); + const customControls = wrapperCustomControls.props().controls; + expect( customControls ).toHaveLength( 2 ); + + // should correctly call on change when right alignment is pressed (active alignment) + const rightControl = customControls.find( + ( { align } ) => align === 'custom-right' + ); + expect( rightControl.title ).toBe( 'My custom right' ); + rightControl.onClick(); + expect( onChangeSpy ).toHaveBeenCalledTimes( 1 ); + expect( onChangeSpy ).toHaveBeenCalledWith( undefined ); + onChangeSpy.mockClear(); + + // should correctly call on change when right alignment is pressed (inactive alignment) + const leftControl = customControls.find( + ( { align } ) => align === 'custom-left' + ); + expect( leftControl.title ).toBe( 'My custom left' ); + leftControl.onClick(); + expect( onChangeSpy ).toHaveBeenCalledTimes( 1 ); + expect( onChangeSpy ).toHaveBeenCalledWith( 'custom-left' ); + } ); } ); From ddf8c07717ce73c45b8d4c8b97193d8d635502b8 Mon Sep 17 00:00:00 2001 From: Matthew Riley MacPherson <hi@tofumatt.com> Date: Thu, 8 Nov 2018 16:28:03 +0000 Subject: [PATCH 045/106] fix: Improve empty block text (#11560) * fix: Improve empty block text Fix #9076 Fix #5591 * Tweak labels * Make it consistent * Make it MORE consistent * Fix block inserter shortcut text * Use suggested text * Do not use placeholder in code editor * fix tests --- docs/design/block-design.md | 2 +- lib/client-assets.php | 2 +- packages/block-library/src/paragraph/edit.js | 3 ++- .../src/paragraph/test/__snapshots__/index.js.snap | 4 ++-- .../src/components/default-block-appender/index.js | 2 +- .../test/__snapshots__/index.js.snap | 4 ++-- .../editor/src/components/post-text-editor/index.js | 12 ++++-------- 7 files changed, 13 insertions(+), 16 deletions(-) diff --git a/docs/design/block-design.md b/docs/design/block-design.md index 3a2549f756bb8..09dd0ea9ad2ac 100644 --- a/docs/design/block-design.md +++ b/docs/design/block-design.md @@ -97,7 +97,7 @@ The most basic unit of the editor. The paragraph block is a simple input field. ### Placeholder: -- Simple placeholder text that says “Add text or type / to add content.” The placeholder disappears when the block is selected. +- Simple placeholder text that reads “Start writing or press / to insert a block”. The placeholder disappears when the block is selected. ### Selected state: diff --git a/lib/client-assets.php b/lib/client-assets.php index a1e3181344092..3450b21856b07 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -1621,7 +1621,7 @@ function gutenberg_editor_scripts_and_styles( $hook ) { 'disableCustomFontSizes' => get_theme_support( 'disable-custom-font-sizes' ), 'disablePostFormats' => ! current_theme_supports( 'post-formats' ), 'titlePlaceholder' => apply_filters( 'enter_title_here', __( 'Add title', 'gutenberg' ), $post ), - 'bodyPlaceholder' => apply_filters( 'write_your_story', __( 'Write your story', 'gutenberg' ), $post ), + 'bodyPlaceholder' => apply_filters( 'write_your_story', __( 'Start writing or press / to insert a block', 'gutenberg' ), $post ), 'isRTL' => is_rtl(), 'autosaveInterval' => 10, 'maxUploadFileSize' => $max_upload_size, diff --git a/packages/block-library/src/paragraph/edit.js b/packages/block-library/src/paragraph/edit.js index 1290f5566d46b..510d808dc6930 100644 --- a/packages/block-library/src/paragraph/edit.js +++ b/packages/block-library/src/paragraph/edit.js @@ -245,7 +245,8 @@ class ParagraphBlock extends Component { onMerge={ mergeBlocks } onReplace={ this.onReplace } onRemove={ () => onReplace( [] ) } - placeholder={ placeholder || __( 'Add text or type / to add content' ) } + aria-label={ __( 'Empty block; type text or press the forward slash key to insert a block' ) } + placeholder={ placeholder || __( 'Start writing or press / to insert a block' ) } /> </Fragment> ); diff --git a/packages/block-library/src/paragraph/test/__snapshots__/index.js.snap b/packages/block-library/src/paragraph/test/__snapshots__/index.js.snap index 362940d937b8f..a53f564e19258 100644 --- a/packages/block-library/src/paragraph/test/__snapshots__/index.js.snap +++ b/packages/block-library/src/paragraph/test/__snapshots__/index.js.snap @@ -14,7 +14,7 @@ exports[`core/paragraph block edit matches snapshot 1`] = ` <p aria-autocomplete="list" aria-expanded="false" - aria-label="Add text or type / to add content" + aria-label="Empty block; type text or press the forward slash key to insert a block" aria-multiline="true" class="wp-block-paragraph editor-rich-text__tinymce" contenteditable="true" @@ -28,7 +28,7 @@ exports[`core/paragraph block edit matches snapshot 1`] = ` <p class="editor-rich-text__tinymce wp-block-paragraph" > - Add text or type / to add content + Start writing or press / to insert a block </p> </div> </div> diff --git a/packages/editor/src/components/default-block-appender/index.js b/packages/editor/src/components/default-block-appender/index.js index dd6817b674cfe..d280e33019068 100644 --- a/packages/editor/src/components/default-block-appender/index.js +++ b/packages/editor/src/components/default-block-appender/index.js @@ -31,7 +31,7 @@ export function DefaultBlockAppender( { return null; } - const value = decodeEntities( placeholder ) || __( 'Write your story' ); + const value = decodeEntities( placeholder ) || __( 'Start writing or press / to insert a block' ); // The appender "button" is in-fact a text field so as to support // transitions by WritingFlow occurring by arrow key press. WritingFlow 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 71484219b7d98..3e654e54e0ae3 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 @@ -20,7 +20,7 @@ exports[`DefaultBlockAppender should append a default block when input focused 1 readOnly={true} role="button" type="text" - value="Write your story" + value="Start writing or press / to insert a block" /> <WithSelect(WithDispatch(InserterWithShortcuts)) /> <WithSelect(IfCondition(Inserter)) @@ -42,7 +42,7 @@ exports[`DefaultBlockAppender should match snapshot 1`] = ` readOnly={true} role="button" type="text" - value="Write your story" + value="Start writing or press / to insert a block" /> <WithSelect(WithDispatch(InserterWithShortcuts)) /> <WithSelect(IfCondition(Inserter)) diff --git a/packages/editor/src/components/post-text-editor/index.js b/packages/editor/src/components/post-text-editor/index.js index 6ebe27cc88a1a..4b70526f2bc3e 100644 --- a/packages/editor/src/components/post-text-editor/index.js +++ b/packages/editor/src/components/post-text-editor/index.js @@ -7,7 +7,6 @@ import Textarea from 'react-autosize-textarea'; * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { decodeEntities } from '@wordpress/html-entities'; import { Component, Fragment } from '@wordpress/element'; import { parse } from '@wordpress/blocks'; import { withSelect, withDispatch } from '@wordpress/data'; @@ -65,13 +64,12 @@ export class PostTextEditor extends Component { render() { const { value } = this.state; - const { placeholder, instanceId } = this.props; - const decodedPlaceholder = decodeEntities( placeholder ); + const { instanceId } = this.props; return ( <Fragment> <label htmlFor={ `post-content-${ instanceId }` } className="screen-reader-text"> - { decodedPlaceholder || __( 'Write your story' ) } + { __( 'Type text or HTML' ) } </label> <Textarea autoComplete="off" @@ -80,7 +78,7 @@ export class PostTextEditor extends Component { onBlur={ this.stopEditing } className="editor-post-text-editor" id={ `post-content-${ instanceId }` } - placeholder={ decodedPlaceholder || __( 'Write your story' ) } + placeholder={ __( 'Start writing with text or HTML' ) } /> </Fragment> ); @@ -89,11 +87,9 @@ export class PostTextEditor extends Component { export default compose( [ withSelect( ( select ) => { - const { getEditedPostContent, getEditorSettings } = select( 'core/editor' ); - const { bodyPlaceholder } = getEditorSettings(); + const { getEditedPostContent } = select( 'core/editor' ); return { value: getEditedPostContent(), - placeholder: bodyPlaceholder, }; } ), withDispatch( ( dispatch ) => { From 005c79019685b03d99fc5609318887fb0eba34d7 Mon Sep 17 00:00:00 2001 From: Andrew Duthie <andrew@andrewduthie.com> Date: Thu, 8 Nov 2018 13:26:51 -0500 Subject: [PATCH 046/106] Editor: Reshape editor state for optimized and accurate dirtiness detection (#10844) * Editor: Move dirty flag to blocks substate * Editor: Optimize autosaveable condition * Editor: Remove redundant artificial dirtying --- docs/data/data-core-editor.md | 12 + packages/editor/src/store/effects/posts.js | 12 +- packages/editor/src/store/reducer.js | 21 +- packages/editor/src/store/selectors.js | 48 +++- packages/editor/src/store/test/effects.js | 44 +--- packages/editor/src/store/test/reducer.js | 4 +- packages/editor/src/store/test/selectors.js | 239 ++++++++++++++++---- 7 files changed, 269 insertions(+), 111 deletions(-) diff --git a/docs/data/data-core-editor.md b/docs/data/data-core-editor.md index 5b823d1893d0d..5c7e7e41a0ab6 100644 --- a/docs/data/data-core-editor.md +++ b/docs/data/data-core-editor.md @@ -36,6 +36,18 @@ the post has been saved. Whether the post is new. +### hasChangedContent + +Returns true if content includes unsaved changes, or false otherwise. + +*Parameters* + + * state: Editor state. + +*Returns* + +Whether content includes unsaved changes. + ### isEditedPostDirty Returns true if there are unsaved values for the current edit session, or diff --git a/packages/editor/src/store/effects/posts.js b/packages/editor/src/store/effects/posts.js index 6d6d23917c2bc..c88fe00f4d878 100644 --- a/packages/editor/src/store/effects/posts.js +++ b/packages/editor/src/store/effects/posts.js @@ -171,18 +171,8 @@ export const requestPostUpdate = async ( action, store ) => { * @param {Object} action action object. * @param {Object} store Redux Store. */ -export const requestPostUpdateSuccess = ( action, store ) => { +export const requestPostUpdateSuccess = ( action ) => { const { previousPost, post, isAutosave, postType } = action; - const { dispatch, getState } = store; - - // TEMPORARY: If edits remain after a save completes, the user must be - // prompted about unsaved changes. This should be refactored as part of - // the `isEditedPostDirty` selector instead. - // - // See: https://github.com/WordPress/gutenberg/issues/7409 - if ( Object.keys( getPostEdits( getState() ) ).length ) { - dispatch( { type: 'DIRTY_ARTIFICIALLY' } ); - } // Autosaves are neither shown a notice nor redirected. if ( isAutosave ) { diff --git a/packages/editor/src/store/reducer.js b/packages/editor/src/store/reducer.js index 9ea2259b2c791..6bfb468cd7157 100644 --- a/packages/editor/src/store/reducer.js +++ b/packages/editor/src/store/reducer.js @@ -236,13 +236,6 @@ export const editor = flow( [ ignoreTypes: [ 'RECEIVE_BLOCKS', 'RESET_POST', 'UPDATE_POST' ], shouldOverwriteState, } ), - - // Track whether changes exist, resetting at each post save. Relies on - // editor initialization firing post reset as an effect. - withChangeDetection( { - resetTypes: [ 'SETUP_EDITOR_STATE', 'REQUEST_POST_UPDATE_START' ], - ignoreTypes: [ 'RECEIVE_BLOCKS', 'RESET_POST', 'UPDATE_POST' ], - } ), ] )( { edits( state = {}, action ) { switch ( action.type ) { @@ -264,9 +257,6 @@ export const editor = flow( [ return state; - case 'DIRTY_ARTIFICIALLY': - return { ...state }; - case 'UPDATE_POST': case 'RESET_POST': const getCanonicalValue = action.type === 'UPDATE_POST' ? @@ -287,7 +277,16 @@ export const editor = flow( [ return state; }, - blocks: combineReducers( { + blocks: flow( [ + combineReducers, + + // Track whether changes exist, resetting at each post save. Relies on + // editor initialization firing post reset as an effect. + withChangeDetection( { + resetTypes: [ 'SETUP_EDITOR_STATE', 'REQUEST_POST_UPDATE_START' ], + ignoreTypes: [ 'RECEIVE_BLOCKS', 'RESET_POST', 'UPDATE_POST' ], + } ), + ] )( { byClientId( state = {}, action ) { switch ( action.type ) { case 'RESET_BLOCKS': diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index a4e426507f816..783a0ee72d39a 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -102,6 +102,26 @@ export function isEditedPostNew( state ) { return getCurrentPost( state ).status === 'auto-draft'; } +/** + * Returns true if content includes unsaved changes, or false otherwise. + * + * @param {Object} state Editor state. + * + * @return {boolean} Whether content includes unsaved changes. + */ +export function hasChangedContent( state ) { + return ( + state.editor.present.blocks.isDirty || + + // `edits` is intended to contain only values which are different from + // the saved post, so the mere presence of a property is an indicator + // that the value is different than what is known to be saved. While + // content in Visual mode is represented by the blocks state, in Text + // mode it is tracked by `edits.content`. + 'content' in state.editor.present.edits + ); +} + /** * Returns true if there are unsaved values for the current edit session, or * false if the editing state matches the saved or new post. @@ -111,7 +131,23 @@ export function isEditedPostNew( state ) { * @return {boolean} Whether unsaved values exist. */ export function isEditedPostDirty( state ) { - return state.editor.isDirty || inSomeHistory( state, isEditedPostDirty ); + if ( hasChangedContent( state ) ) { + return true; + } + + // Edits should contain only fields which differ from the saved post (reset + // at initial load and save complete). Thus, a non-empty edits state can be + // inferred to contain unsaved values. + if ( Object.keys( state.editor.present.edits ).length > 0 ) { + return true; + } + + // Edits and change detectiona are reset at the start of a save, but a post + // is still considered dirty until the point at which the save completes. + // Because the save is performed optimistically, the prior states are held + // until committed. These can be referenced to determine whether there's a + // chance that state may be reverted into one considered dirty. + return inSomeHistory( state, isEditedPostDirty ); } /** @@ -451,9 +487,17 @@ export function isEditedPostAutosaveable( state ) { return true; } + // To avoid an expensive content serialization, use the content dirtiness + // flag in place of content field comparison against the known autosave. + // This is not strictly accurate, and relies on a tolerance toward autosave + // request failures for unnecessary saves. + if ( hasChangedContent( state ) ) { + return true; + } + // If the title, excerpt or content has changed, the post is autosaveable. const autosave = getAutosave( state ); - return [ 'title', 'excerpt', 'content' ].some( ( field ) => ( + return [ 'title', 'excerpt' ].some( ( field ) => ( autosave[ field ] !== getEditedPostAttribute( state, field ) ) ); } diff --git a/packages/editor/src/store/test/effects.js b/packages/editor/src/store/test/effects.js index 83aedc2e4a84e..ce54c418d5fa1 100644 --- a/packages/editor/src/store/test/effects.js +++ b/packages/editor/src/store/test/effects.js @@ -25,7 +25,6 @@ import actions, { resetBlocks, selectBlock, setTemplateValidity, - editPost, } from '../actions'; import effects, { validateBlocksToTemplate } from '../effects'; import { SAVE_POST_NOTICE_ID } from '../effects/posts'; @@ -222,16 +221,6 @@ describe( 'effects', () => { describe( '.REQUEST_POST_UPDATE_SUCCESS', () => { const handler = effects.REQUEST_POST_UPDATE_SUCCESS; - function createGetState( hasLingeringEdits = false ) { - let state = reducer( undefined, {} ); - if ( hasLingeringEdits ) { - state = reducer( state, editPost( { edited: true } ) ); - } - - const getState = () => state; - return getState; - } - const defaultPost = { id: 1, title: { @@ -259,14 +248,11 @@ describe( 'effects', () => { } ); it( 'should dispatch notices when publishing or scheduling a post', () => { - const dispatch = jest.fn(); - const store = { dispatch, getState: createGetState() }; - const previousPost = getDraftPost(); const post = getPublishedPost(); const postType = getPostType(); - handler( { post, previousPost, postType }, store ); + handler( { post, previousPost, postType } ); expect( dataDispatch( 'core/notices' ).createSuccessNotice ).toHaveBeenCalledWith( 'Post published.', @@ -280,14 +266,11 @@ describe( 'effects', () => { } ); it( 'should dispatch notices when reverting a published post to a draft', () => { - const dispatch = jest.fn(); - const store = { dispatch, getState: createGetState() }; - const previousPost = getPublishedPost(); const post = getDraftPost(); const postType = getPostType(); - handler( { post, previousPost, postType }, store ); + handler( { post, previousPost, postType } ); expect( dataDispatch( 'core/notices' ).createSuccessNotice ).toHaveBeenCalledWith( 'Post reverted to draft.', @@ -299,14 +282,11 @@ describe( 'effects', () => { } ); it( 'should dispatch notices when just updating a published post again', () => { - const dispatch = jest.fn(); - const store = { dispatch, getState: createGetState() }; - const previousPost = getPublishedPost(); const post = getPublishedPost(); const postType = getPostType(); - handler( { post, previousPost, postType }, store ); + handler( { post, previousPost, postType } ); expect( dataDispatch( 'core/notices' ).createSuccessNotice ).toHaveBeenCalledWith( 'Post updated.', @@ -320,29 +300,13 @@ describe( 'effects', () => { } ); it( 'should do nothing if the updated post was autosaved', () => { - const dispatch = jest.fn(); - const store = { dispatch, getState: createGetState() }; - const previousPost = getPublishedPost(); const post = { ...getPublishedPost(), id: defaultPost.id + 1 }; - handler( { post, previousPost, isAutosave: true }, store ); + handler( { post, previousPost, isAutosave: true } ); expect( dataDispatch( 'core/notices' ).createSuccessNotice ).not.toHaveBeenCalled(); } ); - - it( 'should dispatch dirtying action if edits linger after autosave', () => { - const dispatch = jest.fn(); - const store = { dispatch, getState: createGetState( true ) }; - - const previousPost = getPublishedPost(); - const post = { ...getPublishedPost(), id: defaultPost.id + 1 }; - - handler( { post, previousPost, isAutosave: true }, store ); - - expect( dispatch ).toHaveBeenCalledTimes( 1 ); - expect( dispatch ).toHaveBeenCalledWith( { type: 'DIRTY_ARTIFICIALLY' } ); - } ); } ); describe( '.REQUEST_POST_UPDATE_FAILURE', () => { diff --git a/packages/editor/src/store/test/reducer.js b/packages/editor/src/store/test/reducer.js index 005fd9127690f..d40b07b0b36bd 100644 --- a/packages/editor/src/store/test/reducer.js +++ b/packages/editor/src/store/test/reducer.js @@ -279,7 +279,7 @@ describe( 'state', () => { unregisterBlockType( 'core/test-block' ); } ); - it( 'should return history (empty edits, blocks), dirty flag by default', () => { + it( 'should return history (empty edits, blocks) by default', () => { const state = editor( undefined, {} ); expect( state.past ).toEqual( [] ); @@ -287,7 +287,7 @@ describe( 'state', () => { expect( state.present.edits ).toEqual( {} ); expect( state.present.blocks.byClientId ).toEqual( {} ); expect( state.present.blocks.order ).toEqual( {} ); - expect( state.isDirty ).toBe( false ); + expect( state.present.blocks.isDirty ).toBe( false ); } ); it( 'should key by reset blocks clientId', () => { diff --git a/packages/editor/src/store/test/selectors.js b/packages/editor/src/store/test/selectors.js index 2474cc1e2565f..58a839ff93a47 100644 --- a/packages/editor/src/store/test/selectors.js +++ b/packages/editor/src/store/test/selectors.js @@ -28,6 +28,7 @@ const { hasEditorUndo, hasEditorRedo, isEditedPostNew, + hasChangedContent, isEditedPostDirty, isCleanNewPost, getCurrentPost, @@ -271,14 +272,100 @@ describe( 'selectors', () => { } ); } ); + describe( 'hasChangedContent', () => { + it( 'should return false if no dirty blocks nor content property edit', () => { + const state = { + editor: { + present: { + blocks: { + isDirty: false, + }, + edits: {}, + }, + }, + }; + + expect( hasChangedContent( state ) ).toBe( false ); + } ); + + it( 'should return true if dirty blocks', () => { + const state = { + editor: { + present: { + blocks: { + isDirty: true, + }, + edits: {}, + }, + }, + }; + + expect( hasChangedContent( state ) ).toBe( true ); + } ); + + it( 'should return true if content property edit', () => { + const state = { + editor: { + present: { + blocks: { + isDirty: false, + }, + edits: { + content: 'text mode edited', + }, + }, + }, + }; + + expect( hasChangedContent( state ) ).toBe( true ); + } ); + } ); + describe( 'isEditedPostDirty', () => { - it( 'should return true when post saved state dirty', () => { + it( 'should return false when blocks state not dirty nor edits exist', () => { + const state = { + optimist: [], + editor: { + present: { + blocks: { + isDirty: false, + }, + edits: {}, + }, + }, + }; + + expect( isEditedPostDirty( state ) ).toBe( false ); + } ); + + it( 'should return true when blocks state dirty', () => { const state = { + optimist: [], editor: { - isDirty: true, + present: { + blocks: { + isDirty: true, + }, + edits: {}, + }, }, - saving: { - requesting: false, + }; + + expect( isEditedPostDirty( state ) ).toBe( true ); + } ); + + it( 'should return true when edits exist', () => { + const state = { + optimist: [], + editor: { + present: { + blocks: { + isDirty: false, + }, + edits: { + excerpt: 'hello world', + }, + }, }, }; @@ -291,38 +378,40 @@ describe( 'selectors', () => { { beforeState: { editor: { - isDirty: true, + present: { + blocks: { + isDirty: true, + }, + edits: {}, + }, }, }, }, ], editor: { - isDirty: false, + present: { + blocks: { + isDirty: false, + }, + edits: {}, + }, }, }; expect( isEditedPostDirty( state ) ).toBe( true ); } ); - - it( 'should return false when post saved state not dirty', () => { - const state = { - editor: { - isDirty: false, - }, - saving: { - requesting: false, - }, - }; - - expect( isEditedPostDirty( state ) ).toBe( false ); - } ); } ); describe( 'isCleanNewPost', () => { it( 'should return true when the post is not dirty and has not been saved before', () => { const state = { editor: { - isDirty: false, + present: { + blocks: { + isDirty: false, + }, + edits: {}, + }, }, currentPost: { id: 1, @@ -339,7 +428,12 @@ describe( 'selectors', () => { it( 'should return false when the post is not dirty but the post has been saved', () => { const state = { editor: { - isDirty: false, + present: { + blocks: { + isDirty: false, + }, + edits: {}, + }, }, currentPost: { id: 1, @@ -356,7 +450,12 @@ describe( 'selectors', () => { it( 'should return false when the post is dirty but the post has not been saved', () => { const state = { editor: { - isDirty: true, + present: { + blocks: { + isDirty: true, + }, + edits: {}, + }, }, currentPost: { id: 1, @@ -851,7 +950,12 @@ describe( 'selectors', () => { it( 'should return true for pending posts', () => { const state = { editor: { - isDirty: false, + present: { + blocks: { + isDirty: false, + }, + edits: {}, + }, }, currentPost: { status: 'pending', @@ -867,7 +971,12 @@ describe( 'selectors', () => { it( 'should return true for draft posts', () => { const state = { editor: { - isDirty: false, + present: { + blocks: { + isDirty: false, + }, + edits: {}, + }, }, currentPost: { status: 'draft', @@ -883,7 +992,12 @@ describe( 'selectors', () => { it( 'should return false for published posts', () => { const state = { editor: { - isDirty: false, + present: { + blocks: { + isDirty: false, + }, + edits: {}, + }, }, currentPost: { status: 'publish', @@ -899,7 +1013,12 @@ describe( 'selectors', () => { it( 'should return true for published, dirty posts', () => { const state = { editor: { - isDirty: true, + present: { + blocks: { + isDirty: true, + }, + edits: {}, + }, }, currentPost: { status: 'publish', @@ -915,7 +1034,12 @@ describe( 'selectors', () => { it( 'should return false for private posts', () => { const state = { editor: { - isDirty: false, + present: { + blocks: { + isDirty: false, + }, + edits: {}, + }, }, currentPost: { status: 'private', @@ -931,7 +1055,12 @@ describe( 'selectors', () => { it( 'should return false for scheduled posts', () => { const state = { editor: { - isDirty: false, + present: { + blocks: { + isDirty: false, + }, + edits: {}, + }, }, currentPost: { status: 'future', @@ -950,7 +1079,12 @@ describe( 'selectors', () => { status: 'private', }, editor: { - isDirty: true, + present: { + blocks: { + isDirty: true, + }, + edits: {}, + }, }, saving: { requesting: false, @@ -1214,24 +1348,19 @@ describe( 'selectors', () => { editor: { present: { blocks: { - byClientId: {}, - order: {}, - }, - edits: { - content: 'foo', + isDirty: false, }, + edits: {}, }, }, initialEdits: {}, currentPost: { title: 'foo', - content: 'foo', excerpt: 'foo', }, saving: {}, autosave: { title: 'foo', - content: 'foo', excerpt: 'foo', }, }; @@ -1239,26 +1368,46 @@ describe( 'selectors', () => { expect( isEditedPostAutosaveable( state ) ).toBe( false ); } ); - it( 'should return true if title, excerpt, or content have changed', () => { - for ( const variantField of [ 'title', 'excerpt', 'content' ] ) { - for ( const constantField of without( [ 'title', 'excerpt', 'content' ], variantField ) ) { + it( 'should return true if content has changes', () => { + const state = { + editor: { + present: { + blocks: { + isDirty: true, + }, + edits: {}, + }, + }, + currentPost: { + title: 'foo', + excerpt: 'foo', + }, + saving: {}, + autosave: { + title: 'foo', + excerpt: 'foo', + }, + }; + + expect( isEditedPostAutosaveable( state ) ).toBe( true ); + } ); + + it( 'should return true if title or excerpt have changed', () => { + for ( const variantField of [ 'title', 'excerpt' ] ) { + for ( const constantField of without( [ 'title', 'excerpt' ], variantField ) ) { const state = { editor: { present: { blocks: { - byClientId: {}, - order: {}, - }, - edits: { - content: 'foo', + isDirty: false, }, + edits: {}, }, }, initialEdits: {}, currentPost: { title: 'foo', content: 'foo', - excerpt: 'foo', }, saving: {}, autosave: { From a0d0e1a74d66fb683f89dff1943448897d94231f Mon Sep 17 00:00:00 2001 From: Andrew Duthie <andrew@andrewduthie.com> Date: Thu, 8 Nov 2018 14:59:18 -0500 Subject: [PATCH 047/106] Components: Restore click-to-close behavior of Dropdown toggle (#11633) * Testing: Correct truthy test of immediately saveable demo Previously assumed the selector would throw if not found, but in-fact returns `null`. See: https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#pageselector * Revert "Components: Remove redundant onClickOutside handler from Dropdown (#11253)" This reverts commit 58725c42980f81dbcf18d921be7ca43507c968e0. * Testing: Verify Popover toggle behavior * Components: Document Dropdown click outside behavior * Components: Convert Dropdown container to use createRef --- packages/components/src/dropdown/index.js | 19 ++++++++++++++++- test/e2e/specs/demo.test.js | 2 +- test/e2e/specs/popovers.test.js | 26 +++++++++++++++++++++++ 3 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 test/e2e/specs/popovers.test.js diff --git a/packages/components/src/dropdown/index.js b/packages/components/src/dropdown/index.js index c99c0c3e8ea47..29b7700a0e636 100644 --- a/packages/components/src/dropdown/index.js +++ b/packages/components/src/dropdown/index.js @@ -14,8 +14,10 @@ class Dropdown extends Component { this.toggle = this.toggle.bind( this ); this.close = this.close.bind( this ); + this.closeIfClickOutside = this.closeIfClickOutside.bind( this ); this.refresh = this.refresh.bind( this ); + this.containerRef = createRef(); this.popoverRef = createRef(); this.state = { @@ -56,6 +58,20 @@ class Dropdown extends Component { } ) ); } + /** + * Closes the dropdown if a click occurs outside the dropdown wrapper. This + * is intentionally distinct from `onClose` in that a click outside the + * popover may occur in the toggling of the dropdown via its toggle button. + * The correct behavior is to keep the dropdown closed. + * + * @param {MouseEvent} event Click event triggering `onClickOutside`. + */ + closeIfClickOutside( event ) { + if ( ! this.containerRef.current.contains( event.target ) ) { + this.close(); + } + } + close() { this.setState( { isOpen: false } ); } @@ -75,7 +91,7 @@ class Dropdown extends Component { const args = { isOpen, onToggle: this.toggle, onClose: this.close }; return ( - <div className={ className }> + <div className={ className } ref={ this.containerRef }> { renderToggle( args ) } { isOpen && ( <Popover @@ -83,6 +99,7 @@ class Dropdown extends Component { ref={ this.popoverRef } position={ position } onClose={ this.close } + onClickOutside={ this.closeIfClickOutside } expandOnMobile={ expandOnMobile } headerTitle={ headerTitle } > diff --git a/test/e2e/specs/demo.test.js b/test/e2e/specs/demo.test.js index bcf0987ae8771..cb8ef8b85dc40 100644 --- a/test/e2e/specs/demo.test.js +++ b/test/e2e/specs/demo.test.js @@ -40,6 +40,6 @@ describe( 'new editor state', () => { } ); it( 'should be immediately saveable', async () => { - await page.$( 'button.editor-post-save-draft' ); + expect( await page.$( 'button.editor-post-save-draft' ) ).toBeTruthy(); } ); } ); diff --git a/test/e2e/specs/popovers.test.js b/test/e2e/specs/popovers.test.js new file mode 100644 index 0000000000000..202c8d7cb9362 --- /dev/null +++ b/test/e2e/specs/popovers.test.js @@ -0,0 +1,26 @@ +/** + * Internal dependencies + */ +import { newPost } from '../support/utils'; + +describe( 'popovers', () => { + beforeEach( async () => { + await newPost(); + } ); + + describe( 'dropdown', () => { + it( 'toggles via click', async () => { + const isMoreMenuOpen = async () => !! await page.$( '.edit-post-more-menu__content' ); + + expect( await isMoreMenuOpen() ).toBe( false ); + + // Toggle opened. + await page.click( '.edit-post-more-menu > button' ); + expect( await isMoreMenuOpen() ).toBe( true ); + + // Toggle closed. + await page.click( '.edit-post-more-menu > button' ); + expect( await isMoreMenuOpen() ).toBe( false ); + } ); + } ); +} ); From 4369d93f283f5203ad4a91ad04cce015b2699654 Mon Sep 17 00:00:00 2001 From: Christian Glingener <1089098+CGlingener@users.noreply.github.com> Date: Thu, 8 Nov 2018 22:03:20 +0100 Subject: [PATCH 048/106] Fix save lock control (length check on object) (#11636) * Fix length check on object * Editor: Correct state shape of tested post saving lock --- packages/editor/src/store/selectors.js | 2 +- packages/editor/src/store/test/selectors.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index 783a0ee72d39a..81fa98f7fab2f 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -2100,7 +2100,7 @@ export function isPostLocked( state ) { * @return {boolean} Is locked. */ export function isPostSavingLocked( state ) { - return state.postSavingLock.length > 0; + return Object.keys( state.postSavingLock ).length > 0; } /** diff --git a/packages/editor/src/store/test/selectors.js b/packages/editor/src/store/test/selectors.js index 58a839ff93a47..1470f2261db39 100644 --- a/packages/editor/src/store/test/selectors.js +++ b/packages/editor/src/store/test/selectors.js @@ -1098,7 +1098,7 @@ describe( 'selectors', () => { describe( 'isPostSavingLocked', () => { it( 'should return true if the post has postSavingLocks', () => { const state = { - postSavingLock: [ { 1: true } ], + postSavingLock: { example: true }, currentPost: {}, saving: {}, }; @@ -1108,7 +1108,7 @@ describe( 'selectors', () => { it( 'should return false if the post has no postSavingLocks', () => { const state = { - postSavingLock: [], + postSavingLock: {}, currentPost: {}, saving: {}, }; From 90ec74445c8670eefa46b25cab8aec91a6e65b7d Mon Sep 17 00:00:00 2001 From: Jorge <jorge.costa@developer.pt> Date: Thu, 8 Nov 2018 22:27:35 +0000 Subject: [PATCH 049/106] Update isSelectionEnabled selector documentation (#11538) --- docs/data/data-core-editor.md | 4 ++-- packages/editor/src/store/selectors.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/data/data-core-editor.md b/docs/data/data-core-editor.md index 5c7e7e41a0ab6..1517f5b5317cc 100644 --- a/docs/data/data-core-editor.md +++ b/docs/data/data-core-editor.md @@ -897,7 +897,7 @@ True if multi-selecting, false if not. ### isSelectionEnabled -Whether is selection disable or not. +Selector that returns if multi-selection is enabled or not. *Parameters* @@ -905,7 +905,7 @@ Whether is selection disable or not. *Returns* -True if multi is disable, false if not. +True if it should be possible to multi-select blocks, false if multi-selection is disabled. ### getBlockMode diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index 81fa98f7fab2f..619c0e381712a 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -1316,11 +1316,11 @@ export function isMultiSelecting( state ) { } /** - * Whether is selection disable or not. + * Selector that returns if multi-selection is enabled or not. * * @param {Object} state Global application state. * - * @return {boolean} True if multi is disable, false if not. + * @return {boolean} True if it should be possible to multi-select blocks, false if multi-selection is disabled. */ export function isSelectionEnabled( state ) { return state.blockSelection.isEnabled; From c74658167ca205bed00ab4eb7232ac5b777c831f Mon Sep 17 00:00:00 2001 From: Miguel Torres <miguelmariatorresrojas@gmail.com> Date: Thu, 8 Nov 2018 23:43:31 +0100 Subject: [PATCH 050/106] Auto-refresh Popovers (#11257) * New action for refreshing and repositioning the tips when the fullscreen mode is toggled * Refresh the popovers every 0.2s for resizing/repositioning them --- docs/reference/deprecated.md | 3 ++ packages/components/CHANGELOG.md | 9 +++- packages/components/src/dropdown/index.js | 12 +++-- packages/components/src/popover/index.js | 51 ++++++++++++------- packages/components/src/popover/test/index.js | 12 ++--- 5 files changed, 55 insertions(+), 32 deletions(-) diff --git a/docs/reference/deprecated.md b/docs/reference/deprecated.md index 282d8c20940d7..e1a54e3498c1d 100644 --- a/docs/reference/deprecated.md +++ b/docs/reference/deprecated.md @@ -1,5 +1,8 @@ Gutenberg's deprecation policy is intended to support backwards-compatibility for releases, when possible. The current deprecations are listed below and are grouped by _the version at which they will be removed completely_. If your plugin depends on these behaviors, you must update to the recommended alternative before the noted version. +## 4.5.0 +- `Dropdown.refresh()` has been deprecated as the contained `Popover` is now automatically refreshed. + ## 4.4.0 - `wp.date.getSettings` has been removed. Please use `wp.date.__experimentalGetSettings` instead. diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 4e91e6f7869da..6c70475da0384 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -1,8 +1,13 @@ -## 5.1.0 (unreleased) +## 5.1.0 (Unreleased) -### New Features +### New Feature - Adjust a11y roles for MenuItem component, so that aria-checked is used properly, related change in Editor/Components/BlockNavigationList ([#11431](https://github.com/WordPress/gutenberg/issues/11431)). +- `Popover` components are now automatically refreshed every 0.5s in order to recalculate their size or position. + +### Deprecation + +- `Dropdown.refresh()` has been deprecated as the contained `Popover` is now automatically refreshed. ## 5.0.2 (2018-11-03) diff --git a/packages/components/src/dropdown/index.js b/packages/components/src/dropdown/index.js index 29b7700a0e636..71042b40bd7ef 100644 --- a/packages/components/src/dropdown/index.js +++ b/packages/components/src/dropdown/index.js @@ -1,6 +1,7 @@ /** * WordPress dependencies */ +import deprecated from '@wordpress/deprecated'; import { Component, createRef } from '@wordpress/element'; /** @@ -18,7 +19,6 @@ class Dropdown extends Component { this.refresh = this.refresh.bind( this ); this.containerRef = createRef(); - this.popoverRef = createRef(); this.state = { isOpen: false, @@ -45,11 +45,14 @@ class Dropdown extends Component { * When contents change height due to user interaction, * `refresh` can be called to re-render Popover with correct * attributes which allow scroll, if need be. + * @deprecated */ refresh() { - if ( this.popoverRef.current ) { - this.popoverRef.current.refresh(); - } + deprecated( 'Dropdown.refresh()', { + plugin: 'Gutenberg', + version: '4.5', + hint: 'Popover is now automatically re-rendered without needing to execute "refresh"', + } ); } toggle() { @@ -96,7 +99,6 @@ class Dropdown extends Component { { isOpen && ( <Popover className={ contentClassName } - ref={ this.popoverRef } position={ position } onClose={ this.close } onClickOutside={ this.closeIfClickOutside } diff --git a/packages/components/src/popover/index.js b/packages/components/src/popover/index.js index 7ce7163006a21..9ad4c6143e050 100644 --- a/packages/components/src/popover/index.js +++ b/packages/components/src/popover/index.js @@ -39,8 +39,8 @@ class Popover extends Component { this.getAnchorRect = this.getAnchorRect.bind( this ); this.updatePopoverSize = this.updatePopoverSize.bind( this ); this.computePopoverPosition = this.computePopoverPosition.bind( this ); - this.throttledComputePopoverPosition = this.throttledComputePopoverPosition.bind( this ); this.maybeClose = this.maybeClose.bind( this ); + this.throttledRefresh = this.throttledRefresh.bind( this ); this.contentNode = createRef(); this.anchorNode = createRef(); @@ -58,7 +58,7 @@ class Popover extends Component { } componentDidMount() { - this.toggleWindowEvents( true ); + this.toggleAutoRefresh( true ); this.refresh(); /* @@ -79,24 +79,38 @@ class Popover extends Component { } componentWillUnmount() { - this.toggleWindowEvents( false ); - clearTimeout( this.focusTimeout ); + this.toggleAutoRefresh( false ); } - toggleWindowEvents( isListening ) { - const handler = isListening ? 'addEventListener' : 'removeEventListener'; - + toggleAutoRefresh( isActive ) { window.cancelAnimationFrame( this.rafHandle ); - window[ handler ]( 'resize', this.throttledComputePopoverPosition ); - window[ handler ]( 'scroll', this.throttledComputePopoverPosition, true ); + + // Refresh the popover every time the window is resized or scrolled + const handler = isActive ? 'addEventListener' : 'removeEventListener'; + window[ handler ]( 'resize', this.throttledRefresh ); + window[ handler ]( 'scroll', this.throttledRefresh, true ); + + /* + * There are sometimes we need to reposition or resize the popover that are not + * handled by the resize/scroll window events (i.e. CSS changes in the layout + * that changes the position of the anchor). + * + * For these situations, we refresh the popover every 0.5s + */ + if ( isActive ) { + this.autoRefresh = setInterval( this.throttledRefresh, 500 ); + } else { + clearInterval( this.autoRefresh ); + } } - throttledComputePopoverPosition( event ) { - if ( event.type === 'scroll' && this.contentNode.current.contains( event.target ) ) { + throttledRefresh( event ) { + window.cancelAnimationFrame( this.rafHandle ); + if ( event && event.type === 'scroll' && this.contentNode.current.contains( event.target ) ) { return; } - this.rafHandle = window.requestAnimationFrame( () => this.computePopoverPosition() ); + this.rafHandle = window.requestAnimationFrame( this.refresh ); } /** @@ -161,16 +175,15 @@ class Popover extends Component { } updatePopoverSize() { - const rect = this.contentNode.current.getBoundingClientRect(); + const popoverSize = { + width: this.contentNode.current.scrollWidth, + height: this.contentNode.current.scrollHeight, + }; if ( ! this.state.popoverSize || - rect.width !== this.state.popoverSize.width || - rect.height !== this.state.popoverSize.height + popoverSize.width !== this.state.popoverSize.width || + popoverSize.height !== this.state.popoverSize.height ) { - const popoverSize = { - height: rect.height, - width: rect.width, - }; this.setState( { popoverSize } ); return popoverSize; } diff --git a/packages/components/src/popover/test/index.js b/packages/components/src/popover/test/index.js index 742f096f3533a..a66e68023f0ae 100644 --- a/packages/components/src/popover/test/index.js +++ b/packages/components/src/popover/test/index.js @@ -15,7 +15,7 @@ describe( 'Popover', () => { let wrapper; beforeEach( () => { jest.spyOn( Popover.prototype, 'computePopoverPosition' ).mockImplementation( noop ); - jest.spyOn( Popover.prototype, 'toggleWindowEvents' ).mockImplementation( noop ); + jest.spyOn( Popover.prototype, 'toggleAutoRefresh' ).mockImplementation( noop ); } ); afterEach( () => { @@ -30,19 +30,19 @@ describe( 'Popover', () => { } } ); - it( 'should add window events', () => { + it( 'should turn on auto refresh', () => { wrapper = TestUtils.renderIntoDocument( <Popover /> ); - expect( Popover.prototype.toggleWindowEvents ).toHaveBeenCalledWith( true ); + expect( Popover.prototype.toggleAutoRefresh ).toHaveBeenCalledWith( true ); expect( Popover.prototype.computePopoverPosition ).toHaveBeenCalled(); } ); - it( 'should remove window events', () => { + it( 'should turn off auto refresh', () => { wrapper = TestUtils.renderIntoDocument( <Popover /> ); /* eslint-disable react/no-find-dom-node */ ReactDOM.unmountComponentAtNode( ReactDOM.findDOMNode( wrapper ).parentNode ); /* eslint-enable react/no-find-dom-node */ - expect( Popover.prototype.toggleWindowEvents ).toHaveBeenCalledWith( false ); + expect( Popover.prototype.toggleAutoRefresh ).toHaveBeenCalledWith( false ); } ); it( 'should set offset and forced positions on changed position', () => { @@ -52,7 +52,7 @@ describe( 'Popover', () => { ReactDOM.render( <Popover position={ 'bottom right' } />, node ); - expect( Popover.prototype.toggleWindowEvents ).not.toHaveBeenCalled(); + expect( Popover.prototype.toggleAutoRefresh ).not.toHaveBeenCalled(); expect( Popover.prototype.computePopoverPosition ).toHaveBeenCalled(); } ); From 2484f4d2b16367677369ac6c5846856f4a2e8776 Mon Sep 17 00:00:00 2001 From: Stephen Edgar <stephen@netweb.com.au> Date: Fri, 9 Nov 2018 17:39:21 +1100 Subject: [PATCH 051/106] chore(release): update changelog files --- packages/block-library/CHANGELOG.md | 2 +- packages/block-serialization-default-parser/CHANGELOG.md | 5 +++++ packages/block-serialization-spec-parser/CHANGELOG.md | 5 +++++ packages/blocks/CHANGELOG.md | 6 ++++++ packages/components/CHANGELOG.md | 2 +- packages/compose/CHANGELOG.md | 2 ++ packages/core-data/CHANGELOG.md | 2 ++ packages/data/CHANGELOG.md | 2 ++ packages/date/CHANGELOG.md | 2 +- packages/dom/CHANGELOG.md | 2 ++ packages/edit-post/CHANGELOG.md | 7 +++++++ packages/editor/CHANGELOG.md | 2 +- packages/element/CHANGELOG.md | 7 ++++++- packages/format-library/CHANGELOG.md | 6 ++++++ packages/jest-preset-default/CHANGELOG.md | 2 ++ packages/list-reusable-blocks/CHANGELOG.md | 2 ++ packages/notices/CHANGELOG.md | 2 ++ packages/nux/CHANGELOG.md | 2 ++ packages/plugins/CHANGELOG.md | 6 +++++- packages/rich-text/CHANGELOG.md | 7 +++++++ packages/scripts/CHANGELOG.md | 2 ++ packages/viewport/CHANGELOG.md | 2 ++ 22 files changed, 71 insertions(+), 6 deletions(-) diff --git a/packages/block-library/CHANGELOG.md b/packages/block-library/CHANGELOG.md index ba78e2353863e..d15afcdf9e4f3 100644 --- a/packages/block-library/CHANGELOG.md +++ b/packages/block-library/CHANGELOG.md @@ -1,4 +1,4 @@ -## 2.2.0 (unreleased) +## 2.2.0 (2018-11-09) ### New Features diff --git a/packages/block-serialization-default-parser/CHANGELOG.md b/packages/block-serialization-default-parser/CHANGELOG.md index 5d36a4261ec53..34c834f196f63 100644 --- a/packages/block-serialization-default-parser/CHANGELOG.md +++ b/packages/block-serialization-default-parser/CHANGELOG.md @@ -1,3 +1,8 @@ +## 1.1.0 (2018-11-09) + +- Add new list of HTML fragments to parse output. +- Optimize JSON-attribute parsing. + ## 1.0.2 (2018-11-03) ## 1.0.1 (2018-10-11) diff --git a/packages/block-serialization-spec-parser/CHANGELOG.md b/packages/block-serialization-spec-parser/CHANGELOG.md index bf29009a468c0..2ee5fd63a9b77 100644 --- a/packages/block-serialization-spec-parser/CHANGELOG.md +++ b/packages/block-serialization-spec-parser/CHANGELOG.md @@ -1,3 +1,8 @@ +## 1.1.0 (2018-11-09) + +- Add new list of HTML fragments to parse output. +- Optimize JSON-attribute parsing. + ## 1.0.4 (2018-11-03) ## 1.0.3 (2018-10-11) diff --git a/packages/blocks/CHANGELOG.md b/packages/blocks/CHANGELOG.md index 11ce02b40f630..f77cfe3b809a6 100644 --- a/packages/blocks/CHANGELOG.md +++ b/packages/blocks/CHANGELOG.md @@ -1,3 +1,9 @@ +## 5.2.0 (2018-11-09) + +- Paste: Google Docs: fix nested formatting, sub, sup and del. +- Expose @wordpress/editor to Gutenberg mobile. +- Separate Paste Handler. + ## 5.1.2 (2018-11-03) ## 5.1.1 (2018-10-30) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 6c70475da0384..74ea05bf93cf4 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -1,4 +1,4 @@ -## 5.1.0 (Unreleased) +## 5.1.0 (2018-11-09) ### New Feature diff --git a/packages/compose/CHANGELOG.md b/packages/compose/CHANGELOG.md index 7c6410addac03..5e967f6838033 100644 --- a/packages/compose/CHANGELOG.md +++ b/packages/compose/CHANGELOG.md @@ -1,3 +1,5 @@ +## 2.1.1 (2018-11-9) + ## 2.1.0 (2018-10-29) ### Deprecation diff --git a/packages/core-data/CHANGELOG.md b/packages/core-data/CHANGELOG.md index 4e5bd6f54b950..e5ac6490fd50a 100644 --- a/packages/core-data/CHANGELOG.md +++ b/packages/core-data/CHANGELOG.md @@ -1,3 +1,5 @@ +## 2.0.10 (2018-11-09) + ## 2.0.9 (2018-11-03) ## 2.0.8 (2018-10-30) diff --git a/packages/data/CHANGELOG.md b/packages/data/CHANGELOG.md index a30ae72854b74..89cc14fcc049a 100644 --- a/packages/data/CHANGELOG.md +++ b/packages/data/CHANGELOG.md @@ -1,3 +1,5 @@ +## 3.1.1 (2018-11-09) + ## 3.1.0 (2018-11-03) ### New Features diff --git a/packages/date/CHANGELOG.md b/packages/date/CHANGELOG.md index 82c11f6a046cb..48d4a1077876a 100644 --- a/packages/date/CHANGELOG.md +++ b/packages/date/CHANGELOG.md @@ -1,4 +1,4 @@ -## 2.2.0 (Unreleased) +## 2.2.0 (2018-11-09) ### Deprecations diff --git a/packages/dom/CHANGELOG.md b/packages/dom/CHANGELOG.md index 8a0d841d17433..15d8a90386a97 100644 --- a/packages/dom/CHANGELOG.md +++ b/packages/dom/CHANGELOG.md @@ -1,3 +1,5 @@ +## 2.0.5 (2018-11-09) + ## 2.0.4 (2018-10-19) ## 2.0.3 (2018-10-18) diff --git a/packages/edit-post/CHANGELOG.md b/packages/edit-post/CHANGELOG.md index 50cf71f8fb6f6..8ba5213ff7052 100644 --- a/packages/edit-post/CHANGELOG.md +++ b/packages/edit-post/CHANGELOG.md @@ -1,3 +1,10 @@ +## 2.1.0 (2018-11-09) + +### Bug Fixes + +- "View as" link is not updated after the post is updated and the permalink is changed. +- Hide custom fields option when the meta box is disabled. + ## 2.0.3 (2018-11-03) ## 2.0.2 (2018-10-30) diff --git a/packages/editor/CHANGELOG.md b/packages/editor/CHANGELOG.md index 6c3f5dd6a8d75..38da9bc69d69a 100644 --- a/packages/editor/CHANGELOG.md +++ b/packages/editor/CHANGELOG.md @@ -1,4 +1,4 @@ -## 6.2.0 (unreleased) +## 6.2.0 (2018-11-09) ### New Features diff --git a/packages/element/CHANGELOG.md b/packages/element/CHANGELOG.md index 7010a1b87680d..0e00c1371bd44 100644 --- a/packages/element/CHANGELOG.md +++ b/packages/element/CHANGELOG.md @@ -1,4 +1,9 @@ -## 2.1.4 (2018-10-19) + +## 2.1.6 (2018-11-09) + +## 2.1.5 (2018-10-29) + +## 2.1.4 (2018-10-20) ## 2.1.3 (2018-10-18) diff --git a/packages/format-library/CHANGELOG.md b/packages/format-library/CHANGELOG.md index 15f7281ffceb1..d8f00ab6594cd 100644 --- a/packages/format-library/CHANGELOG.md +++ b/packages/format-library/CHANGELOG.md @@ -1,3 +1,9 @@ +## 1.1.0 (2018-11-09) + +### New Features + +- Import Format API components instead of passing as props. + ## 1.0.3 (2018-11-03) ## 1.0.2 (2018-10-30) diff --git a/packages/jest-preset-default/CHANGELOG.md b/packages/jest-preset-default/CHANGELOG.md index 2841d776f39d9..fa06662472016 100644 --- a/packages/jest-preset-default/CHANGELOG.md +++ b/packages/jest-preset-default/CHANGELOG.md @@ -1,3 +1,5 @@ +## 3.0.1 (2018-11-09) + ## 3.0.0 (2018-11-03) ### Breaking Change diff --git a/packages/list-reusable-blocks/CHANGELOG.md b/packages/list-reusable-blocks/CHANGELOG.md index fa0926f2d653d..23c271cd229db 100644 --- a/packages/list-reusable-blocks/CHANGELOG.md +++ b/packages/list-reusable-blocks/CHANGELOG.md @@ -1,3 +1,5 @@ +## 1.1.8 (2018-11-09) + ## 1.1.7 (2018-11-03) ## 1.1.6 (2018-10-30) diff --git a/packages/notices/CHANGELOG.md b/packages/notices/CHANGELOG.md index b1cc104e4e775..b8124567073bc 100644 --- a/packages/notices/CHANGELOG.md +++ b/packages/notices/CHANGELOG.md @@ -1,3 +1,5 @@ +## 1.0.3 (2018-11-09) + ## 1.0.2 (2018-11-03) ## 1.0.1 (2018-10-30) diff --git a/packages/nux/CHANGELOG.md b/packages/nux/CHANGELOG.md index 36bb6a41d4650..7e8a523b74ac2 100644 --- a/packages/nux/CHANGELOG.md +++ b/packages/nux/CHANGELOG.md @@ -1,3 +1,5 @@ +## 2.0.10 (2018-11-09) + ## 2.0.9 (2018-11-03) ## 2.0.8 (2018-10-30) diff --git a/packages/plugins/CHANGELOG.md b/packages/plugins/CHANGELOG.md index 9b4bad0bd4aa5..c4e9c3c062f5f 100644 --- a/packages/plugins/CHANGELOG.md +++ b/packages/plugins/CHANGELOG.md @@ -1,4 +1,8 @@ -## 2.0.5 (2018-10-19) +## 2.0.7 (2018-11-09) + +## 2.0.6 (2018-10-29) + +## 2.0.5 (2018-10-20) ## 2.0.4 (2018-10-18) diff --git a/packages/rich-text/CHANGELOG.md b/packages/rich-text/CHANGELOG.md index 72d2bed44a8df..a35fe3c21ceb6 100644 --- a/packages/rich-text/CHANGELOG.md +++ b/packages/rich-text/CHANGELOG.md @@ -1,3 +1,10 @@ +## 2.0.3 (2018-11-09) + +### Bug Fix + +- Fix Format Type Assignment During Parsing. +- Fix applying formats on multiline values without wrapper tags. + ## 2.0.2 (2018-11-03) ## 2.0.1 (2018-10-30) diff --git a/packages/scripts/CHANGELOG.md b/packages/scripts/CHANGELOG.md index 8ea5135b5d597..16cb1138773a0 100644 --- a/packages/scripts/CHANGELOG.md +++ b/packages/scripts/CHANGELOG.md @@ -1,3 +1,5 @@ +## 2.4.2 (2018-11-09) + ## 2.4.1 (2018-11-03) ## 2.4.0 (2018-10-16) diff --git a/packages/viewport/CHANGELOG.md b/packages/viewport/CHANGELOG.md index 7543ff40a7a43..d5801e0a266a9 100644 --- a/packages/viewport/CHANGELOG.md +++ b/packages/viewport/CHANGELOG.md @@ -1,3 +1,5 @@ +## 2.0.9 (2018-11-09) + ## 2.0.8 (2018-11-03) ## 2.0.7 (2018-10-30) From d06fd6bcd1e0e2eb0890d51da063bff0c737c2bf Mon Sep 17 00:00:00 2001 From: Stephen Edgar <stephen@netweb.com.au> Date: Fri, 9 Nov 2018 17:46:58 +1100 Subject: [PATCH 052/106] chore(release): publish - @wordpress/block-library@2.2.0 - @wordpress/block-serialization-default-parser@1.1.0 - @wordpress/block-serialization-spec-parser@1.1.0 - @wordpress/blocks@5.2.0 - @wordpress/components@5.1.0 - @wordpress/compose@2.1.1 - @wordpress/core-data@2.0.10 - @wordpress/data@3.1.1 - @wordpress/date@2.2.0 - @wordpress/dom@2.0.5 - @wordpress/edit-post@2.1.0 - @wordpress/editor@6.2.0 - @wordpress/element@2.1.6 - @wordpress/format-library@1.1.0 - @wordpress/jest-preset-default@3.0.1 - @wordpress/list-reusable-blocks@1.1.8 - @wordpress/notices@1.0.3 - @wordpress/nux@2.0.10 - @wordpress/plugins@2.0.7 - @wordpress/rich-text@2.0.3 - @wordpress/scripts@2.4.2 - @wordpress/viewport@2.0.9 --- packages/block-library/package.json | 2 +- packages/block-serialization-default-parser/package.json | 2 +- packages/block-serialization-spec-parser/package.json | 2 +- packages/blocks/package.json | 2 +- packages/components/package.json | 2 +- packages/compose/package.json | 2 +- packages/core-data/package.json | 2 +- packages/data/package.json | 2 +- packages/date/package.json | 2 +- packages/dom/package.json | 2 +- packages/edit-post/package.json | 2 +- packages/editor/package.json | 2 +- packages/element/package.json | 2 +- packages/format-library/package.json | 2 +- packages/jest-preset-default/package.json | 2 +- packages/list-reusable-blocks/package.json | 2 +- packages/notices/package.json | 2 +- packages/nux/package.json | 2 +- packages/plugins/package.json | 2 +- packages/rich-text/package.json | 2 +- packages/scripts/package.json | 2 +- packages/viewport/package.json | 2 +- 22 files changed, 22 insertions(+), 22 deletions(-) diff --git a/packages/block-library/package.json b/packages/block-library/package.json index 844b958b3fb86..e088ab8becc1a 100644 --- a/packages/block-library/package.json +++ b/packages/block-library/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-library", - "version": "2.1.8", + "version": "2.2.0", "description": "Block library for the WordPress editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/block-serialization-default-parser/package.json b/packages/block-serialization-default-parser/package.json index cf3785c8c2876..ba3e8054be233 100644 --- a/packages/block-serialization-default-parser/package.json +++ b/packages/block-serialization-default-parser/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-serialization-default-parser", - "version": "1.0.2", + "version": "1.1.0", "description": "Block serialization specification parser for WordPress posts.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/block-serialization-spec-parser/package.json b/packages/block-serialization-spec-parser/package.json index 2631598decc2c..8615df4e5ffb8 100644 --- a/packages/block-serialization-spec-parser/package.json +++ b/packages/block-serialization-spec-parser/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-serialization-spec-parser", - "version": "1.0.4", + "version": "1.1.0", "description": "Block serialization specification parser for WordPress posts.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/blocks/package.json b/packages/blocks/package.json index b45f42939af45..178ea4a5a97b4 100644 --- a/packages/blocks/package.json +++ b/packages/blocks/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/blocks", - "version": "5.1.2", + "version": "5.2.0", "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 a13d4c61b9aab..7e7883ab2a7c2 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/components", - "version": "5.0.2", + "version": "5.1.0", "description": "UI components for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/compose/package.json b/packages/compose/package.json index 237ff1af8dd1e..3ccd038b7ed23 100644 --- a/packages/compose/package.json +++ b/packages/compose/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/compose", - "version": "2.1.0", + "version": "2.1.1", "description": "WordPress higher-order components (HOCs).", "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 85f80be8f2a7e..90bae3f376570 100644 --- a/packages/core-data/package.json +++ b/packages/core-data/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/core-data", - "version": "2.0.9", + "version": "2.0.10", "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 49ab7df773ca0..e11cc59c820df 100644 --- a/packages/data/package.json +++ b/packages/data/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/data", - "version": "3.1.0", + "version": "3.1.1", "description": "Data module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/date/package.json b/packages/date/package.json index 9735a52fbed28..3e6e1627e01f7 100644 --- a/packages/date/package.json +++ b/packages/date/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/date", - "version": "2.1.0", + "version": "2.2.0", "description": "Date module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/dom/package.json b/packages/dom/package.json index 38177cbfd2f96..d9e6f69773d42 100644 --- a/packages/dom/package.json +++ b/packages/dom/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/dom", - "version": "2.0.4", + "version": "2.0.5", "description": "DOM utilities 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 7d26bc0de2cd9..4ba3448309c43 100644 --- a/packages/edit-post/package.json +++ b/packages/edit-post/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/edit-post", - "version": "2.0.3", + "version": "2.1.0", "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 a56ad455c1f75..041b7eadcb689 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/editor", - "version": "6.1.1", + "version": "6.2.0", "description": "Building blocks for WordPress editors.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/element/package.json b/packages/element/package.json index efdf718f6370b..8f815b650458e 100644 --- a/packages/element/package.json +++ b/packages/element/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/element", - "version": "2.1.5", + "version": "2.1.6", "description": "Element React module for WordPress.", "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 acda8b1ca8b3f..b4cef66fd8204 100644 --- a/packages/format-library/package.json +++ b/packages/format-library/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/format-library", - "version": "1.0.3", + "version": "1.1.0", "description": "Format library for the WordPress editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/jest-preset-default/package.json b/packages/jest-preset-default/package.json index fb797c500079b..534e14beb7686 100644 --- a/packages/jest-preset-default/package.json +++ b/packages/jest-preset-default/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/jest-preset-default", - "version": "3.0.0", + "version": "3.0.1", "description": "Default Jest preset for WordPress development.", "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 317bcc03e9d74..4bd15d9a77316 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.7", + "version": "1.1.8", "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 e6ac37b13387d..b04caf91b6215 100644 --- a/packages/notices/package.json +++ b/packages/notices/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/notices", - "version": "1.0.2", + "version": "1.0.3", "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 c7e52cf9efd4a..cdce9865f5b3e 100644 --- a/packages/nux/package.json +++ b/packages/nux/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/nux", - "version": "2.0.9", + "version": "2.0.10", "description": "NUX (New User eXperience) module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/plugins/package.json b/packages/plugins/package.json index d6aa563e3b928..d16cfff2464e8 100644 --- a/packages/plugins/package.json +++ b/packages/plugins/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/plugins", - "version": "2.0.6", + "version": "2.0.7", "description": "Plugins 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 51ecad8ab483a..e210f79155e5c 100644 --- a/packages/rich-text/package.json +++ b/packages/rich-text/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/rich-text", - "version": "2.0.2", + "version": "2.0.3", "description": "Rich text value and manipulation API.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/scripts/package.json b/packages/scripts/package.json index 17d35fe265dcd..2b41184329e62 100644 --- a/packages/scripts/package.json +++ b/packages/scripts/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/scripts", - "version": "2.4.1", + "version": "2.4.2", "description": "Collection of JS scripts for WordPress development.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/viewport/package.json b/packages/viewport/package.json index dd417eea9663a..a3a1ed966c450 100644 --- a/packages/viewport/package.json +++ b/packages/viewport/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/viewport", - "version": "2.0.8", + "version": "2.0.9", "description": "Viewport module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", From 4741104c2e035a6b80ab7e01031a9d4086b3f75d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20Van=C2=A0Dorpe?= <iseulde@automattic.com> Date: Fri, 9 Nov 2018 09:01:09 +0100 Subject: [PATCH 053/106] Support RichText tree preparation --- packages/block-library/src/heading/edit.js | 1 + packages/block-library/src/list/index.js | 1 + .../src/list/test/__snapshots__/index.js.snap | 8 ++-- packages/block-library/src/paragraph/edit.js | 1 + .../test/__snapshots__/index.js.snap | 8 ++-- packages/block-library/src/quote/index.js | 9 +++- .../quote/test/__snapshots__/index.js.snap | 8 ++-- .../editor/src/components/rich-text/index.js | 48 ++++++++++++++++--- .../src/components/rich-text/tinymce.js | 28 +---------- packages/rich-text/src/create.js | 16 +++++-- .../rich-text/src/register-format-type.js | 43 ++++++++++++----- .../src/test/register-format-type.js | 18 ------- packages/rich-text/src/to-dom.js | 14 +++++- .../rich-text/src/unregister-format-type.js | 8 ++++ .../blocks/__snapshots__/quote.test.js.snap | 6 +-- 15 files changed, 130 insertions(+), 87 deletions(-) diff --git a/packages/block-library/src/heading/edit.js b/packages/block-library/src/heading/edit.js index f0f02afb2579b..711f0c100e431 100644 --- a/packages/block-library/src/heading/edit.js +++ b/packages/block-library/src/heading/edit.js @@ -42,6 +42,7 @@ export default function HeadingEdit( { </PanelBody> </InspectorControls> <RichText + identifier="content" wrapperClassName="wp-block-heading" tagName={ tagName } value={ content } diff --git a/packages/block-library/src/list/index.js b/packages/block-library/src/list/index.js index e7c38a543f56b..f2c9485e2bccb 100644 --- a/packages/block-library/src/list/index.js +++ b/packages/block-library/src/list/index.js @@ -334,6 +334,7 @@ export const settings = { ] } /> <RichText + identifier="values" multiline="li" tagName={ tagName } unstableGetSettings={ this.getEditorSettings } diff --git a/packages/block-library/src/list/test/__snapshots__/index.js.snap b/packages/block-library/src/list/test/__snapshots__/index.js.snap index 4eb99b8986972..80e123d44c6b4 100644 --- a/packages/block-library/src/list/test/__snapshots__/index.js.snap +++ b/packages/block-library/src/list/test/__snapshots__/index.js.snap @@ -19,9 +19,11 @@ exports[`core/list block edit matches snapshot 1`] = ` data-is-placeholder-visible="true" role="textbox" > - <br - data-mce-bogus="1" - /> + <li> + <br + data-mce-bogus="1" + /> + </li> </ul> <ul class="editor-rich-text__tinymce" diff --git a/packages/block-library/src/paragraph/edit.js b/packages/block-library/src/paragraph/edit.js index 510d808dc6930..503870ebc0ab6 100644 --- a/packages/block-library/src/paragraph/edit.js +++ b/packages/block-library/src/paragraph/edit.js @@ -219,6 +219,7 @@ class ParagraphBlock extends Component { </PanelColorSettings> </InspectorControls> <RichText + identifier="content" tagName="p" className={ classnames( 'wp-block-paragraph', className, { 'has-text-color': textColor.color, diff --git a/packages/block-library/src/pullquote/test/__snapshots__/index.js.snap b/packages/block-library/src/pullquote/test/__snapshots__/index.js.snap index 3d79d564433c8..4c13da3da08b1 100644 --- a/packages/block-library/src/pullquote/test/__snapshots__/index.js.snap +++ b/packages/block-library/src/pullquote/test/__snapshots__/index.js.snap @@ -23,9 +23,11 @@ exports[`core/pullquote block edit matches snapshot 1`] = ` data-is-placeholder-visible="true" role="textbox" > - <br - data-mce-bogus="1" - /> + <p> + <br + data-mce-bogus="1" + /> + </p> </div> <div class="editor-rich-text__tinymce" diff --git a/packages/block-library/src/quote/index.js b/packages/block-library/src/quote/index.js index 67cf5cc67692a..dd60905cded3b 100644 --- a/packages/block-library/src/quote/index.js +++ b/packages/block-library/src/quote/index.js @@ -17,15 +17,18 @@ import { import { join, split, create, toHTMLString } from '@wordpress/rich-text'; import { G, Path, SVG } from '@wordpress/components'; +const ATTRIBUTE_QUOTE = 'value'; +const ATTRIBUTE_CITATION = 'citation'; + const blockAttributes = { - value: { + [ ATTRIBUTE_QUOTE ]: { type: 'string', source: 'html', selector: 'blockquote', multiline: 'p', default: '', }, - citation: { + [ ATTRIBUTE_CITATION ]: { type: 'string', source: 'html', selector: 'cite', @@ -201,6 +204,7 @@ export const settings = { </BlockControls> <blockquote className={ className } style={ { textAlign: align } }> <RichText + identifier={ ATTRIBUTE_QUOTE } multiline value={ value } onChange={ @@ -222,6 +226,7 @@ export const settings = { /> { ( ! RichText.isEmpty( citation ) || isSelected ) && ( <RichText + identifier={ ATTRIBUTE_CITATION } value={ citation } onChange={ ( nextCitation ) => setAttributes( { 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 54f0293d643a5..ecc391ed5da7f 100644 --- a/packages/block-library/src/quote/test/__snapshots__/index.js.snap +++ b/packages/block-library/src/quote/test/__snapshots__/index.js.snap @@ -22,9 +22,11 @@ exports[`core/quote block edit matches snapshot 1`] = ` data-is-placeholder-visible="true" role="textbox" > - <br - data-mce-bogus="1" - /> + <p> + <br + data-mce-bogus="1" + /> + </p> </div> <div class="editor-rich-text__tinymce" diff --git a/packages/editor/src/components/rich-text/index.js b/packages/editor/src/components/rich-text/index.js index b048c442f594e..a59fc64bf28b6 100644 --- a/packages/editor/src/components/rich-text/index.js +++ b/packages/editor/src/components/rich-text/index.js @@ -44,6 +44,7 @@ import { isCollapsed, } from '@wordpress/rich-text'; import { decodeEntities } from '@wordpress/html-entities'; +import { withFilters } from '@wordpress/components'; /** * Internal dependencies @@ -238,6 +239,7 @@ export class RichText extends Component { unwrapNode: ( node ) => !! node.getAttribute( 'data-mce-bogus' ), removeAttribute: ( attribute ) => attribute.indexOf( 'data-mce-' ) === 0, filterString: ( string ) => string.replace( TINYMCE_ZWSP, '' ), + prepareEditableTree: this.props.prepareEditableTree, } ); } @@ -252,6 +254,7 @@ export class RichText extends Component { element.setAttribute( 'data-mce-bogus', '1' ); return element; }, + prepareEditableTree: this.props.prepareEditableTree, } ); } @@ -413,10 +416,7 @@ export class RichText extends Component { const record = this.createRecord(); const transformed = this.patterns.reduce( ( accumlator, transform ) => transform( accumlator ), record ); - // Don't apply changes if there's no transform. Content will be up to - // date. In the future we could always let it flow back in the live DOM - // if there are no performance issues. - this.onChange( transformed, record === transformed ); + this.onChange( transformed ); } /** @@ -780,6 +780,22 @@ export class RichText extends Component { record.end = length; this.applyRecord( record ); } + + // If any format props update, reapply value. + const shouldReapply = Object.keys( this.props ).some( ( name ) => { + if ( name.indexOf( 'format_' ) !== 0 ) { + return false; + } + + return Object.keys( this.props[ name ] ).some( ( subName ) => { + return this.props[ name ][ subName ] !== prevProps[ name ][ subName ]; + } ); + } ); + + if ( shouldReapply ) { + const record = this.formatToValue( value ); + this.applyRecord( record ); + } } formatToValue( value ) { @@ -809,6 +825,20 @@ export class RichText extends Component { return value; } + valueToEditableHTML( value ) { + return unstableToDom( { + value, + multilineTag: this.multilineTag, + multilineWrapperTags: this.multilineWrapperTags, + createLinePadding( doc ) { + const element = doc.createElement( 'br' ); + element.setAttribute( 'data-mce-bogus', '1' ); + return element; + }, + prepareEditableTree: this.props.prepareEditableTree, + } ).body.innerHTML; + } + valueToFormat( { formats, text } ) { // Handle deprecated `children` and `node` sources. if ( this.usedDeprecatedChildrenSource ) { @@ -834,7 +864,6 @@ export class RichText extends Component { const { tagName: Tagname = 'div', style, - value, wrapperClassName, className, inlineToolbar = false, @@ -883,7 +912,7 @@ export class RichText extends Component { getSettings={ this.getSettings } onSetup={ this.onSetup } style={ style } - defaultValue={ value } + defaultValue={ this.valueToEditableHTML( record ) } isPlaceholderVisible={ isPlaceholderVisible } aria-label={ placeholder } aria-autocomplete="list" @@ -930,12 +959,15 @@ const RichTextContainer = compose( [ withBlockEditContext( ( context, ownProps ) => { // When explicitly set as not selected, do nothing. if ( ownProps.isSelected === false ) { - return {}; + return { + clientId: context.clientId, + }; } // When explicitly set as selected, use the value stored in the context instead. if ( ownProps.isSelected === true ) { return { isSelected: context.isSelected, + clientId: context.clientId, }; } @@ -943,6 +975,7 @@ const RichTextContainer = compose( [ return { isSelected: context.isSelected && context.focusedElement === ownProps.instanceId, setFocusedElement: context.setFocusedElement, + clientId: context.clientId, }; } ), withSelect( ( select ) => { @@ -973,6 +1006,7 @@ const RichTextContainer = compose( [ }; } ), withSafeTimeout, + withFilters( 'experimentalRichText' ), ] )( RichText ); RichTextContainer.Content = ( { value, tagName: Tag, multiline, ...props } ) => { diff --git a/packages/editor/src/components/rich-text/tinymce.js b/packages/editor/src/components/rich-text/tinymce.js index e3dfb4145a67c..11ebd13591e12 100644 --- a/packages/editor/src/components/rich-text/tinymce.js +++ b/packages/editor/src/components/rich-text/tinymce.js @@ -10,8 +10,6 @@ import classnames from 'classnames'; */ import { Component, createElement } from '@wordpress/element'; import { BACKSPACE, DELETE, ENTER, LEFT, RIGHT } from '@wordpress/keycodes'; -import { toHTMLString } from '@wordpress/rich-text'; -import { children } from '@wordpress/blocks'; /** * Internal dependencies @@ -313,8 +311,6 @@ export default class TinyMCE extends Component { isPlaceholderVisible, onPaste, onInput, - multilineTag, - multilineWrapperTags, onKeyDown, onKeyUp, } = this.props; @@ -332,28 +328,6 @@ export default class TinyMCE extends Component { // If a default value is provided, render it into the DOM even before // TinyMCE finishes initializing. This avoids a short delay by allowing // us to show and focus the content before it's truly ready to edit. - let initialHTML = defaultValue; - - // Guard for blocks passing `null` in onSplit callbacks. May be removed - // if onSplit is revised to not pass a `null` value. - if ( defaultValue === null ) { - initialHTML = ''; - // Handle deprecated `children` and `node` sources. - } else if ( Array.isArray( defaultValue ) ) { - initialHTML = children.toHTML( defaultValue ); - } else if ( typeof defaultValue !== 'string' ) { - initialHTML = toHTMLString( { - value: defaultValue, - multilineTag, - multilineWrapperTags, - } ); - } - - if ( initialHTML === '' ) { - // Ensure the field is ready to receive focus by TinyMCE. - initialHTML = '<br data-mce-bogus="1">'; - } - return createElement( tagName, { ...ariaProps, className: classnames( className, 'editor-rich-text__tinymce' ), @@ -362,7 +336,7 @@ export default class TinyMCE extends Component { ref: this.bindEditorNode, style, suppressContentEditableWarning: true, - dangerouslySetInnerHTML: { __html: initialHTML }, + dangerouslySetInnerHTML: { __html: defaultValue }, onPaste, onInput, onFocus: this.onFocus, diff --git a/packages/rich-text/src/create.js b/packages/rich-text/src/create.js index ec5a7eb175938..5ab6a34e6c903 100644 --- a/packages/rich-text/src/create.js +++ b/packages/rich-text/src/create.js @@ -57,6 +57,10 @@ function toFormat( { type, attributes } ) { return attributes ? { type, attributes } : { type }; } + if ( formatType.__experimentalCreatePrepareEditableTree ) { + return null; + } + if ( ! attributes ) { return { type: formatType.name }; } @@ -359,11 +363,13 @@ function createFromElement( { } ), } ); - // Reuse the last format if it's equal. - if ( isFormatEqual( newFormat, lastFormat ) ) { - format = lastFormat; - } else { - format = newFormat; + if ( newFormat ) { + // Reuse the last format if it's equal. + if ( isFormatEqual( newFormat, lastFormat ) ) { + format = lastFormat; + } else { + format = newFormat; + } } } diff --git a/packages/rich-text/src/register-format-type.js b/packages/rich-text/src/register-format-type.js index 1b475bf5d89d9..38af31b958e90 100644 --- a/packages/rich-text/src/register-format-type.js +++ b/packages/rich-text/src/register-format-type.js @@ -1,12 +1,8 @@ -/** - * External dependencies - */ -import { isFunction } from 'lodash'; - /** * WordPress dependencies */ -import { select, dispatch } from '@wordpress/data'; +import { select, dispatch, withSelect } from '@wordpress/data'; +import { addFilter } from '@wordpress/hooks'; /** * Registers a new format provided a unique name and an object defining its @@ -45,13 +41,6 @@ export function registerFormatType( name, settings ) { return; } - if ( ! settings || ! isFunction( settings.edit ) ) { - window.console.error( - 'The "edit" property must be specified and must be a valid function.' - ); - return; - } - if ( typeof settings.tagName !== 'string' || settings.tagName === '' @@ -124,5 +113,33 @@ export function registerFormatType( name, settings ) { dispatch( 'core/rich-text' ).addFormatTypes( settings ); + if ( + settings.__experimentalCreatePrepareEditableTree && + settings.__experimentalGetPropsForEditableTreePreparation + ) { + addFilter( 'experimentalRichText', name, ( OriginalComponent ) => { + return withSelect( ( sel, { clientId, identifier } ) => ( { + [ `format_${ name }` ]: settings.__experimentalGetPropsForEditableTreePreparation( + sel, + { + richTextIdentifier: identifier, + blockClientId: clientId, + } + ), + } ) )( ( props ) => ( + <OriginalComponent + { ...props } + prepareEditableTree={ [ + ...( props.prepareEditableTree || [] ), + settings.__experimentalCreatePrepareEditableTree( props[ `format_${ name }` ], { + richTextIdentifier: props.identifier, + blockClientId: props.clientId, + } ), + ] } + /> + ) ); + } ); + } + return settings; } diff --git a/packages/rich-text/src/test/register-format-type.js b/packages/rich-text/src/test/register-format-type.js index cde3c900e8b41..5fc78630be2ba 100644 --- a/packages/rich-text/src/test/register-format-type.js +++ b/packages/rich-text/src/test/register-format-type.js @@ -94,24 +94,6 @@ describe( 'registerFormatType', () => { expect( duplicateFormat ).toBeUndefined(); } ); - it( 'should error on undefined edit property', () => { - const format = registerFormatType( 'plugin/test', { - ...validSettings, - edit: undefined, - } ); - expect( console ).toHaveErroredWith( 'The "edit" property must be specified and must be a valid function.' ); - expect( format ).toBeUndefined(); - } ); - - it( 'should reject formats with an invalid edit function', () => { - const format = registerFormatType( validName, { - ...validSettings, - edit: 'not-a-function', - } ); - expect( console ).toHaveErroredWith( 'The "edit" property must be specified and must be a valid function.' ); - expect( format ).toBeUndefined(); - } ); - it( 'should reject formats without tag name', () => { const settings = { ...validSettings }; delete settings.tagName; diff --git a/packages/rich-text/src/to-dom.js b/packages/rich-text/src/to-dom.js index b5851817b5db7..b582c77083d9a 100644 --- a/packages/rich-text/src/to-dom.js +++ b/packages/rich-text/src/to-dom.js @@ -132,17 +132,27 @@ function padEmptyLines( { element, createLinePadding, multilineWrapperTags } ) { } } +function prepareFormats( prepareEditableTree = [], value ) { + return prepareEditableTree.reduce( ( accumlator, fn ) => { + return fn( accumlator, value.text ); + }, value.formats ); +} + export function toDom( { value, multilineTag, multilineWrapperTags, createLinePadding, + prepareEditableTree, } ) { let startPath = []; let endPath = []; const tree = toTree( { - value, + value: { + ...value, + formats: prepareFormats( prepareEditableTree, value ), + }, multilineTag, multilineWrapperTags, createEmpty, @@ -188,6 +198,7 @@ export function apply( { multilineTag, multilineWrapperTags, createLinePadding, + prepareEditableTree, } ) { // Construct a new element tree in memory. const { body, selection } = toDom( { @@ -195,6 +206,7 @@ export function apply( { multilineTag, multilineWrapperTags, createLinePadding, + prepareEditableTree, } ); applyValue( body, current ); diff --git a/packages/rich-text/src/unregister-format-type.js b/packages/rich-text/src/unregister-format-type.js index a01b648622342..cffaa3d025b94 100644 --- a/packages/rich-text/src/unregister-format-type.js +++ b/packages/rich-text/src/unregister-format-type.js @@ -2,6 +2,7 @@ * WordPress dependencies */ import { select, dispatch } from '@wordpress/data'; +import { removeFilter } from '@wordpress/hooks'; /** * Unregisters a format. @@ -21,6 +22,13 @@ export function unregisterFormatType( name ) { return; } + if ( + oldFormat.__experimentalCreatePrepareEditableTree && + oldFormat.__experimentalGetPropsForEditableTreePreparation + ) { + removeFilter( 'experimentalRichText', name ); + } + dispatch( 'core/rich-text' ).removeFormatTypes( name ); return oldFormat; diff --git a/test/e2e/specs/blocks/__snapshots__/quote.test.js.snap b/test/e2e/specs/blocks/__snapshots__/quote.test.js.snap index 5257716d67d00..6a46b767390ef 100644 --- a/test/e2e/specs/blocks/__snapshots__/quote.test.js.snap +++ b/test/e2e/specs/blocks/__snapshots__/quote.test.js.snap @@ -58,11 +58,7 @@ exports[`Quote can be converted to paragraphs and renders a paragraph for the ci <!-- /wp:paragraph -->" `; -exports[`Quote can be converted to paragraphs and renders a void paragraph if both the cite and quote are void 1`] = ` -"<!-- wp:paragraph --> -<p></p> -<!-- /wp:paragraph -->" -`; +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 <p> within quote 1`] = ` "<!-- wp:paragraph --> From a3785cd15bcfaa17df20f7a6afc74d9254fe2c47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20=28Greg=29=20Zi=C3=B3=C5=82kowski?= <grzegorz@gziolo.pl> Date: Fri, 9 Nov 2018 09:10:58 +0100 Subject: [PATCH 054/106] Block API: Normalize block type function argument (#11490) * Block API: Normalize usage of getBlockAttributes by allowing block name as param * Block API: Normalize usage of getSaveContent by allowing block name as param * Block API: Normalize usage of getSaveElement by allowing block name as param * Tests: Add unit tests covering normalization of block type * Docs: Add a note about more flexible block API methods * Refactor getBlockTransforms to use block type normalization * Blocks: Fix JSDoc for getBlockAttributes function --- packages/block-library/src/heading/index.js | 3 +-- packages/block-library/src/image/index.js | 4 +--- packages/block-library/src/list/index.js | 3 +-- packages/blocks/CHANGELOG.md | 6 +++++ packages/blocks/src/api/factory.js | 10 ++++---- packages/blocks/src/api/parser.js | 10 ++++---- packages/blocks/src/api/raw-handling/index.js | 3 +-- packages/blocks/src/api/serializer.js | 23 +++++++++++-------- packages/blocks/src/api/test/factory.js | 21 ++++++++++++++++- packages/blocks/src/api/test/parser.js | 20 ++++++++++++++++ packages/blocks/src/api/test/serializer.js | 19 ++++++++++++++- packages/blocks/src/api/test/validation.js | 16 +++++++++++-- packages/blocks/src/api/utils.js | 19 ++++++++++++++- packages/blocks/src/api/validation.js | 10 ++++---- .../src/components/block-compare/index.js | 11 ++++----- .../src/components/inner-blocks/test/index.js | 3 +-- 16 files changed, 136 insertions(+), 45 deletions(-) diff --git a/packages/block-library/src/heading/index.js b/packages/block-library/src/heading/index.js index 6f98add18effe..148d062934074 100644 --- a/packages/block-library/src/heading/index.js +++ b/packages/block-library/src/heading/index.js @@ -11,7 +11,6 @@ import { createBlock, getPhrasingContentSchema, getBlockAttributes, - getBlockType, } from '@wordpress/blocks'; import { RichText } from '@wordpress/editor'; import { @@ -101,7 +100,7 @@ export const settings = { transform( node ) { return createBlock( 'core/heading', { ...getBlockAttributes( - getBlockType( 'core/heading' ), + 'core/heading', node.outerHTML ), level: getLevelFromHeadingNodeName( node.nodeName ), diff --git a/packages/block-library/src/image/index.js b/packages/block-library/src/image/index.js index 83a022388d35d..4f6619a6c6e52 100644 --- a/packages/block-library/src/image/index.js +++ b/packages/block-library/src/image/index.js @@ -11,7 +11,6 @@ import { __ } from '@wordpress/i18n'; import { createBlock, getBlockAttributes, - getBlockType, getPhrasingContentSchema, } from '@wordpress/blocks'; import { RichText } from '@wordpress/editor'; @@ -133,8 +132,7 @@ export const settings = { const anchorElement = node.querySelector( 'a' ); const linkDestination = anchorElement && anchorElement.href ? 'custom' : undefined; const href = anchorElement && anchorElement.href ? anchorElement.href : undefined; - const blockType = getBlockType( 'core/image' ); - const attributes = getBlockAttributes( blockType, node.outerHTML, { align, id, linkDestination, href } ); + const attributes = getBlockAttributes( 'core/image', node.outerHTML, { align, id, linkDestination, href } ); return createBlock( 'core/image', attributes ); }, }, diff --git a/packages/block-library/src/list/index.js b/packages/block-library/src/list/index.js index f2c9485e2bccb..9fe52078e8993 100644 --- a/packages/block-library/src/list/index.js +++ b/packages/block-library/src/list/index.js @@ -12,7 +12,6 @@ import { createBlock, getPhrasingContentSchema, getBlockAttributes, - getBlockType, } from '@wordpress/blocks'; import { BlockControls, @@ -108,7 +107,7 @@ export const settings = { transform( node ) { return createBlock( 'core/list', { ...getBlockAttributes( - getBlockType( 'core/list' ), + 'core/list', node.outerHTML ), ordered: node.nodeName === 'OL', diff --git a/packages/blocks/CHANGELOG.md b/packages/blocks/CHANGELOG.md index f77cfe3b809a6..b3eaf4372a0de 100644 --- a/packages/blocks/CHANGELOG.md +++ b/packages/blocks/CHANGELOG.md @@ -1,3 +1,9 @@ +## 5.3.0 (Unreleased) + +### New feature + +- `getBlockAttributes`, `getBlockTransforms`, `getSaveContent`, `getSaveElement` and `isValidBlockContent` methods can now take also block's name as the first param ([#11490](https://github.com/WordPress/gutenberg/pull/11490)). Passing a block's type object continues to work as before. + ## 5.2.0 (2018-11-09) - Paste: Google Docs: fix nested formatting, sub, sup and del. diff --git a/packages/blocks/src/api/factory.js b/packages/blocks/src/api/factory.js index d59707e59bd89..f51dfb7e6576d 100644 --- a/packages/blocks/src/api/factory.js +++ b/packages/blocks/src/api/factory.js @@ -25,6 +25,7 @@ import { createHooks, applyFilters } from '@wordpress/hooks'; * Internal dependencies */ import { getBlockType, getBlockTypes } from './registration'; +import { normalizeBlockType } from './utils'; /** * Returns a block object given its type and attributes. @@ -279,13 +280,13 @@ export function findTransform( transforms, predicate ) { * transform object includes `blockName` as a property. * * @param {string} direction Transform direction ("to", "from"). - * @param {?string} blockName Optional block name. + * @param {string|Object} blockTypeOrName Block type or name. * * @return {Array} Block transforms for direction. */ -export function getBlockTransforms( direction, blockName ) { +export function getBlockTransforms( direction, blockTypeOrName ) { // When retrieving transforms for all block types, recurse into self. - if ( blockName === undefined ) { + if ( blockTypeOrName === undefined ) { return flatMap( getBlockTypes(), ( { name } ) => getBlockTransforms( direction, name ) @@ -293,7 +294,8 @@ export function getBlockTransforms( direction, blockName ) { } // Validate that block type exists and has array of direction. - const { transforms } = getBlockType( blockName ) || {}; + const blockType = normalizeBlockType( blockTypeOrName ); + const { name: blockName, transforms } = blockType || {}; if ( ! transforms || ! Array.isArray( transforms[ direction ] ) ) { return []; } diff --git a/packages/blocks/src/api/parser.js b/packages/blocks/src/api/parser.js index 3e9ce946409ab..597ceffff726c 100644 --- a/packages/blocks/src/api/parser.js +++ b/packages/blocks/src/api/parser.js @@ -23,6 +23,7 @@ import { createBlock } from './factory'; import { isValidBlockContent } from './validation'; import { getCommentDelimitedContent } from './serializer'; import { attr, html, text, query, node, children, prop } from './matchers'; +import { normalizeBlockType } from './utils'; /** * Sources which are guaranteed to return a string value. @@ -277,13 +278,14 @@ export function getBlockAttribute( attributeKey, attributeSchema, innerHTML, com /** * Returns the block attributes of a registered block node given its type. * - * @param {?Object} blockType Block type. - * @param {string} innerHTML Raw block content. - * @param {?Object} attributes Known block attributes (from delimiters). + * @param {string|Object} blockTypeOrName Block type or name. + * @param {string} innerHTML Raw block content. + * @param {?Object} attributes Known block attributes (from delimiters). * * @return {Object} All block attributes. */ -export function getBlockAttributes( blockType, innerHTML, attributes = {} ) { +export function getBlockAttributes( blockTypeOrName, innerHTML, attributes = {} ) { + const blockType = normalizeBlockType( blockTypeOrName ); const blockAttributes = mapValues( blockType.attributes, ( attributeSchema, attributeKey ) => { return getBlockAttribute( attributeKey, attributeSchema, innerHTML, attributes ); } ); diff --git a/packages/blocks/src/api/raw-handling/index.js b/packages/blocks/src/api/raw-handling/index.js index c38cdab46e0dc..db51f13e0fc6c 100644 --- a/packages/blocks/src/api/raw-handling/index.js +++ b/packages/blocks/src/api/raw-handling/index.js @@ -7,7 +7,6 @@ import { flatMap, filter, compact } from 'lodash'; * Internal dependencies */ import { createBlock, getBlockTransforms, findTransform } from '../factory'; -import { getBlockType } from '../registration'; import { getBlockContent } from '../serializer'; import { getBlockAttributes, parseWithGrammar } from '../parser'; import normaliseBlocks from './normalise-blocks'; @@ -103,7 +102,7 @@ function htmlToBlocks( { html, rawTransforms } ) { return createBlock( blockName, getBlockAttributes( - getBlockType( blockName ), + blockName, node.outerHTML ) ); diff --git a/packages/blocks/src/api/serializer.js b/packages/blocks/src/api/serializer.js index c3fc87e8f7023..443b68a14a593 100644 --- a/packages/blocks/src/api/serializer.js +++ b/packages/blocks/src/api/serializer.js @@ -18,6 +18,7 @@ import { getFreeformContentHandlerName, getUnregisteredTypeHandlerName, } from './registration'; +import { normalizeBlockType } from './utils'; import BlockContentProvider from '../block-content-provider'; /** @@ -54,13 +55,14 @@ export function getBlockMenuDefaultClassName( blockName ) { * Given a block type containing a save render implementation and attributes, returns the * enhanced element to be saved or string when raw HTML expected. * - * @param {Object} blockType Block type. - * @param {Object} attributes Block attributes. - * @param {?Array} innerBlocks Nested blocks. + * @param {string|Object} blockTypeOrName Block type or name. + * @param {Object} attributes Block attributes. + * @param {?Array} innerBlocks Nested blocks. * * @return {Object|string} Save element or raw HTML string. */ -export function getSaveElement( blockType, attributes, innerBlocks = [] ) { +export function getSaveElement( blockTypeOrName, attributes, innerBlocks = [] ) { + const blockType = normalizeBlockType( blockTypeOrName ); let { save } = blockType; // Component classes are unsupported for save since serialization must @@ -113,13 +115,15 @@ export function getSaveElement( blockType, attributes, innerBlocks = [] ) { * Given a block type containing a save render implementation and attributes, returns the * static markup to be saved. * - * @param {Object} blockType Block type. - * @param {Object} attributes Block attributes. - * @param {?Array} innerBlocks Nested blocks. + * @param {string|Object} blockTypeOrName Block type or name. + * @param {Object} attributes Block attributes. + * @param {?Array} innerBlocks Nested blocks. * * @return {string} Save content. */ -export function getSaveContent( blockType, attributes, innerBlocks ) { +export function getSaveContent( blockTypeOrName, attributes, innerBlocks ) { + const blockType = normalizeBlockType( blockTypeOrName ); + return renderToString( getSaveElement( blockType, attributes, innerBlocks ) ); } @@ -199,7 +203,6 @@ export function serializeAttributes( attributes ) { */ export function getBlockContent( block ) { // @todo why not getBlockInnerHtml? - const blockType = getBlockType( block.name ); // If block was parsed as invalid or encounters an error while generating // save content, use original content instead to avoid content loss. If a @@ -209,7 +212,7 @@ export function getBlockContent( block ) { let saveContent = block.originalContent; if ( block.isValid || block.innerBlocks.length ) { try { - saveContent = getSaveContent( blockType, block.attributes, block.innerBlocks ); + saveContent = getSaveContent( block.name, block.attributes, block.innerBlocks ); } catch ( error ) {} } diff --git a/packages/blocks/src/api/test/factory.js b/packages/blocks/src/api/test/factory.js index d8c46a857f90d..eaf222b8b5c86 100644 --- a/packages/blocks/src/api/test/factory.js +++ b/packages/blocks/src/api/test/factory.js @@ -15,7 +15,12 @@ import { getBlockTransforms, findTransform, } from '../factory'; -import { getBlockTypes, unregisterBlockType, registerBlockType } from '../registration'; +import { + getBlockType, + getBlockTypes, + registerBlockType, + unregisterBlockType, +} from '../registration'; describe( 'block factory', () => { const defaultBlockSettings = { @@ -1181,6 +1186,20 @@ describe( 'block factory', () => { }, ] ); } ); + + it( 'should return single block type transforms when passed as an object', () => { + const transforms = getBlockTransforms( + 'from', + getBlockType( 'core/transform-from-text-block-1' ) + ); + + expect( transforms ).toEqual( [ + { + blocks: [ 'core/text-block' ], + blockName: 'core/transform-from-text-block-1', + }, + ] ); + } ); } ); describe( 'findTransform', () => { diff --git a/packages/blocks/src/api/test/parser.js b/packages/blocks/src/api/test/parser.js index e55a2546a9bca..bc059542b0142 100644 --- a/packages/blocks/src/api/test/parser.js +++ b/packages/blocks/src/api/test/parser.js @@ -343,6 +343,26 @@ describe( 'block parser', () => { undefAmbiguousStringWithDefault: 'ok', } ); } ); + + it( 'should work when block type is passed as string', () => { + registerBlockType( 'core/meal', { + title: 'Meal', + category: 'widgets', + attributes: { + content: { + source: 'text', + selector: 'div', + }, + }, + save: () => {}, + } ); + + const innerHTML = '<div data-number="10">Ribs</div>'; + + expect( getBlockAttributes( 'core/meal', innerHTML ) ).toEqual( { + content: 'Ribs', + } ); + } ); } ); describe( 'getMigratedBlock', () => { diff --git a/packages/blocks/src/api/test/serializer.js b/packages/blocks/src/api/test/serializer.js index d0be732c60efb..a0a5eb2461d3b 100644 --- a/packages/blocks/src/api/test/serializer.js +++ b/packages/blocks/src/api/test/serializer.js @@ -39,17 +39,34 @@ describe( 'block serializer', () => { describe( 'getSaveContent()', () => { describe( 'function save', () => { + const fruitBlockSave = ( { attributes } ) => createElement( 'div', null, attributes.fruit ); + it( 'should return element as string if save returns element', () => { const saved = getSaveContent( { - save: ( { attributes } ) => createElement( 'div', null, attributes.fruit ), name: 'core/fruit', + save: fruitBlockSave, }, { fruit: 'Bananas' } ); expect( saved ).toBe( '<div>Bananas</div>' ); } ); + + it( 'should work when block type is passed as string', () => { + registerBlockType( 'core/fruit', { + title: 'Fruit', + category: 'widgets', + save: fruitBlockSave, + } ); + + const saved = getSaveContent( + 'core/fruit', + { fruit: 'Bananas' } + ); + + expect( saved ).toBe( '<div>Bananas</div>' ); + } ); } ); describe( 'component save', () => { diff --git a/packages/blocks/src/api/test/validation.js b/packages/blocks/src/api/test/validation.js index fcc02e490447d..0fd199f43b477 100644 --- a/packages/blocks/src/api/test/validation.js +++ b/packages/blocks/src/api/test/validation.js @@ -558,7 +558,7 @@ describe( 'validation', () => { registerBlockType( 'core/test-block', defaultBlockSettings ); const isValid = isValidBlockContent( - getBlockType( 'core/test-block' ), + 'core/test-block', { fruit: 'Bananas' }, 'Apples' ); @@ -577,7 +577,7 @@ describe( 'validation', () => { } ); const isValid = isValidBlockContent( - getBlockType( 'core/test-block' ), + 'core/test-block', { fruit: 'Bananas' }, 'Bananas' ); @@ -589,6 +589,18 @@ describe( 'validation', () => { it( 'returns true is block is valid', () => { registerBlockType( 'core/test-block', defaultBlockSettings ); + const isValid = isValidBlockContent( + 'core/test-block', + { fruit: 'Bananas' }, + 'Bananas' + ); + + expect( isValid ).toBe( true ); + } ); + + it( 'works also when block type object is passed as object', () => { + registerBlockType( 'core/test-block', defaultBlockSettings ); + const isValid = isValidBlockContent( getBlockType( 'core/test-block' ), { fruit: 'Bananas' }, diff --git a/packages/blocks/src/api/utils.js b/packages/blocks/src/api/utils.js index 810aa24278456..d5552a492aaa1 100644 --- a/packages/blocks/src/api/utils.js +++ b/packages/blocks/src/api/utils.js @@ -13,7 +13,7 @@ import { Component, isValidElement } from '@wordpress/element'; /** * Internal dependencies */ -import { getDefaultBlockName } from './registration'; +import { getBlockType, getDefaultBlockName } from './registration'; import { createBlock } from './factory'; /** @@ -104,3 +104,20 @@ export function normalizeIconObject( icon ) { return icon; } + +/** + * Normalizes block type passed as param. When string is passed then + * it converts it to the matching block type object. + * It passes the original object otherwise. + * + * @param {string|Object} blockTypeOrName Block type or name. + * + * @return {?Object} Block type. + */ +export function normalizeBlockType( blockTypeOrName ) { + if ( isString( blockTypeOrName ) ) { + return getBlockType( blockTypeOrName ); + } + + return blockTypeOrName; +} diff --git a/packages/blocks/src/api/validation.js b/packages/blocks/src/api/validation.js index a1ac55c1dbe48..5685756d7f4dc 100644 --- a/packages/blocks/src/api/validation.js +++ b/packages/blocks/src/api/validation.js @@ -13,6 +13,7 @@ import deprecated from '@wordpress/deprecated'; * Internal dependencies */ import { getSaveContent } from './serializer'; +import { normalizeBlockType } from './utils'; /** * Globally matches any consecutive whitespace @@ -508,13 +509,14 @@ export function isValidBlock( innerHTML, blockType, attributes ) { * * Logs to console in development environments when invalid. * - * @param {string} blockType Block type. - * @param {Object} attributes Parsed block attributes. - * @param {string} innerHTML Original block content. + * @param {string|Object} blockTypeOrName Block type. + * @param {Object} attributes Parsed block attributes. + * @param {string} innerHTML Original block content. * * @return {boolean} Whether block is valid. */ -export function isValidBlockContent( blockType, attributes, innerHTML ) { +export function isValidBlockContent( blockTypeOrName, attributes, innerHTML ) { + const blockType = normalizeBlockType( blockTypeOrName ); let saveContent; try { saveContent = getSaveContent( blockType, attributes ); diff --git a/packages/editor/src/components/block-compare/index.js b/packages/editor/src/components/block-compare/index.js index 87ec2208a8108..0782976519201 100644 --- a/packages/editor/src/components/block-compare/index.js +++ b/packages/editor/src/components/block-compare/index.js @@ -10,7 +10,7 @@ import { diffChars } from 'diff'; */ import { __ } from '@wordpress/i18n'; import { Component } from '@wordpress/element'; -import { getBlockType, getSaveContent, getSaveElement } from '@wordpress/blocks'; +import { getSaveContent, getSaveElement } from '@wordpress/blocks'; /** * Internal dependencies @@ -32,12 +32,9 @@ class BlockCompare extends Component { } getOriginalContent( block ) { - // Get current block details - const blockType = getBlockType( block.name ); - return { rawContent: block.originalContent, - renderedContent: getSaveElement( blockType, block.attributes ), + renderedContent: getSaveElement( block.name, block.attributes ), }; } @@ -46,8 +43,8 @@ class BlockCompare extends Component { const newBlocks = castArray( block ); // Get converted block details - const newContent = newBlocks.map( ( item ) => getSaveContent( getBlockType( item.name ), item.attributes, item.innerBlocks ) ); - const renderedContent = newBlocks.map( ( item ) => getSaveElement( getBlockType( item.name ), item.attributes, item.innerBlocks ) ); + const newContent = newBlocks.map( ( item ) => getSaveContent( item.name, item.attributes, item.innerBlocks ) ); + const renderedContent = newBlocks.map( ( item ) => getSaveElement( item.name, item.attributes, item.innerBlocks ) ); return { rawContent: newContent.join( '' ), diff --git a/packages/editor/src/components/inner-blocks/test/index.js b/packages/editor/src/components/inner-blocks/test/index.js index 6bca8a8d761ea..19bfd1bc383d7 100644 --- a/packages/editor/src/components/inner-blocks/test/index.js +++ b/packages/editor/src/components/inner-blocks/test/index.js @@ -3,7 +3,6 @@ */ import { createBlock, - getBlockType, getBlockTypes, getSaveElement, registerBlockType, @@ -52,7 +51,7 @@ describe( 'InnerBlocks', () => { const saved = renderToString( getSaveElement( - getBlockType( 'core/fruit' ), + 'core/fruit', { fruit: 'Bananas' }, [ createBlock( 'core/fruit', { fruit: 'Apples' } ) ], ) From b9c909f87b93c56309a6c6b02babc45c93026c0d Mon Sep 17 00:00:00 2001 From: Jorge <jorge.costa@developer.pt> Date: Fri, 9 Nov 2018 08:14:25 +0000 Subject: [PATCH 055/106] Always show block icon on the block switcher when a single block is selected (#11600) --- .../src/components/block-switcher/index.js | 15 +++++++++++++- .../src/components/block-switcher/style.scss | 20 +++++++++++++++---- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/packages/editor/src/components/block-switcher/index.js b/packages/editor/src/components/block-switcher/index.js index d298834293471..292dacf98567e 100644 --- a/packages/editor/src/components/block-switcher/index.js +++ b/packages/editor/src/components/block-switcher/index.js @@ -58,7 +58,20 @@ export class BlockSwitcher extends Component { const hasStyles = blocks.length === 1 && get( blockType, [ 'styles' ], [] ).length !== 0; if ( ! hasStyles && ! possibleBlockTransformations.length ) { - return null; + if ( blocks.length > 1 ) { + return null; + } + return ( + <Toolbar> + <IconButton + disabled + className="editor-block-switcher__no-switcher-icon" + label={ __( 'Block icon' ) } + > + <BlockIcon icon={ blockType.icon } showColors /> + </IconButton> + </Toolbar> + ); } return ( diff --git a/packages/editor/src/components/block-switcher/style.scss b/packages/editor/src/components/block-switcher/style.scss index 503b312d3b647..daebe5a47ea72 100644 --- a/packages/editor/src/components/block-switcher/style.scss +++ b/packages/editor/src/components/block-switcher/style.scss @@ -3,15 +3,27 @@ height: $icon-button-size; } -// Style this the same as the block buttons in the library. -// Needs specificiity to override the icon button. -.components-icon-button.editor-block-switcher__toggle { - width: auto; +.components-icon-button.editor-block-switcher__toggle, +.components-icon-button.editor-block-switcher__no-switcher-icon { margin: 0; display: block; height: $icon-button-size; padding: 3px; +} + +.components-icon-button.editor-block-switcher__no-switcher-icon { + width: $icon-button-size + 6px + 6px; + .editor-block-icon { + margin-right: auto; + margin-left: auto; + } +} + +// Style this the same as the block buttons in the library. +// Needs specificiity to override the icon button. +.components-icon-button.editor-block-switcher__toggle { + width: auto; // Unset icon button styles. &:active, &:not(:disabled):not([aria-disabled="true"]):hover, From b814d88612b7c1d4452508aeb9906c98605ca723 Mon Sep 17 00:00:00 2001 From: Andrew Ozz <azaozz@users.noreply.github.com> Date: Fri, 9 Nov 2018 10:17:15 +0200 Subject: [PATCH 056/106] Make the kitchensink button removable from plugins (#10964) --- lib/client-assets.php | 40 ++++++++++------------ packages/block-library/src/classic/edit.js | 9 +++++ 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/lib/client-assets.php b/lib/client-assets.php index 3450b21856b07..9bb0180c92963 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -598,28 +598,26 @@ function gutenberg_register_scripts_and_styles() { ), 'toolbar1' => implode( ',', - array_merge( - apply_filters( - 'mce_buttons', - array( - 'formatselect', - 'bold', - 'italic', - 'bullist', - 'numlist', - 'blockquote', - 'alignleft', - 'aligncenter', - 'alignright', - 'link', - 'unlink', - 'wp_more', - 'spellchecker', - 'wp_add_media', - ), - 'editor' + apply_filters( + 'mce_buttons', + array( + 'formatselect', + 'bold', + 'italic', + 'bullist', + 'numlist', + 'blockquote', + 'alignleft', + 'aligncenter', + 'alignright', + 'link', + 'unlink', + 'wp_more', + 'spellchecker', + 'wp_add_media', + 'kitchensink', ), - array( 'kitchensink' ) + 'editor' ) ), 'toolbar2' => implode( diff --git a/packages/block-library/src/classic/edit.js b/packages/block-library/src/classic/edit.js index 6bede6a2df318..97307e9119553 100644 --- a/packages/block-library/src/classic/edit.js +++ b/packages/block-library/src/classic/edit.js @@ -115,6 +115,7 @@ export default class ClassicEdit extends Component { } } ); + // TODO: the following is for back-compat with WP 4.9, not needed in WP 5.0. Remove it after the release. editor.addButton( 'kitchensink', { tooltip: _x( 'More', 'button to expand options' ), icon: 'dashicon dashicons-editor-kitchensink', @@ -127,11 +128,19 @@ export default class ClassicEdit extends Component { }, } ); + // Show the second, third, etc. toolbars when the `kitchensink` button is removed by a plugin. + editor.on( 'init', function() { + if ( editor.settings.toolbar1 && editor.settings.toolbar1.indexOf( 'kitchensink' ) === -1 ) { + editor.dom.addClass( ref, 'has-advanced-toolbar' ); + } + } ); + editor.addButton( 'wp_add_media', { tooltip: __( 'Insert Media' ), icon: 'dashicon dashicons-admin-media', cmd: 'WP_Medialib', } ); + // End TODO. editor.on( 'init', () => { const rootNode = this.editor.getBody(); From 8e396e793a37c921cd5cb68716a1d4962690cb5c Mon Sep 17 00:00:00 2001 From: Joen Asmussen <joen@automattic.com> Date: Fri, 9 Nov 2018 09:41:36 +0100 Subject: [PATCH 057/106] Try: Improve columns block (#11620) * Try: Improve columns block This PR aims to improve the column block usability, as well as fix a few issues: 1. It makes the individual columns unclickable by using pointer events. Since these are not actionable yet, being able to select them is not really helpful. 2. It improves the UI around reusable blocks, by fixing an issue where even though contents should be disabled when in a reusable block, it was still selectable if there were nested blocks. 3. It further improves the reusable block UI by fixing a regression where the reusable block editing bar did not push down content sufficiently. 4. It refactors how margins in the columns block work, to be simpler and more predictable, and makes it behave similar to every other block in that regard. * Improve the margins of an empty colums block. --- assets/stylesheets/_z-index.scss | 3 ++ .../src/block/edit-panel/style.scss | 4 ++- .../block-library/src/columns/editor.scss | 28 +++++++++++-------- packages/components/src/disabled/style.scss | 5 ++++ .../src/components/block-list/style.scss | 7 +---- 5 files changed, 29 insertions(+), 18 deletions(-) diff --git a/assets/stylesheets/_z-index.scss b/assets/stylesheets/_z-index.scss index b494e9f6fdc50..39e1bb461d46b 100644 --- a/assets/stylesheets/_z-index.scss +++ b/assets/stylesheets/_z-index.scss @@ -37,6 +37,9 @@ $z-layers: ( // Active pill button ".components-button.is-button {:focus or .is-primary}": 1, + // Reusable blocks UI, needs to be above sibling inserter. + ".editor-block-list__layout .reusable-block-edit-panel": 7, + // The draggable element should show up above the entire UI ".components-draggable__clone": 1000000000, diff --git a/packages/block-library/src/block/edit-panel/style.scss b/packages/block-library/src/block/edit-panel/style.scss index ff73c68fee7e3..854cfe03f282f 100644 --- a/packages/block-library/src/block/edit-panel/style.scss +++ b/packages/block-library/src/block/edit-panel/style.scss @@ -8,8 +8,10 @@ font-size: $default-font-size; position: relative; top: -$block-padding; - margin: 0 (-$block-padding) (-$block-padding) (-$block-padding); + margin: 0 (-$block-padding); padding: $grid-size $block-padding; + position: relative; + z-index: z-index(".editor-block-list__layout .reusable-block-edit-panel"); // Show a smaller padding when nested. .editor-block-list__layout & { diff --git a/packages/block-library/src/columns/editor.scss b/packages/block-library/src/columns/editor.scss index ebe614483c4a5..a96a6dcf8c1ea 100644 --- a/packages/block-library/src/columns/editor.scss +++ b/packages/block-library/src/columns/editor.scss @@ -25,15 +25,6 @@ } } -// This block has flex container children. Flex container margins do not collapse: https://www.w3.org/TR/css-flexbox-1/#flex-containers. -// Therefore, let's at least not add any additional margins here. -// The goal is for the editor to look more like the front-end. -.editor-block-list__layout .editor-block-list__block[data-type="core/columns"] > .editor-block-list__block-edit, -.editor-block-list__layout .editor-block-list__block[data-type="core/column"] > .editor-block-list__block-edit { - margin-top: 0; - margin-bottom: 0; -} - .wp-block-columns { display: block; @@ -55,8 +46,13 @@ // The Column block is a child of the Columns block and is mostly a passthrough container. // Therefore it shouldn't add additional paddings and margins, so we reset these, and compensate for margins. // @todo In the future, if a passthrough feature lands, it would be good to apply these rules to such an element in a more generic way. - margin-top: -$block-padding; - margin-bottom: -$block-padding; + margin-top: -$block-padding - $block-padding; + margin-bottom: -$block-padding - $block-padding; + + > .editor-block-list__block-edit { + margin-top: 0; + margin-bottom: 0; + } // On mobile, only a single column is shown, so match adjacent block paddings. padding-left: 0; @@ -130,3 +126,13 @@ } } } + +// In absence of making the individual columns resizable, we prevent them from being clickable. +// This makes them less fiddly. This will be revisited as the interface is refined. +.wp-block-columns [data-type="core/column"] { + pointer-events: none; +} + +:not(.components-disabled) > .wp-block-columns > .editor-inner-blocks > .editor-block-list__layout > [data-type="core/column"] > .editor-block-list__block-edit > .editor-inner-blocks { + pointer-events: all; +} diff --git a/packages/components/src/disabled/style.scss b/packages/components/src/disabled/style.scss index 8ca29bb801e15..77e0dab63a0b8 100644 --- a/packages/components/src/disabled/style.scss +++ b/packages/components/src/disabled/style.scss @@ -10,4 +10,9 @@ bottom: 0; left: 0; } + + // Also make nested blocks unselectable. + * { + pointer-events: none; + } } diff --git a/packages/editor/src/components/block-list/style.scss b/packages/editor/src/components/block-list/style.scss index c66a202741175..28632c0eda60a 100644 --- a/packages/editor/src/components/block-list/style.scss +++ b/packages/editor/src/components/block-list/style.scss @@ -57,14 +57,9 @@ margin-right: -$block-padding; } - // Adjust the spacing of the appender to match text blocks. - .editor-default-block-appender > .editor-default-block-appender__content { - margin-top: $block-padding * 2 + $block-spacing; - } - // Space every block, and the default appender, using margins. // This allows margins to collapse, which gives a better representation of how it looks on the frontend. - > .editor-default-block-appender__content, + .editor-default-block-appender > .editor-default-block-appender__content, > .editor-block-list__block > .editor-block-list__block-edit, > .editor-block-list__layout > .editor-block-list__block > .editor-block-list__block-edit { margin-top: $block-padding * 2 + $block-spacing; From 24e37f1dc6cca928e2a82eedd9df0a0efdc83d67 Mon Sep 17 00:00:00 2001 From: Maedah Batool <MaedahBatool@users.noreply.github.com> Date: Fri, 9 Nov 2018 13:44:18 +0500 Subject: [PATCH 058/106] =?UTF-8?q?=F0=9F=90=9B=20FIX:=20Grammatical=20Err?= =?UTF-8?q?or=20(#11656)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix a tiny-miny grammatical error in the CONTRIBUTORS.md file. --- CONTRIBUTORS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 3b526bb045107..e3dd47a328047 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -2,7 +2,7 @@ Gutenberg is built by many contributors and volunteers. Thanks to all of them for their work! -This list is manually curated to include valuable contributions by volunteers that do not include code, such as user testing, providing feedback, or mockups. Please edit this list to include new contributors as they come in. There is no particular order to this list. If you or someone else were omitted from this list, we assure you that was not intentional. Please let us know and we'll add you. For volunteers who contributed their translations, your names are listed on [WordPress Translate site here](https://translate.wordpress.org/projects/wp-plugins/gutenberg/contributors). +This list is manually curated to include valuable contributions by volunteers that do not include code, such as user testing, providing feedback, or mockups. Please edit this list to include new contributors as they come in. There is no particular order to this list. If you or someone else was omitted from this list, we assure you that was not intentional. Please let us know and we'll add you. For volunteers who contributed their translations, your names are listed on [WordPress Translate site here](https://translate.wordpress.org/projects/wp-plugins/gutenberg/contributors). | GitHub Username | WordPress.org Username| | --------------- | --------------------- | From 5531bd14abfe6b1366e3aa913f98311f824e4ff8 Mon Sep 17 00:00:00 2001 From: Pinar Olguc <pinarolguc@gmail.com> Date: Fri, 9 Nov 2018 12:01:22 +0300 Subject: [PATCH 059/106] Update media url only if it exists in image block(gb-mobile) (#11635) * Update media url only if it exists This is for handling situations where user just presses Cancel on the media picker. We don't want the image block to update in that case. * Fix lint issues --- packages/block-library/src/image/edit.native.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/block-library/src/image/edit.native.js b/packages/block-library/src/image/edit.native.js index d339d077eb572..ab7af710e4e6d 100644 --- a/packages/block-library/src/image/edit.native.js +++ b/packages/block-library/src/image/edit.native.js @@ -22,7 +22,11 @@ export default function ImageEdit( props ) { const onMediaLibraryPress = () => { // Call onMediaLibraryPress from the Native<->RN bridge. It should trigger an image picker from // the WordPress media library and call the provided callback to set the image URL. - RNReactNativeGutenbergBridge.onMediaLibraryPress( ( mediaUrl ) => setAttributes( { url: mediaUrl } ) ); + RNReactNativeGutenbergBridge.onMediaLibraryPress( ( mediaUrl ) => { + if ( mediaUrl ) { + setAttributes( { url: mediaUrl } ); + } + } ); }; if ( ! url ) { From 448d9f47abf6d78728aec14cf3acf262de6b78c1 Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Fri, 9 Nov 2018 10:54:49 +0100 Subject: [PATCH 060/106] Update the block styles registration to avoid timing issues (#11532) --- docs/data/data-core-blocks.md | 31 +++++++ packages/blocks/CHANGELOG.md | 3 +- packages/blocks/src/api/registration.js | 29 +------ packages/blocks/src/api/test/registration.js | 86 ------------------- packages/blocks/src/store/actions.js | 32 +++++++ packages/blocks/src/store/reducer.js | 50 ++++++++++- packages/blocks/src/store/selectors.js | 12 +++ packages/blocks/src/store/test/reducer.js | 84 +++++++++++++++++- packages/editor/CHANGELOG.md | 6 ++ .../src/components/block-inspector/index.js | 9 +- .../src/components/block-styles/index.js | 11 +-- .../src/components/block-switcher/index.js | 16 ++-- 12 files changed, 239 insertions(+), 130 deletions(-) diff --git a/docs/data/data-core-blocks.md b/docs/data/data-core-blocks.md index 9a2524905fa40..aea753fad4757 100644 --- a/docs/data/data-core-blocks.md +++ b/docs/data/data-core-blocks.md @@ -23,6 +23,19 @@ Returns a block type by name. Block Type. +### getBlockStyles + +Returns block styles by block name. + +*Parameters* + + * state: Data state. + * name: Block type name. + +*Returns* + +Block Styles. + ### getCategories Returns all the available categories. @@ -161,6 +174,24 @@ Returns an action object used to remove a registered block type. * names: Block name. +### addBlockStyles + +Returns an action object used in signalling that new block styles have been added. + +*Parameters* + + * blockName: Block name. + * styles: Block styles. + +### removeBlockStyles + +Returns an action object used in signalling that block styles have been removed. + +*Parameters* + + * blockName: Block name. + * styleNames: Block style names. + ### setDefaultBlockName Returns an action object used to set the default block name. diff --git a/packages/blocks/CHANGELOG.md b/packages/blocks/CHANGELOG.md index b3eaf4372a0de..d0e0f71f43e36 100644 --- a/packages/blocks/CHANGELOG.md +++ b/packages/blocks/CHANGELOG.md @@ -3,6 +3,7 @@ ### New feature - `getBlockAttributes`, `getBlockTransforms`, `getSaveContent`, `getSaveElement` and `isValidBlockContent` methods can now take also block's name as the first param ([#11490](https://github.com/WordPress/gutenberg/pull/11490)). Passing a block's type object continues to work as before. +- `registerBlockStyles` and `unregisterBlockStyles` can be triggered at any moment (before or after block registration). ## 5.2.0 (2018-11-09) @@ -16,7 +17,7 @@ ## 5.1.0 (2018-10-30) -### New feature +### New features - `isValidBlockContent` function has been added ([#10891](https://github.com/WordPress/gutenberg/pull/10891)). diff --git a/packages/blocks/src/api/registration.js b/packages/blocks/src/api/registration.js index 8c56373da24b4..d0d21a897c526 100644 --- a/packages/blocks/src/api/registration.js +++ b/packages/blocks/src/api/registration.js @@ -3,12 +3,12 @@ /** * External dependencies */ -import { get, isFunction, some, reject } from 'lodash'; +import { get, isFunction, some } from 'lodash'; /** * WordPress dependencies */ -import { applyFilters, addFilter } from '@wordpress/hooks'; +import { applyFilters } from '@wordpress/hooks'; import { select, dispatch } from '@wordpress/data'; /** @@ -329,19 +329,7 @@ export const hasChildBlocksWithInserterSupport = ( blockName ) => { * @param {Object} styleVariation Object containing `name` which is the class name applied to the block and `label` which identifies the variation to the user. */ export const registerBlockStyle = ( blockName, styleVariation ) => { - addFilter( 'blocks.registerBlockType', `${ blockName }/${ styleVariation.name }`, ( settings, name ) => { - if ( blockName !== name ) { - return settings; - } - - return { - ...settings, - styles: [ - ...get( settings, [ 'styles' ], [] ), - styleVariation, - ], - }; - } ); + dispatch( 'core/blocks' ).addBlockStyles( blockName, styleVariation ); }; /** @@ -351,14 +339,5 @@ export const registerBlockStyle = ( blockName, styleVariation ) => { * @param {string} styleVariationName Name of class applied to the block. */ export const unregisterBlockStyle = ( blockName, styleVariationName ) => { - addFilter( 'blocks.registerBlockType', `${ blockName }/${ styleVariationName }/unregister`, ( settings, name ) => { - if ( blockName !== name ) { - return settings; - } - - return { - ...settings, - styles: reject( get( settings, [ 'styles' ], [] ), { name: styleVariationName } ), - }; - } ); + dispatch( 'core/blocks' ).removeBlockStyles( blockName, styleVariationName ); }; diff --git a/packages/blocks/src/api/test/registration.js b/packages/blocks/src/api/test/registration.js index 047fa53a4fb47..e881943d5168e 100644 --- a/packages/blocks/src/api/test/registration.js +++ b/packages/blocks/src/api/test/registration.js @@ -28,8 +28,6 @@ import { hasBlockSupport, isReusableBlock, unstable__bootstrapServerSideBlockDefinitions, // eslint-disable-line camelcase - registerBlockStyle, - unregisterBlockStyle, } from '../registration'; describe( 'blocks', () => { @@ -584,88 +582,4 @@ describe( 'blocks', () => { expect( isReusableBlock( block ) ).toBe( false ); } ); } ); - - describe( 'registerBlockStyle', () => { - afterEach( () => { - removeFilter( 'blocks.registerBlockType', 'my-plugin/block-without-styles/big' ); - removeFilter( 'blocks.registerBlockType', 'my-plugin/block-without-styles/small' ); - } ); - - it( 'should add styles', () => { - registerBlockStyle( 'my-plugin/block-without-styles', { name: 'big', label: 'Big style' } ); - const settings = registerBlockType( 'my-plugin/block-without-styles', defaultBlockSettings ); - - expect( settings.styles ).toEqual( [ - { name: 'big', label: 'Big style' }, - ] ); - } ); - - it( 'should accumulate styles', () => { - registerBlockStyle( 'my-plugin/block-without-styles', { name: 'small', label: 'Small style' } ); - registerBlockStyle( 'my-plugin/block-without-styles', { name: 'big', label: 'Big style' } ); - const settings = registerBlockType( 'my-plugin/block-without-styles', { - ...defaultBlockSettings, - styles: [ { name: 'normal', label: 'Normal style' } ], - } ); - - expect( settings.styles ).toEqual( [ - { name: 'normal', label: 'Normal style' }, - { name: 'small', label: 'Small style' }, - { name: 'big', label: 'Big style' }, - ] ); - } ); - } ); - - describe( 'unregisterBlockStyle', () => { - afterEach( () => { - removeFilter( 'blocks.registerBlockType', 'my-plugin/block-with-styles/big/unregister' ); - removeFilter( 'blocks.registerBlockType', 'my-plugin/block-with-styles/small/unregister' ); - removeFilter( 'blocks.registerBlockType', 'my-plugin/block-with-styles/big' ); - removeFilter( 'blocks.registerBlockType', 'my-plugin/block-with-styles/small' ); - } ); - - it( 'should remove styles', () => { - unregisterBlockStyle( 'my-plugin/block-with-styles', 'big' ); - const settings = registerBlockType( 'my-plugin/block-with-styles', { - ...defaultBlockSettings, - styles: [ { name: 'big', label: 'Big style' } ], - } ); - - expect( settings.styles ).toEqual( [] ); - } ); - - it( 'should keep other styles', () => { - unregisterBlockStyle( 'my-plugin/block-with-styles', 'small' ); - const settings = registerBlockType( 'my-plugin/block-with-styles', { - ...defaultBlockSettings, - styles: [ - { name: 'normal', label: 'Normal style' }, - { name: 'small', label: 'Small style' }, - { name: 'big', label: 'Big style' }, - ], - } ); - - expect( settings.styles ).toEqual( [ - { name: 'normal', label: 'Normal style' }, - { name: 'big', label: 'Big style' }, - ] ); - } ); - - it( 'should remove a prior registerBlockStyle', () => { - registerBlockStyle( 'my-plugin/block-with-styles', { name: 'big', label: 'Big style' } ); - registerBlockStyle( 'my-plugin/block-with-styles', { name: 'small', label: 'Small style' } ); - unregisterBlockStyle( 'my-plugin/block-with-styles', 'big' ); - const settings = registerBlockType( 'my-plugin/block-with-styles', { - ...defaultBlockSettings, - styles: [ - { name: 'normal', label: 'Normal style' }, - ], - } ); - - expect( settings.styles ).toEqual( [ - { name: 'normal', label: 'Normal style' }, - { name: 'small', label: 'Small style' }, - ] ); - } ); - } ); } ); diff --git a/packages/blocks/src/store/actions.js b/packages/blocks/src/store/actions.js index c2da126f4458d..bef332bb2b5fc 100644 --- a/packages/blocks/src/store/actions.js +++ b/packages/blocks/src/store/actions.js @@ -31,6 +31,38 @@ export function removeBlockTypes( names ) { }; } +/** + * Returns an action object used in signalling that new block styles have been added. + * + * @param {string} blockName Block name. + * @param {Array|Object} styles Block styles. + * + * @return {Object} Action object. + */ +export function addBlockStyles( blockName, styles ) { + return { + type: 'ADD_BLOCK_STYLES', + styles: castArray( styles ), + blockName, + }; +} + +/** + * Returns an action object used in signalling that block styles have been removed. + * + * @param {string} blockName Block name. + * @param {Array|string} styleNames Block style names. + * + * @return {Object} Action object. + */ +export function removeBlockStyles( blockName, styleNames ) { + return { + type: 'REMOVE_BLOCK_STYLES', + styleNames: castArray( styleNames ), + blockName, + }; +} + /** * Returns an action object used to set the default block name. * diff --git a/packages/blocks/src/store/reducer.js b/packages/blocks/src/store/reducer.js index e1ed669e0b217..0ce2c1e88ef3f 100644 --- a/packages/blocks/src/store/reducer.js +++ b/packages/blocks/src/store/reducer.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { keyBy, omit } from 'lodash'; +import { keyBy, omit, mapValues, get, uniqBy, filter, map } from 'lodash'; /** * WordPress dependencies @@ -34,7 +34,10 @@ export function blockTypes( state = {}, action ) { case 'ADD_BLOCK_TYPES': return { ...state, - ...keyBy( action.blockTypes, 'name' ), + ...keyBy( + map( action.blockTypes, ( blockType ) => omit( blockType, 'styles ' ) ), + 'name' + ), }; case 'REMOVE_BLOCK_TYPES': return omit( state, action.names ); @@ -43,6 +46,47 @@ export function blockTypes( state = {}, action ) { return state; } +/** + * Reducer managing the block style variations. + * + * @param {Object} state Current state. + * @param {Object} action Dispatched action. + * + * @return {Object} Updated state. + */ +export function blockStyles( state = {}, action ) { + switch ( action.type ) { + case 'ADD_BLOCK_TYPES': + return { + ...state, + ...mapValues( keyBy( action.blockTypes, 'name' ), ( blockType ) => { + return uniqBy( [ + ...get( blockType, [ 'styles' ], [] ), + ...get( state, [ blockType.name ], [] ), + ], ( style ) => style.name ); + } ), + }; + case 'ADD_BLOCK_STYLES': + return { + ...state, + [ action.blockName ]: uniqBy( [ + ...get( state, [ action.blockName ], [] ), + ...( action.styles ), + ], ( style ) => style.name ), + }; + case 'REMOVE_BLOCK_STYLES': + return { + ...state, + [ action.blockName ]: filter( + get( state, [ action.blockName ], [] ), + ( style ) => action.styleNames.indexOf( style.name ) === -1, + ), + }; + } + + return state; +} + /** * Higher-order Reducer creating a reducer keeping track of given block name. * @@ -68,7 +112,6 @@ export function createBlockNameSetterReducer( setActionType ) { } export const defaultBlockName = createBlockNameSetterReducer( 'SET_DEFAULT_BLOCK_NAME' ); - export const freeformFallbackBlockName = createBlockNameSetterReducer( 'SET_FREEFORM_FALLBACK_BLOCK_NAME' ); export const unregisteredFallbackBlockName = createBlockNameSetterReducer( 'SET_UNREGISTERED_FALLBACK_BLOCK_NAME' ); @@ -90,6 +133,7 @@ export function categories( state = DEFAULT_CATEGORIES, action ) { export default combineReducers( { blockTypes, + blockStyles, defaultBlockName, freeformFallbackBlockName, unregisteredFallbackBlockName, diff --git a/packages/blocks/src/store/selectors.js b/packages/blocks/src/store/selectors.js index 6f36f8fed2992..9ae3373cbd5dc 100644 --- a/packages/blocks/src/store/selectors.js +++ b/packages/blocks/src/store/selectors.js @@ -30,6 +30,18 @@ export function getBlockType( state, name ) { return state.blockTypes[ name ]; } +/** + * Returns block styles by block name. + * + * @param {Object} state Data state. + * @param {string} name Block type name. + * + * @return {Array?} Block Styles. + */ +export function getBlockStyles( state, name ) { + return state.blockStyles[ name ]; +} + /** * Returns all the available categories. * diff --git a/packages/blocks/src/store/test/reducer.js b/packages/blocks/src/store/test/reducer.js index 9c3e6c5d1d1dc..dcd2a15c18e08 100644 --- a/packages/blocks/src/store/test/reducer.js +++ b/packages/blocks/src/store/test/reducer.js @@ -7,6 +7,7 @@ import deepFreeze from 'deep-freeze'; * Internal dependencies */ import { + blockStyles, blockTypes, categories, defaultBlockName, @@ -20,7 +21,7 @@ describe( 'blockTypes', () => { expect( blockTypes( undefined, {} ) ).toEqual( {} ); } ); - it( 'should add add a new block type', () => { + it( 'should add a new block type', () => { const original = deepFreeze( { 'core/paragraph': { name: 'core/paragraph' }, } ); @@ -53,6 +54,87 @@ describe( 'blockTypes', () => { } ); } ); +describe( 'blockStyles', () => { + it( 'should return an empty object as default state', () => { + expect( blockStyles( undefined, {} ) ).toEqual( {} ); + } ); + + it( 'should add a new block styles', () => { + const original = deepFreeze( {} ); + + let state = blockStyles( original, { + type: 'ADD_BLOCK_STYLES', + blockName: 'core/image', + styles: [ { name: 'fancy' } ], + } ); + + expect( state ).toEqual( { + 'core/image': [ + { name: 'fancy' }, + ], + } ); + + state = blockStyles( state, { + type: 'ADD_BLOCK_STYLES', + blockName: 'core/image', + styles: [ { name: 'lightbox' } ], + } ); + + expect( state ).toEqual( { + 'core/image': [ + { name: 'fancy' }, + { name: 'lightbox' }, + ], + } ); + } ); + + it( 'should add block styles when adding a block', () => { + const original = deepFreeze( { + 'core/image': [ + { name: 'fancy' }, + ], + } ); + + const state = blockStyles( original, { + type: 'ADD_BLOCK_TYPES', + blockTypes: [ { + name: 'core/image', + styles: [ + { name: 'original' }, + ], + } ], + } ); + + expect( state ).toEqual( { + 'core/image': [ + { name: 'original' }, + { name: 'fancy' }, + ], + } ); + } ); + + it( 'should remove block styles', () => { + const original = deepFreeze( { + 'core/image': [ + { name: 'fancy' }, + { name: 'lightbox' }, + ], + } ); + + const state = blockStyles( original, { + type: 'REMOVE_BLOCK_STYLES', + blockName: 'core/image', + styleNames: [ 'fancy' ], + } ); + + expect( state ).toEqual( { + 'core/image': [ + { name: 'lightbox' }, + ], + } ); + } ); +} ); + describe( 'defaultBlockName', () => { it( 'should return null as default state', () => { expect( defaultBlockName( undefined, {} ) ).toBeNull(); diff --git a/packages/editor/CHANGELOG.md b/packages/editor/CHANGELOG.md index 38da9bc69d69a..9379ec67de996 100644 --- a/packages/editor/CHANGELOG.md +++ b/packages/editor/CHANGELOG.md @@ -1,3 +1,9 @@ +## 6.2.1 (Unreleased) + +### Polish + +- Reactive block styles. + ## 6.2.0 (2018-11-09) ### New Features diff --git a/packages/editor/src/components/block-inspector/index.js b/packages/editor/src/components/block-inspector/index.js index 7f2fc1c4654f8..1b51fedac4a1a 100644 --- a/packages/editor/src/components/block-inspector/index.js +++ b/packages/editor/src/components/block-inspector/index.js @@ -21,7 +21,7 @@ import InspectorControls from '../inspector-controls'; import InspectorAdvancedControls from '../inspector-advanced-controls'; import BlockStyles from '../block-styles'; -const BlockInspector = ( { selectedBlock, blockType, count } ) => { +const BlockInspector = ( { selectedBlock, blockType, count, hasBlockStyles } ) => { if ( count > 1 ) { return <span className="editor-block-inspector__multi-blocks">{ __( 'Coming Soon' ) }</span>; } @@ -46,7 +46,7 @@ const BlockInspector = ( { selectedBlock, blockType, count } ) => { <div className="editor-block-inspector__card-description">{ blockType.description }</div> </div> </div> - { !! blockType.styles && ( + { hasBlockStyles && ( <div> <PanelBody title={ __( 'Styles' ) } @@ -80,12 +80,15 @@ const BlockInspector = ( { selectedBlock, blockType, count } ) => { export default withSelect( ( select ) => { const { getSelectedBlock, getSelectedBlockCount } = select( 'core/editor' ); + const { getBlockStyles } = select( 'core/blocks' ); const selectedBlock = getSelectedBlock(); const blockType = selectedBlock && getBlockType( selectedBlock.name ); + const blockStyles = selectedBlock && getBlockStyles( selectedBlock.name ); return { + count: getSelectedBlockCount(), + hasBlockStyles: blockStyles && blockStyles.length > 0, selectedBlock, blockType, - count: getSelectedBlockCount(), }; } )( BlockInspector ); diff --git a/packages/editor/src/components/block-styles/index.js b/packages/editor/src/components/block-styles/index.js index 5c7c496d0ab61..6892df08c6d06 100644 --- a/packages/editor/src/components/block-styles/index.js +++ b/packages/editor/src/components/block-styles/index.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { find, get, noop } from 'lodash'; +import { find, noop } from 'lodash'; import classnames from 'classnames'; /** @@ -9,7 +9,6 @@ import classnames from 'classnames'; */ import { compose } from '@wordpress/compose'; import { withSelect, withDispatch } from '@wordpress/data'; -import { getBlockType } from '@wordpress/blocks'; import TokenList from '@wordpress/token-list'; import { ENTER, SPACE } from '@wordpress/keycodes'; @@ -72,7 +71,7 @@ function BlockStyles( { onSwitch = noop, onHoverClassName = noop, } ) { - if ( ! styles ) { + if ( ! styles || styles.length === 0 ) { return null; } @@ -129,13 +128,15 @@ function BlockStyles( { export default compose( [ withSelect( ( select, { clientId } ) => { - const block = select( 'core/editor' ).getBlock( clientId ); + const { getBlock } = select( 'core/editor' ); + const { getBlockStyles } = select( 'core/blocks' ); + const block = getBlock( clientId ); return { name: block.name, attributes: block.attributes, className: block.attributes.className || '', - styles: get( getBlockType( block.name ), [ 'styles' ] ), + styles: getBlockStyles( block.name ), }; } ), withDispatch( ( dispatch, { clientId } ) => { diff --git a/packages/editor/src/components/block-switcher/index.js b/packages/editor/src/components/block-switcher/index.js index 292dacf98567e..390e6a7f32137 100644 --- a/packages/editor/src/components/block-switcher/index.js +++ b/packages/editor/src/components/block-switcher/index.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { castArray, filter, first, get, mapKeys, orderBy } from 'lodash'; +import { castArray, filter, first, mapKeys, orderBy } from 'lodash'; /** * WordPress dependencies @@ -36,7 +36,7 @@ export class BlockSwitcher extends Component { } render() { - const { blocks, onTransform, inserterItems } = this.props; + const { blocks, onTransform, inserterItems, hasBlockStyles } = this.props; const { hoveredClassName } = this.state; if ( ! blocks || ! blocks.length ) { @@ -55,9 +55,8 @@ export class BlockSwitcher extends Component { const sourceBlockName = blocks[ 0 ].name; const blockType = getBlockType( sourceBlockName ); - const hasStyles = blocks.length === 1 && get( blockType, [ 'styles' ], [] ).length !== 0; - if ( ! hasStyles && ! possibleBlockTransformations.length ) { + if ( ! hasBlockStyles && ! possibleBlockTransformations.length ) { if ( blocks.length > 1 ) { return null; } @@ -119,7 +118,7 @@ export class BlockSwitcher extends Component { } } renderContent={ ( { onClose } ) => ( <Fragment> - { hasStyles && + { hasBlockStyles && <PanelBody title={ __( 'Block Styles' ) } initialOpen @@ -167,10 +166,15 @@ export class BlockSwitcher extends Component { export default compose( withSelect( ( select, { clientIds } ) => { const { getBlocksByClientId, getBlockRootClientId, getInserterItems } = select( 'core/editor' ); + const { getBlockStyles } = select( 'core/blocks' ); const rootClientId = getBlockRootClientId( first( castArray( clientIds ) ) ); + const blocks = getBlocksByClientId( clientIds ); + const firstBlock = blocks && blocks.length === 1 ? blocks[ 0 ] : null; + const styles = firstBlock && getBlockStyles( firstBlock.name ); return { - blocks: getBlocksByClientId( clientIds ), + blocks, inserterItems: getInserterItems( rootClientId ), + hasBlockStyles: styles && styles.length > 0, }; } ), withDispatch( ( dispatch, ownProps ) => ( { From ca67e13fc5e5df67c1619076bfd83e73271b7c41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s?= <nosolosw@users.noreply.github.com> Date: Fri, 9 Nov 2018 10:57:03 +0100 Subject: [PATCH 061/106] Disabled conditions for publish buttons (#11584) --- .../header/post-publish-button-or-toggle.js | 1 + .../components/post-publish-button/index.js | 25 +++++-- .../post-publish-button/test/index.js | 67 +++++++++++++++++-- .../post-publish-panel/test/toggle.js | 52 +++++++++++--- .../components/post-publish-panel/toggle.js | 12 ++-- test/e2e/specs/publish-button.test.js | 43 ++++++++++++ 6 files changed, 173 insertions(+), 27 deletions(-) create mode 100644 test/e2e/specs/publish-button.test.js diff --git a/packages/edit-post/src/components/header/post-publish-button-or-toggle.js b/packages/edit-post/src/components/header/post-publish-button-or-toggle.js index 55e4084e5327a..efa0621f72a99 100644 --- a/packages/edit-post/src/components/header/post-publish-button-or-toggle.js +++ b/packages/edit-post/src/components/header/post-publish-button-or-toggle.js @@ -35,6 +35,7 @@ export function PostPublishButtonOrToggle( { isOpen={ isPublishSidebarOpened } onToggle={ togglePublishSidebar } forceIsSaving={ forceIsSaving } + forceIsDirty={ forceIsDirty } /> ); diff --git a/packages/editor/src/components/post-publish-button/index.js b/packages/editor/src/components/post-publish-button/index.js index 842f04554c10d..69233c86931d8 100644 --- a/packages/editor/src/components/post-publish-button/index.js +++ b/packages/editor/src/components/post-publish-button/index.js @@ -35,11 +35,19 @@ export class PostPublishButton extends Component { visibility, isPublishable, isSaveable, + isPostSavingLocked, + isPublished, hasPublishAction, onSubmit = noop, + forceIsDirty, forceIsSaving, } = this.props; - const isButtonEnabled = isPublishable && isSaveable; + const isButtonDisabled = + isSaving || + forceIsSaving || + ! isSaveable || + isPostSavingLocked || + ( ! isPublishable && ! forceIsDirty ); let publishStatus; if ( ! hasPublishAction ) { @@ -65,8 +73,8 @@ export class PostPublishButton extends Component { isPrimary isLarge onClick={ onClick } - disabled={ ! isButtonEnabled } - isBusy={ isSaving } + disabled={ isButtonDisabled } + isBusy={ isSaving && isPublished } > <PublishButtonLabel forceIsSaving={ forceIsSaving } /> </Button> @@ -75,11 +83,12 @@ export class PostPublishButton extends Component { } export default compose( [ - withSelect( ( select, { forceIsSaving, forceIsDirty } ) => { + withSelect( ( select ) => { const { isSavingPost, isEditedPostBeingScheduled, getEditedPostVisibility, + isCurrentPostPublished, isEditedPostSaveable, isEditedPostPublishable, isPostSavingLocked, @@ -87,11 +96,13 @@ export default compose( [ getCurrentPostType, } = select( 'core/editor' ); return { - isSaving: forceIsSaving || isSavingPost(), + isSaving: isSavingPost(), isBeingScheduled: isEditedPostBeingScheduled(), visibility: getEditedPostVisibility(), - isSaveable: isEditedPostSaveable() && ! isPostSavingLocked(), - isPublishable: forceIsDirty || isEditedPostPublishable(), + isSaveable: isEditedPostSaveable(), + isPostSavingLocked: isPostSavingLocked(), + isPublishable: isEditedPostPublishable(), + isPublished: isCurrentPostPublished(), hasPublishAction: get( getCurrentPost(), [ '_links', 'wp:action-publish' ], false ), postType: getCurrentPostType(), }; diff --git a/packages/editor/src/components/post-publish-button/test/index.js b/packages/editor/src/components/post-publish-button/test/index.js index 4e1c57c14b328..b48d60f836671 100644 --- a/packages/editor/src/components/post-publish-button/test/index.js +++ b/packages/editor/src/components/post-publish-button/test/index.js @@ -14,15 +14,35 @@ describe( 'PostPublishButton', () => { describe( 'disabled', () => { it( 'should be disabled if post is currently saving', () => { const wrapper = shallow( - <PostPublishButton hasPublishAction={ true } isSaving /> + <PostPublishButton + isPublishable + isSaveable + isSaving + /> ); expect( wrapper.prop( 'disabled' ) ).toBe( true ); } ); - it( 'should be disabled if post is not publishable', () => { + it( 'should be disabled if forceIsSaving is true', () => { const wrapper = shallow( - <PostPublishButton hasPublishAction={ true } isPublishable={ false } /> + <PostPublishButton + isPublishable + isSaveable + forceIsSaving + /> + ); + + expect( wrapper.prop( 'disabled' ) ).toBe( true ); + } ); + + it( 'should be disabled if post is not publishable and not forceIsDirty', () => { + const wrapper = shallow( + <PostPublishButton + isSaveable + isPublishable={ false } + forceIsDirty={ false } + /> ); expect( wrapper.prop( 'disabled' ) ).toBe( true ); @@ -30,15 +50,45 @@ describe( 'PostPublishButton', () => { it( 'should be disabled if post is not saveable', () => { const wrapper = shallow( - <PostPublishButton hasPublishAction={ true } isSaveable={ false } /> + <PostPublishButton + isPublishable + isSaveable={ false } + /> ); expect( wrapper.prop( 'disabled' ) ).toBe( true ); } ); - it( 'should be enabled otherwise', () => { + it( 'should be disabled if post saving is locked', () => { const wrapper = shallow( - <PostPublishButton hasPublishAction={ true } isPublishable isSaveable /> + <PostPublishButton + isPublishable + isSaveable + isPostSavingLocked + /> + ); + + expect( wrapper.prop( 'disabled' ) ).toBe( true ); + } ); + + it( 'should be enabled if post is saveable but not publishable and forceIsDirty is true', () => { + const wrapper = shallow( + <PostPublishButton + isSaveable + isPublishable={ false } + forceIsDirty + /> + ); + + expect( wrapper.prop( 'disabled' ) ).toBe( false ); + } ); + + it( 'should be enabled if post is publishave and saveable', () => { + const wrapper = shallow( + <PostPublishButton + isPublishable + isSaveable + /> ); expect( wrapper.prop( 'disabled' ) ).toBe( false ); @@ -129,7 +179,10 @@ describe( 'PostPublishButton', () => { it( 'should have save modifier class', () => { const wrapper = shallow( - <PostPublishButton hasPublishAction={ true } isSaving /> + <PostPublishButton + isSaving + isPublished + /> ); expect( wrapper.find( 'Button' ).prop( 'isBusy' ) ).toBe( true ); diff --git a/packages/editor/src/components/post-publish-panel/test/toggle.js b/packages/editor/src/components/post-publish-panel/test/toggle.js index 3226b638a3567..81126aa76a14d 100644 --- a/packages/editor/src/components/post-publish-panel/test/toggle.js +++ b/packages/editor/src/components/post-publish-panel/test/toggle.js @@ -12,7 +12,11 @@ describe( 'PostPublishPanelToggle', () => { describe( 'disabled', () => { it( 'should be disabled if post is currently saving', () => { const wrapper = shallow( - <PostPublishPanelToggle isSaving /> + <PostPublishPanelToggle + isPublishable + isSaveable + isSaving + /> ); expect( wrapper.prop( 'disabled' ) ).toBe( true ); @@ -20,15 +24,23 @@ describe( 'PostPublishPanelToggle', () => { it( 'should be disabled if post is currently force saving', () => { const wrapper = shallow( - <PostPublishPanelToggle forceIsSaving /> + <PostPublishPanelToggle + isPublishable + isSaveable + forceIsSaving + /> ); expect( wrapper.prop( 'disabled' ) ).toBe( true ); } ); - it( 'should be disabled if post is not publishable', () => { + it( 'should be disabled if post is not publishable and not forceIsDirty', () => { const wrapper = shallow( - <PostPublishPanelToggle isPublishable={ false } /> + <PostPublishPanelToggle + isSaveable + isPublishable={ false } + forceIsDirty={ false } + /> ); expect( wrapper.prop( 'disabled' ) ).toBe( true ); @@ -36,23 +48,45 @@ describe( 'PostPublishPanelToggle', () => { it( 'should be disabled if post is not saveable', () => { const wrapper = shallow( - <PostPublishPanelToggle isSaveable={ false } /> + <PostPublishPanelToggle + isSaveable={ false } + isPublishable + /> ); expect( wrapper.prop( 'disabled' ) ).toBe( true ); } ); - it( 'should be disabled if post is not published', () => { + it( 'should be disabled if post is published', () => { const wrapper = shallow( - <PostPublishPanelToggle isPublished={ false } /> + <PostPublishPanelToggle + isSaveable + isPublishable + isPublished + /> ); expect( wrapper.prop( 'disabled' ) ).toBe( true ); } ); - it( 'should be enabled otherwise', () => { + it( 'should be enabled if post is saveable but not publishable and forceIsDirty is true', () => { const wrapper = shallow( - <PostPublishPanelToggle isPublishable isSaveable /> + <PostPublishPanelToggle + isSaveable + isPublishable={ false } + forceIsDirty={ true } + /> + ); + + expect( wrapper.prop( 'disabled' ) ).toBe( false ); + } ); + + it( 'should be enabled if post is publishave and saveable', () => { + const wrapper = shallow( + <PostPublishPanelToggle + isPublishable + isSaveable + /> ); expect( wrapper.prop( 'disabled' ) ).toBe( false ); diff --git a/packages/editor/src/components/post-publish-panel/toggle.js b/packages/editor/src/components/post-publish-panel/toggle.js index c4edfa2f2292e..a617354392031 100644 --- a/packages/editor/src/components/post-publish-panel/toggle.js +++ b/packages/editor/src/components/post-publish-panel/toggle.js @@ -16,10 +16,14 @@ export function PostPublishPanelToggle( { onToggle, isOpen, forceIsSaving, + forceIsDirty, } ) { - const isButtonEnabled = ( - ! isSaving && ! forceIsSaving && isPublishable && isSaveable - ) || isPublished; + const isButtonDisabled = + isPublished || + isSaving || + forceIsSaving || + ! isSaveable || + ( ! isPublishable && ! forceIsDirty ); return ( <Button @@ -27,7 +31,7 @@ export function PostPublishPanelToggle( { isPrimary onClick={ onToggle } aria-expanded={ isOpen } - disabled={ ! isButtonEnabled } + disabled={ isButtonDisabled } isBusy={ isSaving && isPublished } > { isBeingScheduled ? __( 'Schedule…' ) : __( 'Publish…' ) } diff --git a/test/e2e/specs/publish-button.test.js b/test/e2e/specs/publish-button.test.js new file mode 100644 index 0000000000000..92acf24a53740 --- /dev/null +++ b/test/e2e/specs/publish-button.test.js @@ -0,0 +1,43 @@ +import { + arePrePublishChecksEnabled, + disablePrePublishChecks, + enablePrePublishChecks, + newPost, +} from '../support/utils'; + +describe( 'PostPublishButton', () => { + let werePrePublishChecksEnabled; + beforeEach( async () => { + await newPost( ); + werePrePublishChecksEnabled = await arePrePublishChecksEnabled(); + if ( werePrePublishChecksEnabled ) { + await disablePrePublishChecks(); + } + } ); + + afterEach( async () => { + if ( werePrePublishChecksEnabled ) { + await enablePrePublishChecks(); + } + } ); + + it( 'should be disabled when post is not saveable', async () => { + const publishButton = await page.$( '.editor-post-publish-button:disabled' ); + + expect( publishButton ).not.toBeNull(); + } ); + + it( 'should be disabled when post is being saved', async () => { + await page.type( '.editor-post-title__input', 'E2E Test Post' ); // Make it saveable + expect( await page.$( '.editor-post-publish-button:disabled' ) ).toBeNull(); + await page.click( '.editor-post-save-draft' ); + expect( await page.$( '.editor-post-publish-button:disabled' ) ).not.toBeNull(); + } ); + + it( 'should be disabled when metabox is being saved', async () => { + await page.type( '.editor-post-title__input', 'E2E Test Post' ); // Make it saveable + expect( await page.$( '.editor-post-publish-button:disabled' ) ).toBeNull(); + await page.evaluate( () => window.wp.data.dispatch( 'core/edit-post' ).requestMetaBoxUpdates() ); + expect( await page.$( '.editor-post-publish-button:disabled' ) ).not.toBeNull(); + } ); +} ); From a134d0c3c5e7640f306d5020a1b4257b21982ed2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20Van=C2=A0Dorpe?= <iseulde@automattic.com> Date: Fri, 9 Nov 2018 11:59:15 +0100 Subject: [PATCH 062/106] Restore native backspace behaviour (#11627) --- .../editor/src/components/rich-text/index.js | 60 ++++++++++++------- .../src/components/rich-text/tinymce.js | 4 +- test/e2e/specs/writing-flow.test.js | 18 ++++++ 3 files changed, 58 insertions(+), 24 deletions(-) diff --git a/packages/editor/src/components/rich-text/index.js b/packages/editor/src/components/rich-text/index.js index a59fc64bf28b6..9b5945ff39df1 100644 --- a/packages/editor/src/components/rich-text/index.js +++ b/packages/editor/src/components/rich-text/index.js @@ -42,6 +42,8 @@ import { getSelectionEnd, remove, isCollapsed, + LINE_SEPARATOR, + charAt, } from '@wordpress/rich-text'; import { decodeEntities } from '@wordpress/html-entities'; import { withFilters } from '@wordpress/components'; @@ -492,8 +494,6 @@ export class RichText extends Component { * @link https://en.wikipedia.org/wiki/Caret_navigation * * @param {KeyboardEvent} event Keydown event. - * - * @return {?boolean} True if the event was handled. */ onDeleteKeyDown( event ) { const { onMerge, onRemove } = this.props; @@ -533,7 +533,7 @@ export class RichText extends Component { onRemove( ! isReverse ); } - return true; + event.preventDefault(); } /** @@ -545,32 +545,46 @@ export class RichText extends Component { const { keyCode } = event; if ( keyCode === DELETE || keyCode === BACKSPACE ) { - event.preventDefault(); + const value = this.createRecord(); + const start = getSelectionStart( value ); + const end = getSelectionEnd( value ); - if ( this.onDeleteKeyDown( event ) ) { + // Always handle uncollapsed selections ourselves. + if ( ! isCollapsed( value ) ) { + this.onChange( remove( value ) ); + event.preventDefault(); return; } - const value = this.createRecord(); - const start = getSelectionStart( value ); - const end = getSelectionEnd( value ); + if ( this.multilineTag ) { + 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 ( 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, - ) ); + if ( newValue ) { + this.onChange( newValue ); + event.preventDefault(); + } } + + this.onDeleteKeyDown( event ); } else if ( keyCode === ENTER ) { event.preventDefault(); diff --git a/packages/editor/src/components/rich-text/tinymce.js b/packages/editor/src/components/rich-text/tinymce.js index 11ebd13591e12..9efd051c66908 100644 --- a/packages/editor/src/components/rich-text/tinymce.js +++ b/packages/editor/src/components/rich-text/tinymce.js @@ -250,9 +250,11 @@ export default class TinyMCE extends Component { onKeyDown( event ) { const { keyCode } = event; + const { startContainer, startOffset, endContainer, endOffset } = getSelection().getRangeAt( 0 ); + const isCollapsed = startContainer === endContainer && startOffset === endOffset; // Disables TinyMCE behaviour. - if ( keyCode === ENTER || keyCode === BACKSPACE || keyCode === DELETE ) { + if ( keyCode === ENTER || ( ! isCollapsed && ( keyCode === DELETE || keyCode === BACKSPACE ) ) ) { event.preventDefault(); // For some reason this is needed to also prevent the insertion of // line breaks. diff --git a/test/e2e/specs/writing-flow.test.js b/test/e2e/specs/writing-flow.test.js index 67c91280ba410..aa7456533ec58 100644 --- a/test/e2e/specs/writing-flow.test.js +++ b/test/e2e/specs/writing-flow.test.js @@ -223,4 +223,22 @@ describe( 'adding blocks', () => { ) ); expect( isInBlock ).toBe( true ); } ); + + it( 'should not delete trailing spaces when deleting a word with backspace', async () => { + await clickBlockAppender(); + await page.keyboard.type( '1 2 3 4' ); + await page.keyboard.press( 'Backspace' ); + await page.keyboard.type( '4' ); + const blockText = await page.evaluate( () => document.activeElement.textContent ); + expect( blockText ).toBe( '1 2 3 4' ); + } ); + + it( 'should not delete trailing spaces when deleting a word with alt + backspace', async () => { + await clickBlockAppender(); + await page.keyboard.type( 'alpha beta gamma delta' ); + await pressWithModifier( META_KEY, 'Backspace' ); + await page.keyboard.type( 'delta' ); + const blockText = await page.evaluate( () => document.activeElement.textContent ); + expect( blockText ).toBe( 'alpha beta gamma delta' ); + } ); } ); From 471f749388e065b9ab8bebe1141d8720b6938b96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20=28Greg=29=20Zi=C3=B3=C5=82kowski?= <grzegorz@gziolo.pl> Date: Fri, 9 Nov 2018 12:42:46 +0100 Subject: [PATCH 063/106] Update the last occurence of home page with homepage (#11661) --- packages/block-library/src/more/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-library/src/more/index.js b/packages/block-library/src/more/index.js index 9334ed2e0941c..0aab7198df8f0 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 an excerpt of this post on your home page? Use this block to define where you want the separation.' ), + description: __( 'Want to show only an excerpt of this post on your homepage? Use this block to define where you want the separation.' ), icon: <SVG viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><Path fill="none" d="M0 0h24v24H0V0z" /><G><Path d="M2 9v2h19V9H2zm0 6h5v-2H2v2zm7 0h5v-2H9v2zm7 0h5v-2h-5v2z" /></G></SVG>, From bf45954ba917f4440388b8637568c1540f46f861 Mon Sep 17 00:00:00 2001 From: Jorge <jorge.costa@developer.pt> Date: Fri, 9 Nov 2018 11:59:11 +0000 Subject: [PATCH 064/106] Add image and video transformations in Media & Text block (#11420) ## Description This pr adds "Media & Text" <-> Video, Image transforms. ## How has this been tested? Add a video block. Transform it to Media & Text and verify it worked as expected. Add an image block. Transform it to Media & Text and verify it worked as expected. Transform both blocks back to video and image. Add a media text block select an image and transform it to image block. Add a media text block select a video and transform it to video block. Transform the image and videos back to media & text. --- .../block-library/src/media-text/index.js | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/packages/block-library/src/media-text/index.js b/packages/block-library/src/media-text/index.js index bae1b9b1fc6b9..79e472456daff 100644 --- a/packages/block-library/src/media-text/index.js +++ b/packages/block-library/src/media-text/index.js @@ -13,6 +13,7 @@ import { InnerBlocks, getColorClassName, } from '@wordpress/editor'; +import { createBlock } from '@wordpress/blocks'; /** * Internal dependencies @@ -82,6 +83,63 @@ export const settings = { align: [ 'wide', 'full' ], }, + transforms: { + from: [ + { + type: 'block', + blocks: [ 'core/image' ], + transform: ( { alt, url, id } ) => ( + createBlock( 'core/media-text', { + mediaAlt: alt, + mediaId: id, + mediaUrl: url, + mediaType: 'image', + } ) + ), + }, + { + type: 'block', + blocks: [ 'core/video' ], + transform: ( { src, id } ) => ( + createBlock( 'core/media-text', { + mediaId: id, + mediaUrl: src, + mediaType: 'video', + } ) + ), + }, + ], + to: [ + { + type: 'block', + blocks: [ 'core/image' ], + isMatch: ( { mediaType, mediaUrl } ) => { + return ! mediaUrl || mediaType === 'image'; + }, + transform: ( { mediaAlt, mediaId, mediaUrl } ) => { + return createBlock( 'core/image', { + alt: mediaAlt, + id: mediaId, + url: mediaUrl, + } ); + }, + }, + { + type: 'block', + blocks: [ 'core/video' ], + isMatch: ( { mediaType, mediaUrl } ) => { + return ! mediaUrl || mediaType === 'video'; + }, + transform: ( { mediaId, mediaUrl } ) => { + return createBlock( 'core/video', { + id: mediaId, + src: mediaUrl, + } ); + }, + }, + ], + }, + edit, save( { attributes } ) { From 7a2f7ca93df14af579c84e815f2bdae7a750cee6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20=28Greg=29=20Zi=C3=B3=C5=82kowski?= <grzegorz@gziolo.pl> Date: Fri, 9 Nov 2018 14:12:40 +0100 Subject: [PATCH 065/106] Enforce version increment for recently published npm packages (#11667) --- lerna.json | 1 - packages/block-library/CHANGELOG.md | 2 ++ packages/block-serialization-default-parser/CHANGELOG.md | 2 ++ packages/block-serialization-spec-parser/CHANGELOG.md | 2 ++ packages/blocks/CHANGELOG.md | 2 +- packages/components/CHANGELOG.md | 2 ++ packages/compose/CHANGELOG.md | 4 +++- packages/core-data/CHANGELOG.md | 2 ++ packages/data/CHANGELOG.md | 2 ++ packages/date/CHANGELOG.md | 2 ++ packages/dom/CHANGELOG.md | 2 ++ packages/edit-post/CHANGELOG.md | 2 ++ packages/editor/CHANGELOG.md | 2 +- packages/element/CHANGELOG.md | 1 + packages/format-library/CHANGELOG.md | 2 ++ packages/jest-preset-default/CHANGELOG.md | 2 ++ packages/list-reusable-blocks/CHANGELOG.md | 2 ++ packages/notices/CHANGELOG.md | 2 ++ packages/nux/CHANGELOG.md | 2 ++ packages/plugins/CHANGELOG.md | 2 ++ packages/rich-text/CHANGELOG.md | 2 ++ packages/scripts/CHANGELOG.md | 2 ++ packages/viewport/CHANGELOG.md | 2 ++ 23 files changed, 42 insertions(+), 4 deletions(-) diff --git a/lerna.json b/lerna.json index fd005a960f1e6..431f4dec535da 100644 --- a/lerna.json +++ b/lerna.json @@ -6,7 +6,6 @@ }, "ignoreChanges": [ "**/benchmark/*.js", - "**/CHANGELOG.md", "**/test/**" ], "packages": [ diff --git a/packages/block-library/CHANGELOG.md b/packages/block-library/CHANGELOG.md index d15afcdf9e4f3..29738639f8fc5 100644 --- a/packages/block-library/CHANGELOG.md +++ b/packages/block-library/CHANGELOG.md @@ -1,3 +1,5 @@ +## 2.2.1 (2018-11-09) + ## 2.2.0 (2018-11-09) ### New Features diff --git a/packages/block-serialization-default-parser/CHANGELOG.md b/packages/block-serialization-default-parser/CHANGELOG.md index 34c834f196f63..d707d54620836 100644 --- a/packages/block-serialization-default-parser/CHANGELOG.md +++ b/packages/block-serialization-default-parser/CHANGELOG.md @@ -1,3 +1,5 @@ +## 1.1.1 (2018-11-09) + ## 1.1.0 (2018-11-09) - Add new list of HTML fragments to parse output. diff --git a/packages/block-serialization-spec-parser/CHANGELOG.md b/packages/block-serialization-spec-parser/CHANGELOG.md index 2ee5fd63a9b77..276abd6e3b3a7 100644 --- a/packages/block-serialization-spec-parser/CHANGELOG.md +++ b/packages/block-serialization-spec-parser/CHANGELOG.md @@ -1,3 +1,5 @@ +## 1.1.1 (2018-11-09) + ## 1.1.0 (2018-11-09) - Add new list of HTML fragments to parse output. diff --git a/packages/blocks/CHANGELOG.md b/packages/blocks/CHANGELOG.md index d0e0f71f43e36..26d05ff27a017 100644 --- a/packages/blocks/CHANGELOG.md +++ b/packages/blocks/CHANGELOG.md @@ -1,4 +1,4 @@ -## 5.3.0 (Unreleased) +## 5.3.0 (2018-11-09) ### New feature diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 74ea05bf93cf4..6b55f4e45690d 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -1,3 +1,5 @@ +## 5.1.1 (2018-11-09) + ## 5.1.0 (2018-11-09) ### New Feature diff --git a/packages/compose/CHANGELOG.md b/packages/compose/CHANGELOG.md index 5e967f6838033..1c7e7b35f7a82 100644 --- a/packages/compose/CHANGELOG.md +++ b/packages/compose/CHANGELOG.md @@ -1,4 +1,6 @@ -## 2.1.1 (2018-11-9) +## 2.1.2 (2018-11-09) + +## 2.1.1 (2018-11-09) ## 2.1.0 (2018-10-29) diff --git a/packages/core-data/CHANGELOG.md b/packages/core-data/CHANGELOG.md index e5ac6490fd50a..e08dec7a0b911 100644 --- a/packages/core-data/CHANGELOG.md +++ b/packages/core-data/CHANGELOG.md @@ -1,3 +1,5 @@ +## 2.0.11 (2018-11-09) + ## 2.0.10 (2018-11-09) ## 2.0.9 (2018-11-03) diff --git a/packages/data/CHANGELOG.md b/packages/data/CHANGELOG.md index 89cc14fcc049a..6dad7cf833a79 100644 --- a/packages/data/CHANGELOG.md +++ b/packages/data/CHANGELOG.md @@ -1,3 +1,5 @@ +## 3.1.2 (2018-11-09) + ## 3.1.1 (2018-11-09) ## 3.1.0 (2018-11-03) diff --git a/packages/date/CHANGELOG.md b/packages/date/CHANGELOG.md index 48d4a1077876a..ff9266b3285bd 100644 --- a/packages/date/CHANGELOG.md +++ b/packages/date/CHANGELOG.md @@ -1,3 +1,5 @@ +## 2.2.1 (2018-11-09) + ## 2.2.0 (2018-11-09) ### Deprecations diff --git a/packages/dom/CHANGELOG.md b/packages/dom/CHANGELOG.md index 15d8a90386a97..a14324346a986 100644 --- a/packages/dom/CHANGELOG.md +++ b/packages/dom/CHANGELOG.md @@ -1,3 +1,5 @@ +## 2.0.6 (2018-11-09) + ## 2.0.5 (2018-11-09) ## 2.0.4 (2018-10-19) diff --git a/packages/edit-post/CHANGELOG.md b/packages/edit-post/CHANGELOG.md index 8ba5213ff7052..1be460e3d081c 100644 --- a/packages/edit-post/CHANGELOG.md +++ b/packages/edit-post/CHANGELOG.md @@ -1,3 +1,5 @@ +## 2.1.1 (2018-11-09) + ## 2.1.0 (2018-11-09) ### Bug Fixes diff --git a/packages/editor/CHANGELOG.md b/packages/editor/CHANGELOG.md index 9379ec67de996..0ac5a427b55fb 100644 --- a/packages/editor/CHANGELOG.md +++ b/packages/editor/CHANGELOG.md @@ -1,4 +1,4 @@ -## 6.2.1 (Unreleased) +## 6.2.1 (2018-11-09) ### Polish diff --git a/packages/element/CHANGELOG.md b/packages/element/CHANGELOG.md index 0e00c1371bd44..dd8128f44998a 100644 --- a/packages/element/CHANGELOG.md +++ b/packages/element/CHANGELOG.md @@ -1,3 +1,4 @@ +## 2.1.7 (2018-11-09) ## 2.1.6 (2018-11-09) diff --git a/packages/format-library/CHANGELOG.md b/packages/format-library/CHANGELOG.md index d8f00ab6594cd..2badd261b817c 100644 --- a/packages/format-library/CHANGELOG.md +++ b/packages/format-library/CHANGELOG.md @@ -1,3 +1,5 @@ +## 1.1.1 (2018-11-09) + ## 1.1.0 (2018-11-09) ### New Features diff --git a/packages/jest-preset-default/CHANGELOG.md b/packages/jest-preset-default/CHANGELOG.md index fa06662472016..3d88696419728 100644 --- a/packages/jest-preset-default/CHANGELOG.md +++ b/packages/jest-preset-default/CHANGELOG.md @@ -1,3 +1,5 @@ +## 3.0.2 (2018-11-09) + ## 3.0.1 (2018-11-09) ## 3.0.0 (2018-11-03) diff --git a/packages/list-reusable-blocks/CHANGELOG.md b/packages/list-reusable-blocks/CHANGELOG.md index 23c271cd229db..2e04e42846146 100644 --- a/packages/list-reusable-blocks/CHANGELOG.md +++ b/packages/list-reusable-blocks/CHANGELOG.md @@ -1,3 +1,5 @@ +## 1.1.9 (2018-11-09) + ## 1.1.8 (2018-11-09) ## 1.1.7 (2018-11-03) diff --git a/packages/notices/CHANGELOG.md b/packages/notices/CHANGELOG.md index b8124567073bc..632ca5167774c 100644 --- a/packages/notices/CHANGELOG.md +++ b/packages/notices/CHANGELOG.md @@ -1,3 +1,5 @@ +## 1.0.4 (2018-11-09) + ## 1.0.3 (2018-11-09) ## 1.0.2 (2018-11-03) diff --git a/packages/nux/CHANGELOG.md b/packages/nux/CHANGELOG.md index 7e8a523b74ac2..9b8e754221da8 100644 --- a/packages/nux/CHANGELOG.md +++ b/packages/nux/CHANGELOG.md @@ -1,3 +1,5 @@ +## 2.0.11 (2018-11-09) + ## 2.0.10 (2018-11-09) ## 2.0.9 (2018-11-03) diff --git a/packages/plugins/CHANGELOG.md b/packages/plugins/CHANGELOG.md index c4e9c3c062f5f..30967efbea2bb 100644 --- a/packages/plugins/CHANGELOG.md +++ b/packages/plugins/CHANGELOG.md @@ -1,3 +1,5 @@ +## 2.0.8 (2018-11-09) + ## 2.0.7 (2018-11-09) ## 2.0.6 (2018-10-29) diff --git a/packages/rich-text/CHANGELOG.md b/packages/rich-text/CHANGELOG.md index a35fe3c21ceb6..b940c65226bd2 100644 --- a/packages/rich-text/CHANGELOG.md +++ b/packages/rich-text/CHANGELOG.md @@ -1,3 +1,5 @@ +## 2.0.4 (2018-11-09) + ## 2.0.3 (2018-11-09) ### Bug Fix diff --git a/packages/scripts/CHANGELOG.md b/packages/scripts/CHANGELOG.md index 16cb1138773a0..89b2da214d8ab 100644 --- a/packages/scripts/CHANGELOG.md +++ b/packages/scripts/CHANGELOG.md @@ -1,3 +1,5 @@ +## 2.4.3 (2018-11-09) + ## 2.4.2 (2018-11-09) ## 2.4.1 (2018-11-03) diff --git a/packages/viewport/CHANGELOG.md b/packages/viewport/CHANGELOG.md index d5801e0a266a9..8339f78019566 100644 --- a/packages/viewport/CHANGELOG.md +++ b/packages/viewport/CHANGELOG.md @@ -1,3 +1,5 @@ +## 2.0.10 (2018-11-09) + ## 2.0.9 (2018-11-09) ## 2.0.8 (2018-11-03) From 6d6b849a3c8f107a22037f67fd7262cb773dcf11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20=28Greg=29=20Zi=C3=B3=C5=82kowski?= <grzegorz@gziolo.pl> Date: Fri, 9 Nov 2018 14:20:37 +0100 Subject: [PATCH 066/106] Update Lerna to the latest version (#11666) --- package-lock.json | 1679 ++++++++++++++++++++++++++++++++------------- package.json | 2 +- 2 files changed, 1218 insertions(+), 463 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4353dd993bee9..635e403b6986f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -807,50 +807,52 @@ } }, "@lerna/add": { - "version": "3.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@lerna/add/-/add-3.0.0-rc.0.tgz", - "integrity": "sha512-PZ/dn4UlA/7sd848LHE2TLXIkOzLDk8Uw/Gz6OwXfxXpng/w6mXcyjaScniT3HzLbzw5fP150+im5mL6BQv9JQ==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@lerna/add/-/add-3.4.1.tgz", + "integrity": "sha512-Vf54B42jlD6G52qnv/cAGH70cVQIa+LX//lfsbkxHvzkhIqBl5J4KsnTOPkA9uq3R+zP58ayicCHB9ReiEWGJg==", "dev": true, "requires": { - "@lerna/bootstrap": "^3.0.0-rc.0", - "@lerna/command": "^3.0.0-rc.0", - "@lerna/filter-options": "^3.0.0-rc.0", - "@lerna/validation-error": "^3.0.0-rc.0", + "@lerna/bootstrap": "^3.4.1", + "@lerna/command": "^3.3.0", + "@lerna/filter-options": "^3.3.2", + "@lerna/npm-conf": "^3.4.1", + "@lerna/validation-error": "^3.0.0", "dedent": "^0.7.0", "npm-package-arg": "^6.0.0", "p-map": "^1.2.0", - "package-json": "^4.0.1", + "pacote": "^9.1.0", "semver": "^5.5.0" } }, "@lerna/batch-packages": { - "version": "3.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@lerna/batch-packages/-/batch-packages-3.0.0-rc.0.tgz", - "integrity": "sha512-2FZs545THLHSWZ96xKT5wWFOdIt1UIKnc+Z2IIXCgmhT//fcqEcHFSgq42VxgoELgE4rM7jE2s6wgMatiJwRGg==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@lerna/batch-packages/-/batch-packages-3.1.2.tgz", + "integrity": "sha512-HAkpptrYeUVlBYbLScXgeCgk6BsNVXxDd53HVWgzzTWpXV4MHpbpeKrByyt7viXlNhW0w73jJbipb/QlFsHIhQ==", "dev": true, "requires": { - "@lerna/package-graph": "^3.0.0-rc.0", - "@lerna/validation-error": "^3.0.0-rc.0", + "@lerna/package-graph": "^3.1.2", + "@lerna/validation-error": "^3.0.0", "npmlog": "^4.1.2" } }, "@lerna/bootstrap": { - "version": "3.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@lerna/bootstrap/-/bootstrap-3.0.0-rc.0.tgz", - "integrity": "sha512-69mXMWjXA8R6ldDmSntFIIZTNAgEu1lOUP7DilL4OY55z01ln5fOtG/c65PQBm4mjm5ejh1kJVWiDT8MbCRslQ==", - "dev": true, - "requires": { - "@lerna/batch-packages": "^3.0.0-rc.0", - "@lerna/command": "^3.0.0-rc.0", - "@lerna/filter-options": "^3.0.0-rc.0", - "@lerna/npm-conf": "^3.0.0-rc.0", - "@lerna/npm-install": "^3.0.0-rc.0", - "@lerna/rimraf-dir": "^3.0.0-rc.0", - "@lerna/run-lifecycle": "^3.0.0-rc.0", - "@lerna/run-parallel-batches": "^3.0.0-rc.0", - "@lerna/symlink-binary": "^3.0.0-rc.0", - "@lerna/symlink-dependencies": "^3.0.0-rc.0", - "@lerna/validation-error": "^3.0.0-rc.0", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@lerna/bootstrap/-/bootstrap-3.4.1.tgz", + "integrity": "sha512-yZDJgNm/KDoRH2klzmQGmpWMg/XMzWgeWvauXkrfW/mj1wwmufOuh5pN4fBFxVmUUa/RFZdfMeaaJt3+W3PPBw==", + "dev": true, + "requires": { + "@lerna/batch-packages": "^3.1.2", + "@lerna/command": "^3.3.0", + "@lerna/filter-options": "^3.3.2", + "@lerna/has-npm-version": "^3.3.0", + "@lerna/npm-conf": "^3.4.1", + "@lerna/npm-install": "^3.3.0", + "@lerna/rimraf-dir": "^3.3.0", + "@lerna/run-lifecycle": "^3.4.1", + "@lerna/run-parallel-batches": "^3.0.0", + "@lerna/symlink-binary": "^3.3.0", + "@lerna/symlink-dependencies": "^3.3.0", + "@lerna/validation-error": "^3.0.0", "dedent": "^0.7.0", "get-port": "^3.2.0", "multimatch": "^2.1.0", @@ -865,27 +867,37 @@ } }, "@lerna/changed": { - "version": "3.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@lerna/changed/-/changed-3.0.0-rc.0.tgz", - "integrity": "sha512-DY2Id3Y1jifIdZ8uNymUijUgkVRFg64IUHMTb1sm0g/Nd+n++kLo9oqxRTUfuBkW3e1HTUuMkd3ZygU5UBtz6g==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@lerna/changed/-/changed-3.4.1.tgz", + "integrity": "sha512-gT7fhl4zQWyGETDO4Yy5wsFnqNlBSsezncS1nkMW1uO6jwnolwYqcr1KbrMR8HdmsZBn/00Y0mRnbtbpPPey8w==", + "dev": true, + "requires": { + "@lerna/collect-updates": "^3.3.2", + "@lerna/command": "^3.3.0", + "@lerna/listable": "^3.0.0", + "@lerna/output": "^3.0.0", + "@lerna/version": "^3.4.1" + } + }, + "@lerna/check-working-tree": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@lerna/check-working-tree/-/check-working-tree-3.3.0.tgz", + "integrity": "sha512-oeEP1dNhiiKUaO0pmcIi73YXJpaD0n5JczNctvVNZ8fGZmrALZtEnmC28o6Z7JgQaqq5nd2kO7xbnjoitrC51g==", "dev": true, "requires": { - "@lerna/collect-updates": "^3.0.0-rc.0", - "@lerna/command": "^3.0.0-rc.0", - "@lerna/output": "^3.0.0-rc.0", - "@lerna/publish": "^3.0.0-rc.0", - "chalk": "^2.3.1" + "@lerna/describe-ref": "^3.3.0", + "@lerna/validation-error": "^3.0.0" } }, "@lerna/child-process": { - "version": "3.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@lerna/child-process/-/child-process-3.0.0-rc.0.tgz", - "integrity": "sha512-5LhCU8isfJFj+5V5cJ+wcRR+VkNIbb3rSjm4VVclnD05ZfaY1HmfhBu3VjYt0SulhZKWJEnNBvjG88wgTay1vQ==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@lerna/child-process/-/child-process-3.3.0.tgz", + "integrity": "sha512-q2d/OPlNX/cBXB6Iz1932RFzOmOHq6ZzPjqebkINNaTojHWuuRpvJJY4Uz3NGpJ3kEtPDvBemkZqUBTSO5wb1g==", "dev": true, "requires": { "chalk": "^2.3.1", - "execa": "^0.10.0", - "strong-log-transformer": "^1.0.6" + "execa": "^1.0.0", + "strong-log-transformer": "^2.0.0" }, "dependencies": { "cross-spawn": { @@ -902,63 +914,81 @@ } }, "execa": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.10.0.tgz", - "integrity": "sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", "dev": true, "requires": { "cross-spawn": "^6.0.0", - "get-stream": "^3.0.0", + "get-stream": "^4.0.0", "is-stream": "^1.1.0", "npm-run-path": "^2.0.0", "p-finally": "^1.0.0", "signal-exit": "^3.0.0", "strip-eof": "^1.0.0" } + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } } } }, "@lerna/clean": { - "version": "3.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@lerna/clean/-/clean-3.0.0-rc.0.tgz", - "integrity": "sha512-rVlvO+bhfU/Q9D6bfg5GaLprKMD5rTRjJEqLONpESx/5Ed+NKgbYRiWafpQrB85m2r3c5dSGEPEc2q2rNG3EPg==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@lerna/clean/-/clean-3.3.2.tgz", + "integrity": "sha512-mvqusgSp2ou5SGqQgTEoTvGJpGfH4+L6XSeN+Ims+eNFGXuMazmKCf+rz2PZBMFufaHJ/Os+JF0vPCcWI1Fzqg==", "dev": true, "requires": { - "@lerna/command": "^3.0.0-rc.0", - "@lerna/filter-options": "^3.0.0-rc.0", - "@lerna/prompt": "^3.0.0-rc.0", - "@lerna/rimraf-dir": "^3.0.0-rc.0", + "@lerna/command": "^3.3.0", + "@lerna/filter-options": "^3.3.2", + "@lerna/prompt": "^3.3.1", + "@lerna/rimraf-dir": "^3.3.0", "p-map": "^1.2.0", "p-map-series": "^1.0.0", "p-waterfall": "^1.0.0" } }, "@lerna/cli": { - "version": "3.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@lerna/cli/-/cli-3.0.0-rc.0.tgz", - "integrity": "sha512-MUOrP8BiwjayPZAS3Nww1nlB6j02N4FmJK/f2PhJPMDsXMqm3mn5jBTBvlKbLQZJ/VDro8JbRGUUxCuCQEE+cQ==", - "dev": true, - "requires": { - "@lerna/add": "^3.0.0-rc.0", - "@lerna/bootstrap": "^3.0.0-rc.0", - "@lerna/changed": "^3.0.0-rc.0", - "@lerna/clean": "^3.0.0-rc.0", - "@lerna/create": "^3.0.0-rc.0", - "@lerna/diff": "^3.0.0-rc.0", - "@lerna/exec": "^3.0.0-rc.0", - "@lerna/global-options": "^3.0.0-rc.0", - "@lerna/import": "^3.0.0-rc.0", - "@lerna/init": "^3.0.0-rc.0", - "@lerna/link": "^3.0.0-rc.0", - "@lerna/list": "^3.0.0-rc.0", - "@lerna/publish": "^3.0.0-rc.0", - "@lerna/run": "^3.0.0-rc.0", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@lerna/cli/-/cli-3.2.0.tgz", + "integrity": "sha512-JdbLyTxHqxUlrkI+Ke+ltXbtyA+MPu9zR6kg/n8Fl6uaez/2fZWtReXzYi8MgLxfUFa7+1OHWJv4eAMZlByJ+Q==", + "dev": true, + "requires": { + "@lerna/global-options": "^3.1.3", "dedent": "^0.7.0", - "is-ci": "^1.0.10", "npmlog": "^4.1.2", "yargs": "^12.0.1" }, "dependencies": { + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, "decamelize": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-2.0.0.tgz", @@ -968,6 +998,21 @@ "xregexp": "4.0.0" } }, + "execa": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.10.0.tgz", + "integrity": "sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw==", + "dev": true, + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, "find-up": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", @@ -977,6 +1022,21 @@ "locate-path": "^3.0.0" } }, + "invert-kv": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", + "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", + "dev": true + }, + "lcid": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", + "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", + "dev": true, + "requires": { + "invert-kv": "^2.0.0" + } + }, "locate-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", @@ -987,6 +1047,28 @@ "path-exists": "^3.0.0" } }, + "mem": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-4.0.0.tgz", + "integrity": "sha512-WQxG/5xYc3tMbYLXoXPm81ET2WDULiU5FxbuIoNbJqLOOI8zehXFdZuiUEgfdrU2mVB1pxBZUGlYORSrpuJreA==", + "dev": true, + "requires": { + "map-age-cleaner": "^0.1.1", + "mimic-fn": "^1.0.0", + "p-is-promise": "^1.1.0" + } + }, + "os-locale": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.0.1.tgz", + "integrity": "sha512-7g5e7dmXPtzcP4bgsZ8ixDVqA7oWYuEz4lOSujeWyliPai4gfVDiFIcwBg3aGCPnmSGfzOKTK3ccPn0CKv3DBw==", + "dev": true, + "requires": { + "execa": "^0.10.0", + "lcid": "^2.0.0", + "mem": "^4.0.0" + } + }, "p-limit": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.0.0.tgz", @@ -1012,16 +1094,16 @@ "dev": true }, "yargs": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.1.tgz", - "integrity": "sha512-B0vRAp1hRX4jgIOWFtjfNjd9OA9RWYZ6tqGA9/I/IrTMsxmKvtWy+ersM+jzpQqbC3YfLzeABPdeTgcJ9eu1qQ==", + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.2.tgz", + "integrity": "sha512-e7SkEx6N6SIZ5c5H22RTZae61qtn3PYUE8JYbBFlK9sYmh3DMQ6E5ygtaG/2BW0JZi4WGgTR2IV5ChqlqrDGVQ==", "dev": true, "requires": { "cliui": "^4.0.0", "decamelize": "^2.0.0", "find-up": "^3.0.0", "get-caller-file": "^1.0.1", - "os-locale": "^2.0.0", + "os-locale": "^3.0.0", "require-directory": "^2.1.1", "require-main-filename": "^1.0.1", "set-blocking": "^2.0.0", @@ -1043,33 +1125,32 @@ } }, "@lerna/collect-updates": { - "version": "3.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@lerna/collect-updates/-/collect-updates-3.0.0-rc.0.tgz", - "integrity": "sha512-G1BgTIWc6rSsuMLsgpT+xvrQrSN/a0kC+KqMZ9CbCoImtj0AKvrAm3TeFOsYU9JHBUYfe1HWMIIhUSD4VhXmeg==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@lerna/collect-updates/-/collect-updates-3.3.2.tgz", + "integrity": "sha512-9WyBJI2S5sYgEZEScu525Lbi6nknNrdBKop35sCDIC9y6AIGvH6Dr5tkTd+Kg3n1dE+kHwW/xjERkx3+h7th3w==", "dev": true, "requires": { - "@lerna/child-process": "^3.0.0-rc.0", + "@lerna/child-process": "^3.3.0", + "@lerna/describe-ref": "^3.3.0", "minimatch": "^3.0.4", "npmlog": "^4.1.2", - "semver": "^5.5.0", "slash": "^1.0.0" } }, "@lerna/command": { - "version": "3.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@lerna/command/-/command-3.0.0-rc.0.tgz", - "integrity": "sha512-YzQKhQGSaqhnj/UbygmIneQDuhsTGu9rBqbX84Qs8RhKMMAPWurg+k6sCIihHgBz17tknxEvch/SefMNQxqzAA==", - "dev": true, - "requires": { - "@lerna/child-process": "^3.0.0-rc.0", - "@lerna/collect-updates": "^3.0.0-rc.0", - "@lerna/filter-packages": "^3.0.0-rc.0", - "@lerna/package-graph": "^3.0.0-rc.0", - "@lerna/project": "^3.0.0-rc.0", - "@lerna/validation-error": "^3.0.0-rc.0", - "@lerna/write-log-file": "^3.0.0-rc.0", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@lerna/command/-/command-3.3.0.tgz", + "integrity": "sha512-NTOkLEKlWcBLHSvUr9tzVpV7RJ4GROLeOuZ6RfztGOW/31JPSwVVBD2kPifEXNZunldOx5GVWukR+7+NpAWhsg==", + "dev": true, + "requires": { + "@lerna/child-process": "^3.3.0", + "@lerna/package-graph": "^3.1.2", + "@lerna/project": "^3.0.0", + "@lerna/validation-error": "^3.0.0", + "@lerna/write-log-file": "^3.0.0", "dedent": "^0.7.0", - "execa": "^0.10.0", + "execa": "^1.0.0", + "is-ci": "^1.0.10", "lodash": "^4.17.5", "npmlog": "^4.1.2" }, @@ -1088,53 +1169,92 @@ } }, "execa": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.10.0.tgz", - "integrity": "sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", "dev": true, "requires": { "cross-spawn": "^6.0.0", - "get-stream": "^3.0.0", + "get-stream": "^4.0.0", "is-stream": "^1.1.0", "npm-run-path": "^2.0.0", "p-finally": "^1.0.0", "signal-exit": "^3.0.0", "strip-eof": "^1.0.0" } + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } } } }, "@lerna/conventional-commits": { - "version": "3.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@lerna/conventional-commits/-/conventional-commits-3.0.0-rc.0.tgz", - "integrity": "sha512-J5RC+pd7kHP8EfqzvZqaXW253IoShdlzl1Jrw17KqPDAfL5CoZoUwxgZv4ur3kSSv+jHZGQeuAcg93OsBgkzPw==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@lerna/conventional-commits/-/conventional-commits-3.4.1.tgz", + "integrity": "sha512-3NETrA58aUkaEW3RdwdJ766Bg9NVpLzb26mtdlsJQcvB5sQBWH5dJSHIVQH1QsGloBeH2pE/mDUEVY8ZJXuR4w==", "dev": true, "requires": { - "@lerna/validation-error": "^3.0.0-rc.0", - "conventional-changelog-angular": "^1.6.6", - "conventional-changelog-core": "^2.0.5", - "conventional-recommended-bump": "^2.0.6", - "dedent": "^0.7.0", - "fs-extra": "^6.0.1", - "get-stream": "^3.0.0", + "@lerna/validation-error": "^3.0.0", + "conventional-changelog-angular": "^5.0.1", + "conventional-changelog-core": "^3.1.0", + "conventional-recommended-bump": "^4.0.1", + "fs-extra": "^7.0.0", + "get-stream": "^4.0.0", "npm-package-arg": "^6.0.0", "npmlog": "^4.1.2", "semver": "^5.5.0" + }, + "dependencies": { + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } } }, "@lerna/create": { - "version": "3.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@lerna/create/-/create-3.0.0-rc.0.tgz", - "integrity": "sha512-6GLI+MEKANCAVnuA9pYQX1k+XyyPEBdoPAiSK4lnW2w4E2KSQZxdkI1+9QLv6S2oTrnlrFSxGmqgmmijaZfCGg==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@lerna/create/-/create-3.4.1.tgz", + "integrity": "sha512-l+4t2SRO5nvW0MNYY+EWxbaMHsAN8bkWH3nyt7EzhBjs4+TlRAJRIEqd8o9NWznheE3pzwczFz1Qfl3BWbyM5A==", "dev": true, "requires": { - "@lerna/child-process": "^3.0.0-rc.0", - "@lerna/command": "^3.0.0-rc.0", - "@lerna/npm-conf": "^3.0.0-rc.0", - "@lerna/validation-error": "^3.0.0-rc.0", + "@lerna/child-process": "^3.3.0", + "@lerna/command": "^3.3.0", + "@lerna/npm-conf": "^3.4.1", + "@lerna/validation-error": "^3.0.0", "camelcase": "^4.1.0", "dedent": "^0.7.0", - "fs-extra": "^6.0.1", + "fs-extra": "^7.0.0", "globby": "^8.0.1", "init-package-json": "^1.10.3", "npm-package-arg": "^6.0.0", @@ -1142,7 +1262,8 @@ "semver": "^5.5.0", "slash": "^1.0.0", "validate-npm-package-license": "^3.0.3", - "validate-npm-package-name": "^3.0.0" + "validate-npm-package-name": "^3.0.0", + "whatwg-url": "^7.0.0" }, "dependencies": { "globby": { @@ -1165,139 +1286,193 @@ "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", "dev": true + }, + "whatwg-url": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.0.0.tgz", + "integrity": "sha512-37GeVSIJ3kn1JgKyjiYNmSLP1yzbpb29jdmwBSgkD9h40/hyrR/OifpVUndji3tmwGgD8qpw7iQu3RSbCrBpsQ==", + "dev": true, + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } } } }, "@lerna/create-symlink": { - "version": "3.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@lerna/create-symlink/-/create-symlink-3.0.0-rc.0.tgz", - "integrity": "sha512-wz/C7DB5chTidAOc9+edeg65nJLG1PNO9J/jlAUZOY4G3EPGihLroabIlvbjB5SJ8QKS88ftX+f1pzFZ+nYOdQ==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@lerna/create-symlink/-/create-symlink-3.3.0.tgz", + "integrity": "sha512-0lb88Nnq1c/GG+fwybuReOnw3+ah4dB81PuWwWwuqUNPE0n50qUf/M/7FfSb5JEh/93fcdbZI0La8t3iysNW1w==", "dev": true, "requires": { "cmd-shim": "^2.0.2", - "fs-extra": "^6.0.1", + "fs-extra": "^7.0.0", + "npmlog": "^4.1.2" + } + }, + "@lerna/describe-ref": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@lerna/describe-ref/-/describe-ref-3.3.0.tgz", + "integrity": "sha512-4t7M4OupnYMSPNLrLUau8qkS+dgLEi4w+DkRkV0+A+KNYga1W0jVgNLPIIsxta7OHfodPkCNAqZCzNCw/dmAwA==", + "dev": true, + "requires": { + "@lerna/child-process": "^3.3.0", "npmlog": "^4.1.2" } }, "@lerna/diff": { - "version": "3.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@lerna/diff/-/diff-3.0.0-rc.0.tgz", - "integrity": "sha512-fXeB10qaFeBMucorkV6Q/bJiWayZgGizVsPkuGN1izENd4DBsTxUyh2a6Y3lY5GzCZ33wmuVDe2HdRVHtfrzaQ==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@lerna/diff/-/diff-3.3.0.tgz", + "integrity": "sha512-sIoMjsm3NVxvmt6ofx8Uu/2fxgldQqLl0zmC9X1xW00j831o5hBffx1EoKj9CnmaEvoSP6j/KFjxy2RWjebCIg==", "dev": true, "requires": { - "@lerna/child-process": "^3.0.0-rc.0", - "@lerna/command": "^3.0.0-rc.0", - "@lerna/validation-error": "^3.0.0-rc.0", + "@lerna/child-process": "^3.3.0", + "@lerna/command": "^3.3.0", + "@lerna/validation-error": "^3.0.0", "npmlog": "^4.1.2" } }, "@lerna/exec": { - "version": "3.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@lerna/exec/-/exec-3.0.0-rc.0.tgz", - "integrity": "sha512-DYU00HAAoreqNQmLV0+gfH4mXIJKbd1rbrMvQbnfCTZ3nWYiORXQwvwCYbGgmsMarSaJ/T3eTPP6LvsVt7y1aw==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@lerna/exec/-/exec-3.3.2.tgz", + "integrity": "sha512-mN6vGxNir7JOGvWLwKr3DW3LNy1ecCo2ziZj5rO9Mw5Rew3carUu1XLmhF/4judtsvXViUY+rvGIcqHe0vvb+w==", "dev": true, "requires": { - "@lerna/batch-packages": "^3.0.0-rc.0", - "@lerna/child-process": "^3.0.0-rc.0", - "@lerna/command": "^3.0.0-rc.0", - "@lerna/filter-options": "^3.0.0-rc.0", - "@lerna/run-parallel-batches": "^3.0.0-rc.0", - "@lerna/validation-error": "^3.0.0-rc.0" + "@lerna/batch-packages": "^3.1.2", + "@lerna/child-process": "^3.3.0", + "@lerna/command": "^3.3.0", + "@lerna/filter-options": "^3.3.2", + "@lerna/run-parallel-batches": "^3.0.0", + "@lerna/validation-error": "^3.0.0" } }, "@lerna/filter-options": { - "version": "3.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@lerna/filter-options/-/filter-options-3.0.0-rc.0.tgz", - "integrity": "sha512-aqyb0GWEnQgu7eXHpVSpZLedVd3PrI5WK/CfzDlHGqUT8PCJTo9q2562gmEdeCWWfeSvXbezGm0djTC6RwHV1A==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@lerna/filter-options/-/filter-options-3.3.2.tgz", + "integrity": "sha512-0WHqdDgAnt5WKoByi1q+lFw8HWt5tEKP2DnLlGqWv3YFwVF5DsPRlO7xbzjY9sJgvyJtZcnkMtccdBPFhGGyIQ==", "dev": true, "requires": { + "@lerna/collect-updates": "^3.3.2", + "@lerna/filter-packages": "^3.0.0", "dedent": "^0.7.0" } }, "@lerna/filter-packages": { - "version": "3.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@lerna/filter-packages/-/filter-packages-3.0.0-rc.0.tgz", - "integrity": "sha512-ZVObVh8Nk5d6XS/RAJEdu56KbpqvwJrKruoYnDPFeno/Q6/G1Oi8S/W2NmfEXMBSPaVpYecbGfM4Xu5dLzipNA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@lerna/filter-packages/-/filter-packages-3.0.0.tgz", + "integrity": "sha512-zwbY1J4uRjWRZ/FgYbtVkq7I3Nduwsg2V2HwLKSzwV2vPglfGqgovYOVkND6/xqe2BHwDX4IyA2+e7OJmLaLSA==", "dev": true, "requires": { - "@lerna/validation-error": "^3.0.0-rc.0", + "@lerna/validation-error": "^3.0.0", "multimatch": "^2.1.0", "npmlog": "^4.1.2" } }, "@lerna/get-npm-exec-opts": { - "version": "3.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@lerna/get-npm-exec-opts/-/get-npm-exec-opts-3.0.0-rc.0.tgz", - "integrity": "sha512-n5Oe1LPzyMKGAdYVVDimpuVsWexTCaLlJq9RqXphoijRh5DAB/Mhaw9//5vGrv770m/QNMSceF99SZLI9gyh4g==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@lerna/get-npm-exec-opts/-/get-npm-exec-opts-3.0.0.tgz", + "integrity": "sha512-arcYUm+4xS8J3Palhl+5rRJXnZnFHsLFKHBxznkPIxjwGQeAEw7df38uHdVjEQ+HNeFmHnBgSqfbxl1VIw5DHg==", "dev": true, "requires": { "npmlog": "^4.1.2" } }, "@lerna/global-options": { - "version": "3.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@lerna/global-options/-/global-options-3.0.0-rc.0.tgz", - "integrity": "sha512-Sy8KE1cAcGwjxOxiJOHjTxJecLcJhAeQym4Ge3WP1Jnz5mq03o9mb37X0Hmjpv1W9OblbXVxHdmbyC3hxMEHIA==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@lerna/global-options/-/global-options-3.1.3.tgz", + "integrity": "sha512-LVeZU/Zgc0XkHdGMRYn+EmHfDmmYNwYRv3ta59iCVFXLVp7FRFWF7oB1ss/WRa9x/pYU0o6L8as/5DomLUGASA==", "dev": true }, + "@lerna/has-npm-version": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@lerna/has-npm-version/-/has-npm-version-3.3.0.tgz", + "integrity": "sha512-GX7omRep1eBRZHgjZLRw3MpBJSdA5gPZFz95P7rxhpvsiG384Tdrr/cKFMhm0A09yq27Tk/nuYTaZIj7HsVE6g==", + "dev": true, + "requires": { + "@lerna/child-process": "^3.3.0", + "semver": "^5.5.0" + } + }, "@lerna/import": { - "version": "3.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@lerna/import/-/import-3.0.0-rc.0.tgz", - "integrity": "sha512-ZRrusuAScKg29jRrurEi/qJbpol5RWY98nBhOmq08pibVz8cwS1QzyTwQhxaZECHYrKpL/qdLNigKsNi4+jyYw==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@lerna/import/-/import-3.3.1.tgz", + "integrity": "sha512-2OzTQDkYKbBPpyP2iOI1sWfcvMjNLjjHjmREq/uOWJaSIk5J3Ukt71OPpcOHh4V2CBOlXidCcO+Hyb4FVIy8fw==", "dev": true, "requires": { - "@lerna/child-process": "^3.0.0-rc.0", - "@lerna/command": "^3.0.0-rc.0", - "@lerna/prompt": "^3.0.0-rc.0", - "@lerna/validation-error": "^3.0.0-rc.0", + "@lerna/child-process": "^3.3.0", + "@lerna/command": "^3.3.0", + "@lerna/prompt": "^3.3.1", + "@lerna/validation-error": "^3.0.0", "dedent": "^0.7.0", - "fs-extra": "^6.0.1", + "fs-extra": "^7.0.0", "p-map-series": "^1.0.0" } }, "@lerna/init": { - "version": "3.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@lerna/init/-/init-3.0.0-rc.0.tgz", - "integrity": "sha512-/G4gy5QDNsxVXTEo59YRilcW7hnob31GVPc3omy9AZq8HZYCpj/tAw39mEds7S1wPCx0aotRj+NDf178zj11Mw==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@lerna/init/-/init-3.3.0.tgz", + "integrity": "sha512-HvgRLkIG6nDIeAO6ix5sUVIVV+W9UMk2rSSmFT66CDOefRi7S028amiyYnFUK1QkIAaUbVUyOnYaErtbJwICuw==", "dev": true, "requires": { - "@lerna/child-process": "^3.0.0-rc.0", - "@lerna/command": "^3.0.0-rc.0", - "fs-extra": "^6.0.1", + "@lerna/child-process": "^3.3.0", + "@lerna/command": "^3.3.0", + "fs-extra": "^7.0.0", "p-map": "^1.2.0", "write-json-file": "^2.3.0" } }, "@lerna/link": { - "version": "3.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@lerna/link/-/link-3.0.0-rc.0.tgz", - "integrity": "sha512-Al57Ib56/ojGQ3hcln+DhS5IGWqLv8EElrAmSp0c3t/Ie48VOv9AusQR9nDRIGEn73APu/EG0ILdbylfQBzaKg==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@lerna/link/-/link-3.3.0.tgz", + "integrity": "sha512-8CeXzGL7okrsVXsy2sHXI2KuBaczw3cblAnA2+FJPUqSKMPNbUTRzeU3bOlCjYtK0LbxC4ngENJTL3jJ8RaYQQ==", "dev": true, "requires": { - "@lerna/command": "^3.0.0-rc.0", - "@lerna/package-graph": "^3.0.0-rc.0", - "@lerna/symlink-dependencies": "^3.0.0-rc.0", + "@lerna/command": "^3.3.0", + "@lerna/package-graph": "^3.1.2", + "@lerna/symlink-dependencies": "^3.3.0", "p-map": "^1.2.0", "slash": "^1.0.0" } }, "@lerna/list": { - "version": "3.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@lerna/list/-/list-3.0.0-rc.0.tgz", - "integrity": "sha512-uQuFeRHhF6ALohRkVY6VxpCyeCHHTEqzt0SNvacTAA+gJX/hadtEYS883KOzmmcFibL4UtljQbdfj+DjrLKfUQ==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@lerna/list/-/list-3.3.2.tgz", + "integrity": "sha512-XXEVy7w+i/xx8NeJmGirw4upEoEF9OfD6XPLjISNQc24VgQV+frXdVJ02QcP7Y/PkY1rdIVrOjvo3ipKVLUxaQ==", + "dev": true, + "requires": { + "@lerna/command": "^3.3.0", + "@lerna/filter-options": "^3.3.2", + "@lerna/listable": "^3.0.0", + "@lerna/output": "^3.0.0" + } + }, + "@lerna/listable": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@lerna/listable/-/listable-3.0.0.tgz", + "integrity": "sha512-HX/9hyx1HLg2kpiKXIUc1EimlkK1T58aKQ7ovO7rQdTx9ForpefoMzyLnHE1n4XrUtEszcSWJIICJ/F898M6Ag==", "dev": true, "requires": { - "@lerna/command": "^3.0.0-rc.0", - "@lerna/filter-options": "^3.0.0-rc.0", - "@lerna/output": "^3.0.0-rc.0", "chalk": "^2.3.1", "columnify": "^1.5.4" } }, + "@lerna/log-packed": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@lerna/log-packed/-/log-packed-3.0.4.tgz", + "integrity": "sha512-vVQHgMagE2wnbxhNY9nFkdu+Cx2TsyWalkJfkxbNzmo6gOCrDsxCBDj9vTEV8Q+4aWx0C0Bsc0sB2Eb8y/+ofA==", + "dev": true, + "requires": { + "byte-size": "^4.0.3", + "columnify": "^1.5.4", + "has-unicode": "^2.0.1", + "npmlog": "^4.1.2" + } + }, "@lerna/npm-conf": { - "version": "3.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@lerna/npm-conf/-/npm-conf-3.0.0-rc.0.tgz", - "integrity": "sha512-zdd/c83UTDGHci1MrW61olXh04cqRvwkA1SZgXYiLo7A87yHlBYwVOu1d7Rl0J6lHG7++8X2odWqzYZkFfeVFA==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@lerna/npm-conf/-/npm-conf-3.4.1.tgz", + "integrity": "sha512-i9G6DnbCqiAqxKx2rSXej/n14qxlV/XOebL6QZonxJKzNTB+Q2wglnhTXmfZXTPJfoqimLaY4NfAEtbOXRWOXQ==", "dev": true, "requires": { "config-chain": "^1.1.11", @@ -1313,25 +1488,25 @@ } }, "@lerna/npm-dist-tag": { - "version": "3.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@lerna/npm-dist-tag/-/npm-dist-tag-3.0.0-rc.0.tgz", - "integrity": "sha512-Xa5mPmSIsi0N4pNNSBpKeghQ7XskKCVK+pawEvgKAYHph/J9PIfrddVJUU8o3nk2qkqeqikypoS++bwMdVr8Ew==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@lerna/npm-dist-tag/-/npm-dist-tag-3.3.0.tgz", + "integrity": "sha512-EtZJXzh3w5tqXEev+EBBPrWKWWn0WgJfxm4FihfS9VgyaAW8udIVZHGkIQ3f+tBtupcAzA9Q8cQNUkGF2efwmA==", "dev": true, "requires": { - "@lerna/child-process": "^3.0.0-rc.0", - "@lerna/get-npm-exec-opts": "^3.0.0-rc.0", + "@lerna/child-process": "^3.3.0", + "@lerna/get-npm-exec-opts": "^3.0.0", "npmlog": "^4.1.2" } }, "@lerna/npm-install": { - "version": "3.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@lerna/npm-install/-/npm-install-3.0.0-rc.0.tgz", - "integrity": "sha512-QWvgQ0osTf7e87hc1kKXDcbWPXVCqGUiVnTpjX22+CEiJjUMStWEp4MjLieObgFVLyTtanOZp8VpRqBsPE0HRQ==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@lerna/npm-install/-/npm-install-3.3.0.tgz", + "integrity": "sha512-WoVvKdS8ltROTGSNQwo6NDq0YKnjwhvTG4li1okcN/eHKOS3tL9bxbgPx7No0wOq5DKBpdeS9KhAfee6LFAZ5g==", "dev": true, "requires": { - "@lerna/child-process": "^3.0.0-rc.0", - "@lerna/get-npm-exec-opts": "^3.0.0-rc.0", - "fs-extra": "^6.0.1", + "@lerna/child-process": "^3.3.0", + "@lerna/get-npm-exec-opts": "^3.0.0", + "fs-extra": "^7.0.0", "npm-package-arg": "^6.0.0", "npmlog": "^4.1.2", "signal-exit": "^3.0.2", @@ -1339,40 +1514,44 @@ } }, "@lerna/npm-publish": { - "version": "3.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@lerna/npm-publish/-/npm-publish-3.0.0-rc.0.tgz", - "integrity": "sha512-rP8epG0SsHzqYw9xvwVX6YyAAwPYJAYZvMNxJvyi8fp5KdnD2sTeV/JOrWbQD/RxBxR2WGJxelVRL617KWZMOA==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@lerna/npm-publish/-/npm-publish-3.3.1.tgz", + "integrity": "sha512-bVTlWIcBL6Zpyzqvr9C7rxXYcoPw+l7IPz5eqQDNREj1R39Wj18OWB2KTJq8l7LIX7Wf4C2A1uT5hJaEf9BuvA==", "dev": true, "requires": { - "@lerna/child-process": "^3.0.0-rc.0", - "@lerna/get-npm-exec-opts": "^3.0.0-rc.0", - "npmlog": "^4.1.2" + "@lerna/child-process": "^3.3.0", + "@lerna/get-npm-exec-opts": "^3.0.0", + "@lerna/has-npm-version": "^3.3.0", + "@lerna/log-packed": "^3.0.4", + "fs-extra": "^7.0.0", + "npmlog": "^4.1.2", + "p-map": "^1.2.0" } }, "@lerna/npm-run-script": { - "version": "3.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@lerna/npm-run-script/-/npm-run-script-3.0.0-rc.0.tgz", - "integrity": "sha512-Hm6KLPpeIyRIdc9MntmaAhuboUyw75DJLJ1MTaLwq/RwZ2kKHYk1Hi/R7yjj8LOyD7A25BTYX0Pd1gHd1lsFVw==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@lerna/npm-run-script/-/npm-run-script-3.3.0.tgz", + "integrity": "sha512-YqDguWZzp4jIomaE4aWMUP7MIAJAFvRAf6ziQLpqwoQskfWLqK5mW0CcszT1oLjhfb3cY3MMfSTFaqwbdKmICg==", "dev": true, "requires": { - "@lerna/child-process": "^3.0.0-rc.0", - "@lerna/get-npm-exec-opts": "^3.0.0-rc.0", + "@lerna/child-process": "^3.3.0", + "@lerna/get-npm-exec-opts": "^3.0.0", "npmlog": "^4.1.2" } }, "@lerna/output": { - "version": "3.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@lerna/output/-/output-3.0.0-rc.0.tgz", - "integrity": "sha512-zZmQ94nVUfe1CvexarDnrb/Mqt81OcZNuOCvcKFHJiNkr5VSIn2GJuUwtTaVUtoh5uXdJCJvm696Ptsep10BWw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@lerna/output/-/output-3.0.0.tgz", + "integrity": "sha512-EFxnSbO0zDEVKkTKpoCUAFcZjc3gn3DwPlyTDxbeqPU7neCfxP4rA4+0a6pcOfTlRS5kLBRMx79F2TRCaMM3DA==", "dev": true, "requires": { "npmlog": "^4.1.2" } }, "@lerna/package": { - "version": "3.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@lerna/package/-/package-3.0.0-rc.0.tgz", - "integrity": "sha512-4EUnBc04IxganMamjnEfagajLya3nPKfqlorGc5VLoGh5akOZmrCF9qiqB90nYzrJoMt+5QB1lxYD8bxikX0qg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@lerna/package/-/package-3.0.0.tgz", + "integrity": "sha512-djzEJxzn212wS8d9znBnlXkeRlPL7GqeAYBykAmsuq51YGvaQK67Umh5ejdO0uxexF/4r7yRwgrlRHpQs8Rfqg==", "dev": true, "requires": { "npm-package-arg": "^6.0.0", @@ -1380,23 +1559,24 @@ } }, "@lerna/package-graph": { - "version": "3.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@lerna/package-graph/-/package-graph-3.0.0-rc.0.tgz", - "integrity": "sha512-E2MlL0CwDzcDTLPpMhg9+TESRAD/wYLtEu+Mj1R4OZbF2jlSXSZok1g3LW/gzulVd5oq9q+qBdY3SX5b9PCsGQ==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@lerna/package-graph/-/package-graph-3.1.2.tgz", + "integrity": "sha512-9wIWb49I1IJmyjPdEVZQ13IAi9biGfH/OZHOC04U2zXGA0GLiY+B3CAx6FQvqkZ8xEGfqzmXnv3LvZ0bQfc1aQ==", "dev": true, "requires": { + "@lerna/validation-error": "^3.0.0", "npm-package-arg": "^6.0.0", "semver": "^5.5.0" } }, "@lerna/project": { - "version": "3.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@lerna/project/-/project-3.0.0-rc.0.tgz", - "integrity": "sha512-lNego7jYd24jIDGLTcy1mrESEopCblVaZztKhJJntSZdqWZ/tnExjIITfS55CnZyKAXwPVIus6YRfOc24tvuaA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@lerna/project/-/project-3.0.0.tgz", + "integrity": "sha512-XhDFVfqj79jG2Speggd15RpYaE8uiR25UKcQBDmumbmqvTS7xf2cvl2pq2UTvDafaJ0YwFF3xkxQZeZnFMwdkw==", "dev": true, "requires": { - "@lerna/package": "^3.0.0-rc.0", - "@lerna/validation-error": "^3.0.0-rc.0", + "@lerna/package": "^3.0.0", + "@lerna/validation-error": "^3.0.0", "cosmiconfig": "^5.0.2", "dedent": "^0.7.0", "dot-prop": "^4.2.0", @@ -1492,125 +1672,161 @@ } }, "@lerna/prompt": { - "version": "3.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@lerna/prompt/-/prompt-3.0.0-rc.0.tgz", - "integrity": "sha512-4ABsGTq7/IMh3nX7LfRirJnXt50Yp/Lg837hb/Hp1OXFz1FoXLcbzoIx0QQYyk3q+Gnv2TwSK2M86xoUFIUXnw==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@lerna/prompt/-/prompt-3.3.1.tgz", + "integrity": "sha512-eJhofrUCUaItMIH6et8kI7YqHfhjWqGZoTsE+40NRCfAraOMWx+pDzfRfeoAl3qeRAH2HhNj1bkYn70FbUOxuQ==", "dev": true, "requires": { - "inquirer": "^5.1.0", + "inquirer": "^6.2.0", "npmlog": "^4.1.2" }, "dependencies": { + "chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "external-editor": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.0.3.tgz", + "integrity": "sha512-bn71H9+qWoOQKyZDo25mOMVpSmXROAsTJVVVYzrrtol3d4y+AsKjf4Iwl2Q+IuT0kFSQ1qo166UuIwqYq7mGnA==", + "dev": true, + "requires": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, "inquirer": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-5.2.0.tgz", - "integrity": "sha512-E9BmnJbAKLPGonz0HeWHtbKf+EeSP93paWO3ZYoUpq/aowXvYGjjCSuashhXPpzbArIjBbji39THkxTz9ZeEUQ==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.2.0.tgz", + "integrity": "sha512-QIEQG4YyQ2UYZGDC4srMZ7BjHOmNk1lR2JQj5UknBapklm6WHA+VVH7N+sUdX3A7NeCfGF8o4X1S3Ao7nAcIeg==", "dev": true, "requires": { "ansi-escapes": "^3.0.0", "chalk": "^2.0.0", "cli-cursor": "^2.1.0", "cli-width": "^2.0.0", - "external-editor": "^2.1.0", + "external-editor": "^3.0.0", "figures": "^2.0.0", - "lodash": "^4.3.0", + "lodash": "^4.17.10", "mute-stream": "0.0.7", "run-async": "^2.2.0", - "rxjs": "^5.5.2", + "rxjs": "^6.1.0", "string-width": "^2.1.0", "strip-ansi": "^4.0.0", "through": "^2.3.6" } + }, + "rxjs": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.3.3.tgz", + "integrity": "sha512-JTWmoY9tWCs7zvIk/CvRjhjGaOd+OVBM987mxFo+OW66cGpdKjZcpmc74ES1sB//7Kl/PAe8+wEakuhG4pcgOw==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } } } }, "@lerna/publish": { - "version": "3.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@lerna/publish/-/publish-3.0.0-rc.0.tgz", - "integrity": "sha512-bfjRtCzHAGSS5z8QOUw3UeLn4u5CpJTGUkk9y1PPbzdbMWmJhJnEjSw+TN2LaZFVGzQbE8dsLqR5TL149Y7P/Q==", - "dev": true, - "requires": { - "@lerna/batch-packages": "^3.0.0-rc.0", - "@lerna/child-process": "^3.0.0-rc.0", - "@lerna/collect-updates": "^3.0.0-rc.0", - "@lerna/command": "^3.0.0-rc.0", - "@lerna/conventional-commits": "^3.0.0-rc.0", - "@lerna/npm-dist-tag": "^3.0.0-rc.0", - "@lerna/npm-publish": "^3.0.0-rc.0", - "@lerna/output": "^3.0.0-rc.0", - "@lerna/prompt": "^3.0.0-rc.0", - "@lerna/run-lifecycle": "^3.0.0-rc.0", - "@lerna/run-parallel-batches": "^3.0.0-rc.0", - "@lerna/validation-error": "^3.0.0-rc.0", - "chalk": "^2.3.1", - "dedent": "^0.7.0", - "fs-extra": "^6.0.1", - "minimatch": "^3.0.4", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@lerna/publish/-/publish-3.4.3.tgz", + "integrity": "sha512-baeRL8xmOR25p86cAaS9mL0jdRzdv4dUo04PlK2Wes+YlL705F55cSXeC9npNie+9rGwFyLzCTQe18WdbZyLuw==", + "dev": true, + "requires": { + "@lerna/batch-packages": "^3.1.2", + "@lerna/check-working-tree": "^3.3.0", + "@lerna/child-process": "^3.3.0", + "@lerna/collect-updates": "^3.3.2", + "@lerna/command": "^3.3.0", + "@lerna/describe-ref": "^3.3.0", + "@lerna/get-npm-exec-opts": "^3.0.0", + "@lerna/npm-conf": "^3.4.1", + "@lerna/npm-dist-tag": "^3.3.0", + "@lerna/npm-publish": "^3.3.1", + "@lerna/output": "^3.0.0", + "@lerna/prompt": "^3.3.1", + "@lerna/run-lifecycle": "^3.4.1", + "@lerna/run-parallel-batches": "^3.0.0", + "@lerna/validation-error": "^3.0.0", + "@lerna/version": "^3.4.1", + "fs-extra": "^7.0.0", + "libnpmaccess": "^3.0.0", + "npm-package-arg": "^6.0.0", + "npm-registry-fetch": "^3.8.0", "npmlog": "^4.1.2", "p-finally": "^1.0.0", "p-map": "^1.2.0", + "p-pipe": "^1.2.0", "p-reduce": "^1.0.0", - "p-waterfall": "^1.0.0", - "semver": "^5.5.0", - "slash": "^1.0.0", - "temp-write": "^3.4.0", - "write-json-file": "^2.3.0" + "semver": "^5.5.0" } }, "@lerna/resolve-symlink": { - "version": "3.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@lerna/resolve-symlink/-/resolve-symlink-3.0.0-rc.0.tgz", - "integrity": "sha512-EBRD/65aTCyZMqWzAM7Ac1zqtZBkTE5AHzzIViQaB5el3j7ThBzzWwYuiBNWibNGTMObLQMrp+65yCbew5H+aA==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@lerna/resolve-symlink/-/resolve-symlink-3.3.0.tgz", + "integrity": "sha512-KmoPDcFJ2aOK2inYHbrsiO9SodedUj0L1JDvDgirVNIjMUaQe2Q6Vi4Gh+VCJcyB27JtfHioV9R2NxU72Pk2hg==", "dev": true, "requires": { - "fs-extra": "^6.0.1", + "fs-extra": "^7.0.0", "npmlog": "^4.1.2", "read-cmd-shim": "^1.0.1" } }, "@lerna/rimraf-dir": { - "version": "3.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@lerna/rimraf-dir/-/rimraf-dir-3.0.0-rc.0.tgz", - "integrity": "sha512-aFPX1hMOBL+5AU5xI9SZ2xKTAlnk93+tIw1ZJGWMMc4dzxNs/sT3WeUUH4v1TOYzMqWM3AnLQza3O4OY/xb2hg==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@lerna/rimraf-dir/-/rimraf-dir-3.3.0.tgz", + "integrity": "sha512-vSqOcZ4kZduiSprbt+y40qziyN3VKYh+ygiCdnbBbsaxpdKB6CfrSMUtrLhVFrqUfBHIZRzHIzgjTdtQex1KLw==", "dev": true, "requires": { - "@lerna/child-process": "^3.0.0-rc.0", + "@lerna/child-process": "^3.3.0", "npmlog": "^4.1.2", "path-exists": "^3.0.0", "rimraf": "^2.6.2" } }, "@lerna/run": { - "version": "3.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@lerna/run/-/run-3.0.0-rc.0.tgz", - "integrity": "sha512-biLKshDBwQ7n3XLihV5QvrlOBJ1isnRJC8xC8FeUbH3x1FRX15Ogg+8/GiGG7vaM6kdE4j0ebrW8HPUEh5L4cw==", - "dev": true, - "requires": { - "@lerna/batch-packages": "^3.0.0-rc.0", - "@lerna/command": "^3.0.0-rc.0", - "@lerna/filter-options": "^3.0.0-rc.0", - "@lerna/npm-run-script": "^3.0.0-rc.0", - "@lerna/output": "^3.0.0-rc.0", - "@lerna/run-parallel-batches": "^3.0.0-rc.0", - "@lerna/validation-error": "^3.0.0-rc.0", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@lerna/run/-/run-3.3.2.tgz", + "integrity": "sha512-cruwRGZZWnQ5I0M+AqcoT3Xpq2wj3135iVw4n59/Op6dZu50sMFXZNLiTTTZ15k8rTKjydcccJMdPSpTHbH7/A==", + "dev": true, + "requires": { + "@lerna/batch-packages": "^3.1.2", + "@lerna/command": "^3.3.0", + "@lerna/filter-options": "^3.3.2", + "@lerna/npm-run-script": "^3.3.0", + "@lerna/output": "^3.0.0", + "@lerna/run-parallel-batches": "^3.0.0", + "@lerna/validation-error": "^3.0.0", "p-map": "^1.2.0" } }, "@lerna/run-lifecycle": { - "version": "3.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@lerna/run-lifecycle/-/run-lifecycle-3.0.0-rc.0.tgz", - "integrity": "sha512-/cofDJ5qzAgC99+VYxO602k1wBetv79NYOXUoju3R8xPrCXGJYoBN+/NhrPdj/CciZd9lSsx016M6FcScsaO1Q==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@lerna/run-lifecycle/-/run-lifecycle-3.4.1.tgz", + "integrity": "sha512-N/hi2srM9A4BWEkXccP7vCEbf4MmIuALF00DTBMvc0A/ccItwUpl3XNuM7+ADDRK0mkwE3hDw89lJ3A7f8oUQw==", "dev": true, "requires": { - "@lerna/npm-conf": "^3.0.0-rc.0", + "@lerna/npm-conf": "^3.4.1", "npm-lifecycle": "^2.0.0", "npmlog": "^4.1.2" } }, "@lerna/run-parallel-batches": { - "version": "3.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@lerna/run-parallel-batches/-/run-parallel-batches-3.0.0-rc.0.tgz", - "integrity": "sha512-MpXiDRo02ZHazis3sDqzhmFuMxEEPKv+mmPpzLElkCDO4LHVZYvwa3ib3ezhWwt+FUFPP4u/8w2A4cFO2PbmVQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@lerna/run-parallel-batches/-/run-parallel-batches-3.0.0.tgz", + "integrity": "sha512-Mj1ravlXF7AkkewKd9YFq9BtVrsStNrvVLedD/b2wIVbNqcxp8lS68vehXVOzoL/VWNEDotvqCQtyDBilCodGw==", "dev": true, "requires": { "p-map": "^1.2.0", @@ -1618,14 +1834,14 @@ } }, "@lerna/symlink-binary": { - "version": "3.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@lerna/symlink-binary/-/symlink-binary-3.0.0-rc.0.tgz", - "integrity": "sha512-aFmZmvjNApa7i4SCrq//j7kg7E6mO3U7dzE4J28biFen2Ey1fmxObf1jiv6b24T92D/WztiBGSSTXbFR41yAsw==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@lerna/symlink-binary/-/symlink-binary-3.3.0.tgz", + "integrity": "sha512-zRo6CimhvH/VJqCFl9T4IC6syjpWyQIxEfO2sBhrapEcfwjtwbhoGgKwucsvt4rIpFazCw63jQ/AXMT27KUIHg==", "dev": true, "requires": { - "@lerna/create-symlink": "^3.0.0-rc.0", - "@lerna/package": "^3.0.0-rc.0", - "fs-extra": "^6.0.1", + "@lerna/create-symlink": "^3.3.0", + "@lerna/package": "^3.0.0", + "fs-extra": "^7.0.0", "p-map": "^1.2.0", "read-pkg": "^3.0.0" }, @@ -1678,33 +1894,62 @@ } }, "@lerna/symlink-dependencies": { - "version": "3.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@lerna/symlink-dependencies/-/symlink-dependencies-3.0.0-rc.0.tgz", - "integrity": "sha512-dk7V95ToYw1nfn7ydkGQAn0RWyye9n05vPSCQzME+OP0nL7PcsSY+0umatsRP104VMX0yTmML129Zg5xpYDSAw==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@lerna/symlink-dependencies/-/symlink-dependencies-3.3.0.tgz", + "integrity": "sha512-IRngSNCmuD5uBKVv23tHMvr7Mplti0lKHilFKcvhbvhAfu6m/Vclxhkfs/uLyHzG+DeRpl/9o86SQET3h4XDhg==", "dev": true, "requires": { - "@lerna/create-symlink": "^3.0.0-rc.0", - "@lerna/resolve-symlink": "^3.0.0-rc.0", - "@lerna/symlink-binary": "^3.0.0-rc.0", - "fs-extra": "^6.0.1", + "@lerna/create-symlink": "^3.3.0", + "@lerna/resolve-symlink": "^3.3.0", + "@lerna/symlink-binary": "^3.3.0", + "fs-extra": "^7.0.0", "p-finally": "^1.0.0", "p-map": "^1.2.0", "p-map-series": "^1.0.0" } }, "@lerna/validation-error": { - "version": "3.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@lerna/validation-error/-/validation-error-3.0.0-rc.0.tgz", - "integrity": "sha512-VuYvUC2DUjVq/DG7r52wWKmq1G0doGPB5eq7Uozi4QIIRWj2op1l6yCSogb3H2UKPn/5NrMOV4jztwQBnSJu0w==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@lerna/validation-error/-/validation-error-3.0.0.tgz", + "integrity": "sha512-5wjkd2PszV0kWvH+EOKZJWlHEqCTTKrWsvfHnHhcUaKBe/NagPZFWs+0xlsDPZ3DJt5FNfbAPAnEBQ05zLirFA==", "dev": true, "requires": { "npmlog": "^4.1.2" } }, + "@lerna/version": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@lerna/version/-/version-3.4.1.tgz", + "integrity": "sha512-oefNaQLBJSI2WLZXw5XxDXk4NyF5/ct0V9ys/J308NpgZthPgwRPjk9ZR0o1IOxW1ABi6z3E317W/dxHDjvAkg==", + "dev": true, + "requires": { + "@lerna/batch-packages": "^3.1.2", + "@lerna/check-working-tree": "^3.3.0", + "@lerna/child-process": "^3.3.0", + "@lerna/collect-updates": "^3.3.2", + "@lerna/command": "^3.3.0", + "@lerna/conventional-commits": "^3.4.1", + "@lerna/output": "^3.0.0", + "@lerna/prompt": "^3.3.1", + "@lerna/run-lifecycle": "^3.4.1", + "@lerna/validation-error": "^3.0.0", + "chalk": "^2.3.1", + "dedent": "^0.7.0", + "minimatch": "^3.0.4", + "npmlog": "^4.1.2", + "p-map": "^1.2.0", + "p-pipe": "^1.2.0", + "p-reduce": "^1.0.0", + "p-waterfall": "^1.0.0", + "semver": "^5.5.0", + "slash": "^1.0.0", + "temp-write": "^3.4.0" + } + }, "@lerna/write-log-file": { - "version": "3.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@lerna/write-log-file/-/write-log-file-3.0.0-rc.0.tgz", - "integrity": "sha512-LHQPRY/1eWyq7ly+4A412FT9uzGKHDSGHkLYVro8r6mToPB/6eHdntJFRGOYNzKM5eax6RgrzBImEhs3F9FWSQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@lerna/write-log-file/-/write-log-file-3.0.0.tgz", + "integrity": "sha512-SfbPp29lMeEVOb/M16lJwn4nnx5y+TwCdd7Uom9umd7KcZP0NOvpnX0PHehdonl7TyHZ1Xx2maklYuCLbQrd/A==", "dev": true, "requires": { "npmlog": "^4.1.2", @@ -2508,9 +2753,9 @@ } }, "JSONStream": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.3.tgz", - "integrity": "sha512-3Sp6WZZ/lXl+nTDoGpGWHEpTnnC6X5fnkolYZR6nwIfzbxxvA8utPWe1gCt7i0m9uVGsSz2IS8K8mJ7HmlduMg==", + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", "dev": true, "requires": { "jsonparse": "^1.2.0", @@ -2589,6 +2834,15 @@ "es6-promisify": "^5.0.0" } }, + "agentkeepalive": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-3.5.2.tgz", + "integrity": "sha512-e0L/HNe6qkQ7H19kTlRRqUibEAwDK5AFk6y3PtMsuut2VAH6+Q4xZml1tNDJD7kSAyqmbG/K08K5WEJYtUrSlQ==", + "dev": true, + "requires": { + "humanize-ms": "^1.2.1" + } + }, "airbnb-prop-types": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/airbnb-prop-types/-/airbnb-prop-types-2.10.0.tgz", @@ -4579,6 +4833,12 @@ "integrity": "sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE=", "dev": true }, + "byte-size": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/byte-size/-/byte-size-4.0.4.tgz", + "integrity": "sha512-82RPeneC6nqCdSwCX2hZUz3JPOvN5at/nTEw/CMf05Smu3Hrpo9Psb7LjN+k+XndNArG1EY8L4+BM3aTM4BCvw==", + "dev": true + }, "bytes": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-1.0.0.tgz", @@ -4755,12 +5015,6 @@ "rsvp": "^3.3.3" } }, - "capture-stack-trace": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz", - "integrity": "sha1-Sm+gc5nCa7pH8LJJa00PtAjFVQ0=", - "dev": true - }, "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", @@ -5530,9 +5784,9 @@ } }, "config-chain": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.11.tgz", - "integrity": "sha1-q6CXR9++TD5w52am5BWG4YWfxvI=", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.12.tgz", + "integrity": "sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA==", "dev": true, "requires": { "ini": "^1.3.4", @@ -5584,9 +5838,9 @@ "dev": true }, "conventional-changelog-angular": { - "version": "1.6.6", - "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-1.6.6.tgz", - "integrity": "sha512-suQnFSqCxRwyBxY68pYTsFkG0taIdinHLNEAX5ivtw8bCRnIgnpvcHmlR/yjUyZIrNPYAoXlY1WiEKWgSE4BNg==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-5.0.2.tgz", + "integrity": "sha512-yx7m7lVrXmt4nKWQgWZqxSALEiAKZhOAcbxdUaU9575mB0CzXVbgrgpfSnSP7OqWDUTYGD0YVJ0MSRdyOPgAwA==", "dev": true, "requires": { "compare-func": "^1.3.1", @@ -5594,40 +5848,97 @@ } }, "conventional-changelog-core": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/conventional-changelog-core/-/conventional-changelog-core-2.0.11.tgz", - "integrity": "sha512-HvTE6RlqeEZ/NFPtQeFLsIDOLrGP3bXYr7lFLMhCVsbduF1MXIe8OODkwMFyo1i9ku9NWBwVnVn0jDmIFXjDRg==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/conventional-changelog-core/-/conventional-changelog-core-3.1.5.tgz", + "integrity": "sha512-iwqAotS4zk0wA4S84YY1JCUG7X3LxaRjJxuUo6GI4dZuIy243j5nOg/Ora35ExT4DOiw5dQbMMQvw2SUjh6moQ==", "dev": true, "requires": { - "conventional-changelog-writer": "^3.0.9", - "conventional-commits-parser": "^2.1.7", + "conventional-changelog-writer": "^4.0.2", + "conventional-commits-parser": "^3.0.1", "dateformat": "^3.0.0", "get-pkg-repo": "^1.0.0", - "git-raw-commits": "^1.3.6", + "git-raw-commits": "2.0.0", "git-remote-origin-url": "^2.0.0", - "git-semver-tags": "^1.3.6", + "git-semver-tags": "^2.0.2", "lodash": "^4.2.1", "normalize-package-data": "^2.3.5", "q": "^1.5.1", - "read-pkg": "^1.1.0", - "read-pkg-up": "^1.0.1", + "read-pkg": "^3.0.0", + "read-pkg-up": "^3.0.0", "through2": "^2.0.0" + }, + "dependencies": { + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + } + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "dev": true, + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + } + }, + "read-pkg-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", + "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", + "dev": true, + "requires": { + "find-up": "^2.0.0", + "read-pkg": "^3.0.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + } } }, "conventional-changelog-preset-loader": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/conventional-changelog-preset-loader/-/conventional-changelog-preset-loader-1.1.8.tgz", - "integrity": "sha512-MkksM4G4YdrMlT2MbTsV2F6LXu/hZR0Tc/yenRrDIKRwBl/SP7ER4ZDlglqJsCzLJi4UonBc52Bkm5hzrOVCcw==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/conventional-changelog-preset-loader/-/conventional-changelog-preset-loader-2.0.2.tgz", + "integrity": "sha512-pBY+qnUoJPXAXXqVGwQaVmcye05xi6z231QM98wHWamGAmu/ghkBprQAwmF5bdmyobdVxiLhPY3PrCfSeUNzRQ==", "dev": true }, "conventional-changelog-writer": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-3.0.9.tgz", - "integrity": "sha512-n9KbsxlJxRQsUnK6wIBRnARacvNnN4C/nxnxCkH+B/R1JS2Fa+DiP1dU4I59mEDEjgnFaN2+9wr1P1s7GYB5/Q==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-4.0.2.tgz", + "integrity": "sha512-d8/FQY/fix2xXEBUhOo8u3DCbyEw3UOQgYHxLsPDw+wHUDma/GQGAGsGtoH876WyNs32fViHmTOUrgRKVLvBug==", "dev": true, "requires": { "compare-func": "^1.3.1", - "conventional-commits-filter": "^1.1.6", + "conventional-commits-filter": "^2.0.1", "dateformat": "^3.0.0", "handlebars": "^4.0.2", "json-stringify-safe": "^5.0.1", @@ -5719,9 +6030,9 @@ } }, "conventional-commits-filter": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-1.1.6.tgz", - "integrity": "sha512-KcDgtCRKJCQhyk6VLT7zR+ZOyCnerfemE/CsR3iQpzRRFbLEs0Y6rwk3mpDvtOh04X223z+1xyJ582Stfct/0Q==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-2.0.1.tgz", + "integrity": "sha512-92OU8pz/977udhBjgPEbg3sbYzIxMDFTlQT97w7KdhR9igNqdJvy8smmedAAgn4tPiqseFloKkrVfbXCVd+E7A==", "dev": true, "requires": { "is-subset": "^0.1.1", @@ -5729,9 +6040,9 @@ } }, "conventional-commits-parser": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-2.1.7.tgz", - "integrity": "sha512-BoMaddIEJ6B4QVMSDu9IkVImlGOSGA1I2BQyOZHeLQ6qVOJLcLKn97+fL6dGbzWEiqDzfH4OkcveULmeq2MHFQ==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-3.0.1.tgz", + "integrity": "sha512-P6U5UOvDeidUJ8ebHVDIoXzI7gMlQ1OF/id6oUvp8cnZvOXMt1n8nYl74Ey9YMn0uVQtxmCtjPQawpsssBWtGg==", "dev": true, "requires": { "JSONStream": "^1.0.4", @@ -5824,17 +6135,17 @@ } }, "conventional-recommended-bump": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/conventional-recommended-bump/-/conventional-recommended-bump-2.0.9.tgz", - "integrity": "sha512-YE6/o+648qkX3fTNvfBsvPW3tSnbZ6ec3gF0aBahCPgyoVHU2Mw0nUAZ1h1UN65GazpORngrgRC8QCltNYHPpQ==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/conventional-recommended-bump/-/conventional-recommended-bump-4.0.4.tgz", + "integrity": "sha512-9mY5Yoblq+ZMqJpBzgS+RpSq+SUfP2miOR3H/NR9drGf08WCrY9B6HAGJZEm6+ThsVP917VHAahSOjM6k1vhPg==", "dev": true, "requires": { "concat-stream": "^1.6.0", - "conventional-changelog-preset-loader": "^1.1.8", - "conventional-commits-filter": "^1.1.6", - "conventional-commits-parser": "^2.1.7", - "git-raw-commits": "^1.3.6", - "git-semver-tags": "^1.3.6", + "conventional-changelog-preset-loader": "^2.0.2", + "conventional-commits-filter": "^2.0.1", + "conventional-commits-parser": "^3.0.1", + "git-raw-commits": "2.0.0", + "git-semver-tags": "^2.0.2", "meow": "^4.0.0", "q": "^1.5.1" }, @@ -6055,15 +6366,6 @@ "elliptic": "^6.0.0" } }, - "create-error-class": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", - "integrity": "sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=", - "dev": true, - "requires": { - "capture-stack-trace": "^1.0.0" - } - }, "create-hash": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", @@ -7049,6 +7351,12 @@ "resolved": "https://registry.npmjs.org/equivalent-key-map/-/equivalent-key-map-0.2.2.tgz", "integrity": "sha512-xvHeyCDbZzkpN4VHQj/n+j2lOwL0VWszG30X4cOrc9Y7Tuo2qCdZK/0AMod23Z5dCtNUbaju6p0rwOhHUk05ew==" }, + "err-code": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-1.1.2.tgz", + "integrity": "sha1-BuARbTAo9q70gGhJ6w6mp0iuaWA=", + "dev": true + }, "errno": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", @@ -8030,6 +8338,12 @@ "pend": "~1.2.0" } }, + "figgy-pudding": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz", + "integrity": "sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w==", + "dev": true + }, "figures": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", @@ -8383,9 +8697,9 @@ "dev": true }, "fs-extra": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-6.0.1.tgz", - "integrity": "sha512-GnyIkKhhzXZUWFCaJzvyDLEEgDkPfb4/TPvJCJVuS8MWZgoSsErf++QpiAlDnKFcqhRlm+tIOcencCjyJE6ZCA==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "dev": true, "requires": { "graceful-fs": "^4.1.2", @@ -8393,6 +8707,15 @@ "universalify": "^0.1.0" } }, + "fs-minipass": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz", + "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", + "dev": true, + "requires": { + "minipass": "^2.2.1" + } + }, "fs-write-stream-atomic": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", @@ -9035,6 +9358,12 @@ "globule": "^1.0.0" } }, + "genfun": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/genfun/-/genfun-5.0.0.tgz", + "integrity": "sha512-KGDOARWVga7+rnB3z9Sd2Letx515owfk0hSxHGuqjANb1M+x2bGZGqHLiozPsYMdM2OubeMni/Hpwmjq6qIUhA==", + "dev": true + }, "get-caller-file": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", @@ -9232,9 +9561,9 @@ } }, "git-raw-commits": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-1.3.6.tgz", - "integrity": "sha512-svsK26tQ8vEKnMshTDatSIQSMDdz8CxIIqKsvPqbtV23Etmw6VNaFAitu8zwZ0VrOne7FztwPyRLxK7/DIUTQg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-2.0.0.tgz", + "integrity": "sha512-w4jFEJFgKXMQJ0H0ikBk2S+4KP2VEjhCvLCNqbNRQC8BgGWgLKNCO7a9K9LI+TVT7Gfoloje502sEnctibffgg==", "dev": true, "requires": { "dargs": "^4.0.1", @@ -9335,9 +9664,9 @@ } }, "git-semver-tags": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/git-semver-tags/-/git-semver-tags-1.3.6.tgz", - "integrity": "sha512-2jHlJnln4D/ECk9FxGEBh3k44wgYdWjWDtMmJPaecjoRmxKo3Y1Lh8GMYuOPu04CHw86NTAODchYjC5pnpMQig==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/git-semver-tags/-/git-semver-tags-2.0.2.tgz", + "integrity": "sha512-34lMF7Yo1xEmsK2EkbArdoU79umpvm0MfzaDkSNYSJqtM5QLAVTPWgpiXSVI5o/O9EvZPSrP4Zvnec/CqhSd5w==", "dev": true, "requires": { "meow": "^4.0.0", @@ -9603,25 +9932,6 @@ "delegate": "^3.1.2" } }, - "got": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz", - "integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=", - "dev": true, - "requires": { - "create-error-class": "^3.0.0", - "duplexer3": "^0.1.4", - "get-stream": "^3.0.0", - "is-redirect": "^1.0.0", - "is-retry-allowed": "^1.0.0", - "is-stream": "^1.0.0", - "lowercase-keys": "^1.0.0", - "safe-buffer": "^5.0.1", - "timed-out": "^4.0.0", - "unzip-response": "^2.0.1", - "url-parse-lax": "^1.0.0" - } - }, "graceful-fs": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", @@ -9945,6 +10255,16 @@ "integrity": "sha1-O9bW/ebjFyyTNMOzO2wZPYD+ETc=", "dev": true }, + "http-proxy-agent": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz", + "integrity": "sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==", + "dev": true, + "requires": { + "agent-base": "4", + "debug": "3.1.0" + } + }, "http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", @@ -9972,6 +10292,15 @@ "debug": "^3.1.0" } }, + "humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=", + "dev": true, + "requires": { + "ms": "^2.0.0" + } + }, "husky": { "version": "0.14.3", "resolved": "https://registry.npmjs.org/husky/-/husky-0.14.3.tgz", @@ -10017,6 +10346,15 @@ "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", "dev": true }, + "ignore-walk": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", + "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", + "dev": true, + "requires": { + "minimatch": "^3.0.4" + } + }, "import-lazy": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-3.1.0.tgz", @@ -10152,6 +10490,12 @@ "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=" }, + "ip": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", + "dev": true + }, "ipaddr.js": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz", @@ -10516,12 +10860,6 @@ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" }, - "is-redirect": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", - "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=", - "dev": true - }, "is-regex": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", @@ -12896,12 +13234,26 @@ "dev": true }, "lerna": { - "version": "3.0.0-rc.0", - "resolved": "https://registry.npmjs.org/lerna/-/lerna-3.0.0-rc.0.tgz", - "integrity": "sha512-fj5Ku6vGgJAzdnpXWE3Stlgnex9ZfaHBQvMQzts13qZ57cJNCzEq5AQPVOOFWE6qqqiABLQfE5T2+Yg/IEqWNQ==", - "dev": true, - "requires": { - "@lerna/cli": "^3.0.0-rc.0", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/lerna/-/lerna-3.4.3.tgz", + "integrity": "sha512-tWq1LvpHqkyB+FaJCmkEweivr88yShDMmauofPVdh0M5gU1cVucszYnIgWafulKYu2LMQ3IfUMUU5Pp3+MvADQ==", + "dev": true, + "requires": { + "@lerna/add": "^3.4.1", + "@lerna/bootstrap": "^3.4.1", + "@lerna/changed": "^3.4.1", + "@lerna/clean": "^3.3.2", + "@lerna/cli": "^3.2.0", + "@lerna/create": "^3.4.1", + "@lerna/diff": "^3.3.0", + "@lerna/exec": "^3.3.2", + "@lerna/import": "^3.3.1", + "@lerna/init": "^3.3.0", + "@lerna/link": "^3.3.0", + "@lerna/list": "^3.3.2", + "@lerna/publish": "^3.4.3", + "@lerna/run": "^3.3.2", + "@lerna/version": "^3.4.1", "import-local": "^1.0.0", "npmlog": "^4.1.2" } @@ -12918,8 +13270,47 @@ "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", "dev": true, "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "libnpmaccess": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/libnpmaccess/-/libnpmaccess-3.0.0.tgz", + "integrity": "sha512-SiE4AZAzMpD7pmmXHfgD7rof8QIQGoKaeyAS8exgx2CKA6tzRTbRljq1xM4Tgj8/tIg+KBJPJWkR0ifqKT3irQ==", + "dev": true, + "requires": { + "aproba": "^2.0.0", + "get-stream": "^4.0.0", + "npm-package-arg": "^6.1.0", + "npm-registry-fetch": "^3.8.0" + }, + "dependencies": { + "aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "dev": true + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } } }, "line-height": { @@ -13675,6 +14066,16 @@ "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", "dev": true }, + "lru-cache": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz", + "integrity": "sha512-fFEhvcgzuIoJVUF8fYr5KR0YqxD238zgObTps31YdADwPPAp82a4M8TrckkWyx7ekNlf9aBcVn81cFwwXngrJA==", + "dev": true, + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, "make-dir": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", @@ -13692,6 +14093,92 @@ } } }, + "make-fetch-happen": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-4.0.1.tgz", + "integrity": "sha512-7R5ivfy9ilRJ1EMKIOziwrns9fGeAD4bAha8EB7BIiBBLHm2KeTUGCrICFt2rbHfzheTLynv50GnNTK1zDTrcQ==", + "dev": true, + "requires": { + "agentkeepalive": "^3.4.1", + "cacache": "^11.0.1", + "http-cache-semantics": "^3.8.1", + "http-proxy-agent": "^2.1.0", + "https-proxy-agent": "^2.2.1", + "lru-cache": "^4.1.2", + "mississippi": "^3.0.0", + "node-fetch-npm": "^2.0.2", + "promise-retry": "^1.1.1", + "socks-proxy-agent": "^4.0.0", + "ssri": "^6.0.0" + }, + "dependencies": { + "cacache": { + "version": "11.3.1", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-11.3.1.tgz", + "integrity": "sha512-2PEw4cRRDu+iQvBTTuttQifacYjLPhET+SYO/gEFMy8uhi+jlJREDAjSF5FWSdV/Aw5h18caHA7vMTw2c+wDzA==", + "dev": true, + "requires": { + "bluebird": "^3.5.1", + "chownr": "^1.0.1", + "figgy-pudding": "^3.1.0", + "glob": "^7.1.2", + "graceful-fs": "^4.1.11", + "lru-cache": "^4.1.3", + "mississippi": "^3.0.0", + "mkdirp": "^0.5.1", + "move-concurrently": "^1.0.1", + "promise-inflight": "^1.0.1", + "rimraf": "^2.6.2", + "ssri": "^6.0.0", + "unique-filename": "^1.1.0", + "y18n": "^4.0.0" + } + }, + "mississippi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", + "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==", + "dev": true, + "requires": { + "concat-stream": "^1.5.0", + "duplexify": "^3.4.2", + "end-of-stream": "^1.1.0", + "flush-write-stream": "^1.0.0", + "from2": "^2.1.0", + "parallel-transform": "^1.1.0", + "pump": "^3.0.0", + "pumpify": "^1.3.3", + "stream-each": "^1.1.0", + "through2": "^2.0.0" + } + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "ssri": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", + "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", + "dev": true, + "requires": { + "figgy-pudding": "^3.5.1" + } + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + } + } + }, "makeerror": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz", @@ -13701,6 +14188,15 @@ "tmpl": "1.0.x" } }, + "map-age-cleaner": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.2.tgz", + "integrity": "sha512-UN1dNocxQq44IhJyMI4TU8phc2m9BddacHRPRjKGLYaF0jqd3xLz0jS0skpAU9WgYyoR4gHtUpzytNBS385FWQ==", + "dev": true, + "requires": { + "p-defer": "^1.0.0" + } + }, "map-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", @@ -14119,6 +14615,33 @@ "is-plain-obj": "^1.1.0" } }, + "minipass": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz", + "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + }, + "dependencies": { + "yallist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz", + "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=", + "dev": true + } + } + }, + "minizlib": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.1.1.tgz", + "integrity": "sha512-TrfjCjk4jLhcJyGMYymBH6oTXcWjYbUAXTHDbtnWHjZC25h0cdajHuPE1zxb4DVmu8crfh+HwH/WMuyLG0nHBg==", + "dev": true, + "requires": { + "minipass": "^2.2.1" + } + }, "mississippi": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-2.0.0.tgz", @@ -14330,6 +14853,17 @@ "is-stream": "^1.0.1" } }, + "node-fetch-npm": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/node-fetch-npm/-/node-fetch-npm-2.0.2.tgz", + "integrity": "sha512-nJIxm1QmAj4v3nfCvEeCrYSoVwXyxLnaPBK5W1W5DGEJwjlKuC2VEUycGw5oxk+4zZahRrB84PUJJgEmhFTDFw==", + "dev": true, + "requires": { + "encoding": "^0.1.11", + "json-parse-better-errors": "^1.0.0", + "safe-buffer": "^5.1.1" + } + }, "node-gyp": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz", @@ -14664,20 +15198,26 @@ } } }, + "npm-bundled": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.5.tgz", + "integrity": "sha512-m/e6jgWu8/v5niCUKQi9qQl8QdeEduFA96xHDDzFGqly0OOjI7c+60KM/2sppfnUU9JJagf+zs+yGhqSOFj71g==", + "dev": true + }, "npm-lifecycle": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/npm-lifecycle/-/npm-lifecycle-2.0.3.tgz", - "integrity": "sha512-0U4Iim5ix2NHUT672G7FBpldJX0N2xKBjJqRTAzioEJjb6I6KpQXq+y1sB5EDSjKaAX8VCC9qPK31Jy+p3ix5A==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/npm-lifecycle/-/npm-lifecycle-2.1.0.tgz", + "integrity": "sha512-QbBfLlGBKsktwBZLj6AviHC6Q9Y3R/AY4a2PYSIRhSKSS0/CxRyD/PfxEX6tPeOCXQgMSNdwGeECacstgptc+g==", "dev": true, "requires": { "byline": "^5.0.0", "graceful-fs": "^4.1.11", - "node-gyp": "^3.6.2", + "node-gyp": "^3.8.0", "resolve-from": "^4.0.0", "slide": "^1.1.6", "uid-number": "0.0.6", "umask": "^1.1.0", - "which": "^1.3.0" + "which": "^1.3.1" }, "dependencies": { "resolve-from": { @@ -14746,6 +15286,16 @@ } } }, + "npm-packlist": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.1.12.tgz", + "integrity": "sha512-WJKFOVMeAlsU/pjXuqVdzU0WfgtIBCupkEVwn+1Y0ERAbUfWw8R4GjgVbaKnUjRoD2FoQbHOCbOyT5Mbs9Lw4g==", + "dev": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, "npm-path": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/npm-path/-/npm-path-2.0.4.tgz", @@ -14755,6 +15305,31 @@ "which": "^1.2.10" } }, + "npm-pick-manifest": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-2.2.3.tgz", + "integrity": "sha512-+IluBC5K201+gRU85vFlUwX3PFShZAbAgDNp2ewJdWMVSppdo/Zih0ul2Ecky/X7b51J7LrrUAP+XOmOCvYZqA==", + "dev": true, + "requires": { + "figgy-pudding": "^3.5.1", + "npm-package-arg": "^6.0.0", + "semver": "^5.4.1" + } + }, + "npm-registry-fetch": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-3.8.0.tgz", + "integrity": "sha512-hrw8UMD+Nob3Kl3h8Z/YjmKamb1gf7D1ZZch2otrIXM3uFLB5vjEY6DhMlq80z/zZet6eETLbOXcuQudCB3Zpw==", + "dev": true, + "requires": { + "JSONStream": "^1.3.4", + "bluebird": "^3.5.1", + "figgy-pudding": "^3.4.1", + "lru-cache": "^4.1.3", + "make-fetch-happen": "^4.0.1", + "npm-package-arg": "^6.1.0" + } + }, "npm-run-path": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", @@ -15129,6 +15704,12 @@ "integrity": "sha512-HNa1A8LvB1kie7cERyy21VNeHb2CWJJYqyyC2o3klWFfMGlFmWv2Z7sFgZH8ZiaYL95ydToKTFVXgMV/Os0bBQ==", "dev": true }, + "p-defer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", + "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", + "dev": true + }, "p-each-series": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-1.0.0.tgz", @@ -15186,6 +15767,12 @@ "p-reduce": "^1.0.0" } }, + "p-pipe": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/p-pipe/-/p-pipe-1.2.0.tgz", + "integrity": "sha1-SxoROZoRUgpneQ7loMHViB1r7+k=", + "dev": true + }, "p-reduce": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-reduce/-/p-reduce-1.0.0.tgz", @@ -15215,16 +15802,179 @@ "p-reduce": "^1.0.0" } }, - "package-json": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/package-json/-/package-json-4.0.1.tgz", - "integrity": "sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0=", + "pacote": { + "version": "9.2.3", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-9.2.3.tgz", + "integrity": "sha512-Y3+yY3nBRAxMlZWvr62XLJxOwCmG9UmkGZkFurWHoCjqF0cZL72cTOCRJTvWw8T4OhJS2RTg13x4oYYriauvEw==", "dev": true, "requires": { - "got": "^6.7.1", - "registry-auth-token": "^3.0.1", - "registry-url": "^3.0.3", - "semver": "^5.1.0" + "bluebird": "^3.5.2", + "cacache": "^11.2.0", + "figgy-pudding": "^3.5.1", + "get-stream": "^4.1.0", + "glob": "^7.1.3", + "lru-cache": "^4.1.3", + "make-fetch-happen": "^4.0.1", + "minimatch": "^3.0.4", + "minipass": "^2.3.5", + "mississippi": "^3.0.0", + "mkdirp": "^0.5.1", + "normalize-package-data": "^2.4.0", + "npm-package-arg": "^6.1.0", + "npm-packlist": "^1.1.12", + "npm-pick-manifest": "^2.2.3", + "npm-registry-fetch": "^3.8.0", + "osenv": "^0.1.5", + "promise-inflight": "^1.0.1", + "promise-retry": "^1.1.1", + "protoduck": "^5.0.1", + "rimraf": "^2.6.2", + "safe-buffer": "^5.1.2", + "semver": "^5.6.0", + "ssri": "^6.0.1", + "tar": "^4.4.6", + "unique-filename": "^1.1.1", + "which": "^1.3.1" + }, + "dependencies": { + "bluebird": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.3.tgz", + "integrity": "sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw==", + "dev": true + }, + "cacache": { + "version": "11.3.1", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-11.3.1.tgz", + "integrity": "sha512-2PEw4cRRDu+iQvBTTuttQifacYjLPhET+SYO/gEFMy8uhi+jlJREDAjSF5FWSdV/Aw5h18caHA7vMTw2c+wDzA==", + "dev": true, + "requires": { + "bluebird": "^3.5.1", + "chownr": "^1.0.1", + "figgy-pudding": "^3.1.0", + "glob": "^7.1.2", + "graceful-fs": "^4.1.11", + "lru-cache": "^4.1.3", + "mississippi": "^3.0.0", + "mkdirp": "^0.5.1", + "move-concurrently": "^1.0.1", + "promise-inflight": "^1.0.1", + "rimraf": "^2.6.2", + "ssri": "^6.0.0", + "unique-filename": "^1.1.0", + "y18n": "^4.0.0" + } + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "mississippi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", + "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==", + "dev": true, + "requires": { + "concat-stream": "^1.5.0", + "duplexify": "^3.4.2", + "end-of-stream": "^1.1.0", + "flush-write-stream": "^1.0.0", + "from2": "^2.1.0", + "parallel-transform": "^1.1.0", + "pump": "^3.0.0", + "pumpify": "^1.3.3", + "stream-each": "^1.1.0", + "through2": "^2.0.0" + } + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "semver": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", + "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==", + "dev": true + }, + "ssri": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", + "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", + "dev": true, + "requires": { + "figgy-pudding": "^3.5.1" + } + }, + "tar": { + "version": "4.4.8", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.8.tgz", + "integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==", + "dev": true, + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.3.4", + "minizlib": "^1.1.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.2" + }, + "dependencies": { + "chownr": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", + "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==", + "dev": true + } + } + }, + "unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "dev": true, + "requires": { + "unique-slug": "^2.0.0" + } + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + }, + "yallist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz", + "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=", + "dev": true + } } }, "pako": { @@ -16658,6 +17408,16 @@ "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", "dev": true }, + "promise-retry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-1.1.1.tgz", + "integrity": "sha1-ZznpaOMFHaIM5kl/srUPaRHfPW0=", + "dev": true, + "requires": { + "err-code": "^1.0.0", + "retry": "^0.10.0" + } + }, "prompts": { "version": "0.1.14", "resolved": "https://registry.npmjs.org/prompts/-/prompts-0.1.14.tgz", @@ -16702,6 +17462,15 @@ "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=", "dev": true }, + "protoduck": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/protoduck/-/protoduck-5.0.1.tgz", + "integrity": "sha512-WxoCeDCoCBY55BMvj4cAEjdVUFGRWed9ZxPlqTKYyw1nDDTQ4pqmnIMAGfJlg7Dx35uB/M+PHJPTmGOvaCaPTg==", + "dev": true, + "requires": { + "genfun": "^5.0.0" + } + }, "proxy-addr": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz", @@ -16942,26 +17711,6 @@ } } }, - "rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - } - } - }, "re-resizable": { "version": "4.7.1", "resolved": "https://registry.npmjs.org/re-resizable/-/re-resizable-4.7.1.tgz", @@ -17539,25 +18288,6 @@ "unicode-match-property-value-ecmascript": "^1.0.2" } }, - "registry-auth-token": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.2.tgz", - "integrity": "sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==", - "dev": true, - "requires": { - "rc": "^1.1.6", - "safe-buffer": "^5.0.1" - } - }, - "registry-url": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", - "integrity": "sha1-PU74cPc93h138M+aOBQyRE4XSUI=", - "dev": true, - "requires": { - "rc": "^1.0.1" - } - }, "regjsgen": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.4.0.tgz", @@ -17848,6 +18578,12 @@ "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", "dev": true }, + "retry": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.10.1.tgz", + "integrity": "sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q=", + "dev": true + }, "rgb": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/rgb/-/rgb-0.1.0.tgz", @@ -18481,6 +19217,12 @@ "integrity": "sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=", "dev": true }, + "smart-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.0.1.tgz", + "integrity": "sha512-RFqinRVJVcCAL9Uh1oVqE6FZkqsyLiVOYEZ20TqIOjuX7iFVJ+zsbs4RIghnw/pTs7mZvt8ZHhvm1ZUrR4fykg==", + "dev": true + }, "snapdragon": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", @@ -18597,6 +19339,26 @@ } } }, + "socks": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.2.2.tgz", + "integrity": "sha512-g6wjBnnMOZpE0ym6e0uHSddz9p3a+WsBaaYQaBaSCJYvrC4IXykQR9MNGjLQf38e9iIIhp3b1/Zk8YZI3KGJ0Q==", + "dev": true, + "requires": { + "ip": "^1.1.5", + "smart-buffer": "^4.0.1" + } + }, + "socks-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-4.0.1.tgz", + "integrity": "sha512-Kezx6/VBguXOsEe5oU3lXYyKMi4+gva72TwJ7pQY5JfqUx2nMk7NXA6z/mpNqIlfQjWYVfeuNvQjexiTaTn6Nw==", + "dev": true, + "requires": { + "agent-base": "~4.2.0", + "socks": "~2.2.0" + } + }, "sort-keys": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz", @@ -19040,22 +19802,21 @@ "dev": true }, "strong-log-transformer": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/strong-log-transformer/-/strong-log-transformer-1.0.6.tgz", - "integrity": "sha1-9/uTdYpppXEUAYEnfuoMLrEwH6M=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strong-log-transformer/-/strong-log-transformer-2.0.0.tgz", + "integrity": "sha512-FQmNqAXJgOX8ygOcvPLlGWBNT41mvNJ9ALoYf0GTwVt9t30mGTqpmp/oJx5gLcu52DXK10kS7dVWhx8aPXDTlg==", "dev": true, "requires": { "byline": "^5.0.0", "duplexer": "^0.1.1", - "minimist": "^0.1.0", - "moment": "^2.6.0", + "minimist": "^1.2.0", "through": "^2.3.4" }, "dependencies": { "minimist": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.1.0.tgz", - "integrity": "sha1-md9lelJXTCHJBXSX33QnkLK0wN4=", + "version": "1.2.0", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true } } @@ -19653,9 +20414,9 @@ } }, "text-extensions": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-1.7.0.tgz", - "integrity": "sha512-AKXZeDq230UaSzaO5s3qQUZOaC7iKbzq0jOFL614R7d9R593HLqAOL0cYoqLdkNrjBSOdmoQI06yigq1TSBXAg==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-1.9.0.tgz", + "integrity": "sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==", "dev": true }, "text-table": { @@ -20334,12 +21095,6 @@ "integrity": "sha512-iSk/J8efr8uPT/Z4eSUywnqyrQU7DSdMfdqK4iWEaUVVmcP5JcnpRqmVMwcwcnmI1ATFNgC5V90u09tBynNFKA==", "dev": true }, - "unzip-response": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz", - "integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=", - "dev": true - }, "upath": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.0.tgz", diff --git a/package.json b/package.json index 0f8afe18111e6..ffc1552fbb076 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "glob": "7.1.2", "husky": "0.14.3", "jest-puppeteer": "3.2.1", - "lerna": "3.0.0-rc.0", + "lerna": "3.4.3", "lint-staged": "7.2.0", "lodash": "4.17.10", "mkdirp": "0.5.1", From eba8b7f32835d9b48e07d508ae14e71a4b956537 Mon Sep 17 00:00:00 2001 From: Matthew Riley MacPherson <hi@tofumatt.com> Date: Fri, 9 Nov 2018 13:24:09 +0000 Subject: [PATCH 067/106] fix: Change aria-label depending on content of paragraph block (#11653) * fix: Only show aria-label when content is empty Fix issue introduced in #11560 * Improve aria labels so they reflect state of block/empty block --- docs/design/block-design.md | 2 +- lib/client-assets.php | 2 +- packages/block-library/src/paragraph/edit.js | 4 ++-- .../src/paragraph/test/__snapshots__/index.js.snap | 4 ++-- .../editor/src/components/default-block-appender/index.js | 2 +- .../default-block-appender/test/__snapshots__/index.js.snap | 4 ++-- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/design/block-design.md b/docs/design/block-design.md index 09dd0ea9ad2ac..7849cf693b930 100644 --- a/docs/design/block-design.md +++ b/docs/design/block-design.md @@ -97,7 +97,7 @@ The most basic unit of the editor. The paragraph block is a simple input field. ### Placeholder: -- Simple placeholder text that reads “Start writing or press / to insert a block”. The placeholder disappears when the block is selected. +- Simple placeholder text that reads “Start writing or type / to choose a block”. The placeholder disappears when the block is selected. ### Selected state: diff --git a/lib/client-assets.php b/lib/client-assets.php index 9bb0180c92963..bbc898b8040ae 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -1619,7 +1619,7 @@ function gutenberg_editor_scripts_and_styles( $hook ) { 'disableCustomFontSizes' => get_theme_support( 'disable-custom-font-sizes' ), 'disablePostFormats' => ! current_theme_supports( 'post-formats' ), 'titlePlaceholder' => apply_filters( 'enter_title_here', __( 'Add title', 'gutenberg' ), $post ), - 'bodyPlaceholder' => apply_filters( 'write_your_story', __( 'Start writing or press / to insert a block', 'gutenberg' ), $post ), + 'bodyPlaceholder' => apply_filters( 'write_your_story', __( 'Start writing or type / to choose a block', 'gutenberg' ), $post ), 'isRTL' => is_rtl(), 'autosaveInterval' => 10, 'maxUploadFileSize' => $max_upload_size, diff --git a/packages/block-library/src/paragraph/edit.js b/packages/block-library/src/paragraph/edit.js index 503870ebc0ab6..f538ed4938ac9 100644 --- a/packages/block-library/src/paragraph/edit.js +++ b/packages/block-library/src/paragraph/edit.js @@ -246,8 +246,8 @@ class ParagraphBlock extends Component { onMerge={ mergeBlocks } onReplace={ this.onReplace } onRemove={ () => onReplace( [] ) } - aria-label={ __( 'Empty block; type text or press the forward slash key to insert a block' ) } - placeholder={ placeholder || __( 'Start writing or press / to insert a block' ) } + aria-label={ content ? __( 'Paragraph block' ) : __( 'Empty block; start writing or type forward slash to choose a block' ) } + placeholder={ placeholder || __( 'Start writing or type / to choose a block' ) } /> </Fragment> ); diff --git a/packages/block-library/src/paragraph/test/__snapshots__/index.js.snap b/packages/block-library/src/paragraph/test/__snapshots__/index.js.snap index a53f564e19258..4e273f676b1b3 100644 --- a/packages/block-library/src/paragraph/test/__snapshots__/index.js.snap +++ b/packages/block-library/src/paragraph/test/__snapshots__/index.js.snap @@ -14,7 +14,7 @@ exports[`core/paragraph block edit matches snapshot 1`] = ` <p aria-autocomplete="list" aria-expanded="false" - aria-label="Empty block; type text or press the forward slash key to insert a block" + aria-label="Empty block; start writing or type forward slash to choose a block" aria-multiline="true" class="wp-block-paragraph editor-rich-text__tinymce" contenteditable="true" @@ -28,7 +28,7 @@ exports[`core/paragraph block edit matches snapshot 1`] = ` <p class="editor-rich-text__tinymce wp-block-paragraph" > - Start writing or press / to insert a block + Start writing or type / to choose a block </p> </div> </div> diff --git a/packages/editor/src/components/default-block-appender/index.js b/packages/editor/src/components/default-block-appender/index.js index d280e33019068..8cc05c140878e 100644 --- a/packages/editor/src/components/default-block-appender/index.js +++ b/packages/editor/src/components/default-block-appender/index.js @@ -31,7 +31,7 @@ export function DefaultBlockAppender( { return null; } - const value = decodeEntities( placeholder ) || __( 'Start writing or press / to insert a block' ); + const value = decodeEntities( placeholder ) || __( 'Start writing or type / to choose a block' ); // The appender "button" is in-fact a text field so as to support // transitions by WritingFlow occurring by arrow key press. WritingFlow 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 3e654e54e0ae3..a0e0e9e500326 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 @@ -20,7 +20,7 @@ exports[`DefaultBlockAppender should append a default block when input focused 1 readOnly={true} role="button" type="text" - value="Start writing or press / to insert a block" + value="Start writing or type / to choose a block" /> <WithSelect(WithDispatch(InserterWithShortcuts)) /> <WithSelect(IfCondition(Inserter)) @@ -42,7 +42,7 @@ exports[`DefaultBlockAppender should match snapshot 1`] = ` readOnly={true} role="button" type="text" - value="Start writing or press / to insert a block" + value="Start writing or type / to choose a block" /> <WithSelect(WithDispatch(InserterWithShortcuts)) /> <WithSelect(IfCondition(Inserter)) From 665ac6de67e026b296d24473e4322049d8c06268 Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski <grzegorz@gziolo.pl> Date: Fri, 9 Nov 2018 14:56:14 +0100 Subject: [PATCH 068/106] chore(release): publish - @wordpress/block-library@2.2.1 - @wordpress/block-serialization-default-parser@1.1.1 - @wordpress/block-serialization-spec-parser@1.1.1 - @wordpress/blocks@5.3.0 - @wordpress/components@5.1.1 - @wordpress/compose@2.1.2 - @wordpress/core-data@2.0.11 - @wordpress/data@3.1.2 - @wordpress/date@2.2.1 - @wordpress/dom@2.0.6 - @wordpress/edit-post@2.1.1 - @wordpress/editor@6.2.1 - @wordpress/element@2.1.7 - @wordpress/format-library@1.1.1 - @wordpress/jest-preset-default@3.0.2 - @wordpress/list-reusable-blocks@1.1.9 - @wordpress/notices@1.0.4 - @wordpress/nux@2.0.11 - @wordpress/plugins@2.0.8 - @wordpress/rich-text@2.0.4 - @wordpress/scripts@2.4.3 - @wordpress/viewport@2.0.10 --- packages/block-library/package.json | 2 +- packages/block-serialization-default-parser/package.json | 2 +- packages/block-serialization-spec-parser/package.json | 2 +- packages/blocks/package.json | 2 +- packages/components/package.json | 2 +- packages/compose/package.json | 2 +- packages/core-data/package.json | 2 +- packages/data/package.json | 2 +- packages/date/package.json | 2 +- packages/dom/package.json | 2 +- packages/edit-post/package.json | 2 +- packages/editor/package.json | 2 +- packages/element/package.json | 2 +- packages/format-library/package.json | 2 +- packages/jest-preset-default/package.json | 2 +- packages/list-reusable-blocks/package.json | 2 +- packages/notices/package.json | 2 +- packages/nux/package.json | 2 +- packages/plugins/package.json | 2 +- packages/rich-text/package.json | 2 +- packages/scripts/package.json | 2 +- packages/viewport/package.json | 2 +- 22 files changed, 22 insertions(+), 22 deletions(-) diff --git a/packages/block-library/package.json b/packages/block-library/package.json index e088ab8becc1a..06e8c00e05185 100644 --- a/packages/block-library/package.json +++ b/packages/block-library/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-library", - "version": "2.2.0", + "version": "2.2.1", "description": "Block library for the WordPress editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/block-serialization-default-parser/package.json b/packages/block-serialization-default-parser/package.json index ba3e8054be233..845f3596d78bb 100644 --- a/packages/block-serialization-default-parser/package.json +++ b/packages/block-serialization-default-parser/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-serialization-default-parser", - "version": "1.1.0", + "version": "1.1.1", "description": "Block serialization specification parser for WordPress posts.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/block-serialization-spec-parser/package.json b/packages/block-serialization-spec-parser/package.json index 8615df4e5ffb8..46e23fe37ac52 100644 --- a/packages/block-serialization-spec-parser/package.json +++ b/packages/block-serialization-spec-parser/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-serialization-spec-parser", - "version": "1.1.0", + "version": "1.1.1", "description": "Block serialization specification parser for WordPress posts.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/blocks/package.json b/packages/blocks/package.json index 178ea4a5a97b4..a3b64afc28c9e 100644 --- a/packages/blocks/package.json +++ b/packages/blocks/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/blocks", - "version": "5.2.0", + "version": "5.3.0", "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 7e7883ab2a7c2..ff67b8ea8f9a7 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/components", - "version": "5.1.0", + "version": "5.1.1", "description": "UI components for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/compose/package.json b/packages/compose/package.json index 3ccd038b7ed23..6c8b8f019f150 100644 --- a/packages/compose/package.json +++ b/packages/compose/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/compose", - "version": "2.1.1", + "version": "2.1.2", "description": "WordPress higher-order components (HOCs).", "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 90bae3f376570..72d5d06b05c0c 100644 --- a/packages/core-data/package.json +++ b/packages/core-data/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/core-data", - "version": "2.0.10", + "version": "2.0.11", "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 e11cc59c820df..8fc4107aae4b5 100644 --- a/packages/data/package.json +++ b/packages/data/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/data", - "version": "3.1.1", + "version": "3.1.2", "description": "Data module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/date/package.json b/packages/date/package.json index 3e6e1627e01f7..d408bc1fd0f0d 100644 --- a/packages/date/package.json +++ b/packages/date/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/date", - "version": "2.2.0", + "version": "2.2.1", "description": "Date module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/dom/package.json b/packages/dom/package.json index d9e6f69773d42..896a7bbf21a3e 100644 --- a/packages/dom/package.json +++ b/packages/dom/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/dom", - "version": "2.0.5", + "version": "2.0.6", "description": "DOM utilities 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 4ba3448309c43..d531e335f5885 100644 --- a/packages/edit-post/package.json +++ b/packages/edit-post/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/edit-post", - "version": "2.1.0", + "version": "2.1.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 041b7eadcb689..ec8fcc15bcff5 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/editor", - "version": "6.2.0", + "version": "6.2.1", "description": "Building blocks for WordPress editors.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/element/package.json b/packages/element/package.json index 8f815b650458e..58868f40d783a 100644 --- a/packages/element/package.json +++ b/packages/element/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/element", - "version": "2.1.6", + "version": "2.1.7", "description": "Element React module for WordPress.", "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 b4cef66fd8204..6f1c0b2baf982 100644 --- a/packages/format-library/package.json +++ b/packages/format-library/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/format-library", - "version": "1.1.0", + "version": "1.1.1", "description": "Format library for the WordPress editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/jest-preset-default/package.json b/packages/jest-preset-default/package.json index 534e14beb7686..ce4dee0522c9f 100644 --- a/packages/jest-preset-default/package.json +++ b/packages/jest-preset-default/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/jest-preset-default", - "version": "3.0.1", + "version": "3.0.2", "description": "Default Jest preset for WordPress development.", "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 4bd15d9a77316..4e6cadf8d13f1 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.8", + "version": "1.1.9", "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 b04caf91b6215..ee99ad6e3b2e2 100644 --- a/packages/notices/package.json +++ b/packages/notices/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/notices", - "version": "1.0.3", + "version": "1.0.4", "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 cdce9865f5b3e..50012ed481725 100644 --- a/packages/nux/package.json +++ b/packages/nux/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/nux", - "version": "2.0.10", + "version": "2.0.11", "description": "NUX (New User eXperience) module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/plugins/package.json b/packages/plugins/package.json index d16cfff2464e8..38a61be8ff9c0 100644 --- a/packages/plugins/package.json +++ b/packages/plugins/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/plugins", - "version": "2.0.7", + "version": "2.0.8", "description": "Plugins 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 e210f79155e5c..719c7439e6e58 100644 --- a/packages/rich-text/package.json +++ b/packages/rich-text/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/rich-text", - "version": "2.0.3", + "version": "2.0.4", "description": "Rich text value and manipulation API.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/scripts/package.json b/packages/scripts/package.json index 2b41184329e62..faac1e7c8c788 100644 --- a/packages/scripts/package.json +++ b/packages/scripts/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/scripts", - "version": "2.4.2", + "version": "2.4.3", "description": "Collection of JS scripts for WordPress development.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/viewport/package.json b/packages/viewport/package.json index a3a1ed966c450..bfdf56028da99 100644 --- a/packages/viewport/package.json +++ b/packages/viewport/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/viewport", - "version": "2.0.9", + "version": "2.0.10", "description": "Viewport module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", From 66fc483d2fbc82c45d29a7ef31796054c1dde4a3 Mon Sep 17 00:00:00 2001 From: Nicola Heald <nicola@notnowlewis.com> Date: Fri, 9 Nov 2018 14:02:29 +0000 Subject: [PATCH 069/106] Test for rapid enter presses (#11665) --- test/e2e/specs/writing-flow.test.js | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/test/e2e/specs/writing-flow.test.js b/test/e2e/specs/writing-flow.test.js index aa7456533ec58..0da4e2f29ad8a 100644 --- a/test/e2e/specs/writing-flow.test.js +++ b/test/e2e/specs/writing-flow.test.js @@ -236,9 +236,30 @@ describe( 'adding blocks', () => { it( 'should not delete trailing spaces when deleting a word with alt + backspace', async () => { await clickBlockAppender(); await page.keyboard.type( 'alpha beta gamma delta' ); - await pressWithModifier( META_KEY, 'Backspace' ); + if ( process.platform === 'darwin' ) { + await pressWithModifier( 'Alt', 'Backspace' ); + } else { + await pressWithModifier( META_KEY, 'Backspace' ); + } await page.keyboard.type( 'delta' ); const blockText = await page.evaluate( () => document.activeElement.textContent ); expect( blockText ).toBe( 'alpha beta gamma delta' ); } ); + + it( 'should create valid paragraph blocks when rapidly pressing Enter', async () => { + await clickBlockAppender(); + await page.keyboard.press( 'Enter' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.press( 'Enter' ); + // Check that none of the paragraph blocks have <br> in them. + const postContent = await getEditedPostContent(); + expect( postContent.indexOf( 'br' ) ).toBe( -1 ); + } ); } ); From f8fa752f650f6230c6cacc1a8c134cc27b6ef12a Mon Sep 17 00:00:00 2001 From: Daniel Richards <daniel.p.richards@gmail.com> Date: Fri, 9 Nov 2018 22:07:34 +0800 Subject: [PATCH 070/106] Add validation to link format in RichText component (#11286) * Add URL validation functions * Add utility function for determining whether an href is valid * When an invalid link is added, display it in a red color and speak an assertive message for screenreaders * Add an e2e test to catch a11y regressions when adding an invalid link * Update changelog for URL package. * Update url package docs * Trim assertive content in e2e test * Added a few minor tweaks to the url changelog. --- packages/format-library/src/link/inline.js | 51 ++-- packages/format-library/src/link/style.scss | 4 + .../format-library/src/link/test/utils.js | 76 ++++++ packages/format-library/src/link/utils.js | 74 ++++++ packages/url/CHANGELOG.md | 21 +- packages/url/README.md | 147 ++++++++++- packages/url/src/index.js | 140 ++++++++++ packages/url/src/test/index.test.js | 249 ++++++++++++++++++ test/e2e/specs/links.test.js | 12 + 9 files changed, 745 insertions(+), 29 deletions(-) create mode 100644 packages/format-library/src/link/test/utils.js create mode 100644 packages/format-library/src/link/utils.js diff --git a/packages/format-library/src/link/inline.js b/packages/format-library/src/link/inline.js index ef3ec4513adb5..2384835b65656 100644 --- a/packages/format-library/src/link/inline.js +++ b/packages/format-library/src/link/inline.js @@ -1,3 +1,8 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + /** * WordPress dependencies */ @@ -23,6 +28,7 @@ import { URLInput, URLPopover } from '@wordpress/editor'; * Internal dependencies */ import PositionedAtSelection from './positioned-at-selection'; +import { isValidHref } from './utils'; const stopKeyPropagation = ( event ) => event.stopPropagation(); @@ -78,23 +84,30 @@ const LinkEditor = ( { value, onChangeInputValue, onKeyDown, submitLink, autocom /* eslint-enable jsx-a11y/no-noninteractive-element-interactions */ ); -const LinkViewer = ( { url, editLink } ) => ( - // Disable reason: KeyPress must be suppressed so the block doesn't hide the toolbar - /* eslint-disable jsx-a11y/no-static-element-interactions */ - <div - className="editor-format-toolbar__link-container-content" - onKeyPress={ stopKeyPropagation } - > - <ExternalLink - className="editor-format-toolbar__link-container-value" - href={ url } +const LinkViewer = ( { url, editLink } ) => { + const prependedURL = prependHTTP( url ); + const linkClassName = classnames( 'editor-format-toolbar__link-container-value', { + 'has-invalid-link': ! isValidHref( prependedURL ), + } ); + + return ( + // Disable reason: KeyPress must be suppressed so the block doesn't hide the toolbar + /* eslint-disable jsx-a11y/no-static-element-interactions */ + <div + className="editor-format-toolbar__link-container-content" + onKeyPress={ stopKeyPropagation } > - { filterURLForDisplay( safeDecodeURI( url ) ) } - </ExternalLink> - <IconButton icon="edit" label={ __( 'Edit' ) } onClick={ editLink } /> - </div> - /* eslint-enable jsx-a11y/no-static-element-interactions */ -); + <ExternalLink + className={ linkClassName } + href={ url } + > + { filterURLForDisplay( safeDecodeURI( url ) ) } + </ExternalLink> + <IconButton icon="edit" label={ __( 'Edit' ) } onClick={ editLink } /> + </div> + /* eslint-enable jsx-a11y/no-static-element-interactions */ + ); +}; class InlineLinkUI extends Component { constructor() { @@ -178,8 +191,10 @@ class InlineLinkUI extends Component { this.resetState(); - if ( isActive ) { - speak( __( 'Link edited' ), 'assertive' ); + if ( ! isValidHref( url ) ) { + speak( __( 'Warning: the link has been inserted but may have errors. Please test it.' ), 'assertive' ); + } else if ( isActive ) { + speak( __( 'Link edited.' ), 'assertive' ); } else { speak( __( 'Link inserted' ), 'assertive' ); } diff --git a/packages/format-library/src/link/style.scss b/packages/format-library/src/link/style.scss index e22da7b942272..7db24c50338fa 100644 --- a/packages/format-library/src/link/style.scss +++ b/packages/format-library/src/link/style.scss @@ -11,4 +11,8 @@ white-space: nowrap; min-width: 150px; max-width: 500px; + + &.has-invalid-link { + color: $alert-red; + } } diff --git a/packages/format-library/src/link/test/utils.js b/packages/format-library/src/link/test/utils.js new file mode 100644 index 0000000000000..19feb519e6490 --- /dev/null +++ b/packages/format-library/src/link/test/utils.js @@ -0,0 +1,76 @@ + +/** + * Internal dependencies + */ +import { + isValidHref, +} from '../utils'; + +describe( 'isValidHref', () => { + it( 'returns true if the href cannot be recognised as a url or an anchor link', () => { + expect( isValidHref( 'notaurloranchorlink' ) ).toBe( true ); + } ); + + it( 'returns false if the href is not specified', () => { + expect( isValidHref() ).toBe( false ); + expect( isValidHref( '' ) ).toBe( false ); + expect( isValidHref( ' ' ) ).toBe( false ); + } ); + + describe( 'URLs beginning with a protocol', () => { + it( 'returns true for valid URLs', () => { + expect( isValidHref( 'tel:+123456789' ) ).toBe( true ); + expect( isValidHref( 'mailto:test@somewhere.com' ) ).toBe( true ); + expect( isValidHref( 'file:///c:/WINDOWS/winamp.exe' ) ).toBe( true ); + expect( isValidHref( 'http://test.com' ) ).toBe( true ); + expect( isValidHref( 'https://test.com' ) ).toBe( true ); + expect( isValidHref( 'http://test-with-hyphen.com' ) ).toBe( true ); + expect( isValidHref( 'http://test.com/' ) ).toBe( true ); + expect( isValidHref( 'http://test.com/with/path/separators' ) ).toBe( true ); + expect( isValidHref( 'http://test.com/with?query=string&params' ) ).toBe( true ); + } ); + + it( 'returns false for invalid urls', () => { + expect( isValidHref( 'tel:+12 345 6789' ) ).toBe( false ); + expect( isValidHref( 'mailto:test @ somewhere.com' ) ).toBe( false ); + expect( isValidHref( 'mailto: test@somewhere.com' ) ).toBe( false ); + expect( isValidHref( 'ht#tp://this/is/invalid' ) ).toBe( false ); + expect( isValidHref( 'ht#tp://th&is/is/invalid' ) ).toBe( false ); + expect( isValidHref( 'http://?test.com' ) ).toBe( false ); + expect( isValidHref( 'http://#test.com' ) ).toBe( false ); + expect( isValidHref( 'http://test.com?double?params' ) ).toBe( false ); + expect( isValidHref( 'http://test.com#double#anchor' ) ).toBe( false ); + expect( isValidHref( 'http://test.com?path/after/params' ) ).toBe( false ); + expect( isValidHref( 'http://test.com#path/after/fragment' ) ).toBe( false ); + } ); + + it( 'returns false if the URL has whitespace', () => { + expect( isValidHref( 'http:/ /test.com' ) ).toBe( false ); + expect( isValidHref( 'http://te st.com' ) ).toBe( false ); + expect( isValidHref( 'http:// test.com' ) ).toBe( false ); + expect( isValidHref( 'http://test.c om' ) ).toBe( false ); + expect( isValidHref( 'http://test.com/ee ee/' ) ).toBe( false ); + expect( isValidHref( 'http://test.com/eeee?qwd qwdw' ) ).toBe( false ); + expect( isValidHref( 'http://test.com/eeee#qwd qwdw' ) ).toBe( false ); + } ); + } ); + + describe( 'Anchor links', () => { + it( 'returns true for valid anchor links', () => { + expect( isValidHref( '#yesitis' ) ).toBe( true ); + expect( isValidHref( '#yes_it_is' ) ).toBe( true ); + expect( isValidHref( '#yes~it~is' ) ).toBe( true ); + expect( isValidHref( '#yes-it-is' ) ).toBe( true ); + } ); + + it( 'returns false for invalid anchor links', () => { + expect( isValidHref( '' ) ).toBe( false ); + expect( isValidHref( '#no-it-isnt#' ) ).toBe( false ); + expect( isValidHref( '#no-it-#isnt' ) ).toBe( false ); + expect( isValidHref( '#no-it-isnt?' ) ).toBe( false ); + expect( isValidHref( '#no-it isnt' ) ).toBe( false ); + expect( isValidHref( '#no-it-isnt/' ) ).toBe( false ); + } ); + } ); +} ); + diff --git a/packages/format-library/src/link/utils.js b/packages/format-library/src/link/utils.js new file mode 100644 index 0000000000000..498d96e26d9a7 --- /dev/null +++ b/packages/format-library/src/link/utils.js @@ -0,0 +1,74 @@ +/** + * External dependencies + */ +import { startsWith } from 'lodash'; + +/** + * WordPress dependencies + */ +import { + getProtocol, + isValidProtocol, + getAuthority, + isValidAuthority, + getPath, + isValidPath, + getQueryString, + isValidQueryString, + getFragment, + isValidFragment, +} from '@wordpress/url'; + +/** + * Check for issues with the provided href. + * + * @param {string} href The href. + * + * @return {boolean} Is the href invalid? + */ +export function isValidHref( href ) { + if ( ! href ) { + return false; + } + + const trimmedHref = href.trim(); + + if ( ! trimmedHref ) { + return false; + } + + // Does the href start with something that looks like a url protocol? + if ( /^\S+:/.test( trimmedHref ) ) { + const protocol = getProtocol( trimmedHref ); + if ( ! isValidProtocol( protocol ) ) { + return false; + } + + const authority = getAuthority( trimmedHref ); + if ( ! isValidAuthority( authority ) ) { + return false; + } + + const path = getPath( trimmedHref ); + if ( path && ! isValidPath( path ) ) { + return false; + } + + const queryString = getQueryString( trimmedHref ); + if ( queryString && ! isValidQueryString( queryString ) ) { + return false; + } + + const fragment = getFragment( trimmedHref ); + if ( fragment && ! isValidFragment( trimmedHref ) ) { + return false; + } + } + + // Validate anchor links. + if ( startsWith( trimmedHref, '#' ) && ! isValidFragment( trimmedHref ) ) { + return false; + } + + return true; +} diff --git a/packages/url/CHANGELOG.md b/packages/url/CHANGELOG.md index 0e681c0a0de8d..30dba55b01a2d 100644 --- a/packages/url/CHANGELOG.md +++ b/packages/url/CHANGELOG.md @@ -1,6 +1,21 @@ +## 2.3.0 (Unreleased) + +### New Features + +- Added `getProtocol`. +- Added `isValidProtocol`. +- Added `getAuthority` +- Added `isValidAuthority`. +- Added `getPath`. +- Added `isValidPath`. +- Added `getQueryString`. +- Added `isValidQueryString`. +- Added `getFragment`. +- Added `isValidFragment`. + ## 2.2.0 (2018-10-29) -### Features +### New Features - Added `getQueryArg`. - Added `hasQueryArg`. @@ -8,13 +23,13 @@ ## 2.1.0 (2018-10-16) -### Features +### New Feature - Added `safeDecodeURI`. ## 2.0.1 (2018-09-30) -### Bug Fixes +### Bug Fix - Fix typo in the `qs` dependency definition in the `package.json` diff --git a/packages/url/README.md b/packages/url/README.md index 26f54fbaff57f..4c092d670fc9f 100644 --- a/packages/url/README.md +++ b/packages/url/README.md @@ -14,26 +14,157 @@ _This package assumes that your code will run in an **ES2015+** environment. If ## Usage -```JS -import { isURL, addQueryArgs, prependHTTP } from '@wordpress/url'; +### isURL -// Checks if the argument looks like a URL +```js const isURL = isURL( 'https://wordpress.org' ); // true +``` + +Checks whether the URL is an HTTP or HTTPS URL. + + +### getProtocol + +```js +const protocol1 = getProtocol( 'tel:012345678' ); // 'tel:' +const protocol2 = getProtocol( 'https://wordpress.org' ); // 'https:' +``` + +Returns the protocol part of the provided URL. + + +### isValidProtocol + +```js +const isValid = isValidProtocol( 'https:' ); // true +const isNotValid = isValidProtocol( 'https :' ); // false +``` + +Returns true if the provided protocol is valid. Returns false if the protocol contains invalid characters. + + +### getAuthority + +```js +const authority1 = getAuthority( 'https://wordpress.org/help/' ); // 'wordpress.org' +const authority2 = getAuthority( 'https://localhost:8080/test/' ); // 'localhost:8080' +``` + +Returns the authority part of the provided URL. + + +### isValidAuthority + +```js +const isValid = isValidAuthority( 'wordpress.org' ); // true +const isNotValid = isValidAuthority( 'wordpress#org' ); // false +``` + +Returns true if the provided authority is valid. Returns false if the protocol contains invalid characters. + + +### getPath + +```js +const path1 = getPath( 'http://localhost:8080/this/is/a/test?query=true' ); // 'this/is/a/test' +const path2 = getPath( 'https://wordpress.org/help/faq/' ); // 'help/faq' +``` + +Returns the path part of the provided URL. + + +### isValidPath + +```js +const isValid = isValidPath( 'test/path/' ); // true +const isNotValid = isValidPath( '/invalid?test/path/' ); // false +``` + +Returns true if the provided path is valid. Returns false if the path contains invalid characters. + + +### getQueryString + +```js +const queryString1 = getQueryString( 'http://localhost:8080/this/is/a/test?query=true#fragment' ); // 'query=true' +const queryString2 = getQueryString( 'https://wordpress.org#fragment?query=false&search=hello' ); // 'query=false&search=hello' +``` -// Appends arguments to the query string of a given url +Returns the query string part of the provided URL. + + +### isValidQueryString + +```js +const isValid = isValidQueryString( 'query=true&another=false' ); // true +const isNotValid = isValidQueryString( 'query=true?another=false' ); // false +``` + +Returns true if the provided query string is valid. Returns false if the query string contains invalid characters. + + +### getFragment + +```js +const fragment1 = getFragment( 'http://localhost:8080/this/is/a/test?query=true#fragment' ); // '#fragment' +const fragment2 = getFragment( 'https://wordpress.org#another-fragment?query=true' ); // '#another-fragment' +``` + +Returns the fragment part of the provided URL. + + +### isValidFragment + +```js +const isValid = isValidFragment( '#valid-fragment' ); // true +const isNotValid = isValidFragment( '#invalid-#fragment' ); // false +``` + +Returns true if the provided fragment is valid. Returns false if the fragment contains invalid characters. + + +### addQueryArgs + +```js const newURL = addQueryArgs( 'https://google.com', { q: 'test' } ); // https://google.com/?q=test +``` + +Adds a query string argument to the provided URL. + -// Prepends 'http://' to URLs that are probably mean to have them +### prependHTTP + +```js const actualURL = prependHTTP( 'wordpress.org' ); // http://wordpress.org +``` -// Gets a single query arg from the given URL. +Prepends the provided partial URL with the http:// protocol. + +### getQueryArg + +```js const foo = getQueryArg( 'https://wordpress.org?foo=bar&bar=baz', 'foo' ); // bar +``` + +Retrieve a query string argument from the provided URL. -// Checks whether a URL contains a given query arg. + +### hasQueryArg + +```js const hasBar = hasQueryArg( 'https://wordpress.org?foo=bar&bar=baz', 'bar' ); // true +``` + +Checks whether a URL contains the given query string argument. -// Removes one or more query args from the given URL. + +### removeQueryArgs + +```js const newUrl = removeQueryArgs( 'https://wordpress.org?foo=bar&bar=baz&baz=foobar', 'foo', 'bar' ); // https://wordpress.org?baz=foobar ``` +Removes one or more query string arguments from the given URL. + + <br/><br/><p align="center"><img src="https://s.w.org/style/images/codeispoetry.png?1" alt="Code is Poetry." /></p> diff --git a/packages/url/src/index.js b/packages/url/src/index.js index cc368afdc1973..cd066baa2da43 100644 --- a/packages/url/src/index.js +++ b/packages/url/src/index.js @@ -18,6 +18,146 @@ export function isURL( url ) { return URL_REGEXP.test( url ); } +/** + * Returns the protocol part of the URL. + * + * @param {string} url The full URL. + * + * @return {?string} The protocol part of the URL. + */ +export function getProtocol( url ) { + const matches = /^([^\s:]+:)/.exec( url ); + if ( matches ) { + return matches[ 1 ]; + } +} + +/** + * Tests if a url protocol is valid. + * + * @param {string} protocol The url protocol. + * + * @return {boolean} True if the argument is a valid protocol (e.g. http://, tel:). + */ +export function isValidProtocol( protocol ) { + if ( ! protocol ) { + return false; + } + return /^[a-z\-.\+]+[0-9]*:(?:\/\/)?\/?$/i.test( protocol ); +} + +/** + * Returns the authority part of the URL. + * + * @param {string} url The full URL. + * + * @return {?string} The authority part of the URL. + */ +export function getAuthority( url ) { + const matches = /^[^\/\s:]+:(?:\/\/)?\/?([^\/\s#?]+)[\/#?]{0,1}\S*$/.exec( url ); + if ( matches ) { + return matches[ 1 ]; + } +} + +/** + * Checks for invalid characters within the provided authority. + * + * @param {string} authority A string containing the URL authority. + * + * @return {boolean} True if the argument contains a valid authority. + */ +export function isValidAuthority( authority ) { + if ( ! authority ) { + return false; + } + return /^[^\s#?]+$/.test( authority ); +} + +/** + * Returns the path part of the URL. + * + * @param {string} url The full URL. + * + * @return {?string} The path part of the URL. + */ +export function getPath( url ) { + const matches = /^[^\/\s:]+:(?:\/\/)?[^\/\s#?]+[\/]([^\s#?]+)[#?]{0,1}\S*$/.exec( url ); + if ( matches ) { + return matches[ 1 ]; + } +} + +/** + * Checks for invalid characters within the provided path. + * + * @param {string} path The URL path. + * + * @return {boolean} True if the argument contains a valid path + */ +export function isValidPath( path ) { + if ( ! path ) { + return false; + } + return /^[^\s#?]+$/.test( path ); +} + +/** + * Returns the query string part of the URL. + * + * @param {string} url The full URL. + * + * @return {?string} The query string part of the URL. + */ +export function getQueryString( url ) { + const matches = /^\S+?\?([^\s#]+)/.exec( url ); + if ( matches ) { + return matches[ 1 ]; + } +} + +/** + * Checks for invalid characters within the provided query string. + * + * @param {string} queryString The query string. + * + * @return {boolean} True if the argument contains a valid query string. + */ +export function isValidQueryString( queryString ) { + if ( ! queryString ) { + return false; + } + return /^[^\s#?\/]+$/.test( queryString ); +} + +/** + * Returns the fragment part of the URL. + * + * @param {string} url The full URL + * + * @return {?string} The fragment part of the URL. + */ +export function getFragment( url ) { + const matches = /^\S+(#[^\s\?]*)/.exec( url ); + if ( matches ) { + return matches[ 1 ]; + } +} + +/** + * Checks for invalid characters within the provided fragment. + * + * @param {string} fragment The url fragment. + * + * @return {boolean} True if the argument contains a valid fragment. + */ +export function isValidFragment( fragment ) { + if ( ! fragment ) { + return false; + } + return /^#[^\s#?\/]*$/.test( fragment ); +} + /** * Appends arguments to the query string of the url * diff --git a/packages/url/src/test/index.test.js b/packages/url/src/test/index.test.js index 86363a4f6bd40..cf4d644bfad68 100644 --- a/packages/url/src/test/index.test.js +++ b/packages/url/src/test/index.test.js @@ -8,6 +8,16 @@ import { every } from 'lodash'; */ import { isURL, + getProtocol, + isValidProtocol, + getAuthority, + isValidAuthority, + getPath, + isValidPath, + getQueryString, + isValidQueryString, + getFragment, + isValidFragment, addQueryArgs, getQueryArg, hasQueryArg, @@ -43,6 +53,239 @@ describe( 'isURL', () => { } ); } ); +describe( 'getProtocol', () => { + it( 'returns the protocol part of a URL', () => { + expect( getProtocol( 'http://user:password@www.test-this.com:1020/test-path/file.extension#anchor?query=params&more' ) ).toBe( 'http:' ); + expect( getProtocol( 'https://user:password@www.test-this.com:1020/test-path/file.extension#anchor?query=params&more' ) ).toBe( 'https:' ); + expect( getProtocol( 'https://wordpress.org#test' ) ).toBe( 'https:' ); + expect( getProtocol( 'https://wordpress.org/' ) ).toBe( 'https:' ); + expect( getProtocol( 'https://wordpress.org?test' ) ).toBe( 'https:' ); + expect( getProtocol( 'https://localhost:8080' ) ).toBe( 'https:' ); + expect( getProtocol( 'tel:1234' ) ).toBe( 'tel:' ); + expect( getProtocol( 'blob:data' ) ).toBe( 'blob:' ); + } ); + + it( 'returns undefined when the provided value does not contain a URL protocol', () => { + expect( getProtocol( '' ) ).toBeUndefined(); + expect( getProtocol( 'https' ) ).toBeUndefined(); + expect( getProtocol( 'test.com' ) ).toBeUndefined(); + expect( getProtocol( ' https:// ' ) ).toBeUndefined(); + } ); +} ); + +describe( 'isValidProtocol', () => { + it( 'returns true if the protocol is valid', () => { + expect( isValidProtocol( 'tel:' ) ).toBe( true ); + expect( isValidProtocol( 'http:' ) ).toBe( true ); + expect( isValidProtocol( 'https:' ) ).toBe( true ); + expect( isValidProtocol( 'http://' ) ).toBe( true ); + expect( isValidProtocol( 'https://' ) ).toBe( true ); + expect( isValidProtocol( 'file:///' ) ).toBe( true ); + expect( isValidProtocol( 'test.protocol:' ) ).toBe( true ); + expect( isValidProtocol( 'test-protocol:' ) ).toBe( true ); + expect( isValidProtocol( 'test+protocol:' ) ).toBe( true ); + expect( isValidProtocol( 'test+protocol123:' ) ).toBe( true ); + } ); + + it( 'returns false if the protocol is invalid', () => { + expect( isValidProtocol() ).toBe( false ); + expect( isValidProtocol( '' ) ).toBe( false ); + expect( isValidProtocol( ' http: ' ) ).toBe( false ); + expect( isValidProtocol( 'http :' ) ).toBe( false ); + expect( isValidProtocol( 'http: //' ) ).toBe( false ); + expect( isValidProtocol( 'test protocol:' ) ).toBe( false ); + expect( isValidProtocol( 'test#protocol:' ) ).toBe( false ); + expect( isValidProtocol( 'test?protocol:' ) ).toBe( false ); + expect( isValidProtocol( '123test+protocol:' ) ).toBe( false ); + } ); +} ); + +describe( 'getAuthority', () => { + it( 'returns the authority part of a URL', () => { + expect( getAuthority( 'https://user:password@www.test-this.com:1020/test-path/file.extension#anchor?query=params&more' ) ).toBe( 'user:password@www.test-this.com:1020' ); + expect( getAuthority( 'http://user:password@www.test-this.com:1020/test-path/file.extension#anchor?query=params&more' ) ).toBe( 'user:password@www.test-this.com:1020' ); + expect( getAuthority( 'https://wordpress.org#test' ) ).toBe( 'wordpress.org' ); + expect( getAuthority( 'https://wordpress.org/' ) ).toBe( 'wordpress.org' ); + expect( getAuthority( 'https://wordpress.org?test' ) ).toBe( 'wordpress.org' ); + expect( getAuthority( 'https://localhost:8080' ) ).toBe( 'localhost:8080' ); + } ); + + it( 'returns undefined when the provided value does not contain a URL authority', () => { + expect( getAuthority( '' ) ).toBeUndefined(); + expect( getAuthority( 'https://' ) ).toBeUndefined(); + expect( getAuthority( 'https:///' ) ).toBeUndefined(); + expect( getAuthority( 'https://#' ) ).toBeUndefined(); + expect( getAuthority( 'https://?' ) ).toBeUndefined(); + expect( getAuthority( 'test.com' ) ).toBeUndefined(); + expect( getAuthority( 'https://#?hello' ) ).toBeUndefined(); + } ); +} ); + +describe( 'isValidAuthority', () => { + it( 'returns true if the authority is valid', () => { + expect( isValidAuthority( 'user:password@www.test-this.com:1020' ) ).toBe( true ); + expect( isValidAuthority( 'wordpress.org' ) ).toBe( true ); + expect( isValidAuthority( 'localhost' ) ).toBe( true ); + expect( isValidAuthority( 'localhost:8080' ) ).toBe( true ); + expect( isValidAuthority( 'www.the-best-website.co.uk' ) ).toBe( true ); + expect( isValidAuthority( 'WWW.VERYLOUD.COM' ) ).toBe( true ); + } ); + + it( 'returns false if the authority is invalid', () => { + expect( isValidAuthority() ).toBe( false ); + expect( isValidAuthority( '' ) ).toBe( false ); + expect( isValidAuthority( 'inv alid.website.com' ) ).toBe( false ); + expect( isValidAuthority( 'test#.com' ) ).toBe( false ); + expect( isValidAuthority( 'test?.com' ) ).toBe( false ); + } ); +} ); + +describe( 'getPath', () => { + it( 'returns the path part of a URL', () => { + expect( getPath( 'https://user:password@www.test-this.com:1020/test-path/file.extension#anchor?query=params&more' ) ).toBe( 'test-path/file.extension' ); + expect( getPath( 'http://user:password@www.test-this.com:1020/test-path/file.extension#anchor?query=params&more' ) ).toBe( 'test-path/file.extension' ); + expect( getPath( 'https://wordpress.org/test-path#anchor' ) ).toBe( 'test-path' ); + expect( getPath( 'https://wordpress.org/test-path?query' ) ).toBe( 'test-path' ); + expect( getPath( 'https://www.google.com/search?source=hp&ei=tP7kW8-_FoK89QORoa2QBQ&q=test+url&oq=test+url&gs_l=psy-ab.3..0l10' ) ).toBe( 'search' ); + expect( getPath( 'https://wordpress.org/this%20is%20a%20test' ) ).toBe( 'this%20is%20a%20test' ); + expect( getPath( 'https://wordpress.org/this%20is%20a%20test?query' ) ).toBe( 'this%20is%20a%20test' ); + } ); + + it( 'returns undefined when the provided value does not contain a URL path', () => { + expect( getPath() ).toBeUndefined(); + expect( getPath( '' ) ).toBeUndefined(); + expect( getPath( 'https://wordpress.org#test' ) ).toBeUndefined(); + expect( getPath( 'https://wordpress.org/' ) ).toBeUndefined(); + expect( getPath( 'https://wordpress.org?test' ) ).toBeUndefined(); + expect( getPath( 'https://localhost:8080' ) ).toBeUndefined(); + expect( getPath( 'https://' ) ).toBeUndefined(); + expect( getPath( 'https:///test' ) ).toBeUndefined(); + expect( getPath( 'https://#' ) ).toBeUndefined(); + expect( getPath( 'https://?' ) ).toBeUndefined(); + expect( getPath( 'test.com' ) ).toBeUndefined(); + expect( getPath( 'https://#?hello' ) ).toBeUndefined(); + expect( getPath( 'https' ) ).toBeUndefined(); + } ); +} ); + +describe( 'isValidPath', () => { + it( 'returns true if the path is valid', () => { + expect( isValidPath( 'test-path/file.extension' ) ).toBe( true ); + expect( isValidPath( '/absolute/path' ) ).toBe( true ); + expect( isValidPath( 'relative/path' ) ).toBe( true ); + expect( isValidPath( 'slightly/longer/path/' ) ).toBe( true ); + expect( isValidPath( 'path/with/percent%20encoding' ) ).toBe( true ); + } ); + + it( 'returns false if the path is invalid', () => { + expect( isValidPath() ).toBe( false ); + expect( isValidPath( '' ) ).toBe( false ); + expect( isValidPath( 'path /with/spaces' ) ).toBe( false ); + expect( isValidPath( 'path/with/number/symbol#' ) ).toBe( false ); + expect( isValidPath( 'path/with/question/mark?' ) ).toBe( false ); + expect( isValidPath( ' path/with/padding ' ) ).toBe( false ); + } ); +} ); + +describe( 'getQueryString', () => { + it( 'returns the query string of a URL', () => { + expect( getQueryString( 'https://user:password@www.test-this.com:1020/test-path/file.extension#anchor?query=params&more' ) ).toBe( 'query=params&more' ); + expect( getQueryString( 'http://user:password@www.test-this.com:1020/test-path/file.extension?query=params&more#anchor' ) ).toBe( 'query=params&more' ); + expect( getQueryString( 'https://wordpress.org/test-path?query' ) ).toBe( 'query' ); + expect( getQueryString( 'https://www.google.com/search?source=hp&ei=tP7kW8-_FoK89QORoa2QBQ&q=test+url&oq=test+url&gs_l=psy-ab.3..0l10' ) ) + .toBe( 'source=hp&ei=tP7kW8-_FoK89QORoa2QBQ&q=test+url&oq=test+url&gs_l=psy-ab.3..0l10' ); + expect( getQueryString( 'https://wordpress.org/this%20is%20a%20test?query' ) ).toBe( 'query' ); + expect( getQueryString( 'https://wordpress.org/test?query=something%20with%20spaces' ) ).toBe( 'query=something%20with%20spaces' ); + expect( getQueryString( 'https://andalouses.example/beach?foo[]=bar&foo[]=baz' ) ).toBe( 'foo[]=bar&foo[]=baz' ); + expect( getQueryString( 'test.com?foo[]=bar&foo[]=baz' ) ).toBe( 'foo[]=bar&foo[]=baz' ); + expect( getQueryString( 'test.com?foo=bar&foo=baz?test' ) ).toBe( 'foo=bar&foo=baz?test' ); + } ); + + it( 'returns undefined when the provided does not contain a url query string', () => { + expect( getQueryString( '' ) ).toBeUndefined(); + expect( getQueryString( 'https://wordpress.org/test-path#anchor' ) ).toBeUndefined(); + expect( getQueryString( 'https://wordpress.org/this%20is%20a%20test' ) ).toBeUndefined(); + expect( getQueryString( 'https://wordpress.org#test' ) ).toBeUndefined(); + expect( getQueryString( 'https://wordpress.org/' ) ).toBeUndefined(); + expect( getQueryString( 'https://localhost:8080' ) ).toBeUndefined(); + expect( getQueryString( 'https://' ) ).toBeUndefined(); + expect( getQueryString( 'https:///test' ) ).toBeUndefined(); + expect( getQueryString( 'https://#' ) ).toBeUndefined(); + expect( getQueryString( 'https://?' ) ).toBeUndefined(); + expect( getQueryString( 'test.com' ) ).toBeUndefined(); + } ); +} ); + +describe( 'isValidQueryString', () => { + it( 'returns true if the query string is valid', () => { + expect( isValidQueryString( 'test' ) ).toBe( true ); + expect( isValidQueryString( 'test=true' ) ).toBe( true ); + expect( isValidQueryString( 'test=true&another' ) ).toBe( true ); + expect( isValidQueryString( 'test=true&another=false' ) ).toBe( true ); + expect( isValidQueryString( 'test[]=true&another[]=false' ) ).toBe( true ); + expect( isValidQueryString( 'query=something%20with%20spaces' ) ).toBe( true ); + expect( isValidQueryString( 'source=hp&ei=tP7kW8-_FoK89QORoa2QBQ&q=test+url&oq=test+url&gs_l=psy-ab.3..0l10' ) ).toBe( true ); + } ); + + it( 'returns false if the query string is invalid', () => { + expect( isValidQueryString() ).toBe( false ); + expect( isValidQueryString( '' ) ).toBe( false ); + expect( isValidQueryString( '?test=false' ) ).toBe( false ); + expect( isValidQueryString( ' test=false ' ) ).toBe( false ); + expect( isValidQueryString( 'test = false' ) ).toBe( false ); + expect( isValidQueryString( 'test=f?alse' ) ).toBe( false ); + expect( isValidQueryString( 'test=f#alse' ) ).toBe( false ); + expect( isValidQueryString( 'test=f/alse' ) ).toBe( false ); + expect( isValidQueryString( 'test=f?alse' ) ).toBe( false ); + expect( isValidQueryString( '/test=false' ) ).toBe( false ); + expect( isValidQueryString( 'test=false/' ) ).toBe( false ); + } ); +} ); + +describe( 'getFragment', () => { + it( 'returns the fragment of a URL', () => { + expect( getFragment( 'https://user:password@www.test-this.com:1020/test-path/file.extension#fragment?query=params&more' ) ).toBe( '#fragment' ); + expect( getFragment( 'http://user:password@www.test-this.com:1020/test-path/file.extension?query=params&more#fragment' ) ).toBe( '#fragment' ); + expect( getFragment( 'relative/url/#fragment' ) ).toBe( '#fragment' ); + expect( getFragment( '/absolute/url/#fragment' ) ).toBe( '#fragment' ); + } ); + + it( 'returns undefined when the provided does not contain a url fragment', () => { + expect( getFragment( '' ) ).toBeUndefined(); + expect( getFragment( 'https://wordpress.org/test-path?query' ) ).toBeUndefined(); + expect( getFragment( 'https://wordpress.org/test-path' ) ).toBeUndefined(); + expect( getFragment( 'https://wordpress.org/this%20is%20a%20test' ) ).toBeUndefined(); + expect( getFragment( 'https://www.google.com/search?source=hp&ei=tP7kW8-_FoK89QORoa2QBQ&q=test+url&oq=test+url&gs_l=psy-ab.3..0l10' ) ).toBeUndefined(); + expect( getFragment( 'https://wordpress.org' ) ).toBeUndefined(); + expect( getFragment( 'https://localhost:8080' ) ).toBeUndefined(); + expect( getFragment( 'https://' ) ).toBeUndefined(); + expect( getFragment( 'https:///test' ) ).toBeUndefined(); + expect( getFragment( 'https://?' ) ).toBeUndefined(); + expect( getFragment( 'test.com' ) ).toBeUndefined(); + } ); +} ); + +describe( 'isValidFragment', () => { + it( 'returns true if the fragment is valid', () => { + expect( isValidFragment( '#' ) ).toBe( true ); + expect( isValidFragment( '#yesitis' ) ).toBe( true ); + expect( isValidFragment( '#yes_it_is' ) ).toBe( true ); + expect( isValidFragment( '#yes~it~is' ) ).toBe( true ); + expect( isValidFragment( '#yes-it-is' ) ).toBe( true ); + } ); + + it( 'returns false if the fragment is invalid', () => { + expect( isValidFragment( '' ) ).toBe( false ); + expect( isValidFragment( ' #no-it-isnt ' ) ).toBe( false ); + expect( isValidFragment( '#no-it-isnt#' ) ).toBe( false ); + expect( isValidFragment( '#no-it-#isnt' ) ).toBe( false ); + expect( isValidFragment( '#no-it-isnt?' ) ).toBe( false ); + expect( isValidFragment( '#no-it isnt' ) ).toBe( false ); + expect( isValidFragment( '/#no-it-isnt' ) ).toBe( false ); + expect( isValidFragment( '#no-it-isnt/' ) ).toBe( false ); + } ); +} ); + describe( 'addQueryArgs', () => { it( 'should append args to an URL without query string', () => { const url = 'https://andalouses.example/beach'; @@ -105,6 +348,12 @@ describe( 'getQueryArg', () => { expect( getQueryArg( url, 'foo' ) ).toEqual( [ 'bar', 'baz' ] ); } ); + + it( 'continues to work when an anchor follows the query string', () => { + const url = 'https://andalouses.example/beach?foo=bar&bar=baz#foo'; + + expect( getQueryArg( url, 'foo' ) ).toEqual( 'bar' ); + } ); } ); describe( 'hasQueryArg', () => { diff --git a/test/e2e/specs/links.test.js b/test/e2e/specs/links.test.js index 850ad3d07afc9..14d4c418b2280 100644 --- a/test/e2e/specs/links.test.js +++ b/test/e2e/specs/links.test.js @@ -420,4 +420,16 @@ describe( 'Links', () => { const activeElementValue = await page.evaluate( () => document.activeElement.value ); expect( activeElementValue ).toBe( URL ); } ); + + it( 'adds an assertive message for screenreader users when an invalid link is set', async () => { + await clickBlockAppender(); + await page.keyboard.type( 'This is Gutenberg' ); + await pressWithModifier( SELECT_WORD_MODIFIER_KEYS, 'ArrowLeft' ); + await pressWithModifier( META_KEY, 'K' ); + await waitForAutoFocus(); + await page.keyboard.type( 'http://#test.com' ); + await page.keyboard.press( 'Enter' ); + const assertiveContent = await page.evaluate( () => document.querySelector( '#a11y-speak-assertive' ).textContent ); + expect( assertiveContent.trim() ).toBe( 'Warning: the link has been inserted but may have errors. Please test it.' ); + } ); } ); From 2fb5cecff19891a1d622fbf3651cec558b85ccd9 Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski <grzegorz@gziolo.pl> Date: Fri, 9 Nov 2018 15:29:47 +0100 Subject: [PATCH 071/106] Add changelog files back to the ingored changes by Lerna --- lerna.json | 1 + 1 file changed, 1 insertion(+) diff --git a/lerna.json b/lerna.json index 431f4dec535da..fd005a960f1e6 100644 --- a/lerna.json +++ b/lerna.json @@ -6,6 +6,7 @@ }, "ignoreChanges": [ "**/benchmark/*.js", + "**/CHANGELOG.md", "**/test/**" ], "packages": [ From e5dcd568fd9d6bd522cfd5fb14223ef6df8e5dbd Mon Sep 17 00:00:00 2001 From: Jorge <jorge.costa@developer.pt> Date: Fri, 9 Nov 2018 14:44:43 +0000 Subject: [PATCH 072/106] Update: Font Size Picker component documentation (#11404) ## Description This pr updates the documentation of @wordpress/components/FontSizePicker. The component was updated with additional props added and the structure of existing props changed but the updates to the docs were missing. ## How has this been tested? Just documentation updates. I checked the sample code works and does not contain linting error, by pasting it in an existing block. --- .../components/src/font-size-picker/README.md | 71 ++++++++++++++----- .../components/src/font-size-picker/index.js | 2 +- 2 files changed, 53 insertions(+), 20 deletions(-) diff --git a/packages/components/src/font-size-picker/README.md b/packages/components/src/font-size-picker/README.md index 8d64411a04eed..6024e1403b09b 100644 --- a/packages/components/src/font-size-picker/README.md +++ b/packages/components/src/font-size-picker/README.md @@ -9,54 +9,73 @@ The component renders a series of buttons that allow the user to select predefin ```jsx import { FontSizePicker } from '@wordpress/components'; import { withState } from '@wordpress/compose'; +import { __ } from '@wordpress/i18n'; +... const MyFontSizePicker = withState( { fontSize: 16, -} )( ( { fontSize, setState } ) => { +} )( ( { fontSize, setState } ) => { const fontSizes = [ - { shortName: 'S', size: 12 }, - { shortName: 'M', size: 16 } + { + name: __( 'Small' ), + slug: 'small', + size: 12, + }, + { + name: __( 'Big' ), + slug: 'big', + size: 26, + }, ]; const fallbackFontSize = 16; - - return ( - <FontSizePicker - fontSizes={ fontSizes } + + return ( + <FontSizePicker + fontSizes={ fontSizes } value={ fontSize } fallbackFontSize={ fallbackFontSize } - onChange={ fontSize => setState( { fontSize } ) } + onChange={ ( newFontSize ) => { + setState( { fontSize: newFontSize } ); + } } /> - ); + ); } ); + +... + +<MyFontSizePicker /> ``` ## Props The component accepts the following props: -### fontSizes +### disableCustomFontSizes -An array of font size objects. The object should contain properties size, name, shortName. -The property "size" contains a number with the font size value. The "shortName" property includes a small label used in the buttons. Property "name" is used as the label when shortName is not provided. +If `true`, it will not be possible to choose a custom fontSize. The user will be forced to pick one of the pre-defined sizes passed in fontSizes. -- Type: `Array` -- Required: No +- Type: `Boolean` +- Required: no +- Default: `false` ### fallbackFontSize -In no value exists this prop contains the font size picker slider starting position. +If no value exists, this prop defines the starting position for the font size picker slider. Only relevant if `withSlider` is `true`. - Type: `Number` - Required: No -### value +### fontSizes -The current font size value. If a button value matches the font size value that button is pressed. RangeControl is rendered with this value. +An array of font size objects. The object should contain properties size, name, and slug. +The property `size` contains a number with the font size value, in `px`. +The `name` property includes a label for that font size e.g.: `Small`. +The `slug` property is a string with a unique identifier for the font size. Used for the class generation process. -- Type: `Number` +- Type: `Array` - Required: No -## onChange +### onChange A function that receives the new font size value. If onChange is called without any parameter, it should reset the value, attending to what reset means in that context, e.g., set the font size to undefined or set the font size a starting value. @@ -64,3 +83,17 @@ If onChange is called without any parameter, it should reset the value, attendin - Type: `function` - Required: Yes +### value + +The current font size value. + +- Type: `Number` +- Required: No + +### withSlider + +If `true`, the UI will contain a slider, instead of a numeric text input field. If `false`, no slider will be present. + +- Type: `Boolean` +- Required: no +- Default: `false` diff --git a/packages/components/src/font-size-picker/index.js b/packages/components/src/font-size-picker/index.js index 2df94feb00edc..16329316d5ad1 100644 --- a/packages/components/src/font-size-picker/index.js +++ b/packages/components/src/font-size-picker/index.js @@ -25,7 +25,7 @@ function FontSizePicker( { disableCustomFontSizes = false, onChange, value, - withSlider, + withSlider = false, } ) { const onChangeValue = ( event ) => { const newValue = event.target.value; From 5da5962029649bd7d61f9fe33507a88e0038889b Mon Sep 17 00:00:00 2001 From: Joen Asmussen <joen@automattic.com> Date: Fri, 9 Nov 2018 16:42:58 +0100 Subject: [PATCH 073/106] Further improve columns block. (#11659) * Further improve columns block. This PR is a followup to thoughts in https://github.com/WordPress/gutenberg/pull/11620#issuecomment-437110753. It improves the columns block further by doing two things: 1. It hides the hover effect on the individual _column_ block in the initial state. 2. It changes the non-block "fake" appender to be a textarea instead of an input. To elaborate on 1, in https://github.com/WordPress/gutenberg/pull/11620 we used pointer-events to disable the individual column blocks as selectable using the mouse. This is because those columns are not actionable at the moment, and in the current implementation of the column, being able to hover and select them only causes issues with selecting the parent columns block, which _is_ actionable. However pointer events was not enough for the _empty_ state of a columns block. In this state, the block features no inner blocks, except the individual column blocks, because no content has yet been inserted yet. In this state, you see the "appender", which is not actually a block even though it looks exactly the same as an empty paragraph. Because this is not technically a block, the deepest nested child is a _column_, which is then allowed to be hovered. In 1 I add a workaround to that, to simply visually hide the hover effect. I expect this to be refactored in a future columns update, but for the time being it makes it far more usable. To elaborate on 2, there has been a long time issue with the appender (again, not the block, the fake version that we use before an empty paragraph is inserted) wrapping text in translations. This is because text _cannot_ wrap in an `input` field, which the appender is. By changing this to a `textarea`, the text can wrap. This might affect themes which specify the input element for increased specificity in their editor styles, but overall it still seems like a worthwhile change to make, since it's the only way we can allow the writing prompt to wrap its text. * Fix by using TextareaAutosize. Props @aduth. Also adds resize: none;, props @chrisvanpatten. * Fix unit tests --- packages/block-library/src/columns/editor.scss | 17 +++++++++++++++-- .../components/default-block-appender/index.js | 4 ++-- .../default-block-appender/style.scss | 10 +++++----- .../test/__snapshots__/index.js.snap | 12 ++++++------ .../default-block-appender/test/index.js | 4 ++-- 5 files changed, 30 insertions(+), 17 deletions(-) diff --git a/packages/block-library/src/columns/editor.scss b/packages/block-library/src/columns/editor.scss index a96a6dcf8c1ea..4492a75ca7cbe 100644 --- a/packages/block-library/src/columns/editor.scss +++ b/packages/block-library/src/columns/editor.scss @@ -128,11 +128,24 @@ } // In absence of making the individual columns resizable, we prevent them from being clickable. -// This makes them less fiddly. This will be revisited as the interface is refined. +// This makes them less fiddly. @todo: This should be revisited as the interface is refined. .wp-block-columns [data-type="core/column"] { pointer-events: none; + + // The empty state of a columns block has the default appenders. + // Since those appenders are not blocks, the parent, actual block, appears "hovered" when hovering the appenders. + // Because the column shouldn't be hovered as part of this temporary passhthrough, we unset the hover style. + &.is-hovered { + > .editor-block-list__block-edit::before { + content: none; + } + + .editor-block-list__breadcrumb { + display: none; + } + } } -:not(.components-disabled) > .wp-block-columns > .editor-inner-blocks > .editor-block-list__layout > [data-type="core/column"] > .editor-block-list__block-edit > .editor-inner-blocks { +:not(.components-disabled) > .wp-block-columns > .editor-inner-blocks > .editor-block-list__layout > [data-type="core/column"] > .editor-block-list__block-edit .editor-inner-blocks { pointer-events: all; } diff --git a/packages/editor/src/components/default-block-appender/index.js b/packages/editor/src/components/default-block-appender/index.js index 8cc05c140878e..a1855e93f2ae3 100644 --- a/packages/editor/src/components/default-block-appender/index.js +++ b/packages/editor/src/components/default-block-appender/index.js @@ -2,6 +2,7 @@ * External dependencies */ import { get } from 'lodash'; +import TextareaAutosize from 'react-autosize-textarea'; /** * WordPress dependencies @@ -51,11 +52,10 @@ export function DefaultBlockAppender( { return ( <div data-root-client-id={ rootClientId || '' } className="wp-block editor-default-block-appender"> <BlockDropZone rootClientId={ rootClientId } /> - <input + <TextareaAutosize role="button" aria-label={ __( 'Add block' ) } className="editor-default-block-appender__content" - type="text" readOnly onFocus={ onAppend } value={ showPrompt ? value : '' } diff --git a/packages/editor/src/components/default-block-appender/style.scss b/packages/editor/src/components/default-block-appender/style.scss index b7b657abf06ce..48285e0c86015 100644 --- a/packages/editor/src/components/default-block-appender/style.scss +++ b/packages/editor/src/components/default-block-appender/style.scss @@ -1,23 +1,23 @@ .editor-default-block-appender { clear: both; // The appender doesn't scale well to sit next to floats, so clear them. - input[type="text"].editor-default-block-appender__content { // Needs specificity in order to override input field styles from WP-admin styles. + textarea.editor-default-block-appender__content { // Needs specificity in order to override input field styles from WP-admin styles. font-family: $editor-font; font-size: $editor-font-size; // It should match the default paragraph size. border: none; background: none; box-shadow: none; display: block; - margin-left: 0; - margin-right: 0; - max-width: none; // Overrides upstream CSS from the admin. + margin-left: $border-width; + margin-right: $border-width; cursor: text; width: 100%; outline: $border-width solid transparent; transition: 0.2s outline; + resize: none; // Emulate the dimensions of a paragraph block. - padding: 0 #{ $block-padding + $border-width }; + padding: 0 #{ $block-padding }; // Use opacity to work in various editor styles. color: $dark-opacity-300; 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 a0e0e9e500326..df9daeebd54f2 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 @@ -6,7 +6,7 @@ exports[`DefaultBlockAppender should append a default block when input focused 1 data-root-client-id="" > <WithDispatch(WithSelect(WithFilters(BlockDropZone))) /> - <input + <TextareaAutosize aria-label="Add block" className="editor-default-block-appender__content" onFocus={ @@ -19,7 +19,7 @@ exports[`DefaultBlockAppender should append a default block when input focused 1 } readOnly={true} role="button" - type="text" + rows={1} value="Start writing or type / to choose a block" /> <WithSelect(WithDispatch(InserterWithShortcuts)) /> @@ -35,13 +35,13 @@ exports[`DefaultBlockAppender should match snapshot 1`] = ` data-root-client-id="" > <WithDispatch(WithSelect(WithFilters(BlockDropZone))) /> - <input + <TextareaAutosize aria-label="Add block" className="editor-default-block-appender__content" onFocus={[MockFunction]} readOnly={true} role="button" - type="text" + rows={1} value="Start writing or type / to choose a block" /> <WithSelect(WithDispatch(InserterWithShortcuts)) /> @@ -57,13 +57,13 @@ exports[`DefaultBlockAppender should optionally show without prompt 1`] = ` data-root-client-id="" > <WithDispatch(WithSelect(WithFilters(BlockDropZone))) /> - <input + <TextareaAutosize aria-label="Add block" className="editor-default-block-appender__content" onFocus={[MockFunction]} readOnly={true} role="button" - type="text" + rows={1} value="" /> <WithSelect(WithDispatch(InserterWithShortcuts)) /> diff --git a/packages/editor/src/components/default-block-appender/test/index.js b/packages/editor/src/components/default-block-appender/test/index.js index 94b6030ce0947..80517843ee661 100644 --- a/packages/editor/src/components/default-block-appender/test/index.js +++ b/packages/editor/src/components/default-block-appender/test/index.js @@ -31,7 +31,7 @@ describe( 'DefaultBlockAppender', () => { const onAppend = jest.fn(); const wrapper = shallow( <DefaultBlockAppender isVisible onAppend={ onAppend } showPrompt /> ); - wrapper.find( 'input.editor-default-block-appender__content' ).simulate( 'focus' ); + wrapper.find( 'TextareaAutosize' ).simulate( 'focus' ); expect( wrapper ).toMatchSnapshot(); @@ -41,7 +41,7 @@ describe( 'DefaultBlockAppender', () => { it( 'should optionally show without prompt', () => { const onAppend = jest.fn(); const wrapper = shallow( <DefaultBlockAppender isVisible onAppend={ onAppend } showPrompt={ false } /> ); - const input = wrapper.find( 'input.editor-default-block-appender__content' ); + const input = wrapper.find( 'TextareaAutosize' ); expect( input.prop( 'value' ) ).toEqual( '' ); From 4338dcfac5bdf20ba410dd78f5b408033a66dd40 Mon Sep 17 00:00:00 2001 From: Joen Asmussen <joen@automattic.com> Date: Fri, 9 Nov 2018 16:51:08 +0100 Subject: [PATCH 074/106] Try: Refactor contextual toolbar to work better with floats (#11357) * Try: Refactor contextual toolbar to work better with floats This is rather big surgery this late in the phase. There may be dragons, and it may be necessary to punt this to phase 2. This PR seeks to fix #4764 by doing a number of things: - Move the contextual toolbar into the block edit div, which is the one we float. This helps make it _connected_ to the content. - Add some complex clearing rules so we avoid many of the gnarly situations where a selected block after a floated block has a weirdly tall size. - Fixes so a paragraph block that follows a float does behave as it will on the frontend (i.e. won't clear), but also has a toolbar that is correctly positioned. This moving around of things caused subsequent issues, which means this PR also: - Fixes the toolbar appearance on mobile. - Improves upon the appearance of the toolbar on floated items on mobile. - Fixes hover label positioning, not only so they work with floats, but are positioned correctly as a result of this refactor. - Fixes issues with wide and fullwide toolbar positioning. * Use block navigator to select the blocks * Remove block and inline level rules This removes the rules that were specific to inline blocks or block level blocks and levels the playing field. The benefit is no special rules. The downside is that a block following a float still might not perfectly match the user expectation. This PR also fixes issues with mobile toolbars. Finally, it makes the block toolbar not float. This rule used to be necessary, but for whatever reason it no longer appears to be. * Fix rebase issue. * Fix classic block. --- .../block-library/src/classic/editor.scss | 5 +- .../block-library/src/columns/editor.scss | 9 +- .../src/components/visual-editor/style.scss | 20 ++- .../editor/src/components/block-list/block.js | 67 +++---- .../src/components/block-list/style.scss | 169 ++++++++++-------- test/e2e/specs/block-icons.test.js | 24 ++- 6 files changed, 164 insertions(+), 130 deletions(-) diff --git a/packages/block-library/src/classic/editor.scss b/packages/block-library/src/classic/editor.scss index bc4e371702a87..0720c22d4a2c3 100644 --- a/packages/block-library/src/classic/editor.scss +++ b/packages/block-library/src/classic/editor.scss @@ -216,9 +216,12 @@ div[data-type="core/freeform"] .editor-block-contextual-toolbar + div { // So we move it to the right, and make room for it. @include break-small() { .editor-block-list__block[data-type="core/freeform"] { + .editor-block-switcher__no-switcher-icon { + display: none; + } .editor-block-contextual-toolbar { float: right; - margin-right: -$block-side-ui-clearance - $border-width; + margin-right: $icon-button-size - $block-padding + $border-width; transform: translateY(-#{ $block-padding - $border-width }); top: $block-padding; diff --git a/packages/block-library/src/columns/editor.scss b/packages/block-library/src/columns/editor.scss index 4492a75ca7cbe..7b269d0dd9a0a 100644 --- a/packages/block-library/src/columns/editor.scss +++ b/packages/block-library/src/columns/editor.scss @@ -69,10 +69,10 @@ } @include break-small() { - > .editor-block-contextual-toolbar { + > .editor-block-list__block-edit > .editor-block-contextual-toolbar { top: $block-toolbar-height - $border-width; transform: translateY(-$block-toolbar-height - $border-width); - margin-left: -$block-padding - $block-padding - $border-width; + margin-left: -$grid-size-large - $border-width; } > .editor-block-list__block-edit::before { @@ -82,8 +82,9 @@ left: 0; } - > .editor-block-list__breadcrumb { - margin-right: -$block-padding - $border-width; + > .editor-block-list__block-edit > .editor-block-list__breadcrumb { + top: 0; + right: 0; } } diff --git a/packages/edit-post/src/components/visual-editor/style.scss b/packages/edit-post/src/components/visual-editor/style.scss index 2795f5330ba36..0463c005479ef 100644 --- a/packages/edit-post/src/components/visual-editor/style.scss +++ b/packages/edit-post/src/components/visual-editor/style.scss @@ -29,9 +29,9 @@ // Center the block toolbar on wide and full-wide blocks. // Use specific selector to not affect nested block toolbars. - &[data-align="wide"] > .editor-block-contextual-toolbar, - &[data-align="full"] > .editor-block-contextual-toolbar { - width: calc(100% + #{ $block-side-ui-width * 2 + $block-padding * 2 + $border-width * 2 }); // Matches the negative margins applied to parent blocks. + &[data-align="wide"] > .editor-block-list__block-edit > .editor-block-contextual-toolbar, + &[data-align="full"] > .editor-block-list__block-edit > .editor-block-contextual-toolbar { + width: calc(100% + #{ $block-padding * 2 + $border-width * 2 }); // Matches the negative margins applied to parent blocks. height: 0; // This collapses the container to an invisible element without margin. text-align: center; @@ -42,9 +42,11 @@ } } - // The centering math changes when a fullwide image doesn't have block padding. - &[data-align="full"] > .editor-block-contextual-toolbar { - width: calc(100% + #{ $block-side-ui-width * 2 + $block-padding * 2 }); // Matches the negative margins applied to non-parent blocks, except for borders which are gone in fullwide. + // The centering math is simpler for a fullwide block, which doesn't have any block padding. + &[data-align="full"] > .editor-block-list__block-edit > .editor-block-contextual-toolbar { + width: 100%; + margin-left: 0; + margin-right: 0; .editor-block-toolbar { max-width: $content-width - $border-width - $border-width; @@ -84,6 +86,12 @@ } .edit-post-visual-editor { + // If the first block is floated, it needs top margin, unlike the rule in line 69. + .editor-block-list__layout > .editor-block-list__block[data-align="left"]:first-child, + .editor-block-list__layout > .editor-block-list__block[data-align="right"]:first-child { + margin-top: $block-padding + $block-spacing + $border-width + $border-width + $block-padding; + } + .editor-default-block-appender { // Default to centered and content-width, like blocks margin-left: auto; diff --git a/packages/editor/src/components/block-list/block.js b/packages/editor/src/components/block-list/block.js index 8b656f0616944..bd156cb3f79ec 100644 --- a/packages/editor/src/components/block-list/block.js +++ b/packages/editor/src/components/block-list/block.js @@ -515,45 +515,46 @@ export class BlockListBlock extends Component { onDragEnd={ this.onDragEnd } /> ) } - { shouldShowBreadcrumb && ( - <BlockBreadcrumb - clientId={ clientId } - isHidden={ ! ( isHovered || isSelected ) || hoverArea !== 'left' } - /> - ) } - { shouldShowContextualToolbar && <BlockContextualToolbar /> } { isFirstMultiSelected && ( <BlockMultiControls rootClientId={ rootClientId } /> ) } - <IgnoreNestedEvents - ref={ this.bindBlockNode } - onDragStart={ this.preventDrag } - onMouseDown={ this.onPointerDown } - className="editor-block-list__block-edit" - data-block={ clientId } - > - <BlockCrashBoundary onError={ this.onBlockError }> - { isValid && blockEdit } - { isValid && mode === 'html' && ( - <BlockHtml clientId={ clientId } /> - ) } - { ! isValid && [ - <BlockInvalidWarning - key="invalid-warning" - block={ block } - />, - <div key="invalid-preview"> - { getSaveElement( blockType, block.attributes ) } - </div>, - ] } - </BlockCrashBoundary> - { shouldShowMobileToolbar && ( - <BlockMobileToolbar + <div className="editor-block-list__block-edit"> + { shouldShowBreadcrumb && ( + <BlockBreadcrumb clientId={ clientId } + isHidden={ ! ( isHovered || isSelected ) || hoverArea !== 'left' } /> ) } - { !! error && <BlockCrashWarning /> } - </IgnoreNestedEvents> + { shouldShowContextualToolbar && <BlockContextualToolbar /> } + <IgnoreNestedEvents + ref={ this.bindBlockNode } + onDragStart={ this.preventDrag } + onMouseDown={ this.onPointerDown } + data-block={ clientId } + > + <BlockCrashBoundary onError={ this.onBlockError }> + { isValid && blockEdit } + { isValid && mode === 'html' && ( + <BlockHtml clientId={ clientId } /> + ) } + { ! isValid && [ + <BlockInvalidWarning + key="invalid-warning" + block={ block } + />, + <div key="invalid-preview"> + { getSaveElement( blockType, block.attributes ) } + </div>, + ] } + </BlockCrashBoundary> + { shouldShowMobileToolbar && ( + <BlockMobileToolbar + clientId={ clientId } + /> + ) } + { !! error && <BlockCrashWarning /> } + </IgnoreNestedEvents> + </div> { showEmptyBlockSideInserter && ( <Fragment> <div className="editor-block-list__side-inserter"> diff --git a/packages/editor/src/components/block-list/style.scss b/packages/editor/src/components/block-list/style.scss index 28632c0eda60a..203f5dec76119 100644 --- a/packages/editor/src/components/block-list/style.scss +++ b/packages/editor/src/components/block-list/style.scss @@ -295,13 +295,23 @@ // Position toolbar better on mobile. .editor-block-contextual-toolbar { - border-bottom: 1px solid $light-gray-500; - bottom: $block-padding; - left: auto; - right: auto; + width: auto; + border-bottom: $border-width solid $light-gray-500; + bottom: auto; } } + // Unlike most explicit left/right alignments, this one should be flipped by the auto-RTL system. + &[data-align="left"] .editor-block-contextual-toolbar { + left: 0; + right: auto; + } + + &[data-align="right"] .editor-block-contextual-toolbar { + left: auto; + right: 0; + } + // Left &[data-align="left"] { // This is in the editor only; the image should be floated on the frontend. @@ -407,8 +417,8 @@ // Full-wide &[data-align="full"] { // Position hover label on the right - > .editor-block-list__breadcrumb { - right: -$border-width; + > .editor-block-list__block-edit .editor-block-list__breadcrumb { + right: 0; } // Compensate for main container padding and subtract border. @@ -485,7 +495,8 @@ > .editor-block-mover { position: absolute; width: $block-side-ui-width + $block-side-ui-clearance; - // stretch to fill half of the available space to increase hoverable area + + // Stretch to fill half of the available space to increase hoverable area. height: 100%; max-height: $block-side-ui-width * 4; } @@ -506,7 +517,7 @@ } } - // Left side UI + // Left side UI. > .editor-block-mover { padding-right: $block-side-ui-clearance; @@ -532,15 +543,15 @@ .editor-block-list__block-mobile-toolbar { display: flex; flex-direction: row; + // Make room for the height of the block toolbar above. - margin-top: $grid-size + $block-toolbar-height - $block-padding; + transform: translateY($block-padding + $border-width); + margin-top: $block-toolbar-height; margin-right: -$block-padding; margin-left: -$block-padding; border-top: $border-width solid $light-gray-500; height: $block-toolbar-height; - transform: translateY(#{ $block-padding + $border-width }); - @include break-small() { display: none; } @@ -724,58 +735,72 @@ * Block Toolbar when contextual. */ -.editor-block-list__block .editor-block-contextual-toolbar { - position: sticky; - z-index: z-index(".editor-block-contextual-toolbar"); - white-space: nowrap; - text-align: left; - pointer-events: none; - - // Position toolbar below the block on mobile. - position: absolute; - bottom: $block-toolbar-height - $block-padding - 1px; - left: 0; - right: 0; +.editor-block-list__block { + .editor-block-contextual-toolbar { + position: sticky; + z-index: z-index(".editor-block-contextual-toolbar"); + white-space: nowrap; + text-align: left; + pointer-events: none; - // Paint the borders on the toolbar itself on mobile. - border-top: $border-width solid $light-gray-500; - .components-toolbar { - border-top: none; - border-bottom: none; - } + // Position toolbar below the block on mobile. + position: absolute; + bottom: $block-toolbar-height - $block-padding; + left: -$block-padding; + right: -$block-padding; - @include break-small() { - border-top: none; + // Paint the borders on the toolbar itself on mobile. + border-top: $border-width solid $light-gray-500; .components-toolbar { - border-top: $border-width solid $light-gray-500; - border-bottom: $border-width solid $light-gray-500; + border-top: none; + border-bottom: none; + } + + @include break-small() { + border-top: none; + .components-toolbar { + border-top: $border-width solid $light-gray-500; + border-bottom: $border-width solid $light-gray-500; + } } } // Floated items have special needs for the contextual toolbar position. - .editor-block-list__block[data-align="left"] &, - .editor-block-list__block[data-align="right"] & { + &[data-align="left"] .editor-block-contextual-toolbar, + &[data-align="right"] .editor-block-contextual-toolbar { margin-bottom: $border-width; - margin-top: -$block-toolbar-height - $border-width; + margin-top: -$block-toolbar-height; } // Make block toolbar full width on mobile. - margin-left: 0; - margin-right: 0; + .editor-block-contextual-toolbar { + margin-left: 0; + margin-right: 0; + @include break-small() { + margin-left: -$block-padding - $border-width; + margin-right: -$block-padding - $border-width; + } + } - @include break-small() { - margin-left: -$block-side-ui-width - $block-padding - $border-width; - margin-right: -$block-side-ui-width - $block-padding - $border-width; + // For floats, compensate for this so content doesn't grow smaller. + &[data-align="left"] .editor-block-contextual-toolbar { + /*rtl:ignore*/ + margin-right: $block-padding + $border-width; + } - // Except for wide elements, this causes a horizontal scrollbar. - [data-align="full"] & { - margin-left: -$block-padding - $block-side-ui-width; - margin-right: -$block-padding - $block-side-ui-width; - } + &[data-align="right"] .editor-block-contextual-toolbar { + /*rtl:ignore*/ + margin-left: $block-padding + $border-width; + } + + // Don't do it for wide elements, this causes a horizontal scrollbar. + &[data-align="full"] .editor-block-contextual-toolbar { + margin-left: -$block-padding - $block-side-ui-width; + margin-right: -$block-padding - $block-side-ui-width; } // Reset pointer-events on children. - & > * { + .editor-block-contextual-toolbar > * { pointer-events: auto; } } @@ -804,30 +829,25 @@ // Compensate for translate, so the sticky sticks to the top. top: $block-toolbar-height + $block-padding; } - - // This is an important one. Because the toolbar is sticky, it's part of the flow. - // 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; - } + // 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; - } + // RTL note: this rule should not be auto-flipped based on direction. + /*rtl:ignore*/ + float: right; + } + + .editor-block-list__block[data-align="left"] &, + .editor-block-list__block[data-align="right"] & { + // Move the block toolbar out of the flow using translate, but less for floats. + transform: translateY(-$block-padding -$border-width); } } @@ -835,20 +855,16 @@ .editor-block-contextual-toolbar .editor-block-toolbar { width: 100%; - // Hide right border on desktop, where the .components-toolbar instead has a right border. @include break-small() { + width: auto; + + // Hide right border on desktop, where the .components-toolbar instead has a right border. border-right: none; - } - // This prevents floats from messing up the position. - @include break-small() { + // This prevents floats from messing up the position. position: absolute; left: 0; } - - @include break-small() { - width: auto; - } } @@ -862,7 +878,7 @@ z-index: z-index(".editor-block-list__breadcrumb"); // Position in the top right of the border. - right: 0; + right: -$block-padding; top: -$block-padding - $border-width; .components-toolbar { @@ -887,6 +903,13 @@ background: rgba($white, 0.5); color: $dark-gray-700; } + + // Position the breadcrumb closer on mobile. + [data-align="left"] &, + [data-align="right"] & { + right: 0; + top: 0; + } } .editor-block-list__descendant-arrow::before { diff --git a/test/e2e/specs/block-icons.test.js b/test/e2e/specs/block-icons.test.js index a02c497bbcd1b..0e44cddb124f4 100644 --- a/test/e2e/specs/block-icons.test.js +++ b/test/e2e/specs/block-icons.test.js @@ -2,10 +2,11 @@ * Internal dependencies */ import { + ACCESS_MODIFIER_KEYS, + pressWithModifier, newPost, insertBlock, searchForBlock, - selectBlockByClientId, } from '../support/utils'; import { activatePlugin, deactivatePlugin } from '../support/plugins'; @@ -34,18 +35,17 @@ async function getFirstInserterIcon() { return await getInnerHTML( INSERTER_ICON_SELECTOR ); } +async function selectFirstBlock() { + await pressWithModifier( ACCESS_MODIFIER_KEYS, 'o' ); + const navButtons = await page.$$( '.editor-block-navigation__item-button' ); + await navButtons[ 0 ].click(); +} + describe( 'Correctly Renders Block Icons on Inserter and Inspector', () => { const dashIconRegex = /<svg.*?class=".*?dashicons-cart.*?">.*?<\/svg>/; const circleString = '<circle cx="10" cy="10" r="10" fill="red" stroke="blue" stroke-width="10"></circle>'; const svgIcon = new RegExp( `<svg.*?viewBox="0 0 20 20".*?>${ circleString }</svg>` ); - const getBlockIdFromBlockName = async ( blockName ) => { - return await page.$eval( - `[data-type="${ blockName }"] > .editor-block-list__block-edit`, - ( el ) => el.getAttribute( 'data-block' ) - ); - }; - const validateSvgIcon = ( iconHtml ) => { expect( iconHtml ).toMatch( svgIcon ); }; @@ -81,7 +81,7 @@ describe( 'Correctly Renders Block Icons on Inserter and Inspector', () => { it( 'Renders correctly the icon on the inspector', async () => { await insertBlock( blockTitle ); - await selectBlockByClientId( await getBlockIdFromBlockName( blockName ) ); + await selectFirstBlock(); validateIcon( await getInnerHTML( INSPECTOR_ICON_SELECTOR ) ); } ); } @@ -105,7 +105,6 @@ describe( 'Correctly Renders Block Icons on Inserter and Inspector', () => { } ); describe( 'Block with dash icon and background and foreground colors', () => { - const blockName = 'test/test-dash-icon-colors'; const blockTitle = 'TestDashIconColors'; it( 'Renders the icon in the inserter with the correct colors', async () => { await searchForBlock( blockTitle ); @@ -116,7 +115,7 @@ describe( 'Correctly Renders Block Icons on Inserter and Inspector', () => { it( 'Renders the icon in the inspector with the correct colors', async () => { await insertBlock( blockTitle ); - await selectBlockByClientId( await getBlockIdFromBlockName( blockName ) ); + await selectFirstBlock(); validateDashIcon( await getInnerHTML( INSPECTOR_ICON_SELECTOR ) ); @@ -126,7 +125,6 @@ describe( 'Correctly Renders Block Icons on Inserter and Inspector', () => { } ); describe( 'Block with svg icon and background color', () => { - const blockName = 'test/test-svg-icon-background'; const blockTitle = 'TestSvgIconBackground'; it( 'Renders the icon in the inserter with the correct background color and an automatically compute readable foreground color', async () => { await searchForBlock( blockTitle ); @@ -137,7 +135,7 @@ describe( 'Correctly Renders Block Icons on Inserter and Inspector', () => { it( 'Renders correctly the icon on the inspector', async () => { await insertBlock( blockTitle ); - await selectBlockByClientId( await getBlockIdFromBlockName( blockName ) ); + await selectFirstBlock(); validateSvgIcon( await getInnerHTML( INSPECTOR_ICON_SELECTOR ) ); From 004d7315ca65a5e3f0b003404da14433d1e72ff2 Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Fri, 9 Nov 2018 17:45:38 +0100 Subject: [PATCH 075/106] Remove 4.3 deprecated APIs (#11679) * Remove 4.3 deprecated APIs * Update changelogs * Remove useless package dependency * Remove useless snapshot and mock * Update docs --- docs/data/data-core-edit-post.md | 22 ---------- lib/client-assets.php | 1 - package-lock.json | 1 - packages/components/CHANGELOG.md | 6 +++ packages/components/src/index.js | 1 - packages/components/src/panel/color.js | 40 ------------------ .../panel/test/__snapshots__/color.js.snap | 16 ------- packages/components/src/panel/test/color.js | 21 ---------- packages/edit-post/CHANGELOG.md | 7 ++++ packages/edit-post/package.json | 1 - packages/edit-post/src/store/actions.js | 21 ---------- packages/edit-post/src/store/selectors.js | 23 ---------- .../edit-post/src/store/test/selectors.js | 11 ----- packages/editor/CHANGELOG.md | 6 +++ packages/editor/src/components/index.js | 1 - .../src/components/panel-color/index.js | 42 ------------------- 16 files changed, 19 insertions(+), 201 deletions(-) delete mode 100644 packages/components/src/panel/color.js delete mode 100644 packages/components/src/panel/test/__snapshots__/color.js.snap delete mode 100644 packages/components/src/panel/test/color.js delete mode 100644 packages/editor/src/components/panel-color/index.js diff --git a/docs/data/data-core-edit-post.md b/docs/data/data-core-edit-post.md index 7e0dc7c8ceb5a..93e658e91d479 100644 --- a/docs/data/data-core-edit-post.md +++ b/docs/data/data-core-edit-post.md @@ -103,20 +103,6 @@ enabled by default. Whether or not the panel is enabled. -### isEditorSidebarPanelOpened - -Returns true if the given panel is enabled, or false otherwise. Panels are -enabled by default. - -*Parameters* - - * state: Global application state. - * panel: A string that identifies the panel. - -*Returns* - -Whether or not the panel is enabled. - ### isEditorPanelOpened Returns true if the given panel is open, or false otherwise. Panels are @@ -311,14 +297,6 @@ Returns an action object used to enable or disable a panel in the editor. Returns an action object used to open or close a panel in the editor. -*Parameters* - - * panelName: A string that identifies the panel to open or close. - -### toggleGeneralSidebarEditorPanel - -Returns an action object used to open or close a panel in the editor. - *Parameters* * panelName: A string that identifies the panel to open or close. diff --git a/lib/client-assets.php b/lib/client-assets.php index bbc898b8040ae..ed95afda111d1 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -716,7 +716,6 @@ function gutenberg_register_scripts_and_styles() { 'wp-compose', 'wp-core-data', 'wp-data', - 'wp-deprecated', 'wp-dom-ready', 'wp-editor', 'wp-element', diff --git a/package-lock.json b/package-lock.json index 635e403b6986f..03591075a66eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2479,7 +2479,6 @@ "@wordpress/compose": "file:packages/compose", "@wordpress/core-data": "file:packages/core-data", "@wordpress/data": "file:packages/data", - "@wordpress/deprecated": "file:packages/deprecated", "@wordpress/editor": "file:packages/editor", "@wordpress/element": "file:packages/element", "@wordpress/format-library": "file:packages/format-library", diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 6b55f4e45690d..3d8a39e178245 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -1,3 +1,9 @@ +## 6.0.0 (Unreleased) + +### Breaking Changes + +- The `PanelColor` component has been removed. + ## 5.1.1 (2018-11-09) ## 5.1.0 (2018-11-09) diff --git a/packages/components/src/index.js b/packages/components/src/index.js index 5af9d24cd7077..c1143ba729cce 100644 --- a/packages/components/src/index.js +++ b/packages/components/src/index.js @@ -37,7 +37,6 @@ export { default as Notice } from './notice'; export { default as NoticeList } from './notice/list'; export { default as Panel } from './panel'; export { default as PanelBody } from './panel/body'; -export { default as PanelColor } from './panel/color'; export { default as PanelHeader } from './panel/header'; export { default as PanelRow } from './panel/row'; export { default as Placeholder } from './placeholder'; diff --git a/packages/components/src/panel/color.js b/packages/components/src/panel/color.js deleted file mode 100644 index 3c137e79eeeb6..0000000000000 --- a/packages/components/src/panel/color.js +++ /dev/null @@ -1,40 +0,0 @@ -/** - * WordPress dependencies - */ -import deprecated from '@wordpress/deprecated'; -import { __, sprintf } from '@wordpress/i18n'; - -/** - * Internal dependencies - */ -import PanelBody from './body'; -import ColorIndicator from '../color-indicator'; - -function PanelColor( { colorValue, colorName, title, ...props } ) { - deprecated( 'wp.components.PanelColor', { - version: '4.3', - alternative: 'wp.editor.PanelColorSettings', - plugin: 'Gutenberg', - } ); - - // translators: %s: The name of the color e.g: "vivid red" or color hex code if name is not available e.g: "#f00". - const currentColorLabel = sprintf( __( '(current color: %s)' ), colorName || colorValue ); - return ( - <PanelBody - { ...props } - title={ [ - <span className="components-panel__color-title" key="title">{ title }</span>, - colorValue && ( - <ColorIndicator - key="color" - className="components-panel__color-indicator" - aria-label={ currentColorLabel } - colorValue={ colorValue } - /> - ), - ] } - /> - ); -} - -export default PanelColor; diff --git a/packages/components/src/panel/test/__snapshots__/color.js.snap b/packages/components/src/panel/test/__snapshots__/color.js.snap deleted file mode 100644 index 96c1ee2e38f76..0000000000000 --- a/packages/components/src/panel/test/__snapshots__/color.js.snap +++ /dev/null @@ -1,16 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`PanelColor should match snapshot when title is provided 1`] = ` -<ForwardRef(PanelBody) - title={ - Array [ - <span - className="components-panel__color-title" - > - sample title - </span>, - undefined, - ] - } -/> -`; diff --git a/packages/components/src/panel/test/color.js b/packages/components/src/panel/test/color.js deleted file mode 100644 index 31c00562daaee..0000000000000 --- a/packages/components/src/panel/test/color.js +++ /dev/null @@ -1,21 +0,0 @@ -/** - * External dependencies - */ -import { shallow } from 'enzyme'; - -/** - * Internal dependencies - */ -import PanelColor from '../color'; - -describe( 'PanelColor', () => { - it( 'should match snapshot when title is provided', () => { - const wrapper = shallow( <PanelColor title="sample title" /> ); - - expect( wrapper ).toMatchSnapshot(); - - expect( console ).toHaveWarnedWith( - 'wp.components.PanelColor is deprecated and will be removed from Gutenberg in 4.3. Please use wp.editor.PanelColorSettings instead.' - ); - } ); -} ); diff --git a/packages/edit-post/CHANGELOG.md b/packages/edit-post/CHANGELOG.md index 1be460e3d081c..f3fcee5b7aaef 100644 --- a/packages/edit-post/CHANGELOG.md +++ b/packages/edit-post/CHANGELOG.md @@ -1,3 +1,10 @@ +## 3.0.0 (Unreleased) + +### Breaking Changes + +- `isEditorSidebarPanelOpened` selector (`core/edit-post`) has been removed. Please use `isEditorPanelEnabled` instead. +- `toggleGeneralSidebarEditorPanel` action (`core/edit-post`) has been removed. Please use `toggleEditorPanelOpened` instead. + ## 2.1.1 (2018-11-09) ## 2.1.0 (2018-11-09) diff --git a/packages/edit-post/package.json b/packages/edit-post/package.json index d531e335f5885..b86e62bc153f7 100644 --- a/packages/edit-post/package.json +++ b/packages/edit-post/package.json @@ -28,7 +28,6 @@ "@wordpress/compose": "file:../compose", "@wordpress/core-data": "file:../core-data", "@wordpress/data": "file:../data", - "@wordpress/deprecated": "file:../deprecated", "@wordpress/editor": "file:../editor", "@wordpress/element": "file:../element", "@wordpress/format-library": "file:../format-library", diff --git a/packages/edit-post/src/store/actions.js b/packages/edit-post/src/store/actions.js index f90dcc8e04356..37c03750c3b77 100644 --- a/packages/edit-post/src/store/actions.js +++ b/packages/edit-post/src/store/actions.js @@ -1,8 +1,3 @@ -/** - * WordPress dependencies - */ -import deprecated from '@wordpress/deprecated'; - /** * Returns an action object used in signalling that the user opened an editor sidebar. * @@ -116,22 +111,6 @@ export function toggleEditorPanelOpened( panelName ) { }; } -/** - * Returns an action object used to open or close a panel in the editor. - * - * @param {string} panelName A string that identifies the panel to open or close. - * - * @return {Object} Action object. -*/ -export function toggleGeneralSidebarEditorPanel( panelName ) { - deprecated( 'toggleGeneralSidebarEditorPanel', { - alternative: 'toggleEditorPanelOpened', - plugin: 'Gutenberg', - version: '4.3.0', - } ); - return toggleEditorPanelOpened( panelName ); -} - /** * Returns an action object used to toggle a feature flag. * diff --git a/packages/edit-post/src/store/selectors.js b/packages/edit-post/src/store/selectors.js index 8d7d41b76abae..f01e1e22ed8ab 100644 --- a/packages/edit-post/src/store/selectors.js +++ b/packages/edit-post/src/store/selectors.js @@ -4,11 +4,6 @@ import createSelector from 'rememo'; import { get, includes, some, flatten, values } from 'lodash'; -/** - * WordPress dependencies - */ -import deprecated from '@wordpress/deprecated'; - /** * Returns the current editing mode. * @@ -118,24 +113,6 @@ export function isEditorPanelEnabled( state, panelName ) { return get( panels, [ panelName, 'enabled' ], true ); } -/** - * Returns true if the given panel is enabled, or false otherwise. Panels are - * enabled by default. - * - * @param {Object} state Global application state. - * @param {string} panel A string that identifies the panel. - * - * @return {boolean} Whether or not the panel is enabled. - */ -export function isEditorSidebarPanelOpened( state, panel ) { - deprecated( 'isEditorSidebarPanelOpened', { - alternative: 'isEditorPanelEnabled', - plugin: 'Gutenberg', - version: '4.3', - } ); - return isEditorPanelEnabled( state, panel ); -} - /** * Returns true if the given panel is open, or false otherwise. Panels are * closed by default. diff --git a/packages/edit-post/src/store/test/selectors.js b/packages/edit-post/src/store/test/selectors.js index 32dbce728572f..4a13ec8e4392c 100644 --- a/packages/edit-post/src/store/test/selectors.js +++ b/packages/edit-post/src/store/test/selectors.js @@ -1,8 +1,3 @@ -/** - * WordPress dependencies - */ -import deprecated from '@wordpress/deprecated'; - /** * Internal dependencies */ @@ -23,13 +18,7 @@ import { isEditorPanelEnabled, } from '../selectors'; -jest.mock( '@wordpress/deprecated', () => jest.fn() ); - describe( 'selectors', () => { - beforeEach( () => { - deprecated.mockClear(); - } ); - describe( 'getEditorMode', () => { it( 'should return the selected editor mode', () => { const state = { diff --git a/packages/editor/CHANGELOG.md b/packages/editor/CHANGELOG.md index 0ac5a427b55fb..0e88d46040f13 100644 --- a/packages/editor/CHANGELOG.md +++ b/packages/editor/CHANGELOG.md @@ -1,3 +1,9 @@ +## 7.0.0 (Unreleased) + +### Breaking Changes + +- The `PanelColor` component has been removed. + ## 6.2.1 (2018-11-09) ### Polish diff --git a/packages/editor/src/components/index.js b/packages/editor/src/components/index.js index 3cce6ea3cd87a..55b798561a602 100644 --- a/packages/editor/src/components/index.js +++ b/packages/editor/src/components/index.js @@ -16,7 +16,6 @@ export * from './font-sizes'; export { default as InnerBlocks } from './inner-blocks'; export { default as InspectorAdvancedControls } from './inspector-advanced-controls'; export { default as InspectorControls } from './inspector-controls'; -export { default as PanelColor } from './panel-color'; export { default as PanelColorSettings } from './panel-color-settings'; export { default as PlainText } from './plain-text'; export { diff --git a/packages/editor/src/components/panel-color/index.js b/packages/editor/src/components/panel-color/index.js deleted file mode 100644 index 327384a127653..0000000000000 --- a/packages/editor/src/components/panel-color/index.js +++ /dev/null @@ -1,42 +0,0 @@ -/** - * External dependencies - */ -import { omit } from 'lodash'; - -/** - * WordPress dependencies - */ -import { PanelColor as PanelColorComponent } from '@wordpress/components'; -import { ifCondition, compose } from '@wordpress/compose'; -import deprecated from '@wordpress/deprecated'; - -/** - * Internal dependencies - */ -import ColorPalette from '../color-palette'; -import withColorContext from '../color-palette/with-color-context'; -import { getColorObjectByColorValue } from '../colors'; - -function PanelColor( { colors, title, colorValue, initialOpen, ...props } ) { - deprecated( 'wp.editor.PanelColor', { - version: '4.3', - alternative: 'wp.editor.PanelColorSettings', - plugin: 'Gutenberg', - } ); - - const colorObject = getColorObjectByColorValue( colors, colorValue ); - const colorName = colorObject && colorObject.name; - return ( - <PanelColorComponent { ...{ title, colorName, colorValue, initialOpen } } > - <ColorPalette - value={ colorValue } - { ...omit( props, [ 'disableCustomColors' ] ) } - /> - </PanelColorComponent> - ); -} - -export default compose( [ - withColorContext, - ifCondition( ( { hasColorsToChoose } ) => hasColorsToChoose ), -] )( PanelColor ); From 660e46ed7eda07d236e437007cc437821c02f09b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20=28Greg=29=20Zi=C3=B3=C5=82kowski?= <grzegorz@gziolo.pl> Date: Fri, 9 Nov 2018 18:21:30 +0100 Subject: [PATCH 076/106] Avoid unnecessary re-renders when navigating between blocks (#11495) * Performance: Avoid unnecessary re-renders when navigating between blocks with keyboard * Small tweak per review --- .../editor/src/components/block-list/block.js | 23 ++++++++++++++++++- .../editor/src/components/block-list/index.js | 20 ---------------- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/packages/editor/src/components/block-list/block.js b/packages/editor/src/components/block-list/block.js index bd156cb3f79ec..cf505245b9f5a 100644 --- a/packages/editor/src/components/block-list/block.js +++ b/packages/editor/src/components/block-list/block.js @@ -70,6 +70,7 @@ export class BlockListBlock extends Component { this.onDragStart = this.onDragStart.bind( this ); this.onDragEnd = this.onDragEnd.bind( this ); this.selectOnOpen = this.selectOnOpen.bind( this ); + this.onShiftSelection = this.onShiftSelection.bind( this ); this.hadTouchStart = false; this.state = { @@ -285,7 +286,7 @@ export class BlockListBlock extends Component { if ( event.shiftKey ) { if ( ! this.props.isSelected ) { - this.props.onShiftSelection( this.props.clientId ); + this.onShiftSelection(); event.preventDefault(); } } else { @@ -357,6 +358,20 @@ export class BlockListBlock extends Component { } } + onShiftSelection() { + if ( ! this.props.isSelectionEnabled ) { + return; + } + + const { getBlockSelectionStart, onMultiSelect, onSelect } = this.props; + + if ( getBlockSelectionStart() ) { + onMultiSelect( getBlockSelectionStart(), this.props.clientId ); + } else { + onSelect( this.props.clientId ); + } + } + render() { return ( <HoverArea container={ this.wrapperNode }> @@ -601,6 +616,7 @@ const applyWithSelect = withSelect( ( select, { clientId, rootClientId, isLargeV getEditorSettings, hasSelectedInnerBlock, getTemplateLock, + getBlockSelectionStart, } = select( 'core/editor' ); const isSelected = isBlockSelected( clientId ); const { hasFixedToolbar, focusMode } = getEditorSettings(); @@ -634,6 +650,9 @@ const applyWithSelect = withSelect( ( select, { clientId, rootClientId, isLargeV block, isSelected, isParentOfSelectedBlock, + // We only care about this value when the shift key is pressed. + // We call it dynamically in the event handler to avoid unnecessary re-renders. + getBlockSelectionStart, }; } ); @@ -641,6 +660,7 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps ) => { const { updateBlockAttributes, selectBlock, + multiSelect, insertBlocks, insertDefaultBlock, removeBlock, @@ -657,6 +677,7 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps ) => { onSelect( clientId = ownProps.clientId, initialPosition ) { selectBlock( clientId, initialPosition ); }, + onMultiSelect: multiSelect, onInsertBlocks( blocks, index ) { const { rootClientId } = ownProps; insertBlocks( blocks, index, rootClientId ); diff --git a/packages/editor/src/components/block-list/index.js b/packages/editor/src/components/block-list/index.js index 021a3758dbe80..7d589a329fca3 100644 --- a/packages/editor/src/components/block-list/index.js +++ b/packages/editor/src/components/block-list/index.js @@ -29,7 +29,6 @@ class BlockList extends Component { this.onSelectionStart = this.onSelectionStart.bind( this ); this.onSelectionEnd = this.onSelectionEnd.bind( this ); - this.onShiftSelection = this.onShiftSelection.bind( this ); this.setBlockRef = this.setBlockRef.bind( this ); this.setLastClientY = this.setLastClientY.bind( this ); this.onPointerMove = throttle( this.onPointerMove.bind( this ), 100 ); @@ -170,20 +169,6 @@ class BlockList extends Component { } } - onShiftSelection( clientId ) { - if ( ! this.props.isSelectionEnabled ) { - return; - } - - const { selectionStartClientId, onMultiSelect, onSelect } = this.props; - - if ( selectionStartClientId ) { - onMultiSelect( selectionStartClientId, clientId ); - } else { - onSelect( clientId ); - } - } - render() { const { blockClientIds, @@ -200,7 +185,6 @@ class BlockList extends Component { clientId={ clientId } blockRef={ this.setBlockRef } onSelectionStart={ this.onSelectionStart } - onShiftSelection={ this.onShiftSelection } rootClientId={ rootClientId } isFirst={ blockIndex === 0 } isLast={ blockIndex === blockClientIds.length - 1 } @@ -221,7 +205,6 @@ export default compose( [ isMultiSelecting, getMultiSelectedBlocksStartClientId, getMultiSelectedBlocksEndClientId, - getBlockSelectionStart, } = select( 'core/editor' ); const { rootClientId } = ownProps; @@ -229,7 +212,6 @@ export default compose( [ blockClientIds: getBlockOrder( rootClientId ), selectionStart: getMultiSelectedBlocksStartClientId(), selectionEnd: getMultiSelectedBlocksEndClientId(), - selectionStartClientId: isSelectionEnabled() && getBlockSelectionStart(), isSelectionEnabled: isSelectionEnabled(), isMultiSelecting: isMultiSelecting(), }; @@ -239,14 +221,12 @@ export default compose( [ startMultiSelect, stopMultiSelect, multiSelect, - selectBlock, } = dispatch( 'core/editor' ); return { onStartMultiSelect: startMultiSelect, onStopMultiSelect: stopMultiSelect, onMultiSelect: multiSelect, - onSelect: selectBlock, }; } ), ] )( BlockList ); From 52a3375f9f1d356453aea2af1203fc11d8949805 Mon Sep 17 00:00:00 2001 From: Anton Timmermans <email@atimmer.com> Date: Fri, 9 Nov 2018 18:44:51 +0100 Subject: [PATCH 077/106] Annotation API (#7718) * Implement annotations Integrate Annotation API with Format API Keep annotations when applying formatting Change parameters for toTree after rebasing on master Fix unit test Add files after `npm run docs:build` Only re-render one block when adding annotation Do this by optimizing the annotations state to keep track of which annotations belongs to which block. This information can then be used in the selector to return the same reference if the annotations for a block haven't changed. Add unit tests for annotations reducer Also remove MOVE_ANNOTATION action. Rename block to blockClientId Remove block ID from RichText documentation It is not relevant at that level. Change block annotation to use selector property Build docs Fix low hanging fruit Add annotation support for block attributes Rename blockAttribute to richTextIdentifier Simplify annotations to only `start` and `end` Revert changes to to-tree Add annotation support to lists and headings Build docs Move the applyAnnotations call into formatToValue Refactor applyAnnotation to only loop once Add basic e2e test for annotation API Disallow invalid annotation ranges in state Drop general block annotation className Add assertions for annotations behavior Add selector to retrieve currently selected text Move removeAnnotations to valueToFormat Move applyAnnotations and removeAnnotations They don't use `this`, so it is only fair. Fix failing tests Build docs Move annotations state to @wordpress/annotations * Mark Annotation API as experimental * Move annotation format to annotation package * Move annotation application to annotation package * Remove getCurrentRichTextSelection selector * Remove richText selection remnants * Remove annotations from rich-text docs * Remove duplicate props * Move block annotation to annotations package * Properly define dependencies * Add _experimental flag to e2e tests * Remove wp-annotations as a dependency of wp-editor * Fix spacing * Annotations: Add missing uuid dependency * Annotations: Fix DocBlock inconsistency * Annotations: Add missing Lodash dependency * Annotations: Fix up a few more DocBlocks * Annotations: Add a basic description for package * Docs: Include Annotations document in root manifest * Update dependencies --- docs/data/README.md | 1 + docs/data/data-core-annotations.md | 84 +++++++++ docs/extensibility/annotations.md | 55 ++++++ docs/manifest.json | 18 ++ docs/root-manifest.json | 6 + docs/tool/config.js | 5 + lib/client-assets.php | 7 + package-lock.json | 29 ++- package.json | 3 +- packages/annotations/.npmrc | 1 + packages/annotations/CHANGELOG.md | 5 + packages/annotations/README.md | 15 ++ packages/annotations/package.json | 35 ++++ packages/annotations/src/block/index.js | 25 +++ packages/annotations/src/format/annotation.js | 82 +++++++++ packages/annotations/src/format/index.js | 15 ++ packages/annotations/src/index.js | 7 + packages/annotations/src/store/actions.js | 72 ++++++++ packages/annotations/src/store/index.js | 24 +++ packages/annotations/src/store/reducer.js | 109 ++++++++++++ packages/annotations/src/store/selectors.js | 65 +++++++ .../annotations/src/store/test/reducer.js | 166 ++++++++++++++++++ packages/blocks/package.json | 2 +- packages/components/package.json | 2 +- .../editor/src/components/block-list/block.js | 3 +- .../editor/src/components/rich-text/index.js | 8 +- .../__snapshots__/plugins-api.test.js.snap | 2 +- test/e2e/specs/plugins-api.test.js | 40 +++++ test/e2e/test-plugins/plugins-api.php | 1 + test/e2e/test-plugins/plugins-api/sidebar.js | 20 +++ webpack.config.js | 1 + 31 files changed, 894 insertions(+), 14 deletions(-) create mode 100644 docs/data/data-core-annotations.md create mode 100644 docs/extensibility/annotations.md create mode 100644 packages/annotations/.npmrc create mode 100644 packages/annotations/CHANGELOG.md create mode 100644 packages/annotations/README.md create mode 100644 packages/annotations/package.json create mode 100644 packages/annotations/src/block/index.js create mode 100644 packages/annotations/src/format/annotation.js create mode 100644 packages/annotations/src/format/index.js create mode 100644 packages/annotations/src/index.js create mode 100644 packages/annotations/src/store/actions.js create mode 100644 packages/annotations/src/store/index.js create mode 100644 packages/annotations/src/store/reducer.js create mode 100644 packages/annotations/src/store/selectors.js create mode 100644 packages/annotations/src/store/test/reducer.js diff --git a/docs/data/README.md b/docs/data/README.md index 1f99bcb017f5b..ac44230651976 100644 --- a/docs/data/README.md +++ b/docs/data/README.md @@ -1,6 +1,7 @@ # Data Module Reference - [**core**: WordPress Core Data](../../docs/data/data-core.md) + - [**core/annotations**: Annotations](../../docs/data/data-core-annotations.md) - [**core/blocks**: Block Types Data](../../docs/data/data-core-blocks.md) - [**core/editor**: The Editor’s Data](../../docs/data/data-core-editor.md) - [**core/edit-post**: The Editor’s UI Data](../../docs/data/data-core-edit-post.md) diff --git a/docs/data/data-core-annotations.md b/docs/data/data-core-annotations.md new file mode 100644 index 0000000000000..f4f7d8cb5ff07 --- /dev/null +++ b/docs/data/data-core-annotations.md @@ -0,0 +1,84 @@ +# **core/annotations**: Annotations + +## Selectors + +### __experimentalGetAnnotationsForBlock + +Returns the annotations for a specific client ID. + +*Parameters* + + * state: Editor state. + * clientId: The ID of the block to get the annotations for. + +### __experimentalGetAnnotationsForRichText + +Returns the annotations that apply to the given RichText instance. + +Both a blockClientId and a richTextIdentifier are required. This is because +a block might have multiple `RichText` components. This does mean that every +block needs to implement annotations itself. + +*Parameters* + + * state: Editor state. + * blockClientId: The client ID for the block. + * richTextIdentifier: Unique identifier that identifies the given RichText. + +*Returns* + +All the annotations relevant for the `RichText`. + +### __experimentalGetAnnotations + +Returns all annotations in the editor state. + +*Parameters* + + * state: Editor state. + +*Returns* + +All annotations currently applied. + +## Actions + +### __experimentalAddAnnotation + +Adds an annotation to a block. + +The `block` attribute refers to a block ID that needs to be annotated. +`isBlockAnnotation` controls whether or not the annotation is a block +annotation. The `source` is the source of the annotation, this will be used +to identity groups of annotations. + +The `range` property is only relevant if the selector is 'range'. + +*Parameters* + + * annotation: The annotation to add. + * blockClientId: The blockClientId to add the annotation to. + * richTextIdentifier: Identifier for the RichText instance the annotation applies to. + * range: The range at which to apply this annotation. + * range.start: The offset where the annotation should start. + * range.end: The offset where the annotation should end. + * string: [selector="range"] The way to apply this annotation. + * string: [source="default"] The source that added the annotation. + * string: [id=uuid()] The ID the annotation should have. + Generates a UUID by default. + +### __experimentalRemoveAnnotation + +Removes an annotation with a specific ID. + +*Parameters* + + * annotationId: The annotation to remove. + +### __experimentalRemoveAnnotationsBySource + +Removes all annotations of a specific source. + +*Parameters* + + * source: The source to remove. \ No newline at end of file diff --git a/docs/extensibility/annotations.md b/docs/extensibility/annotations.md new file mode 100644 index 0000000000000..70df5b61e5d18 --- /dev/null +++ b/docs/extensibility/annotations.md @@ -0,0 +1,55 @@ +# Annotations + +**Note: This API is experimental, that means it is subject to non-backward compatible changes or removal in any future version.** + +Annotations are a way to highlight a specific piece in a Gutenberg post. Examples of this include commenting on a piece of text and spellchecking. Both can use the annotations API to mark a piece of text. + +## API + +To see the API for yourself the easiest way is to have a block that is at least 200 characters long without formatting and putting the following in the console: + +```js +wp.data.dispatch( 'core/annotations' ).addAnnotation( { + source: "my-annotations-plugin", + blockClientId: wp.data.select( 'core/editor' ).getBlockOrder()[0], + richTextIdentifier: "content", + range: { + start: 50, + end: 100, + }, +} ); +``` + +The start and the end of the range should be calculated based only on the text of the relevant `RichText`. For example, in the following HTML position 0 will refer to the position before the capital S: + +```html +<strong>Strong text</strong> +``` + +To help with determining the correct positions, the `wp.richText.create` method can be used. This will split a piece of HTML into text and formats. + +All available properties can be found in the API documentation of the `addAnnotation` action. + +## Block annotation + +It is also possible to annotate a block completely. In that case just provide the `selector` property and set it to `block`. The default `selector` is `range`, which can be used for text annotation. + +```js +wp.data.dispatch( 'core/annotations' ).addAnnotation( { + source: "my-annotations-plugin", + blockClientId: wp.data.select( 'core/editor' ).getBlockOrder()[0], + selector: "block", +} ); +``` + +This doesn't provide any styling out of the box, so you have to provide some CSS to make sure your annotation is shown: + +```css +.is-annotated-by-my-annotations-plugin { + outline: 1px solid black; +} +``` + +## Text annotation + +The text annotation is controlled by the `start` and `end` properties. Simple `start` and `end` properties don't work for HTML, so these properties are assumed to be offsets within the `rich-text` internal structure. For simplicity you can think about this as if all HTML would be stripped out and then you calculate the `start` and the `end` of the annotation. diff --git a/docs/manifest.json b/docs/manifest.json index efab957c872c6..075d922f2cad9 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -89,6 +89,12 @@ "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/extensibility/autocomplete.md", "parent": "extensibility" }, + { + "title": "Annotations", + "slug": "annotations", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/extensibility/annotations.md", + "parent": "extensibility" + }, { "title": "Design", "slug": "design", @@ -257,6 +263,12 @@ "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/packages/a11y/README.md", "parent": "packages" }, + { + "title": "@wordpress/annotations", + "slug": "packages-annotations", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/packages/annotations/README.md", + "parent": "packages" + }, { "title": "@wordpress/api-fetch", "slug": "packages-api-fetch", @@ -929,6 +941,12 @@ "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/data/data-core.md", "parent": "data" }, + { + "title": "Annotations", + "slug": "data-core-annotations", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/data/data-core-annotations.md", + "parent": "data" + }, { "title": "Block Types Data", "slug": "data-core-blocks", diff --git a/docs/root-manifest.json b/docs/root-manifest.json index 759792a857b73..1202ab14eacb4 100644 --- a/docs/root-manifest.json +++ b/docs/root-manifest.json @@ -89,6 +89,12 @@ "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/extensibility/autocomplete.md", "parent": "extensibility" }, + { + "title": "Annotations", + "slug": "annotations", + "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/extensibility/annotations.md", + "parent": "extensibility" + }, { "title": "Design", "slug": "design", diff --git a/docs/tool/config.js b/docs/tool/config.js index b7b0fa3ef4ad6..6758ac592a72f 100644 --- a/docs/tool/config.js +++ b/docs/tool/config.js @@ -15,6 +15,11 @@ module.exports = { selectors: [ path.resolve( root, 'packages/core-data/src/selectors.js' ) ], actions: [ path.resolve( root, 'packages/core-data/src/actions.js' ) ], }, + 'core/annotations': { + title: 'Annotations', + selectors: [ path.resolve( root, 'packages/annotations/src/store/selectors.js' ) ], + actions: [ path.resolve( root, 'packages/annotations/src/store/actions.js' ) ], + }, 'core/blocks': { title: 'Block Types Data', selectors: [ path.resolve( root, 'packages/blocks/src/store/selectors.js' ) ], diff --git a/lib/client-assets.php b/lib/client-assets.php index ed95afda111d1..298e1cd0f447c 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -316,6 +316,13 @@ function gutenberg_register_scripts_and_styles() { ) ) ); + gutenberg_override_script( + 'wp-annotations', + gutenberg_url( 'build/annotations/index.js' ), + array( 'wp-polyfill', 'wp-data', 'wp-rich-text', 'wp-hooks', 'wp-i18n' ), + filemtime( gutenberg_dir_path() . 'build/annotations/index.js' ), + true + ); gutenberg_override_script( 'wp-core-data', gutenberg_url( 'build/core-data/index.js' ), diff --git a/package-lock.json b/package-lock.json index 03591075a66eb..21595b51e232f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2247,6 +2247,25 @@ "@wordpress/dom-ready": "file:packages/dom-ready" } }, + "@wordpress/annotations": { + "version": "file:packages/annotations", + "requires": { + "@babel/runtime": "^7.0.0", + "@wordpress/data": "file:packages/data", + "@wordpress/hooks": "file:packages/hooks", + "@wordpress/i18n": "file:packages/i18n", + "@wordpress/rich-text": "file:packages/rich-text", + "lodash": "^4.17.10", + "rememo": "^3.0.0", + "uuid": "^3.3.2" + }, + "dependencies": { + "uuid": { + "version": "3.3.2", + "bundled": true + } + } + }, "@wordpress/api-fetch": { "version": "file:packages/api-fetch", "requires": { @@ -2355,7 +2374,7 @@ "showdown": "^1.8.6", "simple-html-tokenizer": "^0.4.1", "tinycolor2": "^1.4.1", - "uuid": "^3.1.0" + "uuid": "^3.3.2" } }, "@wordpress/browserslist-config": { @@ -2390,7 +2409,7 @@ "react-dates": "^17.1.1", "rememo": "^3.0.0", "tinycolor2": "^1.4.1", - "uuid": "^3.1.0" + "uuid": "^3.3.2" } }, "@wordpress/compose": { @@ -21184,9 +21203,9 @@ "dev": true }, "uuid": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", - "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==" + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" }, "v8-compile-cache": { "version": "1.1.2", diff --git a/package.json b/package.json index ffc1552fbb076..c13873b929e95 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ }, "dependencies": { "@wordpress/a11y": "file:packages/a11y", + "@wordpress/annotations": "file:packages/annotations", "@wordpress/api-fetch": "file:packages/api-fetch", "@wordpress/autop": "file:packages/autop", "@wordpress/blob": "file:packages/blob", @@ -106,7 +107,7 @@ "stylelint": "9.5.0", "stylelint-config-wordpress": "13.1.0", "symlink-or-copy": "1.2.0", - "uuid": "3.1.0", + "uuid": "3.3.2", "webpack": "4.8.3", "webpack-bundle-analyzer": "3.0.2", "webpack-cli": "2.1.3", diff --git a/packages/annotations/.npmrc b/packages/annotations/.npmrc new file mode 100644 index 0000000000000..43c97e719a5a8 --- /dev/null +++ b/packages/annotations/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/packages/annotations/CHANGELOG.md b/packages/annotations/CHANGELOG.md new file mode 100644 index 0000000000000..95745945dd46e --- /dev/null +++ b/packages/annotations/CHANGELOG.md @@ -0,0 +1,5 @@ +## 1.0.0 (unreleased) + +### New Features + +- Implement annotations API in the editor. diff --git a/packages/annotations/README.md b/packages/annotations/README.md new file mode 100644 index 0000000000000..a1585de3106cb --- /dev/null +++ b/packages/annotations/README.md @@ -0,0 +1,15 @@ +# Annotations + +Annotate content in the Gutenberg editor. + +## Installation + +Install the module + +```bash +npm install @wordpress/annotations --save +``` + +_This package assumes that your code will run in an **ES2015+** environment. If you're using an environment that has limited or no support for ES2015+ such as lower versions of IE then using [core-js](https://github.com/zloirock/core-js) or [@babel/polyfill](https://babeljs.io/docs/en/next/babel-polyfill) will add support for these methods. Learn more about it in [Babel docs](https://babeljs.io/docs/en/next/caveats)._ + +## Usage diff --git a/packages/annotations/package.json b/packages/annotations/package.json new file mode 100644 index 0000000000000..b68ce8f7efa25 --- /dev/null +++ b/packages/annotations/package.json @@ -0,0 +1,35 @@ +{ + "name": "@wordpress/annotations", + "version": "1.0.0-beta1", + "description": "Annotate content in the Gutenberg editor.", + "author": "The WordPress Contributors", + "license": "GPL-2.0-or-later", + "keywords": [ + "wordpress", + "annotations" + ], + "homepage": "https://github.com/WordPress/gutenberg/tree/master/packages/annotations/README.md", + "repository": { + "type": "git", + "url": "https://github.com/WordPress/gutenberg.git" + }, + "bugs": { + "url": "https://github.com/WordPress/gutenberg/issues" + }, + "main": "build/index.js", + "module": "build-module/index.js", + "react-native": "src/index", + "dependencies": { + "@babel/runtime": "^7.0.0", + "@wordpress/data": "file:../data", + "@wordpress/hooks": "file:../hooks", + "@wordpress/i18n": "file:../i18n", + "@wordpress/rich-text": "file:../rich-text", + "lodash": "^4.17.10", + "rememo": "^3.0.0", + "uuid": "^3.3.2" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/annotations/src/block/index.js b/packages/annotations/src/block/index.js new file mode 100644 index 0000000000000..5095fc473d67e --- /dev/null +++ b/packages/annotations/src/block/index.js @@ -0,0 +1,25 @@ +/** + * WordPress dependencies + */ +import { addFilter } from '@wordpress/hooks'; +import { withSelect } from '@wordpress/data'; + +/** + * Adds annotation className to the block-list-block component. + * + * @param {Object} OriginalComponent The original BlockListBlock component. + * @return {Object} The enhanced component. + */ +const addAnnotationClassName = ( OriginalComponent ) => { + return withSelect( ( select, { clientId } ) => { + const annotations = select( 'core/annotations' ).__experimentalGetAnnotationsForBlock( clientId ); + + return { + className: annotations.map( ( annotation ) => { + return 'is-annotated-by-' + annotation.source; + } ), + }; + } )( OriginalComponent ); +}; + +addFilter( 'editor.BlockListBlock', 'core/annotations', addAnnotationClassName ); diff --git a/packages/annotations/src/format/annotation.js b/packages/annotations/src/format/annotation.js new file mode 100644 index 0000000000000..b052de27f335f --- /dev/null +++ b/packages/annotations/src/format/annotation.js @@ -0,0 +1,82 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +const name = 'core/annotation'; + +/** + * WordPress dependencies + */ +import { applyFormat, removeFormat } from '@wordpress/rich-text'; + +/** + * Applies given annotations to the given record. + * + * @param {Object} record The record to apply annotations to. + * @param {Array} annotations The annotation to apply. + * @return {Object} A record with the annotations applied. + */ +export function applyAnnotations( record, annotations = [] ) { + annotations.forEach( ( annotation ) => { + let { start, end } = annotation; + + if ( start > record.text.length ) { + start = record.text.length; + } + + if ( end > record.text.length ) { + end = record.text.length; + } + + const className = 'annotation-text-' + annotation.source; + + record = applyFormat( + record, + { type: 'core/annotation', attributes: { className } }, + start, + end + ); + } ); + + return record; +} + +/** + * Removes annotations from the given record. + * + * @param {Object} record Record to remove annotations from. + * @return {Object} The cleaned record. + */ +export function removeAnnotations( record ) { + return removeFormat( record, 'core/annotation', 0, record.text.length ); +} + +export const annotation = { + name, + title: __( 'Annotation' ), + tagName: 'mark', + className: 'annotation-text', + attributes: { + className: 'class', + }, + edit() { + return null; + }, + __experimentalGetPropsForEditableTreePreparation( select, { richTextIdentifier, blockClientId } ) { + return { + annotations: select( 'core/annotations' ).__experimentalGetAnnotationsForRichText( blockClientId, richTextIdentifier ), + }; + }, + __experimentalCreatePrepareEditableTree( props ) { + return ( formats, text ) => { + if ( props.annotations.length === 0 ) { + return formats; + } + + let record = { formats, text }; + record = applyAnnotations( record, props.annotations ); + return record.formats; + }; + }, +}; diff --git a/packages/annotations/src/format/index.js b/packages/annotations/src/format/index.js new file mode 100644 index 0000000000000..1dccbbd5012a0 --- /dev/null +++ b/packages/annotations/src/format/index.js @@ -0,0 +1,15 @@ +/** + * WordPress dependencies + */ +import { + registerFormatType, +} from '@wordpress/rich-text'; + +/** + * Internal dependencies + */ +import { annotation } from './annotation'; + +const { name, ...settings } = annotation; + +registerFormatType( name, settings ); diff --git a/packages/annotations/src/index.js b/packages/annotations/src/index.js new file mode 100644 index 0000000000000..ce64106bf903c --- /dev/null +++ b/packages/annotations/src/index.js @@ -0,0 +1,7 @@ +/** + * Internal dependencies + */ +import './store'; +import './format'; +import './block'; + diff --git a/packages/annotations/src/store/actions.js b/packages/annotations/src/store/actions.js new file mode 100644 index 0000000000000..73f8c9e1fe381 --- /dev/null +++ b/packages/annotations/src/store/actions.js @@ -0,0 +1,72 @@ +/** + * External dependencies + */ +import uuid from 'uuid/v4'; + +/** + * Adds an annotation to a block. + * + * The `block` attribute refers to a block ID that needs to be annotated. + * `isBlockAnnotation` controls whether or not the annotation is a block + * annotation. The `source` is the source of the annotation, this will be used + * to identity groups of annotations. + * + * The `range` property is only relevant if the selector is 'range'. + * + * @param {Object} annotation The annotation to add. + * @param {string} blockClientId The blockClientId to add the annotation to. + * @param {string} richTextIdentifier Identifier for the RichText instance the annotation applies to. + * @param {Object} range The range at which to apply this annotation. + * @param {number} range.start The offset where the annotation should start. + * @param {number} range.end The offset where the annotation should end. + * @param {string} [selector="range"] The way to apply this annotation. + * @param {string} [source="default"] The source that added the annotation. + * @param {string} [id=uuid()] The ID the annotation should have. + * Generates a UUID by default. + * + * @return {Object} Action object. + */ +export function __experimentalAddAnnotation( { blockClientId, richTextIdentifier = null, range = null, selector = 'range', source = 'default', id = uuid() } ) { + const action = { + type: 'ANNOTATION_ADD', + id, + blockClientId, + richTextIdentifier, + source, + selector, + }; + + if ( selector === 'range' ) { + action.range = range; + } + + return action; +} + +/** + * Removes an annotation with a specific ID. + * + * @param {string} annotationId The annotation to remove. + * + * @return {Object} Action object. + */ +export function __experimentalRemoveAnnotation( annotationId ) { + return { + type: 'ANNOTATION_REMOVE', + annotationId, + }; +} + +/** + * Removes all annotations of a specific source. + * + * @param {string} source The source to remove. + * + * @return {Object} Action object. + */ +export function __experimentalRemoveAnnotationsBySource( source ) { + return { + type: 'ANNOTATION_REMOVE_SOURCE', + source, + }; +} diff --git a/packages/annotations/src/store/index.js b/packages/annotations/src/store/index.js new file mode 100644 index 0000000000000..917a342ad9f49 --- /dev/null +++ b/packages/annotations/src/store/index.js @@ -0,0 +1,24 @@ +/** + * WordPress Dependencies + */ +import { registerStore } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import reducer from './reducer'; +import * as selectors from './selectors'; +import * as actions from './actions'; + +/** + * Module Constants + */ +const MODULE_KEY = 'core/annotations'; + +const store = registerStore( MODULE_KEY, { + reducer, + selectors, + actions, +} ); + +export default store; diff --git a/packages/annotations/src/store/reducer.js b/packages/annotations/src/store/reducer.js new file mode 100644 index 0000000000000..cb14165a5d6bd --- /dev/null +++ b/packages/annotations/src/store/reducer.js @@ -0,0 +1,109 @@ +/** + * External dependencies + */ +import { isNumber, mapValues } from 'lodash'; + +/** + * Filters an array based on the predicate, but keeps the reference the same if + * the array hasn't changed. + * + * @param {Array} collection The collection to filter. + * @param {Function} predicate Function that determines if the item should stay + * in the array. + * @return {Array} Filtered array. + */ +function filterWithReference( collection, predicate ) { + const filteredCollection = collection.filter( predicate ); + + return collection.length === filteredCollection.length ? collection : filteredCollection; +} + +/** + * Verifies whether the given annotations is a valid annotation. + * + * @param {Object} annotation The annotation to verify. + * @return {boolean} Whether the given annotation is valid. + */ +function isValidAnnotationRange( annotation ) { + return isNumber( annotation.start ) && + isNumber( annotation.end ) && + annotation.start <= annotation.end; +} + +/** + * Reducer managing annotations. + * + * @param {Array} state The annotations currently shown in the editor. + * @param {Object} action Dispatched action. + * + * @return {Array} Updated state. + */ +export function annotations( state = { all: [], byBlockClientId: {} }, action ) { + switch ( action.type ) { + case 'ANNOTATION_ADD': + const blockClientId = action.blockClientId; + const newAnnotation = { + id: action.id, + blockClientId, + richTextIdentifier: action.richTextIdentifier, + source: action.source, + selector: action.selector, + range: action.range, + }; + + if ( newAnnotation.selector === 'range' && ! isValidAnnotationRange( newAnnotation.range ) ) { + return state; + } + + const previousAnnotationsForBlock = state.byBlockClientId[ blockClientId ] || []; + + return { + all: [ + ...state.all, + newAnnotation, + ], + byBlockClientId: { + ...state.byBlockClientId, + [ blockClientId ]: [ ...previousAnnotationsForBlock, action.id ], + }, + }; + + case 'ANNOTATION_REMOVE': + return { + all: state.all.filter( ( annotation ) => annotation.id !== action.annotationId ), + + // We use filterWithReference to not refresh the reference if a block still has + // the same annotations. + byBlockClientId: mapValues( state.byBlockClientId, ( annotationForBlock ) => { + return filterWithReference( annotationForBlock, ( annotationId ) => { + return annotationId !== action.annotationId; + } ); + } ), + }; + + case 'ANNOTATION_REMOVE_SOURCE': + const idsToRemove = []; + + const allAnnotations = state.all.filter( ( annotation ) => { + if ( annotation.source === action.source ) { + idsToRemove.push( annotation.id ); + return false; + } + + return true; + } ); + + return { + all: allAnnotations, + byBlockClientId: mapValues( state.byBlockClientId, ( annotationForBlock ) => { + return filterWithReference( annotationForBlock, ( annotationId ) => { + return ! idsToRemove.includes( annotationId ); + } ); + } ), + }; + } + + return state; +} + +export default annotations; diff --git a/packages/annotations/src/store/selectors.js b/packages/annotations/src/store/selectors.js new file mode 100644 index 0000000000000..659b83e83e30d --- /dev/null +++ b/packages/annotations/src/store/selectors.js @@ -0,0 +1,65 @@ +/** + * External dependencies + */ +import createSelector from 'rememo'; + +/** + * Returns the annotations for a specific client ID. + * + * @param {Object} state Editor state. + * @param {string} clientId The ID of the block to get the annotations for. + * + * @return {Array} The annotations applicable to this block. + */ +export const __experimentalGetAnnotationsForBlock = createSelector( + ( state, blockClientId ) => { + return state.all.filter( ( annotation ) => { + return annotation.selector === 'block' && annotation.blockClientId === blockClientId; + } ); + }, + ( state, blockClientId ) => [ + state.byBlockClientId[ blockClientId ], + ] +); + +/** + * Returns the annotations that apply to the given RichText instance. + * + * Both a blockClientId and a richTextIdentifier are required. This is because + * a block might have multiple `RichText` components. This does mean that every + * block needs to implement annotations itself. + * + * @param {Object} state Editor state. + * @param {string} blockClientId The client ID for the block. + * @param {string} richTextIdentifier Unique identifier that identifies the given RichText. + * @return {Array} All the annotations relevant for the `RichText`. + */ +export const __experimentalGetAnnotationsForRichText = createSelector( + ( state, blockClientId, richTextIdentifier ) => { + return state.all.filter( ( annotation ) => { + return annotation.selector === 'range' && + annotation.blockClientId === blockClientId && + richTextIdentifier === annotation.richTextIdentifier; + } ).map( ( annotation ) => { + const { range, ...other } = annotation; + + return { + ...range, + ...other, + }; + } ); + }, + ( state, blockClientId ) => [ + state.byBlockClientId[ blockClientId ], + ] +); + +/** + * Returns all annotations in the editor state. + * + * @param {Object} state Editor state. + * @return {Array} All annotations currently applied. + */ +export function __experimentalGetAnnotations( state ) { + return state.all; +} diff --git a/packages/annotations/src/store/test/reducer.js b/packages/annotations/src/store/test/reducer.js new file mode 100644 index 0000000000000..a1dba8db8c8ac --- /dev/null +++ b/packages/annotations/src/store/test/reducer.js @@ -0,0 +1,166 @@ +/** + * Internal dependencies + */ +import { annotations } from '../reducer'; + +describe( 'annotations', () => { + const initialState = { all: [], byBlockClientId: {} }; + + it( 'returns all annotations and annotation IDs per block', () => { + const state = annotations( undefined, {} ); + + expect( state ).toEqual( { all: [], byBlockClientId: {} } ); + } ); + + it( 'returns a state with an annotation that has been added', () => { + const state = annotations( undefined, { + type: 'ANNOTATION_ADD', + id: 'annotationId', + blockClientId: 'blockClientId', + richTextIdentifier: 'identifier', + source: 'default', + selector: 'block', + } ); + + expect( state ).toEqual( { + all: [ + { + id: 'annotationId', + blockClientId: 'blockClientId', + richTextIdentifier: 'identifier', + source: 'default', + selector: 'block', + }, + ], + byBlockClientId: { + blockClientId: [ 'annotationId' ], + }, + } ); + } ); + + it( 'allows an annotation to be removed', () => { + const state = annotations( { + all: [ + { + id: 'annotationId', + blockClientId: 'blockClientId', + richTextIdentifier: 'identifier', + source: 'default', + selector: 'block', + }, + ], + byBlockClientId: { + blockClientId: [ 'annotationId' ], + }, + }, { + type: 'ANNOTATION_REMOVE', + annotationId: 'annotationId', + } ); + + expect( state ).toEqual( { all: [], byBlockClientId: { blockClientId: [] } } ); + } ); + + it( 'allows an annotation to be removed by its source', () => { + const annotation1 = { + id: 'annotationId', + blockClientId: 'blockClientId', + richTextIdentifier: 'identifier', + source: 'default', + selector: 'block', + }; + const annotation2 = { + id: 'annotationId2', + blockClientId: 'blockClientId2', + richTextIdentifier: 'identifier2', + source: 'other-source', + selector: 'block', + }; + const state = annotations( { + all: [ + annotation1, + annotation2, + ], + byBlockClientId: { + blockClientId: [ 'annotationId' ], + blockClientId2: [ 'annotationId2' ], + }, + }, { + type: 'ANNOTATION_REMOVE_SOURCE', + source: 'default', + } ); + + expect( state ).toEqual( { + all: [ annotation2 ], + byBlockClientId: { + blockClientId: [], + blockClientId2: [ 'annotationId2' ], + }, + } ); + } ); + + it( 'allows a range selector', () => { + const state = annotations( undefined, { + type: 'ANNOTATION_ADD', + id: 'annotationId', + blockClientId: 'blockClientId', + richTextIdentifier: 'identifier', + source: 'default', + selector: 'range', + range: { + start: 0, + end: 100, + }, + } ); + + expect( state ).toEqual( { + all: [ + { + id: 'annotationId', + blockClientId: 'blockClientId', + richTextIdentifier: 'identifier', + source: 'default', + selector: 'range', + range: { + start: 0, + end: 100, + }, + }, + ], + byBlockClientId: { + blockClientId: [ 'annotationId' ], + }, + } ); + } ); + + it( 'rejects invalid annotations', () => { + let state = annotations( undefined, { + type: 'ANNOTATION_ADD', + source: 'default', + selector: 'range', + range: { + start: 5, + end: 4, + }, + } ); + state = annotations( state, { + type: 'ANNOTATION_ADD', + source: 'default', + selector: 'range', + range: { + start: 'not a number', + end: 100, + }, + } ); + state = annotations( state, { + type: 'ANNOTATION_ADD', + source: 'default', + selector: 'range', + range: { + start: 100, + end: 'not a number', + }, + } ); + + expect( state ).toEqual( initialState ); + } ); +} ); diff --git a/packages/blocks/package.json b/packages/blocks/package.json index a3b64afc28c9e..593f0c261a188 100644 --- a/packages/blocks/package.json +++ b/packages/blocks/package.json @@ -38,7 +38,7 @@ "showdown": "^1.8.6", "simple-html-tokenizer": "^0.4.1", "tinycolor2": "^1.4.1", - "uuid": "^3.1.0" + "uuid": "^3.3.2" }, "devDependencies": { "deep-freeze": "^0.0.1" diff --git a/packages/components/package.json b/packages/components/package.json index ff67b8ea8f9a7..2d6c70ab312b2 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -45,7 +45,7 @@ "react-dates": "^17.1.1", "rememo": "^3.0.0", "tinycolor2": "^1.4.1", - "uuid": "^3.1.0" + "uuid": "^3.3.2" }, "devDependencies": { "@wordpress/token-list": "file:../token-list", diff --git a/packages/editor/src/components/block-list/block.js b/packages/editor/src/components/block-list/block.js index cf505245b9f5a..15c19c28d186a 100644 --- a/packages/editor/src/components/block-list/block.js +++ b/packages/editor/src/components/block-list/block.js @@ -398,6 +398,7 @@ export class BlockListBlock extends Component { isPreviousBlockADefaultEmptyBlock, isParentOfSelectedBlock, isDraggable, + className, } = this.props; const isHovered = this.state.isHovered && ! isMultiSelecting; const { name: blockName, isValid } = block; @@ -439,7 +440,7 @@ export class BlockListBlock extends Component { 'is-typing': isTypingWithinBlock, 'is-focused': isFocusMode && ( isSelected || isParentOfSelectedBlock ), 'is-focus-mode': isFocusMode, - } ); + }, className ); const { onReplace } = this.props; diff --git a/packages/editor/src/components/rich-text/index.js b/packages/editor/src/components/rich-text/index.js index 9b5945ff39df1..45158696b7a3d 100644 --- a/packages/editor/src/components/rich-text/index.js +++ b/packages/editor/src/components/rich-text/index.js @@ -853,11 +853,11 @@ export class RichText extends Component { } ).body.innerHTML; } - valueToFormat( { formats, text } ) { + valueToFormat( value ) { // Handle deprecated `children` and `node` sources. if ( this.usedDeprecatedChildrenSource ) { return children.fromDOM( unstableToDom( { - value: { formats, text }, + value, multilineTag: this.multilineTag, multilineWrapperTags: this.multilineWrapperTags, } ).body.childNodes ); @@ -865,13 +865,13 @@ export class RichText extends Component { if ( this.props.format === 'string' ) { return toHTMLString( { - value: { formats, text }, + value, multilineTag: this.multilineTag, multilineWrapperTags: this.multilineWrapperTags, } ); } - return { formats, text }; + return value; } render() { diff --git a/test/e2e/specs/__snapshots__/plugins-api.test.js.snap b/test/e2e/specs/__snapshots__/plugins-api.test.js.snap index 0b62fc522bf7e..5916648d1dd2b 100644 --- a/test/e2e/specs/__snapshots__/plugins-api.test.js.snap +++ b/test/e2e/specs/__snapshots__/plugins-api.test.js.snap @@ -1,3 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Using Plugins API Sidebar Should open plugins sidebar using More Menu item and render content 1`] = `"<div class=\\"components-panel__header edit-post-sidebar-header__small\\"><span class=\\"edit-post-sidebar-header__title\\">(no title)</span><button type=\\"button\\" aria-label=\\"Close plugin\\" class=\\"components-button components-icon-button\\"><svg aria-hidden=\\"true\\" role=\\"img\\" focusable=\\"false\\" class=\\"dashicon dashicons-no-alt\\" xmlns=\\"http://www.w3.org/2000/svg\\" width=\\"20\\" height=\\"20\\" viewBox=\\"0 0 20 20\\"><path d=\\"M14.95 6.46L11.41 10l3.54 3.54-1.41 1.41L10 11.42l-3.53 3.53-1.42-1.42L8.58 10 5.05 6.47l1.42-1.42L10 8.58l3.54-3.53z\\"></path></svg></button></div><div class=\\"components-panel__header edit-post-sidebar-header\\"><strong>Sidebar title plugin</strong><button type=\\"button\\" aria-label=\\"Unpin from toolbar\\" aria-expanded=\\"true\\" class=\\"components-button components-icon-button is-toggled\\"><svg aria-hidden=\\"true\\" role=\\"img\\" focusable=\\"false\\" class=\\"dashicon dashicons-star-filled\\" xmlns=\\"http://www.w3.org/2000/svg\\" width=\\"20\\" height=\\"20\\" viewBox=\\"0 0 20 20\\"><path d=\\"M10 1l3 6 6 .75-4.12 4.62L16 19l-6-3-6 3 1.13-6.63L1 7.75 7 7z\\"></path></svg></button><button type=\\"button\\" aria-label=\\"Close plugin\\" class=\\"components-button components-icon-button\\"><svg aria-hidden=\\"true\\" role=\\"img\\" focusable=\\"false\\" class=\\"dashicon dashicons-no-alt\\" xmlns=\\"http://www.w3.org/2000/svg\\" width=\\"20\\" height=\\"20\\" viewBox=\\"0 0 20 20\\"><path d=\\"M14.95 6.46L11.41 10l3.54 3.54-1.41 1.41L10 11.42l-3.53 3.53-1.42-1.42L8.58 10 5.05 6.47l1.42-1.42L10 8.58l3.54-3.53z\\"></path></svg></button></div><div class=\\"components-panel\\"><div class=\\"components-panel__body is-opened\\"><div class=\\"components-panel__row\\"><label for=\\"title-plain-text\\">Title:</label><textarea class=\\"editor-plain-text\\" id=\\"title-plain-text\\" placeholder=\\"(no title)\\" rows=\\"1\\" style=\\"overflow: hidden; overflow-wrap: break-word; resize: none; height: 18px;\\"></textarea></div><div class=\\"components-panel__row\\"><button type=\\"button\\" class=\\"components-button is-button is-primary\\">Reset</button></div></div></div>"`; +exports[`Using Plugins API Sidebar Should open plugins sidebar using More Menu item and render content 1`] = `"<div class=\\"components-panel__header edit-post-sidebar-header__small\\"><span class=\\"edit-post-sidebar-header__title\\">(no title)</span><button type=\\"button\\" aria-label=\\"Close plugin\\" class=\\"components-button components-icon-button\\"><svg aria-hidden=\\"true\\" role=\\"img\\" focusable=\\"false\\" class=\\"dashicon dashicons-no-alt\\" xmlns=\\"http://www.w3.org/2000/svg\\" width=\\"20\\" height=\\"20\\" viewBox=\\"0 0 20 20\\"><path d=\\"M14.95 6.46L11.41 10l3.54 3.54-1.41 1.41L10 11.42l-3.53 3.53-1.42-1.42L8.58 10 5.05 6.47l1.42-1.42L10 8.58l3.54-3.53z\\"></path></svg></button></div><div class=\\"components-panel__header edit-post-sidebar-header\\"><strong>Sidebar title plugin</strong><button type=\\"button\\" aria-label=\\"Unpin from toolbar\\" aria-expanded=\\"true\\" class=\\"components-button components-icon-button is-toggled\\"><svg aria-hidden=\\"true\\" role=\\"img\\" focusable=\\"false\\" class=\\"dashicon dashicons-star-filled\\" xmlns=\\"http://www.w3.org/2000/svg\\" width=\\"20\\" height=\\"20\\" viewBox=\\"0 0 20 20\\"><path d=\\"M10 1l3 6 6 .75-4.12 4.62L16 19l-6-3-6 3 1.13-6.63L1 7.75 7 7z\\"></path></svg></button><button type=\\"button\\" aria-label=\\"Close plugin\\" class=\\"components-button components-icon-button\\"><svg aria-hidden=\\"true\\" role=\\"img\\" focusable=\\"false\\" class=\\"dashicon dashicons-no-alt\\" xmlns=\\"http://www.w3.org/2000/svg\\" width=\\"20\\" height=\\"20\\" viewBox=\\"0 0 20 20\\"><path d=\\"M14.95 6.46L11.41 10l3.54 3.54-1.41 1.41L10 11.42l-3.53 3.53-1.42-1.42L8.58 10 5.05 6.47l1.42-1.42L10 8.58l3.54-3.53z\\"></path></svg></button></div><div class=\\"components-panel\\"><div class=\\"components-panel__body is-opened\\"><div class=\\"components-panel__row\\"><label for=\\"title-plain-text\\">Title:</label><textarea class=\\"editor-plain-text\\" id=\\"title-plain-text\\" placeholder=\\"(no title)\\" rows=\\"1\\" style=\\"overflow: hidden; overflow-wrap: break-word; resize: none; height: 18px;\\"></textarea></div><div class=\\"components-panel__row\\"><button type=\\"button\\" class=\\"components-button is-button is-primary\\">Reset</button></div><button type=\\"button\\" class=\\"components-button is-button is-primary\\">Add annotation</button></div></div>"`; diff --git a/test/e2e/specs/plugins-api.test.js b/test/e2e/specs/plugins-api.test.js index d149cf492a5a3..bc841eb9e52f6 100644 --- a/test/e2e/specs/plugins-api.test.js +++ b/test/e2e/specs/plugins-api.test.js @@ -11,6 +11,14 @@ import { } from '../support/utils'; import { activatePlugin, deactivatePlugin } from '../support/plugins'; +const clickOnBlockSettingsMenuItem = async ( buttonLabel ) => { + await expect( page ).toClick( '.editor-block-settings-menu__toggle' ); + const itemButton = ( await page.$x( `//*[contains(@class, "editor-block-settings-menu__popover")]//button[contains(text(), '${ buttonLabel }')]` ) )[ 0 ]; + await itemButton.click(); +}; + +const ANNOTATIONS_SELECTOR = '.annotation-text-e2e-tests'; + describe( 'Using Plugins API', () => { beforeAll( async () => { await activatePlugin( 'gutenberg-test-plugin-plugins-api' ); @@ -75,4 +83,36 @@ describe( 'Using Plugins API', () => { expect( pluginSidebarClosed ).toBeNull(); } ); } ); + + describe( 'Annotations', () => { + it( 'Allows a block to be annotated', async () => { + await page.keyboard.type( 'Title' + '\n' + 'Paragraph to annotate' ); + await clickOnMoreMenuItem( 'Sidebar title plugin' ); + + let annotations = await page.$$( ANNOTATIONS_SELECTOR ); + expect( annotations ).toHaveLength( 0 ); + + // Click add annotation button. + const addAnnotationButton = ( await page.$x( "//button[contains(text(), 'Add annotation')]" ) )[ 0 ]; + await addAnnotationButton.click(); + + annotations = await page.$$( ANNOTATIONS_SELECTOR ); + expect( annotations ).toHaveLength( 1 ); + + const annotation = annotations[ 0 ]; + + const text = await page.evaluate( ( el ) => el.innerText, annotation ); + expect( text ).toBe( ' to ' ); + + await clickOnBlockSettingsMenuItem( 'Edit as HTML' ); + + const htmlContent = await page.$$( '.editor-block-list__block-html-textarea' ); + const html = await page.evaluate( ( el ) => { + return el.innerHTML; + }, htmlContent[ 0 ] ); + + // There should be no <mark> tags in the raw content. + expect( html ).toBe( '&lt;p&gt;Paragraph to annotate&lt;/p&gt;' ); + } ); + } ); } ); diff --git a/test/e2e/test-plugins/plugins-api.php b/test/e2e/test-plugins/plugins-api.php index fcd9fb04b6a2f..d219eab684f95 100644 --- a/test/e2e/test-plugins/plugins-api.php +++ b/test/e2e/test-plugins/plugins-api.php @@ -45,6 +45,7 @@ 'wp-element', 'wp-i18n', 'wp-plugins', + 'wp-annotations', ), filemtime( plugin_dir_path( __FILE__ ) . 'plugins-api/sidebar.js' ), true diff --git a/test/e2e/test-plugins/plugins-api/sidebar.js b/test/e2e/test-plugins/plugins-api/sidebar.js index 10112e3770155..c97d29c754f23 100644 --- a/test/e2e/test-plugins/plugins-api/sidebar.js +++ b/test/e2e/test-plugins/plugins-api/sidebar.js @@ -5,6 +5,8 @@ var compose = wp.compose.compose; var withDispatch = wp.data.withDispatch; var withSelect = wp.data.withSelect; + var select = wp.data.select; + var dispatch = wp.data.dispatch; var PlainText = wp.editor.PlainText; var Fragment = wp.element.Fragment; var el = wp.element.createElement; @@ -48,6 +50,24 @@ }, __( 'Reset' ) ) + ), + el( + Button, + { + isPrimary: true, + onClick: () => { + dispatch( 'core/annotations' ).__experimentalAddAnnotation( { + source: 'e2e-tests', + blockClientId: select( 'core/editor' ).getBlockOrder()[ 0 ], + richTextIdentifier: 'content', + range: { + start: 9, + end: 13, + }, + } ); + }, + }, + __( 'Add annotation' ) ) ); } diff --git a/webpack.config.js b/webpack.config.js index dedeac0858017..afd5c17c28c93 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -35,6 +35,7 @@ function camelCaseDash( string ) { const gutenbergPackages = [ 'a11y', + 'annotations', 'api-fetch', 'autop', 'blob', From a2fee3318702c3e14635e287110fac3818d5435f Mon Sep 17 00:00:00 2001 From: Jorge <jorge.costa@developer.pt> Date: Fri, 9 Nov 2018 17:56:55 +0000 Subject: [PATCH 078/106] Fix: Blocks: Alt+F10 should navigate to block toolbar regardless of visibility (#11607) ## Description Closes: https://github.com/WordPress/gutenberg/issues/10907 When pressing alt + f10 if the contextual toolbar was not visible because the user was typing, we did not focus the block contextual toolbar, and we focused the header instead. This PR addresses this problem. To solve the problem, the PR performs the following actions: Adds a prop to NavigableToolbar that enables it to focus when mounting. Adds an event handler for pressing alt+f10 key combination on BlockListBlock and state flag the indicates that the handling is in progress. ## How has this been tested? I checked the unified toolbar mode was not enabled. I added a paragraph, I wrote something until the toolbar disappears I pressed alt+f10, and I verified the block toolbar appeared with the first item focused. I repeated the test with other blocks, e.g., writing on the image caption. --- packages/editor/CHANGELOG.md | 4 ++ .../block-list/block-contextual-toolbar.js | 3 +- .../editor/src/components/block-list/block.js | 40 ++++++++++++++++++- .../src/components/navigable-toolbar/index.js | 6 +++ test/e2e/specs/navigable-toolbar.test.js | 7 ---- 5 files changed, 50 insertions(+), 10 deletions(-) diff --git a/packages/editor/CHANGELOG.md b/packages/editor/CHANGELOG.md index 0e88d46040f13..5c281509b8cfe 100644 --- a/packages/editor/CHANGELOG.md +++ b/packages/editor/CHANGELOG.md @@ -6,6 +6,10 @@ ## 6.2.1 (2018-11-09) +### New Features + +- In `NavigableToolbar`, a property focusOnMount was added, if true, the toolbar will get focus as soon as it mounted. Defaults to false. + ### Polish - Reactive block styles. diff --git a/packages/editor/src/components/block-list/block-contextual-toolbar.js b/packages/editor/src/components/block-list/block-contextual-toolbar.js index 2b054851fb34e..7efe23aaea989 100644 --- a/packages/editor/src/components/block-list/block-contextual-toolbar.js +++ b/packages/editor/src/components/block-list/block-contextual-toolbar.js @@ -9,9 +9,10 @@ import { __ } from '@wordpress/i18n'; import NavigableToolbar from '../navigable-toolbar'; import { BlockToolbar } from '../'; -function BlockContextualToolbar() { +function BlockContextualToolbar( { focusOnMount } ) { return ( <NavigableToolbar + focusOnMount={ focusOnMount } className="editor-block-contextual-toolbar" aria-label={ __( 'Block Toolbar' ) } > diff --git a/packages/editor/src/components/block-list/block.js b/packages/editor/src/components/block-list/block.js index 15c19c28d186a..614fbe9f762f0 100644 --- a/packages/editor/src/components/block-list/block.js +++ b/packages/editor/src/components/block-list/block.js @@ -22,7 +22,7 @@ import { isUnmodifiedDefaultBlock, getUnregisteredTypeHandlerName, } from '@wordpress/blocks'; -import { withFilters } from '@wordpress/components'; +import { KeyboardShortcuts, withFilters } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; import { withDispatch, withSelect } from '@wordpress/data'; import { withViewportMatch } from '@wordpress/viewport'; @@ -57,6 +57,7 @@ export class BlockListBlock extends Component { this.bindBlockNode = this.bindBlockNode.bind( this ); this.setAttributes = this.setAttributes.bind( this ); this.maybeHover = this.maybeHover.bind( this ); + this.forceFocusedContextualToolbar = this.forceFocusedContextualToolbar.bind( this ); this.hideHoverEffects = this.hideHoverEffects.bind( this ); this.mergeBlocks = this.mergeBlocks.bind( this ); this.insertBlocksAfter = this.insertBlocksAfter.bind( this ); @@ -78,6 +79,7 @@ export class BlockListBlock extends Component { dragging: false, isHovered: false, }; + this.isForcingContextualToolbar = false; } componentDidMount() { @@ -87,6 +89,11 @@ export class BlockListBlock extends Component { } componentDidUpdate( prevProps ) { + if ( this.isForcingContextualToolbar ) { + // The forcing of contextual toolbar should only be true during one update, + // after the first update normal conditions should apply. + this.isForcingContextualToolbar = false; + } if ( this.props.isTypingWithinBlock || this.props.isSelected ) { this.hideHoverEffects(); } @@ -372,6 +379,12 @@ export class BlockListBlock extends Component { } } + forceFocusedContextualToolbar() { + this.isForcingContextualToolbar = true; + // trigger a re-render + this.setState( () => ( {} ) ); + } + render() { return ( <HoverArea container={ this.wrapperNode }> @@ -541,7 +554,30 @@ export class BlockListBlock extends Component { isHidden={ ! ( isHovered || isSelected ) || hoverArea !== 'left' } /> ) } - { shouldShowContextualToolbar && <BlockContextualToolbar /> } + { ( + shouldShowContextualToolbar || + this.isForcingContextualToolbar + ) && ( + <BlockContextualToolbar + // If the toolbar is being shown because of being forced + // it should focus the toolbar right after the mount. + focusOnMount={ this.isForcingContextualToolbar } + /> + ) } + { ( + ! shouldShowContextualToolbar && + isSelected && + ! hasFixedToolbar && + ! isEmptyDefaultBlock + ) && ( + <KeyboardShortcuts + bindGlobal + eventName="keydown" + shortcuts={ { + 'alt+f10': this.forceFocusedContextualToolbar, + } } + /> + ) } <IgnoreNestedEvents ref={ this.bindBlockNode } onDragStart={ this.preventDrag } diff --git a/packages/editor/src/components/navigable-toolbar/index.js b/packages/editor/src/components/navigable-toolbar/index.js index 803b9c1b4715d..9183e7b7d1978 100644 --- a/packages/editor/src/components/navigable-toolbar/index.js +++ b/packages/editor/src/components/navigable-toolbar/index.js @@ -61,6 +61,12 @@ class NavigableToolbar extends Component { } } + componentDidMount() { + if ( this.props.focusOnMount ) { + this.focusToolbar(); + } + } + render() { const { children, ...props } = this.props; return ( diff --git a/test/e2e/specs/navigable-toolbar.test.js b/test/e2e/specs/navigable-toolbar.test.js index 1bf4089a270fb..aa781f0fa5735 100644 --- a/test/e2e/specs/navigable-toolbar.test.js +++ b/test/e2e/specs/navigable-toolbar.test.js @@ -43,13 +43,6 @@ describe( 'block toolbar', () => { // until starting to type within it. await page.keyboard.type( 'Example' ); - // [TEMPORARY]: With non-unified toolbar, the toolbar will not - // be visible since the user has entered a "typing" mode. - // Future iterations should ensure Alt+F10 works in a block - // to focus the toolbar regardless of whether it is presently - // visible. - await page.keyboard.press( 'Escape' ); - // Upward await pressWithModifier( 'Alt', 'F10' ); expect( await isInBlockToolbar() ).toBe( true ); From fc034921667ee95e4befe1edd89ae69a4bd646bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s?= <nosolosw@users.noreply.github.com> Date: Fri, 9 Nov 2018 19:00:14 +0100 Subject: [PATCH 079/106] PostPublishPanel: return focus to element that opened the panel (#11543) * Toggle disabled if post is published * Return focus to Publish... button - Do not unmount Header settings components. - Hide the "Save draft" button because it's not hidden by the PostPublishPanel slide-in sidebar. * Sort dependencies * Extract props * Toggle should be disabled if post is published * Add toggleProps to button * Inline text for toggle within the button component * Always render the button component * Use aria-disabled instead of disabled to avoid focus loss * Improve comments * Update snapshots * Add deprecation notice in editor CHANGELOG * Add deprecation notice in plugin * Add deprecation in component * Add DotTip to PostPublishButton * Fix deprecation warning in tests * Update PostPublishButton e2e tests to use aria-disabled instead of disabled att * Update e2e test This was relying on the preview button not being mounted while the publish panel was opened. A better logic is to check whether the actual publish panel is shown. * Tweak comment * Update version * Update packages/editor/src/components/post-publish-panel/toggle.js * Place component in a separate line Co-Authored-By: nosolosw <nosolosw@users.noreply.github.com> * Update version number * Add comment * Update comment * Tweak comment * chore: Move deps to 4.5 --- docs/reference/deprecated.md | 1 + .../edit-post/src/components/header/index.js | 49 +++++++------ .../header/post-publish-button-or-toggle.js | 69 ++++++++++--------- .../header/test/__snapshots__/index.js.snap | 36 ++++++++-- packages/editor/CHANGELOG.md | 4 ++ .../components/post-publish-button/index.js | 65 ++++++++++++----- .../post-publish-button/test/index.js | 30 ++++---- .../components/post-publish-panel/index.js | 2 + .../post-publish-panel/test/toggle.js | 3 + .../components/post-publish-panel/toggle.js | 7 ++ test/e2e/specs/preview.test.js | 6 +- test/e2e/specs/publish-button.test.js | 13 ++-- 12 files changed, 183 insertions(+), 102 deletions(-) diff --git a/docs/reference/deprecated.md b/docs/reference/deprecated.md index e1a54e3498c1d..13efe9f8940bd 100644 --- a/docs/reference/deprecated.md +++ b/docs/reference/deprecated.md @@ -2,6 +2,7 @@ Gutenberg's deprecation policy is intended to support backwards-compatibility fo ## 4.5.0 - `Dropdown.refresh()` has been deprecated as the contained `Popover` is now automatically refreshed. +- `wp.editor.PostPublishPanelToggle` has been deprecated in favor of `wp.editor.PostPublishButton`. ## 4.4.0 diff --git a/packages/edit-post/src/components/header/index.js b/packages/edit-post/src/components/header/index.js index 554ae583210c0..c63a7058eeb7c 100644 --- a/packages/edit-post/src/components/header/index.js +++ b/packages/edit-post/src/components/header/index.js @@ -39,34 +39,39 @@ function Header( { tabIndex="-1" > <HeaderToolbar /> - { ! isPublishSidebarOpened && ( - <div className="edit-post-header__settings"> + <div className="edit-post-header__settings"> + { ! isPublishSidebarOpened && ( + // This button isn't completely hidden by the publish sidebar. + // We can't hide the whole toolbar when the publish sidebar is open because + // we want to prevent mounting/unmounting the PostPublishButtonOrToggle DOM node. + // We track that DOM node to return focus to the PostPublishButtonOrToggle + // when the publish sidebar has been closed. <PostSavedState forceIsDirty={ hasActiveMetaboxes } forceIsSaving={ isSaving } /> - <PostPreviewButton /> - <PostPublishButtonOrToggle - forceIsDirty={ hasActiveMetaboxes } - forceIsSaving={ isSaving } + ) } + <PostPreviewButton /> + <PostPublishButtonOrToggle + forceIsDirty={ hasActiveMetaboxes } + forceIsSaving={ isSaving } + /> + <div> + <IconButton + icon="admin-generic" + label={ __( 'Settings' ) } + onClick={ toggleGeneralSidebar } + isToggled={ isEditorSidebarOpened } + aria-expanded={ isEditorSidebarOpened } + shortcut={ shortcuts.toggleSidebar } /> - <div> - <IconButton - icon="admin-generic" - label={ __( 'Settings' ) } - onClick={ toggleGeneralSidebar } - isToggled={ isEditorSidebarOpened } - aria-expanded={ isEditorSidebarOpened } - shortcut={ shortcuts.toggleSidebar } - /> - <DotTip tipId="core/editor.settings"> - { __( 'You’ll find more settings for your page and blocks in the sidebar. Click “Settings” to open it.' ) } - </DotTip> - </div> - <PinnedPlugins.Slot /> - <MoreMenu /> + <DotTip tipId="core/editor.settings"> + { __( 'You’ll find more settings for your page and blocks in the sidebar. Click “Settings” to open it.' ) } + </DotTip> </div> - ) } + <PinnedPlugins.Slot /> + <MoreMenu /> + </div> </div> ); } diff --git a/packages/edit-post/src/components/header/post-publish-button-or-toggle.js b/packages/edit-post/src/components/header/post-publish-button-or-toggle.js index efa0621f72a99..edf6699fc9eaf 100644 --- a/packages/edit-post/src/components/header/post-publish-button-or-toggle.js +++ b/packages/edit-post/src/components/header/post-publish-button-or-toggle.js @@ -6,9 +6,9 @@ import { get } from 'lodash'; /** * WordPress dependencies. */ -import { PostPublishPanelToggle, PostPublishButton } from '@wordpress/editor'; import { compose } from '@wordpress/compose'; import { withDispatch, withSelect } from '@wordpress/data'; +import { PostPublishButton } from '@wordpress/editor'; import { withViewportMatch } from '@wordpress/viewport'; export function PostPublishButtonOrToggle( { @@ -24,54 +24,55 @@ export function PostPublishButtonOrToggle( { isScheduled, togglePublishSidebar, } ) { - const button = ( - <PostPublishButton - forceIsDirty={ forceIsDirty } - forceIsSaving={ forceIsSaving } - /> - ); - const toggle = ( - <PostPublishPanelToggle - isOpen={ isPublishSidebarOpened } - onToggle={ togglePublishSidebar } - forceIsSaving={ forceIsSaving } - forceIsDirty={ forceIsDirty } - /> - ); + const IS_TOGGLE = 'toggle'; + const IS_BUTTON = 'button'; + let component; /** - * We want to show a BUTTON when the post status is at the _final stage_ + * Conditions to show a BUTTON (publish directly) or a TOGGLE (open publish sidebar): + * + * 1) We want to show a BUTTON when the post status is at the _final stage_ * for a particular role (see https://codex.wordpress.org/Post_Status): * * - is published * - is scheduled to be published - * - is pending and can't be published (but only for viewports >= medium) + * - is pending and can't be published (but only for viewports >= medium). + * Originally, we considered showing a button for pending posts that couldn't be published + * (for example, for an author with the contributor role). Some languages can have + * long translations for "Submit for review", so given the lack of UI real estate available + * we decided to take into account the viewport in that case. + * See: https://github.com/WordPress/gutenberg/issues/10475 + * + * 2) Then, in small viewports, we'll show a TOGGLE. * - * Originally we considered showing a button for pending posts - * that couldn't be published (for ex, for a contributor role). - * Some languages can have really long translations for "Submit for review", - * so given the lack of UI real state we decided to take into account the viewport - * in that particular case. + * 3) Finally, we'll use the publish sidebar status to decide: + * + * - if it is enabled, we show a TOGGLE + * - if it is disabled, we show a BUTTON */ if ( isPublished || ( isScheduled && isBeingScheduled ) || ( isPending && ! hasPublishAction && ! isLessThanMediumViewport ) ) { - return button; + component = IS_BUTTON; + } else if ( isLessThanMediumViewport ) { + component = IS_TOGGLE; + } else if ( isPublishSidebarEnabled ) { + component = IS_TOGGLE; + } else { + component = IS_BUTTON; } - /** - * Then, we take other things into account: - * - * - Show TOGGLE if it is small viewport. - * - Otherwise, use publish sidebar status to decide - TOGGLE if enabled, BUTTON if not. - */ - if ( isLessThanMediumViewport ) { - return toggle; - } - - return isPublishSidebarEnabled ? toggle : button; + return ( + <PostPublishButton + forceIsDirty={ forceIsDirty } + forceIsSaving={ forceIsSaving } + isOpen={ isPublishSidebarOpened } + isToggle={ component === IS_TOGGLE } + onToggle={ togglePublishSidebar } + /> + ); } export default compose( diff --git a/packages/edit-post/src/components/header/test/__snapshots__/index.js.snap b/packages/edit-post/src/components/header/test/__snapshots__/index.js.snap index 59d931e6ef389..95ed8d68d7ded 100644 --- a/packages/edit-post/src/components/header/test/__snapshots__/index.js.snap +++ b/packages/edit-post/src/components/header/test/__snapshots__/index.js.snap @@ -1,13 +1,37 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`PostPublishButtonOrToggle should render a button when post is not (1), (2), (3), the viewport is >= medium, and the publish sidebar is disabled 1`] = `<WithSelect(WithDispatch(PostPublishButton)) />`; +exports[`PostPublishButtonOrToggle should render a button when post is not (1), (2), (3), the viewport is >= medium, and the publish sidebar is disabled 1`] = ` +<WithSelect(WithDispatch(PostPublishButton)) + isToggle={false} +/> +`; -exports[`PostPublishButtonOrToggle should render a button when the post is pending and cannot be published but the viewport is >= medium (3) 1`] = `<WithSelect(WithDispatch(PostPublishButton)) />`; +exports[`PostPublishButtonOrToggle should render a button when the post is pending and cannot be published but the viewport is >= medium (3) 1`] = ` +<WithSelect(WithDispatch(PostPublishButton)) + isToggle={false} +/> +`; -exports[`PostPublishButtonOrToggle should render a button when the post is published (1) 1`] = `<WithSelect(WithDispatch(PostPublishButton)) />`; +exports[`PostPublishButtonOrToggle should render a button when the post is published (1) 1`] = ` +<WithSelect(WithDispatch(PostPublishButton)) + isToggle={false} +/> +`; -exports[`PostPublishButtonOrToggle should render a button when the post is scheduled (2) 1`] = `<WithSelect(WithDispatch(PostPublishButton)) />`; +exports[`PostPublishButtonOrToggle should render a button when the post is scheduled (2) 1`] = ` +<WithSelect(WithDispatch(PostPublishButton)) + isToggle={false} +/> +`; -exports[`PostPublishButtonOrToggle should render a toggle when post is not (1), (2), (3), the viewport is >= medium, and the publish sidebar is enabled 1`] = `<WithSelect(PostPublishPanelToggle) />`; +exports[`PostPublishButtonOrToggle should render a toggle when post is not (1), (2), (3), the viewport is >= medium, and the publish sidebar is enabled 1`] = ` +<WithSelect(WithDispatch(PostPublishButton)) + isToggle={true} +/> +`; -exports[`PostPublishButtonOrToggle should render a toggle when post is not published or scheduled and the viewport is < medium 1`] = `<WithSelect(PostPublishPanelToggle) />`; +exports[`PostPublishButtonOrToggle should render a toggle when post is not published or scheduled and the viewport is < medium 1`] = ` +<WithSelect(WithDispatch(PostPublishButton)) + isToggle={true} +/> +`; diff --git a/packages/editor/CHANGELOG.md b/packages/editor/CHANGELOG.md index 5c281509b8cfe..0a5c92529c16b 100644 --- a/packages/editor/CHANGELOG.md +++ b/packages/editor/CHANGELOG.md @@ -10,6 +10,10 @@ - In `NavigableToolbar`, a property focusOnMount was added, if true, the toolbar will get focus as soon as it mounted. Defaults to false. +### Deprecations + +- `wp.editor.PostPublishPanelToggle` has been deprecated in favor of `wp.editor.PostPublishButton`. + ### Polish - Reactive block styles. diff --git a/packages/editor/src/components/post-publish-button/index.js b/packages/editor/src/components/post-publish-button/index.js index 69233c86931d8..cfed6dbd112f5 100644 --- a/packages/editor/src/components/post-publish-button/index.js +++ b/packages/editor/src/components/post-publish-button/index.js @@ -10,6 +10,8 @@ import { Button } from '@wordpress/components'; import { Component, createRef } from '@wordpress/element'; import { withSelect, withDispatch } from '@wordpress/data'; import { compose } from '@wordpress/compose'; +import { __ } from '@wordpress/i18n'; +import { DotTip } from '@wordpress/nux'; /** * Internal dependencies @@ -28,19 +30,22 @@ export class PostPublishButton extends Component { render() { const { - isSaving, - onStatusChange, - onSave, + forceIsDirty, + forceIsSaving, + hasPublishAction, isBeingScheduled, - visibility, - isPublishable, - isSaveable, + isOpen, isPostSavingLocked, + isPublishable, isPublished, - hasPublishAction, + isSaveable, + isSaving, + isToggle, + onSave, + onStatusChange, onSubmit = noop, - forceIsDirty, - forceIsSaving, + onToggle, + visibility, } = this.props; const isButtonDisabled = isSaving || @@ -49,6 +54,13 @@ export class PostPublishButton extends Component { isPostSavingLocked || ( ! isPublishable && ! forceIsDirty ); + const isToggleDisabled = + isPublished || + isSaving || + forceIsSaving || + ! isSaveable || + ( ! isPublishable && ! forceIsDirty ); + let publishStatus; if ( ! hasPublishAction ) { publishStatus = 'pending'; @@ -66,17 +78,38 @@ export class PostPublishButton extends Component { onSave(); }; + const buttonProps = { + 'aria-disabled': isButtonDisabled, + className: 'editor-post-publish-button', + isBusy: isSaving && isPublished, + isLarge: true, + isPrimary: true, + onClick, + }; + + const toggleProps = { + 'aria-disabled': isToggleDisabled, + 'aria-expanded': isOpen, + className: 'editor-post-publish-panel__toggle', + isBusy: isSaving && isPublished, + isPrimary: true, + onClick: onToggle, + }; + + const toggleChildren = isBeingScheduled ? __( 'Schedule…' ) : __( 'Publish…' ); + const buttonChildren = <PublishButtonLabel forceIsSaving={ forceIsSaving } />; + + const componentProps = isToggle ? toggleProps : buttonProps; + const componentChildren = isToggle ? toggleChildren : buttonChildren; return ( <Button ref={ this.buttonNode } - className="editor-post-publish-button" - isPrimary - isLarge - onClick={ onClick } - disabled={ isButtonDisabled } - isBusy={ isSaving && isPublished } + { ...componentProps } > - <PublishButtonLabel forceIsSaving={ forceIsSaving } /> + { componentChildren } + <DotTip tipId="core/editor.publish"> + { __( 'Finished writing? That’s great, let’s get this published right now. Just click “Publish” and you’re good to go.' ) } + </DotTip> </Button> ); } diff --git a/packages/editor/src/components/post-publish-button/test/index.js b/packages/editor/src/components/post-publish-button/test/index.js index b48d60f836671..8fbef6007144a 100644 --- a/packages/editor/src/components/post-publish-button/test/index.js +++ b/packages/editor/src/components/post-publish-button/test/index.js @@ -11,8 +11,8 @@ import { PostPublishButton } from '../'; jest.mock( '../../../../../components/src/button' ); describe( 'PostPublishButton', () => { - describe( 'disabled', () => { - it( 'should be disabled if post is currently saving', () => { + describe( 'aria-disabled', () => { + it( 'should be true if post is currently saving', () => { const wrapper = shallow( <PostPublishButton isPublishable @@ -21,10 +21,10 @@ describe( 'PostPublishButton', () => { /> ); - expect( wrapper.prop( 'disabled' ) ).toBe( true ); + expect( wrapper.prop( 'aria-disabled' ) ).toBe( true ); } ); - it( 'should be disabled if forceIsSaving is true', () => { + it( 'should be true if forceIsSaving is true', () => { const wrapper = shallow( <PostPublishButton isPublishable @@ -33,10 +33,10 @@ describe( 'PostPublishButton', () => { /> ); - expect( wrapper.prop( 'disabled' ) ).toBe( true ); + expect( wrapper.prop( 'aria-disabled' ) ).toBe( true ); } ); - it( 'should be disabled if post is not publishable and not forceIsDirty', () => { + it( 'should be true if post is not publishable and not forceIsDirty', () => { const wrapper = shallow( <PostPublishButton isSaveable @@ -45,10 +45,10 @@ describe( 'PostPublishButton', () => { /> ); - expect( wrapper.prop( 'disabled' ) ).toBe( true ); + expect( wrapper.prop( 'aria-disabled' ) ).toBe( true ); } ); - it( 'should be disabled if post is not saveable', () => { + it( 'should be true if post is not saveable', () => { const wrapper = shallow( <PostPublishButton isPublishable @@ -56,10 +56,10 @@ describe( 'PostPublishButton', () => { /> ); - expect( wrapper.prop( 'disabled' ) ).toBe( true ); + expect( wrapper.prop( 'aria-disabled' ) ).toBe( true ); } ); - it( 'should be disabled if post saving is locked', () => { + it( 'should be true if post saving is locked', () => { const wrapper = shallow( <PostPublishButton isPublishable @@ -68,10 +68,10 @@ describe( 'PostPublishButton', () => { /> ); - expect( wrapper.prop( 'disabled' ) ).toBe( true ); + expect( wrapper.prop( 'aria-disabled' ) ).toBe( true ); } ); - it( 'should be enabled if post is saveable but not publishable and forceIsDirty is true', () => { + it( 'should be false if post is saveable but not publishable and forceIsDirty is true', () => { const wrapper = shallow( <PostPublishButton isSaveable @@ -80,10 +80,10 @@ describe( 'PostPublishButton', () => { /> ); - expect( wrapper.prop( 'disabled' ) ).toBe( false ); + expect( wrapper.prop( 'aria-disabled' ) ).toBe( false ); } ); - it( 'should be enabled if post is publishave and saveable', () => { + it( 'should be false if post is publishave and saveable', () => { const wrapper = shallow( <PostPublishButton isPublishable @@ -91,7 +91,7 @@ describe( 'PostPublishButton', () => { /> ); - expect( wrapper.prop( 'disabled' ) ).toBe( false ); + expect( wrapper.prop( 'aria-disabled' ) ).toBe( false ); } ); } ); diff --git a/packages/editor/src/components/post-publish-panel/index.js b/packages/editor/src/components/post-publish-panel/index.js index e6d324624fab0..0b38b0dd78400 100644 --- a/packages/editor/src/components/post-publish-panel/index.js +++ b/packages/editor/src/components/post-publish-panel/index.js @@ -12,6 +12,7 @@ import { IconButton, Spinner, CheckboxControl, + withFocusReturn, withConstrainedTabbing, } from '@wordpress/components'; import { withSelect, withDispatch } from '@wordpress/data'; @@ -142,5 +143,6 @@ export default compose( [ }, }; } ), + withFocusReturn, withConstrainedTabbing, ] )( PostPublishPanel ); diff --git a/packages/editor/src/components/post-publish-panel/test/toggle.js b/packages/editor/src/components/post-publish-panel/test/toggle.js index 81126aa76a14d..1f1a4971dbd35 100644 --- a/packages/editor/src/components/post-publish-panel/test/toggle.js +++ b/packages/editor/src/components/post-publish-panel/test/toggle.js @@ -20,6 +20,9 @@ describe( 'PostPublishPanelToggle', () => { ); expect( wrapper.prop( 'disabled' ) ).toBe( true ); + expect( console ).toHaveWarnedWith( + 'PostPublishPanelToggle is deprecated and will be removed from Gutenberg in 4.5. Please use PostPublishButton instead.' + ); } ); it( 'should be disabled if post is currently force saving', () => { diff --git a/packages/editor/src/components/post-publish-panel/toggle.js b/packages/editor/src/components/post-publish-panel/toggle.js index a617354392031..0cf8b56019cb4 100644 --- a/packages/editor/src/components/post-publish-panel/toggle.js +++ b/packages/editor/src/components/post-publish-panel/toggle.js @@ -3,6 +3,7 @@ */ import { Button } from '@wordpress/components'; import { compose } from '@wordpress/compose'; +import deprecated from '@wordpress/deprecated'; import { __ } from '@wordpress/i18n'; import { withSelect } from '@wordpress/data'; import { DotTip } from '@wordpress/nux'; @@ -25,6 +26,12 @@ export function PostPublishPanelToggle( { ! isSaveable || ( ! isPublishable && ! forceIsDirty ); + deprecated( 'PostPublishPanelToggle', { + version: '4.5', + alternative: 'PostPublishButton', + plugin: 'Gutenberg', + } ); + return ( <Button className="editor-post-publish-panel__toggle" diff --git a/test/e2e/specs/preview.test.js b/test/e2e/specs/preview.test.js index 29f2f4c86998a..f18eaa251e10f 100644 --- a/test/e2e/specs/preview.test.js +++ b/test/e2e/specs/preview.test.js @@ -94,12 +94,12 @@ describe( 'Preview', () => { previewTitle = await previewPage.$eval( '.entry-title', ( node ) => node.textContent ); expect( previewTitle ).toBe( 'Hello World!' ); - // Preview for published post (no unsaved changes) directs to canonical - // URL for post. + // Preview for published post (no unsaved changes) directs to canonical URL for post. await editorPage.bringToFront(); await publishPost(); + // Wait until the publish panel is closed await Promise.all( [ - editorPage.waitForFunction( () => ! document.querySelector( '.editor-post-preview' ) ), + editorPage.waitForFunction( () => ! document.querySelector( '.editor-post-publish-panel' ) ), editorPage.click( '.editor-post-publish-panel__header button' ), ] ); expectedPreviewURL = await editorPage.$eval( '.components-notice.is-success a', ( node ) => node.href ); diff --git a/test/e2e/specs/publish-button.test.js b/test/e2e/specs/publish-button.test.js index 92acf24a53740..0db024ce7ac81 100644 --- a/test/e2e/specs/publish-button.test.js +++ b/test/e2e/specs/publish-button.test.js @@ -22,22 +22,23 @@ describe( 'PostPublishButton', () => { } ); it( 'should be disabled when post is not saveable', async () => { - const publishButton = await page.$( '.editor-post-publish-button:disabled' ); - + const publishButton = await page.$( '.editor-post-publish-button[aria-disabled="true"]' ); expect( publishButton ).not.toBeNull(); } ); it( 'should be disabled when post is being saved', async () => { await page.type( '.editor-post-title__input', 'E2E Test Post' ); // Make it saveable - expect( await page.$( '.editor-post-publish-button:disabled' ) ).toBeNull(); + expect( await page.$( '.editor-post-publish-button[aria-disabled="true"]' ) ).toBeNull(); + await page.click( '.editor-post-save-draft' ); - expect( await page.$( '.editor-post-publish-button:disabled' ) ).not.toBeNull(); + expect( await page.$( '.editor-post-publish-button[aria-disabled="true"]' ) ).not.toBeNull(); } ); it( 'should be disabled when metabox is being saved', async () => { await page.type( '.editor-post-title__input', 'E2E Test Post' ); // Make it saveable - expect( await page.$( '.editor-post-publish-button:disabled' ) ).toBeNull(); + expect( await page.$( '.editor-post-publish-button[aria-disabled="true"]' ) ).toBeNull(); + await page.evaluate( () => window.wp.data.dispatch( 'core/edit-post' ).requestMetaBoxUpdates() ); - expect( await page.$( '.editor-post-publish-button:disabled' ) ).not.toBeNull(); + expect( await page.$( '.editor-post-publish-button[aria-disabled="true"]' ) ).not.toBeNull(); } ); } ); From e0305d7c94904c689f42f4aeab666b033cd7d4a6 Mon Sep 17 00:00:00 2001 From: Andrew Duthie <andrew@andrewduthie.com> Date: Fri, 9 Nov 2018 13:01:56 -0500 Subject: [PATCH 080/106] Editor: Capture focus on self in InsertionPoint inserter (#11684) --- .../editor/src/components/block-list/insertion-point.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/editor/src/components/block-list/insertion-point.js b/packages/editor/src/components/block-list/insertion-point.js index e88c128b49637..ba5c98d21924f 100644 --- a/packages/editor/src/components/block-list/insertion-point.js +++ b/packages/editor/src/components/block-list/insertion-point.js @@ -61,6 +61,13 @@ class BlockInsertionPoint extends Component { <div onFocus={ this.onFocusInserter } onBlur={ this.onBlurInserter } + // While ideally it would be enough to capture the + // bubbling focus event from the Inserter, due to the + // characteristics of click focusing of `button`s in + // Firefox and Safari, it is not reliable. + // + // See: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#Clicking_and_focus + tabIndex={ -1 } className={ classnames( 'editor-block-list__insertion-point-inserter', { 'is-visible': isInserterFocused, From 4156bbadbdcb07c2f93d38839220c089d12358e3 Mon Sep 17 00:00:00 2001 From: Andrew Duthie <andrew@andrewduthie.com> Date: Fri, 9 Nov 2018 13:23:39 -0500 Subject: [PATCH 081/106] Editor: Correct insertion point opacity selector (#11685) --- packages/editor/src/components/block-list/style.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor/src/components/block-list/style.scss b/packages/editor/src/components/block-list/style.scss index 203f5dec76119..a1f80ddf8c9ee 100644 --- a/packages/editor/src/components/block-list/style.scss +++ b/packages/editor/src/components/block-list/style.scss @@ -672,7 +672,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-inserter__toggle { + .is-selected > .editor-block-list__insertion-point-inserter { opacity: 0; pointer-events: none; From 4dcf338ba603f47bf6027a764ab0b0d389e81700 Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Fri, 9 Nov 2018 19:38:06 +0100 Subject: [PATCH 082/106] Update the Gutenberg plugin to 4.3 RC1 (#11686) --- 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 37215edb4a89d..7e130a7034bc2 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.2.0 + * Version: 4.3.0-rc.1 * Author: Gutenberg Team * * @package gutenberg diff --git a/package-lock.json b/package-lock.json index 21595b51e232f..9355492ccc340 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "4.2.0", + "version": "4.3.0-rc.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index c13873b929e95..395b975363abf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "4.2.0", + "version": "4.3.0-rc.1", "private": true, "description": "A new WordPress editor experience", "repository": "git+https://github.com/WordPress/gutenberg.git", From 77bd99b9acc7fd633095dc1d31bbd54f590e6a4f Mon Sep 17 00:00:00 2001 From: Andrew Duthie <andrew@andrewduthie.com> Date: Fri, 9 Nov 2018 15:19:55 -0500 Subject: [PATCH 083/106] Framework: Move wp-polyfill-ecmascript override to scripts registration (#11696) --- lib/client-assets.php | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/lib/client-assets.php b/lib/client-assets.php index 298e1cd0f447c..71fb20403897e 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -865,6 +865,24 @@ function gutenberg_register_scripts_and_styles() { $live_reload_url ); } + + // Temporary backward compatibility for `wp-polyfill-ecmascript`, which has + // since been absorbed into `wp-polyfill`. + // + // [TODO][REMOVEME] To be removed in Gutenberg v4.5. + gutenberg_override_script( + 'wp-polyfill-ecmascript', + null, + array( + 'wp-polyfill', + 'wp-deprecated', + ) + ); + wp_script_add_data( + 'wp-polyfill-ecmascript', + 'data', + 'wp.deprecated( "wp-polyfill-ecmascript script handle", { plugin: "Gutenberg", version: "4.5" } );' + ); } add_action( 'wp_enqueue_scripts', 'gutenberg_register_scripts_and_styles', 5 ); add_action( 'admin_enqueue_scripts', 'gutenberg_register_scripts_and_styles', 5 ); @@ -980,20 +998,6 @@ function gutenberg_register_vendor_scripts() { 'wp-polyfill', 'https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/7.0.0/polyfill' . $suffix . '.js' ); - // Ensure backwards compatibility after renaming to wp-polyfill. - gutenberg_override_script( - 'wp-polyfill-ecmascript', - null, - array( - 'wp-polyfill', - 'wp-deprecated', - ) - ); - wp_script_add_data( - 'wp-polyfill-ecmascript', - 'data', - 'wp.deprecated( "wp-polyfill-ecmascript script handle", { plugin: "Gutenberg", version: "4.5" } );' - ); } /** From 29a2b7f403e9d3481527d6854f5942620cc70b4a Mon Sep 17 00:00:00 2001 From: Daniel Bachhuber <daniel@bachhuber.co> Date: Fri, 9 Nov 2018 12:54:29 -0800 Subject: [PATCH 084/106] Fetch all attachments when opening a gallery in the Media Library (#11655) --- packages/edit-post/src/hooks/components/media-upload/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/edit-post/src/hooks/components/media-upload/index.js b/packages/edit-post/src/hooks/components/media-upload/index.js index b2eaa4d86fef5..be68a9e2ce960 100644 --- a/packages/edit-post/src/hooks/components/media-upload/index.js +++ b/packages/edit-post/src/hooks/components/media-upload/index.js @@ -67,7 +67,7 @@ const getAttachmentsCollection = ( ids ) => { order: 'ASC', orderby: 'post__in', post__in: ids, - per_page: 100, + posts_per_page: -1, query: true, type: 'image', } ); From 19881788c95b4ffae44c3ad51d202761616112a3 Mon Sep 17 00:00:00 2001 From: Maedah Batool <MaedahBatool@users.noreply.github.com> Date: Sat, 10 Nov 2018 02:00:32 +0500 Subject: [PATCH 085/106] =?UTF-8?q?=F0=9F=93=A6=20NEW:=20Add=20my=20name?= =?UTF-8?q?=20to=20CONTRIBUTORS.md=20file=20(#11675)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I made a PR which got accepted. 🎉 Here is the link → `https://github.com/WordPress/gutenberg/pull/11656#issuecomment-437374775` Adding my name to CONTRIBUTORS.md file. ✌️ --- CONTRIBUTORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index e3dd47a328047..83d57337774fc 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -115,3 +115,4 @@ This list is manually curated to include valuable contributions by volunteers th | @LukePettway | @luke_pettway | | @pratikthink | @pratikthink | | @amdrew | @sumobi | +| @MaedahBatool | @MaedahBatool | From 1040ea457eed097e1ea96f696124dd8db237c526 Mon Sep 17 00:00:00 2001 From: Dennis Snell <dennis.snell@automattic.com> Date: Fri, 9 Nov 2018 17:49:47 -0600 Subject: [PATCH 086/106] RTL: Set code editor as RTL (#10973) The code editor is an odd beast when it comes to text direction. There's a confusing interplay of stored bytes, styling, and internal directionality in the display of text. If we set `dir="rtl"` on the code editor, which happens when chaining down from the same attribute on the `<html>` tag, then the block comments display incorrectly. The browser is smart enough to handle text and HTML tags/attributes but gets confused in the comments. With this change we force the browser to treat the textarea for the code editor as `ltr` for its display to preserve the ability to interact with the block delimiters. It's noteworthy that the browser still properly handles RTL text snippets. This change may mean that the cursor doesn't appear on the right side of the code editor when an RTL language is set, but once we start typing characters associated with RTL directions they should display fine, maybe left-aligned. There is a tradeoff here in semantics, which is more appropriate: that RTL languages display right-aligned or that structural content is preserved. I think that the preservation of the block comments in the code view is most important. Since this is a display issue it's worth pointing out that the underlying stream of bytes doesn't change and so no block comments are broken with the `rtl` value, but they _look_ broken and become hard to work with. --- packages/editor/src/components/post-text-editor/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/editor/src/components/post-text-editor/index.js b/packages/editor/src/components/post-text-editor/index.js index 4b70526f2bc3e..c08e59f35a400 100644 --- a/packages/editor/src/components/post-text-editor/index.js +++ b/packages/editor/src/components/post-text-editor/index.js @@ -73,6 +73,7 @@ export class PostTextEditor extends Component { </label> <Textarea autoComplete="off" + dir="auto" value={ value } onChange={ this.edit } onBlur={ this.stopEditing } From 50380b3af2b8669ab9b8c81931ff270dc1bc9895 Mon Sep 17 00:00:00 2001 From: Dennis Snell <dennis.snell@automattic.com> Date: Fri, 9 Nov 2018 19:43:27 -0600 Subject: [PATCH 087/106] Parser: Fix default PHP parser to cast inner blocks as arrays (#11678) Since the introduction of the default parsers we have had a bug in the PHP version whereby inner blocks were being popped onto the output stack as PHP classes instead of as simple associated arrays. Blocks that weren't inner blocks were being properly type-casted as arrays. This patch adds the cast in where it's needed in order to fix this inconsistent behavior. So far this hasn't caused any troubles or exposed itself but while working on other PRs like #11434 it became evident in test code. --- packages/block-serialization-default-parser/parser.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-serialization-default-parser/parser.php b/packages/block-serialization-default-parser/parser.php index 2211cf0c19226..edda34b571568 100644 --- a/packages/block-serialization-default-parser/parser.php +++ b/packages/block-serialization-default-parser/parser.php @@ -457,7 +457,7 @@ function add_freeform( $length = null ) { */ function add_inner_block( WP_Block_Parser_Block $block, $token_start, $token_length, $last_offset = null ) { $parent = $this->stack[ count( $this->stack ) - 1 ]; - $parent->block->innerBlocks[] = $block; + $parent->block->innerBlocks[] = (array) $block; $html = substr( $this->document, $parent->prev_offset, $token_start - $parent->prev_offset ); if ( ! empty( $html ) ) { From fdb8add2cf4b3682fe5112524804524644dea4c7 Mon Sep 17 00:00:00 2001 From: Dennis Snell <dennis.snell@automattic.com> Date: Fri, 9 Nov 2018 20:13:35 -0600 Subject: [PATCH 088/106] Parser: Fix JS/PHP inconsistencies with empty attributes (#11434) This patch updates a couple of inconsistent behaviors when dealing with empty attributes and with the way PHP and JS treat empty objects differently. ## PHP empty attributes When returning blocks without attributes the spec PHP parser has been sending `[]` instead of `{}` due to complications of how PHP serializes empty arrays to JSON. In this patch we're adding a new test to verify the behavior and then fixing the spec parser so that the output from the PHP version matches the output from the JS version identically. ## Bug with empty attributes in comments The default parser introduced a bug where `<!-- wp:anything {} /-->` would fail to parse because it expected some content inside the curly brackets for the JSOn attributes. This is fixed in this patch by changing the `+?` quantifier in the RegExp tokenizer with a `*?` to allow for empty attributes. A test suite has been added to verify that when we parse an array of different block formulations that they produce what we expect. A bug in a test was the reason I didn't originally find this bug. --- lib/parser.php | 22 ++++-- .../parser.php | 25 ++++--- .../src/index.js | 2 +- .../test/__snapshots__/index.js.snap | 29 -------- .../grammar.pegjs | 22 ++++-- .../block-serialization-spec-parser/parser.js | 22 ++++-- .../shared-tests.js | 71 +++++++++++++++++-- .../test/__snapshots__/index.js.snap | 29 -------- 8 files changed, 136 insertions(+), 86 deletions(-) delete mode 100644 packages/block-serialization-default-parser/test/__snapshots__/index.js.snap delete mode 100644 packages/block-serialization-spec-parser/test/__snapshots__/index.js.snap diff --git a/lib/parser.php b/lib/parser.php index 85ca8213c3e60..8fbcaa19bb458 100644 --- a/lib/parser.php +++ b/lib/parser.php @@ -260,7 +260,7 @@ private function peg_f2($blockName, $a) { return $a; } private function peg_f3($blockName, $attrs) { return array( 'blockName' => $blockName, - 'attrs' => isset( $attrs ) ? $attrs : array(), + 'attrs' => empty( $attrs ) ? peg_empty_attrs() : $attrs, 'innerBlocks' => array(), 'innerHTML' => '', 'innerContent' => array(), @@ -271,7 +271,7 @@ private function peg_f4($s, $children, $e) { return array( 'blockName' => $s['blockName'], - 'attrs' => $s['attrs'], + 'attrs' => empty( $s['attrs'] ) ? peg_empty_attrs() : $s['attrs'], 'innerBlocks' => $innerBlocks, 'innerHTML' => $innerHTML, 'innerContent' => $innerContent, @@ -1582,6 +1582,18 @@ public function parse($input) { // The `maybeJSON` function is not needed in PHP because its return semantics // are the same as `json_decode` + if ( ! function_exists( 'peg_empty_attrs' ) ) { + function peg_empty_attrs() { + static $empty_attrs = null; + + if ( null === $empty_attrs ) { + $empty_attrs = json_decode( '{}', true ); + } + + return $empty_attrs; + } + } + // array arguments are backwards because of PHP if ( ! function_exists( 'peg_process_inner_content' ) ) { function peg_process_inner_content( $array ) { @@ -1610,7 +1622,7 @@ function peg_join_blocks( $pre, $tokens, $post ) { if ( ! empty( $pre ) ) { $blocks[] = array( 'blockName' => null, - 'attrs' => array(), + 'attrs' => peg_empty_attrs(), 'innerBlocks' => array(), 'innerHTML' => $pre, 'innerContent' => array( $pre ), @@ -1625,7 +1637,7 @@ function peg_join_blocks( $pre, $tokens, $post ) { if ( ! empty( $html ) ) { $blocks[] = array( 'blockName' => null, - 'attrs' => array(), + 'attrs' => peg_empty_attrs(), 'innerBlocks' => array(), 'innerHTML' => $html, 'innerContent' => array( $html ), @@ -1636,7 +1648,7 @@ function peg_join_blocks( $pre, $tokens, $post ) { if ( ! empty( $post ) ) { $blocks[] = array( 'blockName' => null, - 'attrs' => array(), + 'attrs' => peg_empty_attrs(), 'innerBlocks' => array(), 'innerHTML' => $post, 'innerContent' => array( $post ), diff --git a/packages/block-serialization-default-parser/parser.php b/packages/block-serialization-default-parser/parser.php index edda34b571568..c9fa5db1f32e9 100644 --- a/packages/block-serialization-default-parser/parser.php +++ b/packages/block-serialization-default-parser/parser.php @@ -173,6 +173,14 @@ class WP_Block_Parser { */ public $stack; + /** + * Empty associative array, here due to PHP quirks + * + * @since 4.4.0 + * @var array empty associative array + */ + public $empty_attrs; + /** * Parses a document and returns a list of block structures * @@ -186,10 +194,11 @@ class WP_Block_Parser { * @return WP_Block_Parser_Block[] */ function parse( $document ) { - $this->document = $document; - $this->offset = 0; - $this->output = array(); - $this->stack = array(); + $this->document = $document; + $this->offset = 0; + $this->output = array(); + $this->stack = array(); + $this->empty_attrs = json_decode( '{}', true ); do { // twiddle our thumbs @@ -364,7 +373,7 @@ function next_token() { * match back in PHP to see which one it was. */ $has_match = preg_match( - '/<!--\s+(?<closer>\/)?wp:(?<namespace>[a-z][a-z0-9_-]*\/)?(?<name>[a-z][a-z0-9_-]*)\s+(?<attrs>{(?:[^}]+|}+(?=})|(?!}\s+-->).)+?}\s+)?(?<void>\/)?-->/s', + '/<!--\s+(?<closer>\/)?wp:(?<namespace>[a-z][a-z0-9_-]*\/)?(?<name>[a-z][a-z0-9_-]*)\s+(?<attrs>{(?:[^}]+|}+(?=})|(?!}\s+-->).)*?}\s+)?(?<void>\/)?-->/s', $this->document, $matches, PREG_OFFSET_CAPTURE, @@ -392,7 +401,7 @@ function next_token() { */ $attrs = $has_attrs ? json_decode( $matches[ 'attrs' ][ 0 ], /* as-associative */ true ) - : json_decode( '{}', /* don't ask why, just verify in PHP */ false ); + : $this->empty_attrs; /* * This state isn't allowed @@ -422,8 +431,8 @@ function next_token() { * @param string $innerHTML HTML content of block * @return WP_Block_Parser_Block freeform block object */ - static function freeform( $innerHTML ) { - return new WP_Block_Parser_Block( null, array(), array(), $innerHTML, array( $innerHTML ) ); + function freeform( $innerHTML ) { + return new WP_Block_Parser_Block( null, $this->empty_attrs, array(), $innerHTML, array( $innerHTML ) ); } /** diff --git a/packages/block-serialization-default-parser/src/index.js b/packages/block-serialization-default-parser/src/index.js index 577284fc1e539..936c4a380969c 100644 --- a/packages/block-serialization-default-parser/src/index.js +++ b/packages/block-serialization-default-parser/src/index.js @@ -2,7 +2,7 @@ let document; let offset; let output; let stack; -const tokenizer = /<!--\s+(\/)?wp:([a-z][a-z0-9_-]*\/)?([a-z][a-z0-9_-]*)\s+({(?:[^}]+|}+(?=})|(?!}\s+-->)[^])+?}\s+)?(\/)?-->/g; +const tokenizer = /<!--\s+(\/)?wp:([a-z][a-z0-9_-]*\/)?([a-z][a-z0-9_-]*)\s+({(?:[^}]+|}+(?=})|(?!}\s+-->)[^])*?}\s+)?(\/)?-->/g; function Block( blockName, attrs, innerBlocks, innerHTML, innerContent ) { return { diff --git a/packages/block-serialization-default-parser/test/__snapshots__/index.js.snap b/packages/block-serialization-default-parser/test/__snapshots__/index.js.snap deleted file mode 100644 index 4a6910fa35f61..0000000000000 --- a/packages/block-serialization-default-parser/test/__snapshots__/index.js.snap +++ /dev/null @@ -1,29 +0,0 @@ -// 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 [], - "innerContent": Array [ - "<!--more-->", - ], - "innerHTML": "<!--more-->", - }, -] -`; - -exports[`block-serialization-default-parser-php basic parsing parse() works properly 1`] = ` -Array [ - Object { - "attrs": Object {}, - "blockName": "core/more", - "innerBlocks": Array [], - "innerContent": Array [ - "<!--more-->", - ], - "innerHTML": "<!--more-->", - }, -] -`; diff --git a/packages/block-serialization-spec-parser/grammar.pegjs b/packages/block-serialization-spec-parser/grammar.pegjs index 4bf80d2f6755a..19b8dd9e70487 100644 --- a/packages/block-serialization-spec-parser/grammar.pegjs +++ b/packages/block-serialization-spec-parser/grammar.pegjs @@ -50,6 +50,18 @@ // The `maybeJSON` function is not needed in PHP because its return semantics // are the same as `json_decode` +if ( ! function_exists( 'peg_empty_attrs' ) ) { + function peg_empty_attrs() { + static $empty_attrs = null; + + if ( null === $empty_attrs ) { + $empty_attrs = json_decode( '{}', true ); + } + + return $empty_attrs; + } +} + // array arguments are backwards because of PHP if ( ! function_exists( 'peg_process_inner_content' ) ) { function peg_process_inner_content( $array ) { @@ -78,7 +90,7 @@ if ( ! function_exists( 'peg_join_blocks' ) ) { if ( ! empty( $pre ) ) { $blocks[] = array( 'blockName' => null, - 'attrs' => array(), + 'attrs' => peg_empty_attrs(), 'innerBlocks' => array(), 'innerHTML' => $pre, 'innerContent' => array( $pre ), @@ -93,7 +105,7 @@ if ( ! function_exists( 'peg_join_blocks' ) ) { if ( ! empty( $html ) ) { $blocks[] = array( 'blockName' => null, - 'attrs' => array(), + 'attrs' => peg_empty_attrs(), 'innerBlocks' => array(), 'innerHTML' => $html, 'innerContent' => array( $html ), @@ -104,7 +116,7 @@ if ( ! function_exists( 'peg_join_blocks' ) ) { if ( ! empty( $post ) ) { $blocks[] = array( 'blockName' => null, - 'attrs' => array(), + 'attrs' => peg_empty_attrs(), 'innerBlocks' => array(), 'innerHTML' => $post, 'innerContent' => array( $post ), @@ -212,7 +224,7 @@ Block_Void /** <?php return array( 'blockName' => $blockName, - 'attrs' => isset( $attrs ) ? $attrs : array(), + 'attrs' => empty( $attrs ) ? peg_empty_attrs() : $attrs, 'innerBlocks' => array(), 'innerHTML' => '', 'innerContent' => array(), @@ -236,7 +248,7 @@ Block_Balanced return array( 'blockName' => $s['blockName'], - 'attrs' => $s['attrs'], + 'attrs' => empty( $s['attrs'] ) ? peg_empty_attrs() : $s['attrs'], 'innerBlocks' => $innerBlocks, 'innerHTML' => $innerHTML, 'innerContent' => $innerContent, diff --git a/packages/block-serialization-spec-parser/parser.js b/packages/block-serialization-spec-parser/parser.js index 85fdc6ec9b5d6..be39ccaa3046d 100644 --- a/packages/block-serialization-spec-parser/parser.js +++ b/packages/block-serialization-spec-parser/parser.js @@ -166,7 +166,7 @@ /** <?php return array( 'blockName' => $blockName, - 'attrs' => isset( $attrs ) ? $attrs : array(), + 'attrs' => empty( $attrs ) ? peg_empty_attrs() : $attrs, 'innerBlocks' => array(), 'innerHTML' => '', 'innerContent' => array(), @@ -187,7 +187,7 @@ return array( 'blockName' => $s['blockName'], - 'attrs' => $s['attrs'], + 'attrs' => empty( $s['attrs'] ) ? peg_empty_attrs() : $s['attrs'], 'innerBlocks' => $innerBlocks, 'innerHTML' => $innerHTML, 'innerContent' => $innerContent, @@ -1618,6 +1618,18 @@ // The `maybeJSON` function is not needed in PHP because its return semantics // are the same as `json_decode` + if ( ! function_exists( 'peg_empty_attrs' ) ) { + function peg_empty_attrs() { + static $empty_attrs = null; + + if ( null === $empty_attrs ) { + $empty_attrs = json_decode( '{}', true ); + } + + return $empty_attrs; + } + } + // array arguments are backwards because of PHP if ( ! function_exists( 'peg_process_inner_content' ) ) { function peg_process_inner_content( $array ) { @@ -1646,7 +1658,7 @@ if ( ! empty( $pre ) ) { $blocks[] = array( 'blockName' => null, - 'attrs' => array(), + 'attrs' => peg_empty_attrs(), 'innerBlocks' => array(), 'innerHTML' => $pre, 'innerContent' => array( $pre ), @@ -1661,7 +1673,7 @@ if ( ! empty( $html ) ) { $blocks[] = array( 'blockName' => null, - 'attrs' => array(), + 'attrs' => peg_empty_attrs(), 'innerBlocks' => array(), 'innerHTML' => $html, 'innerContent' => array( $html ), @@ -1672,7 +1684,7 @@ if ( ! empty( $post ) ) { $blocks[] = array( 'blockName' => null, - 'attrs' => array(), + 'attrs' => peg_empty_attrs(), 'innerBlocks' => array(), 'innerHTML' => $post, 'innerContent' => array( $post ), diff --git a/packages/block-serialization-spec-parser/shared-tests.js b/packages/block-serialization-spec-parser/shared-tests.js index 54137636d3afd..19f3c9b4d7079 100644 --- a/packages/block-serialization-spec-parser/shared-tests.js +++ b/packages/block-serialization-spec-parser/shared-tests.js @@ -1,7 +1,64 @@ export const jsTester = ( parse ) => () => { - describe( 'basic parsing', () => { - test( 'parse() works properly', () => { - expect( parse( '<!-- wp:core/more --><!--more--><!-- /wp:core/more -->' ) ).toMatchSnapshot(); + describe( 'output structure', () => { + test( 'output is an array', () => { + expect( parse( '' ) ).toEqual( expect.any( Array ) ); + expect( parse( 'test' ) ).toEqual( expect.any( Array ) ); + expect( parse( '<!-- wp:void /-->' ) ).toEqual( expect.any( Array ) ); + expect( parse( '<!-- wp:block --><!-- wp:inner /--><!-- /wp:block -->' ) ).toEqual( expect.any( Array ) ); + expect( parse( '<!-- wp:first /--><!-- wp:second /-->' ) ).toEqual( expect.any( Array ) ); + } ); + + test( 'parses blocks of various types', () => { + expect( parse( '<!-- wp:void /-->' )[ 0 ] ).toHaveProperty( 'blockName', 'core/void' ); + expect( parse( '<!-- wp:void {} /-->' )[ 0 ] ).toHaveProperty( 'blockName', 'core/void' ); + expect( parse( '<!-- wp:void {"value":true} /-->' )[ 0 ] ).toHaveProperty( 'blockName', 'core/void' ); + expect( parse( '<!-- wp:void {"a":{}} /-->' )[ 0 ] ).toHaveProperty( 'blockName', 'core/void' ); + expect( parse( '<!-- wp:void { "value" : true } /-->' )[ 0 ] ).toHaveProperty( 'blockName', 'core/void' ); + expect( parse( '<!-- wp:void {\n\t"value" : true\n} /-->' )[ 0 ] ).toHaveProperty( 'blockName', 'core/void' ); + expect( parse( '<!-- wp:block --><!-- /wp:block -->' )[ 0 ] ).toHaveProperty( 'blockName', 'core/block' ); + expect( parse( '<!-- wp:block {} --><!-- /wp:block -->' )[ 0 ] ).toHaveProperty( 'blockName', 'core/block' ); + expect( parse( '<!-- wp:block {"value":true} --><!-- /wp:block -->' )[ 0 ] ).toHaveProperty( 'blockName', 'core/block' ); + expect( parse( '<!-- wp:block {} -->inner<!-- /wp:block -->' )[ 0 ] ).toHaveProperty( 'blockName', 'core/block' ); + expect( parse( '<!-- wp:block {"value":{"a" : "true"}} -->inner<!-- /wp:block -->' )[ 0 ] ).toHaveProperty( 'blockName', 'core/block' ); + } ); + + test( 'blockName is namespaced string (except freeform)', () => { + expect( parse( 'freeform has null name' )[ 0 ] ).toHaveProperty( 'blockName', null ); + expect( parse( '<!-- wp:more /-->' )[ 0 ] ).toHaveProperty( 'blockName', 'core/more' ); + expect( parse( '<!-- wp:core/more /-->' )[ 0 ] ).toHaveProperty( 'blockName', 'core/more' ); + expect( parse( '<!-- wp:my/more /-->' )[ 0 ] ).toHaveProperty( 'blockName', 'my/more' ); + } ); + + test( 'JSON attributes are key/value object', () => { + expect( parse( 'freeform has empty attrs' )[ 0 ] ).toHaveProperty( 'attrs', {} ); + expect( parse( '<!-- wp:void /-->' )[ 0 ] ).toHaveProperty( 'attrs', {} ); + expect( parse( '<!-- wp:void {} /-->' )[ 0 ] ).toHaveProperty( 'blockName', 'core/void' ); + expect( parse( '<!-- wp:void {} /-->' )[ 0 ] ).toHaveProperty( 'attrs', {} ); + expect( parse( '<!-- wp:void {"key": "value"} /-->' )[ 0 ] ).toHaveProperty( 'attrs', { key: 'value' } ); + expect( parse( '<!-- wp:block --><!-- /wp:block -->' )[ 0 ] ).toHaveProperty( 'attrs', {} ); + expect( parse( '<!-- wp:block {} --><!-- /wp:block -->' )[ 0 ] ).toHaveProperty( 'attrs', {} ); + expect( parse( '<!-- wp:block {"key": "value"} --><!-- /wp:block -->' )[ 0 ] ).toHaveProperty( 'attrs', { key: 'value' } ); + } ); + + test( 'innerBlocks is a list', () => { + expect( parse( 'freeform has empty innerBlocks' )[ 0 ] ).toHaveProperty( 'innerBlocks', [] ); + expect( parse( '<!-- wp:void /-->' )[ 0 ] ).toHaveProperty( 'innerBlocks', [] ); + expect( parse( '<!-- wp:block --><!-- /wp:block -->' )[ 0 ] ).toHaveProperty( 'innerBlocks', [] ); + + const withInner = parse( '<!-- wp:block --><!-- wp:inner /--><!-- /wp:block -->' )[ 0 ]; + expect( withInner ).toHaveProperty( 'innerBlocks', expect.any( Array ) ); + expect( withInner.innerBlocks ).toHaveLength( 1 ); + + const withTwoInner = parse( '<!-- wp:block -->a<!-- wp:first /-->b<!-- wp:second /-->c<!-- /wp:block -->' )[ 0 ]; + expect( withTwoInner ).toHaveProperty( 'innerBlocks', expect.any( Array ) ); + expect( withTwoInner.innerBlocks ).toHaveLength( 2 ); + } ); + + test( 'innerHTML is a string', () => { + expect( parse( 'test' )[ 0 ] ).toHaveProperty( 'innerHTML', expect.any( String ) ); + expect( parse( '<!-- wp:test /-->' )[ 0 ] ).toHaveProperty( 'innerHTML', expect.any( String ) ); + expect( parse( '<!-- wp:test --><!-- /wp:test -->' )[ 0 ] ).toHaveProperty( 'innerHTML', expect.any( String ) ); + expect( parse( '<!-- wp:test -->test<!-- /wp:test -->' )[ 0 ] ).toHaveProperty( 'innerHTML', expect.any( String ) ); } ); } ); @@ -131,7 +188,13 @@ export const phpTester = ( name, filename ) => makeTest( } try { - return JSON.parse( process.stdout ); + /* + * Due to an issue with PHP's json_encode() serializing an empty associative array + * as an empty list `[]` we're manually replacing the already-encoded bit here. + * + * This is an issue with the test runner, not with the parser. + */ + return JSON.parse( process.stdout.replace( /"attrs":\s*\[\]/g, '"attrs":{}' ) ); } catch ( e ) { throw new Error( 'failed to parse JSON:\n' + process.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 deleted file mode 100644 index 480cde5428505..0000000000000 --- a/packages/block-serialization-spec-parser/test/__snapshots__/index.js.snap +++ /dev/null @@ -1,29 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`block-serialization-spec-parser-js basic parsing parse() works properly 1`] = ` -Array [ - Object { - "attrs": Object {}, - "blockName": "core/more", - "innerBlocks": Array [], - "innerContent": Array [ - "<!--more-->", - ], - "innerHTML": "<!--more-->", - }, -] -`; - -exports[`block-serialization-spec-parser-php basic parsing parse() works properly 1`] = ` -Array [ - Object { - "attrs": Array [], - "blockName": "core/more", - "innerBlocks": Array [], - "innerContent": Array [ - "<!--more-->", - ], - "innerHTML": "<!--more-->", - }, -] -`; From 283193f611eff56f0c7e0e911bf0c14cdf58d128 Mon Sep 17 00:00:00 2001 From: Dennis Snell <dennis.snell@automattic.com> Date: Fri, 9 Nov 2018 21:07:48 -0600 Subject: [PATCH 089/106] Parsing: Use full parser in `do_blocks` with nested block support (#11141) Updates do_blocks() and gutenberg_render_block() so that we can support nested blocks inside of dynamic blocks. This replaces the use of the partial parser which extracts registered dynamic blocks with the full parser. This change will allow dynamic blocks which contain nested blocks inside of them and it will pave the way for a filtering API to structurally process blocks. The partial parser came about at a time before the default parser was written; it was faster than the spec parser and was a tradeoff to get dynamic blocks rendering. The default parser, however, has been fast enough for a while to run on page render and so this PR exists to finally get it into the pipeline. --- lib/blocks.php | 127 ++++--------------- phpunit/class-do-blocks-test.php | 51 +++++++- phpunit/class-dynamic-blocks-render-test.php | 78 +++++++++++- phpunit/fixtures/do-blocks-expected.html | 5 + 4 files changed, 156 insertions(+), 105 deletions(-) diff --git a/lib/blocks.php b/lib/blocks.php index 6ef34c4176eef..a8083466a0405 100644 --- a/lib/blocks.php +++ b/lib/blocks.php @@ -58,21 +58,6 @@ function unregister_block_type( $name ) { * @return array Array of parsed block objects. */ function gutenberg_parse_blocks( $content ) { - /* - * If there are no blocks in the content, return a single block, rather - * than wasting time trying to parse the string. - */ - if ( ! has_blocks( $content ) ) { - return array( - array( - 'blockName' => null, - 'attrs' => array(), - 'innerBlocks' => array(), - 'innerHTML' => $content, - ), - ); - } - /** * Filter to allow plugins to replace the server-side block parser * @@ -148,27 +133,34 @@ function get_dynamic_blocks_regex() { * Renders a single block into a HTML string. * * @since 1.9.0 + * @since 4.4.0 renders full nested tree of blocks before reassembling into HTML string + * @global WP_Post $post The post to edit. * * @param array $block A single parsed block object. * @return string String of rendered HTML. */ function gutenberg_render_block( $block ) { - $block_name = isset( $block['blockName'] ) ? $block['blockName'] : null; - $attributes = is_array( $block['attrs'] ) ? $block['attrs'] : array(); - $raw_content = isset( $block['innerHTML'] ) ? $block['innerHTML'] : null; + global $post; - if ( $block_name ) { - $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block_name ); - if ( null !== $block_type && $block_type->is_dynamic() ) { - return $block_type->render( $attributes ); - } + $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] ); + $is_dynamic = $block['blockName'] && null !== $block_type && $block_type->is_dynamic(); + $inner_content = ''; + $index = 0; + + foreach ( $block['innerContent'] as $chunk ) { + $inner_content .= is_string( $chunk ) ? $chunk : gutenberg_render_block( $block['innerBlocks'][ $index++ ] ); } - if ( $raw_content ) { - return $raw_content; + if ( $is_dynamic ) { + $attributes = is_array( $block['attrs'] ) ? (array) $block['attrs'] : array(); + $global_post = $post; + $output = $block_type->render( $attributes, $inner_content ); + $post = $global_post; + + return $output; } - return ''; + return $inner_content; } if ( ! function_exists( 'do_blocks' ) ) { @@ -176,91 +168,20 @@ function gutenberg_render_block( $block ) { * Parses dynamic blocks out of `post_content` and re-renders them. * * @since 0.1.0 - * @global WP_Post $post The post to edit. + * @since 4.4.0 performs full parse on input post content * * @param string $content Post content. * @return string Updated post content. */ function do_blocks( $content ) { - global $post; - - $rendered_content = ''; - $dynamic_block_pattern = get_dynamic_blocks_regex(); - - /* - * Back up global post, to restore after render callback. - * Allows callbacks to run new WP_Query instances without breaking the global post. - */ - $global_post = $post; - - while ( preg_match( $dynamic_block_pattern, $content, $block_match, PREG_OFFSET_CAPTURE ) ) { - $opening_tag = $block_match[0][0]; - $offset = $block_match[0][1]; - $block_name = $block_match[1][0]; - $is_self_closing = isset( $block_match[4] ); - - // Reset attributes JSON to prevent scope bleed from last iteration. - $block_attributes_json = null; - if ( isset( $block_match[3] ) ) { - $block_attributes_json = $block_match[3][0]; - } + $blocks = gutenberg_parse_blocks( $content ); + $output = ''; - // Since content is a working copy since the last match, append to - // rendered content up to the matched offset... - $rendered_content .= substr( $content, 0, $offset ); - - // ...then update the working copy of content. - $content = substr( $content, $offset + strlen( $opening_tag ) ); - - // Make implicit core namespace explicit. - $is_implicit_core_namespace = ( false === strpos( $block_name, '/' ) ); - $normalized_block_name = $is_implicit_core_namespace ? 'core/' . $block_name : $block_name; - - // Find registered block type. We can assume it exists since we use the - // `get_dynamic_block_names` function as a source for pattern matching. - $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $normalized_block_name ); - - // Attempt to parse attributes JSON, if available. - $attributes = array(); - if ( ! empty( $block_attributes_json ) ) { - $decoded_attributes = json_decode( $block_attributes_json, true ); - if ( ! is_null( $decoded_attributes ) ) { - $attributes = $decoded_attributes; - } - } - - $inner_content = ''; - - if ( ! $is_self_closing ) { - $end_tag_pattern = '/<!--\s+\/wp:' . preg_quote( $block_name, '/' ) . '\s+-->/'; - if ( ! preg_match( $end_tag_pattern, $content, $block_match_end, PREG_OFFSET_CAPTURE ) ) { - // If no closing tag is found, abort all matching, and continue - // to append remainder of content to rendered output. - break; - } - - // Update content to omit text up to and including closing tag. - $end_tag = $block_match_end[0][0]; - $end_offset = $block_match_end[0][1]; - - $inner_content = substr( $content, 0, $end_offset ); - $content = substr( $content, $end_offset + strlen( $end_tag ) ); - } - - // Replace dynamic block with server-rendered output. - $rendered_content .= $block_type->render( $attributes, $inner_content ); - - // Restore global $post. - $post = $global_post; + foreach ( $blocks as $block ) { + $output .= gutenberg_render_block( $block ); } - // Append remaining unmatched content. - $rendered_content .= $content; - - // Strip remaining block comment demarcations. - $rendered_content = preg_replace( '/<!--\s+\/?wp:.*?-->\r?\n?/m', '', $rendered_content ); - - return $rendered_content; + return $output; } add_filter( 'the_content', 'do_blocks', 7 ); // BEFORE do_shortcode() and oembed. diff --git a/phpunit/class-do-blocks-test.php b/phpunit/class-do-blocks-test.php index 9da7ddc0fcb27..147545e65ca0a 100644 --- a/phpunit/class-do-blocks-test.php +++ b/phpunit/class-do-blocks-test.php @@ -9,6 +9,19 @@ * Test do_blocks */ class Do_Blocks_Test extends WP_UnitTestCase { + /** + * Tear down. + */ + function tearDown() { + parent::tearDown(); + + $registry = WP_Block_Type_Registry::get_instance(); + + if ( $registry->is_registered( 'core/dummy' ) ) { + $registry->unregister( 'core/dummy' ); + } + } + /** * Test do_blocks removes comment demarcations. * @@ -30,7 +43,7 @@ function test_the_content() { add_shortcode( 'someshortcode', array( $this, 'handle_shortcode' ) ); $classic_content = "Foo\n\n[someshortcode]\n\nBar\n\n[/someshortcode]\n\nBaz"; - $block_content = "<!-- wp:core/paragraph -->\n<p>Foo</p>\n<!-- /wp:core/paragraph -->\n\n<!-- wp:core/shortcode -->[someshortcode]\n\nBar\n\n[/someshortcode]<!-- /wp:core/shortcode -->\n\n<!-- wp:core/paragraph -->\n<p>Baz</p>\n<!-- /wp:core/paragraph -->"; + $block_content = "<!-- wp:core/paragraph --><p>Foo</p>\n<!-- /wp:core/paragraph -->\n\n<!-- wp:core/shortcode -->[someshortcode]\n\nBar\n\n[/someshortcode]<!-- /wp:core/shortcode -->\n\n<!-- wp:core/paragraph -->\n<p>Baz</p>\n<!-- /wp:core/paragraph -->"; $classic_filtered_content = apply_filters( 'the_content', $classic_content ); $block_filtered_content = apply_filters( 'the_content', $block_content ); @@ -41,7 +54,43 @@ function test_the_content() { $this->assertEquals( $classic_filtered_content, $block_filtered_content ); } + function test_can_nest_at_least_so_deep() { + $minimum_depth = 99; + + $content = 'deep inside'; + for ( $i = 0; $i < $minimum_depth; $i++ ) { + $content = '<!-- wp:dummy -->' . $content . '<!-- /wp:dummy -->'; + } + + $this->assertEquals( 'deep inside', do_blocks( $content ) ); + } + + function test_can_nest_at_least_so_deep_with_dynamic_blocks() { + $minimum_depth = 99; + + $content = '0'; + for ( $i = 0; $i < $minimum_depth; $i++ ) { + $content = '<!-- wp:dummy -->' . $content . '<!-- /wp:dummy -->'; + } + + register_block_type( + 'core/dummy', + array( + 'render_callback' => array( + $this, + 'render_dynamic_incrementer', + ), + ) + ); + + $this->assertEquals( $minimum_depth, (int) do_blocks( $content ) ); + } + function handle_shortcode( $atts, $content ) { return $content; } + + function render_dynamic_incrementer( $attrs, $content ) { + return (string) ( 1 + (int) $content ); + } } diff --git a/phpunit/class-dynamic-blocks-render-test.php b/phpunit/class-dynamic-blocks-render-test.php index 812d6aa8d21e1..35799bc9bf73d 100644 --- a/phpunit/class-dynamic-blocks-render-test.php +++ b/phpunit/class-dynamic-blocks-render-test.php @@ -38,6 +38,10 @@ function render_dummy_block_numeric() { return 10; } + function render_serialize_dynamic_block( $attributes, $content ) { + return base64_encode( serialize( array( $attributes, $content ) ) ); + } + /** * Dummy block rendering function, creating a new WP_Query instance. * @@ -74,7 +78,14 @@ function tearDown() { $this->dummy_block_instance_number = 0; $registry = WP_Block_Type_Registry::get_instance(); - $registry->unregister( 'core/dummy' ); + + if ( $registry->is_registered( 'core/dummy' ) ) { + $registry->unregister( 'core/dummy' ); + } + + if ( $registry->is_registered( 'core/dynamic' ) ) { + $registry->unregister( 'core/dynamic' ); + } } /** @@ -164,4 +175,69 @@ function test_dynamic_block_renders_string() { $this->assertSame( '10', $rendered ); $this->assertInternalType( 'string', $rendered ); } + + function test_dynamic_block_gets_inner_html() { + register_block_type( + 'core/dynamic', + array( + 'render_callback' => array( + $this, + 'render_serialize_dynamic_block', + ), + ) + ); + + $output = do_blocks( '<!-- wp:dynamic -->inner<!-- /wp:dynamic -->' ); + + list( /* attrs */, $content ) = unserialize( base64_decode( $output ) ); + + $this->assertEquals( 'inner', $content ); + } + + function test_dynamic_block_gets_rendered_inner_blocks() { + register_block_type( + 'core/dummy', + array( + 'render_callback' => array( + $this, + 'render_dummy_block_numeric', + ), + ) + ); + register_block_type( + 'core/dynamic', + array( + 'render_callback' => array( + $this, + 'render_serialize_dynamic_block', + ), + ) + ); + + $output = do_blocks( '<!-- wp:dynamic -->before<!-- wp:dummy /-->after<!-- /wp:dynamic -->' ); + + list( /* attrs */, $content ) = unserialize( base64_decode( $output ) ); + + $this->assertEquals( 'before10after', $content ); + } + + function test_dynamic_block_gets_rendered_inner_dynamic_blocks() { + register_block_type( + 'core/dynamic', + array( + 'render_callback' => array( + $this, + 'render_serialize_dynamic_block', + ), + ) + ); + + $output = do_blocks( '<!-- wp:dynamic -->before<!-- wp:dynamic -->deep inner<!-- /wp:dynamic -->after<!-- /wp:dynamic -->' ); + + list( /* attrs */, $content ) = unserialize( base64_decode( $output ) ); + + $inner = $this->render_serialize_dynamic_block( array(), 'deep inner' ); + + $this->assertEquals( $content, 'before' . $inner . 'after' ); + } } diff --git a/phpunit/fixtures/do-blocks-expected.html b/phpunit/fixtures/do-blocks-expected.html index 4a3dc379ef48f..f413182051334 100644 --- a/phpunit/fixtures/do-blocks-expected.html +++ b/phpunit/fixtures/do-blocks-expected.html @@ -2,13 +2,18 @@ <!--more--> + <p>First Gutenberg Paragraph</p> + <p>Second Auto Paragraph</p> + + <p>Third Gutenberg Paragraph</p> + <p>Third Auto Paragraph</p> <p>[someshortcode]</p> From 81dee8b1f591736b809b7866ce9d2eb0dd64bfcf Mon Sep 17 00:00:00 2001 From: Derek Sifford <dereksifford@gmail.com> Date: Sun, 11 Nov 2018 07:57:31 -0500 Subject: [PATCH 090/106] Fix typo in @wordpress/components/panel README (#11716) Pretty self explanatory. Cheers :beers: --- packages/components/src/panel/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/src/panel/README.md b/packages/components/src/panel/README.md index 0b90e01213ba1..1c5c2bcf95df0 100644 --- a/packages/components/src/panel/README.md +++ b/packages/components/src/panel/README.md @@ -81,7 +81,7 @@ The is a generic container for panel content. Default styles add a top margin an ##### className -The class that will be added with `components-panel__row`. to the classes of the wrapper div. If no `className` is passed only `components-panel__body` is used. +The class that will be added with `components-panel__row`. to the classes of the wrapper div. If no `className` is passed only `components-panel__row` is used. - Type: `String` - Required: No From f1e1fd7e8a6084a704c86c02446941dc600df250 Mon Sep 17 00:00:00 2001 From: Hendrik Luehrsen <Luehrsen@users.noreply.github.com> Date: Sun, 11 Nov 2018 16:09:48 +0100 Subject: [PATCH 091/106] Add @luehrsen to contributors.md (#11731) --- CONTRIBUTORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 83d57337774fc..15f1331b338a0 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -116,3 +116,4 @@ This list is manually curated to include valuable contributions by volunteers th | @pratikthink | @pratikthink | | @amdrew | @sumobi | | @MaedahBatool | @MaedahBatool | +| @luehrsen | @luehrsen | From 627e7600cc68a4609cb249988445e78c8e78cdc6 Mon Sep 17 00:00:00 2001 From: Gary Pendergast <gary@pento.net> Date: Mon, 12 Nov 2018 11:07:00 +1100 Subject: [PATCH 092/106] chore(release): update changelog files --- packages/annotations/CHANGELOG.md | 2 +- packages/api-fetch/CHANGELOG.md | 2 ++ packages/block-library/CHANGELOG.md | 6 ++++++ .../CHANGELOG.md | 7 +++++++ .../CHANGELOG.md | 6 ++++++ packages/blocks/CHANGELOG.md | 2 ++ packages/components/CHANGELOG.md | 4 ++-- packages/core-data/CHANGELOG.md | 2 ++ packages/edit-post/CHANGELOG.md | 4 ++-- packages/editor/CHANGELOG.md | 18 +++++++++++++----- packages/format-library/CHANGELOG.md | 6 ++++++ packages/list-reusable-blocks/CHANGELOG.md | 2 ++ packages/nux/CHANGELOG.md | 2 ++ packages/url/CHANGELOG.md | 2 +- 14 files changed, 54 insertions(+), 11 deletions(-) diff --git a/packages/annotations/CHANGELOG.md b/packages/annotations/CHANGELOG.md index 95745945dd46e..15ac80d2f10e2 100644 --- a/packages/annotations/CHANGELOG.md +++ b/packages/annotations/CHANGELOG.md @@ -1,4 +1,4 @@ -## 1.0.0 (unreleased) +## 1.0.0 (2018-11-12) ### New Features diff --git a/packages/api-fetch/CHANGELOG.md b/packages/api-fetch/CHANGELOG.md index 451bdd2aaf686..d616ccb148a87 100644 --- a/packages/api-fetch/CHANGELOG.md +++ b/packages/api-fetch/CHANGELOG.md @@ -1,3 +1,5 @@ +## 2.2.3 (2018-11-12) + ## 2.2.2 (2018-11-03) ## 2.2.1 (2018-10-30) diff --git a/packages/block-library/CHANGELOG.md b/packages/block-library/CHANGELOG.md index 29738639f8fc5..f8694cedbcafa 100644 --- a/packages/block-library/CHANGELOG.md +++ b/packages/block-library/CHANGELOG.md @@ -1,3 +1,9 @@ +## 2.2.2 (2018-11-12) + +### Polish + +- Columns Block: Improve usability while editing columns. + ## 2.2.1 (2018-11-09) ## 2.2.0 (2018-11-09) diff --git a/packages/block-serialization-default-parser/CHANGELOG.md b/packages/block-serialization-default-parser/CHANGELOG.md index d707d54620836..e651970cc3bdc 100644 --- a/packages/block-serialization-default-parser/CHANGELOG.md +++ b/packages/block-serialization-default-parser/CHANGELOG.md @@ -1,3 +1,10 @@ +## 2.0.0 (2018-11-12) + +### Breaking Changes + +- Inner blocks are now cast as arrays instead of objects. +- JS and PHP parsers now behave consistently when parsing empty attributes. + ## 1.1.1 (2018-11-09) ## 1.1.0 (2018-11-09) diff --git a/packages/block-serialization-spec-parser/CHANGELOG.md b/packages/block-serialization-spec-parser/CHANGELOG.md index 276abd6e3b3a7..d31eb025e0275 100644 --- a/packages/block-serialization-spec-parser/CHANGELOG.md +++ b/packages/block-serialization-spec-parser/CHANGELOG.md @@ -1,3 +1,9 @@ +## 2.0.0 (2018-11-12) + +### Breaking Change + +- JS and PHP parsers now behave consistently when parsing empty attributes. + ## 1.1.1 (2018-11-09) ## 1.1.0 (2018-11-09) diff --git a/packages/blocks/CHANGELOG.md b/packages/blocks/CHANGELOG.md index 26d05ff27a017..9c9395b24eff6 100644 --- a/packages/blocks/CHANGELOG.md +++ b/packages/blocks/CHANGELOG.md @@ -1,3 +1,5 @@ +## 5.3.1 (2018-11-12) + ## 5.3.0 (2018-11-09) ### New feature diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 3d8a39e178245..6ecf3e3961e14 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -1,6 +1,6 @@ -## 6.0.0 (Unreleased) +## 6.0.0 (2018-11-12) -### Breaking Changes +### Breaking Change - The `PanelColor` component has been removed. diff --git a/packages/core-data/CHANGELOG.md b/packages/core-data/CHANGELOG.md index e08dec7a0b911..3351e852b3369 100644 --- a/packages/core-data/CHANGELOG.md +++ b/packages/core-data/CHANGELOG.md @@ -1,3 +1,5 @@ +## 2.0.12 (2018-11-12) + ## 2.0.11 (2018-11-09) ## 2.0.10 (2018-11-09) diff --git a/packages/edit-post/CHANGELOG.md b/packages/edit-post/CHANGELOG.md index f3fcee5b7aaef..931e6e6b17a70 100644 --- a/packages/edit-post/CHANGELOG.md +++ b/packages/edit-post/CHANGELOG.md @@ -1,6 +1,6 @@ -## 3.0.0 (Unreleased) +## 3.0.0 (2018-11-12) -### Breaking Changes +### Breaking Change - `isEditorSidebarPanelOpened` selector (`core/edit-post`) has been removed. Please use `isEditorPanelEnabled` instead. - `toggleGeneralSidebarEditorPanel` action (`core/edit-post`) has been removed. Please use `toggleEditorPanelOpened` instead. diff --git a/packages/editor/CHANGELOG.md b/packages/editor/CHANGELOG.md index 0a5c92529c16b..cfa181b65cf68 100644 --- a/packages/editor/CHANGELOG.md +++ b/packages/editor/CHANGELOG.md @@ -1,15 +1,23 @@ -## 7.0.0 (Unreleased) +## 7.0.0 (2018-11-12) -### Breaking Changes +### Breaking Change - The `PanelColor` component has been removed. -## 6.2.1 (2018-11-09) - -### New Features +### New Feature - In `NavigableToolbar`, a property focusOnMount was added, if true, the toolbar will get focus as soon as it mounted. Defaults to false. +### Bug Fixes + +- Avoid unnecessary re-renders when navigating between blocks. +- PostPublishPanel: return focus to element that opened the panel +- Capture focus on self in InsertionPoint inserter +- Correct insertion point opacity selector +- Set code editor as RTL + +## 6.2.1 (2018-11-09) + ### Deprecations - `wp.editor.PostPublishPanelToggle` has been deprecated in favor of `wp.editor.PostPublishButton`. diff --git a/packages/format-library/CHANGELOG.md b/packages/format-library/CHANGELOG.md index 2badd261b817c..4a039dba8ecf2 100644 --- a/packages/format-library/CHANGELOG.md +++ b/packages/format-library/CHANGELOG.md @@ -1,3 +1,9 @@ +## 1.2.0 (2018-11-12) + +## New Feature + +- Add URL validation to links. + ## 1.1.1 (2018-11-09) ## 1.1.0 (2018-11-09) diff --git a/packages/list-reusable-blocks/CHANGELOG.md b/packages/list-reusable-blocks/CHANGELOG.md index 2e04e42846146..3a28915b10d6c 100644 --- a/packages/list-reusable-blocks/CHANGELOG.md +++ b/packages/list-reusable-blocks/CHANGELOG.md @@ -1,3 +1,5 @@ +## 1.1.10 (2018-11-10) + ## 1.1.9 (2018-11-09) ## 1.1.8 (2018-11-09) diff --git a/packages/nux/CHANGELOG.md b/packages/nux/CHANGELOG.md index 9b8e754221da8..3719df408a5d1 100644 --- a/packages/nux/CHANGELOG.md +++ b/packages/nux/CHANGELOG.md @@ -1,3 +1,5 @@ +## 2.0.12 (2018-11-12) + ## 2.0.11 (2018-11-09) ## 2.0.10 (2018-11-09) diff --git a/packages/url/CHANGELOG.md b/packages/url/CHANGELOG.md index 30dba55b01a2d..f583459887f5a 100644 --- a/packages/url/CHANGELOG.md +++ b/packages/url/CHANGELOG.md @@ -1,4 +1,4 @@ -## 2.3.0 (Unreleased) +## 2.3.0 (2018-11-12) ### New Features From baf00986b5624ae88949feadd2b28cd5c94fab72 Mon Sep 17 00:00:00 2001 From: Gary Pendergast <gary@pento.net> Date: Mon, 12 Nov 2018 11:21:41 +1100 Subject: [PATCH 093/106] chore(release): publish - @wordpress/annotations@1.0.0 - @wordpress/api-fetch@2.2.3 - @wordpress/block-library@2.2.2 - @wordpress/block-serialization-default-parser@2.0.0 - @wordpress/block-serialization-spec-parser@2.0.0 - @wordpress/blocks@5.3.1 - @wordpress/components@6.0.0 - @wordpress/core-data@2.0.12 - @wordpress/edit-post@3.0.0 - @wordpress/editor@7.0.0 - @wordpress/format-library@1.2.0 - @wordpress/list-reusable-blocks@1.1.10 - @wordpress/nux@2.0.12 - @wordpress/url@2.3.0 --- packages/annotations/package.json | 2 +- packages/api-fetch/package.json | 2 +- packages/block-library/package.json | 2 +- packages/block-serialization-default-parser/package.json | 2 +- packages/block-serialization-spec-parser/package.json | 2 +- packages/blocks/package.json | 2 +- packages/components/package.json | 2 +- packages/core-data/package.json | 2 +- packages/edit-post/package.json | 2 +- packages/editor/package.json | 2 +- packages/format-library/package.json | 2 +- packages/list-reusable-blocks/package.json | 2 +- packages/nux/package.json | 2 +- packages/url/package.json | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/annotations/package.json b/packages/annotations/package.json index b68ce8f7efa25..805129a11b84d 100644 --- a/packages/annotations/package.json +++ b/packages/annotations/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/annotations", - "version": "1.0.0-beta1", + "version": "1.0.0", "description": "Annotate content in the Gutenberg editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/api-fetch/package.json b/packages/api-fetch/package.json index b34a81862930f..df838409b3b60 100644 --- a/packages/api-fetch/package.json +++ b/packages/api-fetch/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/api-fetch", - "version": "2.2.2", + "version": "2.2.3", "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 06e8c00e05185..9c3c5b8c785ac 100644 --- a/packages/block-library/package.json +++ b/packages/block-library/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-library", - "version": "2.2.1", + "version": "2.2.2", "description": "Block library for the WordPress editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/block-serialization-default-parser/package.json b/packages/block-serialization-default-parser/package.json index 845f3596d78bb..aed2a538f5961 100644 --- a/packages/block-serialization-default-parser/package.json +++ b/packages/block-serialization-default-parser/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-serialization-default-parser", - "version": "1.1.1", + "version": "2.0.0", "description": "Block serialization specification parser for WordPress posts.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/block-serialization-spec-parser/package.json b/packages/block-serialization-spec-parser/package.json index 46e23fe37ac52..73a8859f5feac 100644 --- a/packages/block-serialization-spec-parser/package.json +++ b/packages/block-serialization-spec-parser/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-serialization-spec-parser", - "version": "1.1.1", + "version": "2.0.0", "description": "Block serialization specification parser for WordPress posts.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/blocks/package.json b/packages/blocks/package.json index 593f0c261a188..8db57d04433a6 100644 --- a/packages/blocks/package.json +++ b/packages/blocks/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/blocks", - "version": "5.3.0", + "version": "5.3.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 2d6c70ab312b2..cd9718030c93c 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/components", - "version": "5.1.1", + "version": "6.0.0", "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 72d5d06b05c0c..4ca83c952d04e 100644 --- a/packages/core-data/package.json +++ b/packages/core-data/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/core-data", - "version": "2.0.11", + "version": "2.0.12", "description": "Access to and manipulation of core WordPress entities.", "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 b86e62bc153f7..bfbc8efdb363e 100644 --- a/packages/edit-post/package.json +++ b/packages/edit-post/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/edit-post", - "version": "2.1.1", + "version": "3.0.0", "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 ec8fcc15bcff5..b68d654ca8f00 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/editor", - "version": "6.2.1", + "version": "7.0.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 6f1c0b2baf982..db94fa6776d30 100644 --- a/packages/format-library/package.json +++ b/packages/format-library/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/format-library", - "version": "1.1.1", + "version": "1.2.0", "description": "Format library for the WordPress editor.", "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 4e6cadf8d13f1..bfdc2b592ad48 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.9", + "version": "1.1.10", "description": "Adding Export/Import support to the reusable blocks listing.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/nux/package.json b/packages/nux/package.json index 50012ed481725..d7034a7469ba0 100644 --- a/packages/nux/package.json +++ b/packages/nux/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/nux", - "version": "2.0.11", + "version": "2.0.12", "description": "NUX (New User eXperience) module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/url/package.json b/packages/url/package.json index 86c9ad4a98a83..ddf20dd9cde91 100644 --- a/packages/url/package.json +++ b/packages/url/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/url", - "version": "2.2.0", + "version": "2.3.0", "description": "WordPress URL utilities.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", From 5238c11e8ced50c8fa3550806dc2fde6757639fe Mon Sep 17 00:00:00 2001 From: Brandon Payton <brandon@happycode.net> Date: Mon, 12 Nov 2018 00:50:40 -0700 Subject: [PATCH 094/106] Avoid calling missing get_current_screen function (#11721) --- gutenberg.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/gutenberg.php b/gutenberg.php index 7e130a7034bc2..431607dcf051f 100644 --- a/gutenberg.php +++ b/gutenberg.php @@ -101,6 +101,14 @@ function is_gutenberg_page() { return false; } + /* + * There have been reports of specialized loading scenarios where `get_current_screen` + * does not exist. In these cases, it is safe to say we are not loading Gutenberg. + */ + if ( ! function_exists( 'get_current_screen' ) ) { + return false; + } + if ( get_current_screen()->base !== 'post' ) { return false; } From 567875eaa8dfddd863dd972e7e96ec4306798801 Mon Sep 17 00:00:00 2001 From: Joen Asmussen <joen@automattic.com> Date: Mon, 12 Nov 2018 11:39:00 +0100 Subject: [PATCH 095/106] Add min-width to audio block. (#11749) * Add min-width to audio block. Fixes #11740. The audio block has no intrinsic width, and collapses if we don't set an explicit min-width on it. This PR does that. * Update changelog * Move to style.scss instead. * Refactor, address feedback. --- packages/block-library/CHANGELOG.md | 6 ++++++ packages/block-library/src/audio/editor.scss | 4 ---- packages/block-library/src/audio/style.scss | 21 +++++++++++++++----- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/packages/block-library/CHANGELOG.md b/packages/block-library/CHANGELOG.md index f8694cedbcafa..4a86877ce74d4 100644 --- a/packages/block-library/CHANGELOG.md +++ b/packages/block-library/CHANGELOG.md @@ -1,3 +1,9 @@ +## 2.2.3 (Unreleased) + +### Bug Fixes + +- Add a minimum width for the audio block to fixed floated audio blocks. + ## 2.2.2 (2018-11-12) ### Polish diff --git a/packages/block-library/src/audio/editor.scss b/packages/block-library/src/audio/editor.scss index fa16dddc22471..e09d789e3c9ea 100644 --- a/packages/block-library/src/audio/editor.scss +++ b/packages/block-library/src/audio/editor.scss @@ -1,7 +1,3 @@ .wp-block-audio { margin: 0; } - -.wp-block-audio audio { - width: 100%; -} diff --git a/packages/block-library/src/audio/style.scss b/packages/block-library/src/audio/style.scss index f8a99467a694d..64d79cafd85dc 100644 --- a/packages/block-library/src/audio/style.scss +++ b/packages/block-library/src/audio/style.scss @@ -1,6 +1,17 @@ -.wp-block-audio figcaption { - margin-top: 0.5em; - color: $dark-gray-300; - text-align: center; - font-size: $default-font-size; +.wp-block-audio { + // Supply caption styles to audio blocks, even if the theme hasn't opted in. + // Reason being: the new markup, <figcaptions>, are not likely to be styled in the majority of existing themes, + // so we supply the styles so as to not appear broken or unstyled in those themes. + figcaption { + @include caption-style(); + } + + // Show full-width when not aligned. + audio { + width: 100%; + + // The browser natively applies a 300px width to the audio block. + // We restore this as a min-width instead, for alignments. + min-width: 300px; + } } From 96df8eeae90285f4b33406a068f59077397c658e Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Mon, 12 Nov 2018 11:42:35 +0100 Subject: [PATCH 096/106] Only refresh the popover if the anchor position changes (#11751) --- packages/components/CHANGELOG.md | 6 ++ packages/components/src/popover/index.js | 72 ++++++++++++++---------- 2 files changed, 49 insertions(+), 29 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 6ecf3e3961e14..b947202a49f0f 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -1,3 +1,9 @@ +## 6.0.1 (Unreleased) + +### Bug Fixes + +- Avoid constantly recomputing the popover position. + ## 6.0.0 (2018-11-12) ### Breaking Change diff --git a/packages/components/src/popover/index.js b/packages/components/src/popover/index.js index 9ad4c6143e050..be82c743af300 100644 --- a/packages/components/src/popover/index.js +++ b/packages/components/src/popover/index.js @@ -2,6 +2,7 @@ * External dependencies */ import classnames from 'classnames'; +import isShallowEqual from '@wordpress/is-shallow-equal'; /** * WordPress dependencies @@ -34,13 +35,12 @@ class Popover extends Component { constructor() { super( ...arguments ); - this.focus = this.focus.bind( this ); - this.refresh = this.refresh.bind( this ); this.getAnchorRect = this.getAnchorRect.bind( this ); - this.updatePopoverSize = this.updatePopoverSize.bind( this ); this.computePopoverPosition = this.computePopoverPosition.bind( this ); this.maybeClose = this.maybeClose.bind( this ); this.throttledRefresh = this.throttledRefresh.bind( this ); + this.refresh = this.refresh.bind( this ); + this.refreshOnAnchorMove = this.refreshOnAnchorMove.bind( this ); this.contentNode = createRef(); this.anchorNode = createRef(); @@ -55,6 +55,10 @@ class Popover extends Component { isMobile: false, popoverSize: null, }; + + // Property used keep track of the previous anchor rect + // used to compute the popover position and size. + this.anchorRect = {}; } componentDidMount() { @@ -74,7 +78,7 @@ class Popover extends Component { componentDidUpdate( prevProps ) { if ( prevProps.position !== this.props.position ) { - this.computePopoverPosition(); + this.computePopoverPosition( this.state.popoverSize, this.anchorRect ); } } @@ -99,7 +103,7 @@ class Popover extends Component { * For these situations, we refresh the popover every 0.5s */ if ( isActive ) { - this.autoRefresh = setInterval( this.throttledRefresh, 500 ); + this.autoRefresh = setInterval( this.refreshOnAnchorMove, 500 ); } else { clearInterval( this.autoRefresh ); } @@ -113,16 +117,42 @@ class Popover extends Component { this.rafHandle = window.requestAnimationFrame( this.refresh ); } + /** + * Calling refreshOnAnchorMove + * will only refresh the popover position if the anchor moves. + */ + refreshOnAnchorMove() { + const { getAnchorRect = this.getAnchorRect } = this.props; + const anchorRect = getAnchorRect( this.anchorNode.current ); + const didAnchorRectChange = ! isShallowEqual( anchorRect, this.anchorRect ); + if ( didAnchorRectChange ) { + this.anchorRect = anchorRect; + this.computePopoverPosition( this.state.popoverSize, anchorRect ); + } + } + /** * Calling `refresh()` will force the Popover to recalculate its size and * position. This is useful when a DOM change causes the anchor node to change * position. - * - * @return {void} */ refresh() { - const popoverSize = this.updatePopoverSize(); - this.computePopoverPosition( popoverSize ); + const { getAnchorRect = this.getAnchorRect } = this.props; + const anchorRect = getAnchorRect( this.anchorNode.current ); + const contentRect = this.contentNode.current.getBoundingClientRect(); + const popoverSize = { + width: contentRect.width, + height: contentRect.height, + }; + const didPopoverSizeChange = ! this.state.popoverSize || ( + popoverSize.width !== this.state.popoverSize.width || + popoverSize.height !== this.state.popoverSize.height + ); + if ( didPopoverSizeChange ) { + this.setState( { popoverSize } ); + } + this.anchorRect = anchorRect; + this.computePopoverPosition( popoverSize, anchorRect ); } focus() { @@ -174,27 +204,11 @@ class Popover extends Component { }; } - updatePopoverSize() { - const popoverSize = { - width: this.contentNode.current.scrollWidth, - height: this.contentNode.current.scrollHeight, - }; - if ( - ! this.state.popoverSize || - popoverSize.width !== this.state.popoverSize.width || - popoverSize.height !== this.state.popoverSize.height - ) { - this.setState( { popoverSize } ); - return popoverSize; - } - return this.state.popoverSize; - } - - computePopoverPosition( popoverSize ) { - const { getAnchorRect = this.getAnchorRect, position = 'top', expandOnMobile } = this.props; + computePopoverPosition( popoverSize, anchorRect ) { + const { position = 'top', expandOnMobile } = this.props; const newPopoverPosition = computePopoverPosition( - getAnchorRect( this.anchorNode.current ), - popoverSize || this.state.popoverSize, + anchorRect, + popoverSize, position, expandOnMobile ); From 4c487f782816e2b92d53b89ce45b99bf6e5c76b4 Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Mon, 12 Nov 2018 12:00:33 +0100 Subject: [PATCH 097/106] Fix multiselection for float elements (#11748) * Fix multiselection for float elements * Update changelog * Add a comment to explain the fix * revert unrelated change --- packages/editor/CHANGELOG.md | 6 ++++++ packages/editor/src/components/block-list/index.js | 12 ++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/editor/CHANGELOG.md b/packages/editor/CHANGELOG.md index cfa181b65cf68..f192a1fbcb14a 100644 --- a/packages/editor/CHANGELOG.md +++ b/packages/editor/CHANGELOG.md @@ -1,3 +1,9 @@ +## 7.0.1 (Unreleased) + +### Bug Fixes + +- Fix multi-selection triggering too often when using floated blocks. + ## 7.0.0 (2018-11-12) ### Breaking Change diff --git a/packages/editor/src/components/block-list/index.js b/packages/editor/src/components/block-list/index.js index 7d589a329fca3..b8e7e2926225f 100644 --- a/packages/editor/src/components/block-list/index.js +++ b/packages/editor/src/components/block-list/index.js @@ -22,6 +22,7 @@ import { compose } from '@wordpress/compose'; */ import BlockListBlock from './block'; import BlockListAppender from '../block-list-appender'; +import { getBlockDOMNode } from '../../utils/dom'; class BlockList extends Component { constructor( props ) { @@ -78,8 +79,15 @@ class BlockList extends Component { this.props.onStartMultiSelect(); } - const boundaries = this.nodes[ this.selectionAtStart ].getBoundingClientRect(); - const y = clientY - boundaries.top; + const blockContentBoundaries = getBlockDOMNode( this.selectionAtStart ).getBoundingClientRect(); + + // prevent multi-selection from triggering when the selected block is a float + // and the cursor is still between the top and the bottom of the block. + if ( clientY >= blockContentBoundaries.top && clientY <= blockContentBoundaries.bottom ) { + return; + } + + const y = clientY - blockContentBoundaries.top; const key = findLast( this.coordMapKeys, ( coordY ) => coordY < y ); this.onSelectionChange( this.coordMap[ key ] ); From 6ec319c6c71ed431d06911e09cc0847c1859e88d Mon Sep 17 00:00:00 2001 From: Daniel Bachhuber <daniel@bachhuber.co> Date: Mon, 12 Nov 2018 03:21:43 -0800 Subject: [PATCH 098/106] Fetch all categories to display in Latest Posts dropdown (#11654) Using `per_page=-1` triggers the `apiFetch()` middleware to resolve all items. --- packages/block-library/src/latest-posts/edit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-library/src/latest-posts/edit.js b/packages/block-library/src/latest-posts/edit.js index a9e66200ff5bb..50310e883ab3c 100644 --- a/packages/block-library/src/latest-posts/edit.js +++ b/packages/block-library/src/latest-posts/edit.js @@ -33,7 +33,7 @@ import { withSelect } from '@wordpress/data'; * Module Constants */ const CATEGORIES_LIST_QUERY = { - per_page: 100, + per_page: -1, }; const MAX_POSTS_COLUMNS = 6; From c2852c26e76470ec80a3b1c6b76974a3b21aaf0d Mon Sep 17 00:00:00 2001 From: Joost de Valk <joost@yoast.com> Date: Mon, 12 Nov 2018 12:59:46 +0100 Subject: [PATCH 099/106] Make cssnano remove all comments (#11754) * Make cssnano remove all comments As referenced by @jasmussen [here](https://github.com/WordPress/gutenberg/pull/11752#issuecomment-437832125), this should fix the fact that `/*` style comments are not removed by cssnano. * Linting. --- webpack.config.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/webpack.config.js b/webpack.config.js index afd5c17c28c93..acf041927ea48 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -177,7 +177,11 @@ const config = { if ( config.mode === 'production' ) { return postcss( [ require( 'cssnano' )( { - preset: 'default', + preset: [ 'default', { + discardComments: { + removeAll: true, + }, + } ], } ), ] ) .process( content, { from: 'src/app.css', to: 'dest/app.css' } ) From 992a43cdd8b0838ba81f9089208921adc98111c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milan=20Dini=C4=87?= <milan@srpski.biz> Date: Mon, 12 Nov 2018 14:13:07 +0100 Subject: [PATCH 100/106] Use apostrophe instead of single-quote character. (#11710) --- packages/block-library/src/missing/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/block-library/src/missing/index.js b/packages/block-library/src/missing/index.js index 82de41b1d6eff..8ec90b2d5cd44 100644 --- a/packages/block-library/src/missing/index.js +++ b/packages/block-library/src/missing/index.js @@ -17,7 +17,7 @@ function MissingBlockWarning( { attributes, convertToHTML } ) { let messageHTML; if ( hasContent && hasHTMLBlock ) { messageHTML = sprintf( - __( 'Your site doesn\'t include support for the "%s" block. You can leave this block intact, convert its content to a Custom HTML block, or remove it entirely.' ), + __( 'Your site doesn’t include support for the "%s" block. You can leave this block intact, convert its content to a Custom HTML block, or remove it entirely.' ), originalName ); actions.push( @@ -27,7 +27,7 @@ function MissingBlockWarning( { attributes, convertToHTML } ) { ); } else { messageHTML = sprintf( - __( 'Your site doesn\'t include support for the "%s" block. You can leave this block intact or remove it entirely.' ), + __( 'Your site doesn’t include support for the "%s" block. You can leave this block intact or remove it entirely.' ), originalName ); } @@ -59,7 +59,7 @@ export const settings = { name, category: 'common', title: __( 'Unrecognized Block' ), - description: __( 'Your site doesn\'t include support for this block.' ), + description: __( 'Your site doesn’t include support for this block.' ), supports: { className: false, From 0c734e1867b50a9ee1eb19a08d6b740d66c5f2ad Mon Sep 17 00:00:00 2001 From: Darren Ethier <darren@roughsmootheng.in> Date: Mon, 12 Nov 2018 09:44:35 -0500 Subject: [PATCH 101/106] Remove obsolete props being passed through to DatePicker component (#11649) * remove unnecessary locale and is12Hour passthru props to DatePicker component * add changelog entries --- packages/components/CHANGELOG.md | 4 ++++ packages/components/src/date-time/README.md | 8 -------- packages/components/src/date-time/index.js | 4 +--- packages/editor/CHANGELOG.md | 4 ++++ packages/editor/src/components/post-schedule/index.js | 1 - 5 files changed, 9 insertions(+), 12 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index b947202a49f0f..24c04a2313f25 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -14,6 +14,10 @@ ## 5.1.0 (2018-11-09) +### Polish + +- Remove `<DateTimePicker />` obsolete `locale` prop (and pass-through to child components) and obsolete `is12Hour` prop pass through to `<DateTime />` [#11649](https://github.com/WordPress/gutenberg/pull/11649) + ### New Feature - Adjust a11y roles for MenuItem component, so that aria-checked is used properly, related change in Editor/Components/BlockNavigationList ([#11431](https://github.com/WordPress/gutenberg/issues/11431)). diff --git a/packages/components/src/date-time/README.md b/packages/components/src/date-time/README.md index dc23cc9cd097f..c70c8199952b3 100644 --- a/packages/components/src/date-time/README.md +++ b/packages/components/src/date-time/README.md @@ -29,7 +29,6 @@ const MyDateTimePicker = withState( { <DateTimePicker currentDate={ date } onChange={ ( date ) => setState( { date } ) } - locale={ settings.l10n.locale } is12Hour={ is12HourTime } /> ); @@ -55,13 +54,6 @@ The function called when a new date or time has been selected. It is passed the - Required: No - Default: `noop` -### locale - -The localization for the display of the date and time. - -- Type: `string` -- Required: No - ### is12Hour Whether we use a 12-hour clock. With a 12-hour clock, an AM/PM widget is displayed and the time format is assumed to be MM-DD-YYYY. diff --git a/packages/components/src/date-time/index.js b/packages/components/src/date-time/index.js index 72268765dd405..e3782393fe2e8 100644 --- a/packages/components/src/date-time/index.js +++ b/packages/components/src/date-time/index.js @@ -34,7 +34,7 @@ export class DateTimePicker extends Component { } render() { - const { currentDate, is12Hour, locale, onChange } = this.props; + const { currentDate, is12Hour, onChange } = this.props; return ( <div className="components-datetime"> @@ -48,8 +48,6 @@ export class DateTimePicker extends Component { <DatePicker currentDate={ currentDate } onChange={ onChange } - locale={ locale } - is12Hour={ is12Hour } /> </Fragment> ) } diff --git a/packages/editor/CHANGELOG.md b/packages/editor/CHANGELOG.md index f192a1fbcb14a..9e27d05050b4c 100644 --- a/packages/editor/CHANGELOG.md +++ b/packages/editor/CHANGELOG.md @@ -34,6 +34,10 @@ ## 6.2.0 (2018-11-09) +### Polish + +- Remove unnecessary `locale` prop usage [#11649](https://github.com/WordPress/gutenberg/pull/11649) + ### New Features - Adjust a11y roles for menu items, and make sure screen readers can properly use BlockNavigationList ([#11431](https://github.com/WordPress/gutenberg/issues/11431)). diff --git a/packages/editor/src/components/post-schedule/index.js b/packages/editor/src/components/post-schedule/index.js index daf37674780ab..78a8a60c583b2 100644 --- a/packages/editor/src/components/post-schedule/index.js +++ b/packages/editor/src/components/post-schedule/index.js @@ -22,7 +22,6 @@ export function PostSchedule( { date, onUpdateDate } ) { key="date-time-picker" currentDate={ date } onChange={ onUpdateDate } - locale={ settings.l10n.locale } is12Hour={ is12HourTime } /> ); From 7bbde1c7dca0e0fd2a58917986f9dfc96030640e Mon Sep 17 00:00:00 2001 From: Darren Ethier <darren@roughsmootheng.in> Date: Mon, 12 Nov 2018 09:59:23 -0500 Subject: [PATCH 102/106] fix incorrect changelog entries (#11761) ## Description <!-- Please describe what you have changed or added --> I merged #11649 to master but the changelog entries ended up getting merged to the wrong section. --- packages/components/CHANGELOG.md | 8 ++++---- packages/editor/CHANGELOG.md | 9 +++++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 24c04a2313f25..b45cafa24f74e 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -1,5 +1,9 @@ ## 6.0.1 (Unreleased) +### Polish + +- Remove `<DateTimePicker />` obsolete `locale` prop (and pass-through to child components) and obsolete `is12Hour` prop pass through to `<DateTime />` [#11649](https://github.com/WordPress/gutenberg/pull/11649) + ### Bug Fixes - Avoid constantly recomputing the popover position. @@ -14,10 +18,6 @@ ## 5.1.0 (2018-11-09) -### Polish - -- Remove `<DateTimePicker />` obsolete `locale` prop (and pass-through to child components) and obsolete `is12Hour` prop pass through to `<DateTime />` [#11649](https://github.com/WordPress/gutenberg/pull/11649) - ### New Feature - Adjust a11y roles for MenuItem component, so that aria-checked is used properly, related change in Editor/Components/BlockNavigationList ([#11431](https://github.com/WordPress/gutenberg/issues/11431)). diff --git a/packages/editor/CHANGELOG.md b/packages/editor/CHANGELOG.md index 9e27d05050b4c..42d702f72303f 100644 --- a/packages/editor/CHANGELOG.md +++ b/packages/editor/CHANGELOG.md @@ -1,5 +1,10 @@ ## 7.0.1 (Unreleased) +### Polish + +- Remove unnecessary `locale` prop usage [#11649](https://github.com/WordPress/gutenberg/pull/11649) + + ### Bug Fixes - Fix multi-selection triggering too often when using floated blocks. @@ -34,10 +39,6 @@ ## 6.2.0 (2018-11-09) -### Polish - -- Remove unnecessary `locale` prop usage [#11649](https://github.com/WordPress/gutenberg/pull/11649) - ### New Features - Adjust a11y roles for menu items, and make sure screen readers can properly use BlockNavigationList ([#11431](https://github.com/WordPress/gutenberg/issues/11431)). From 7703a7ac110f34411ba57e172f406814f71393b5 Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Mon, 12 Nov 2018 16:38:36 +0100 Subject: [PATCH 103/106] Update Plugin Version to 4.3.0 (#11762) * Only refresh the popover if the anchor position changes (#11751) * Update plugin version to 4.3.0 --- gutenberg.php | 2 +- package-lock.json | 2 +- package.json | 2 +- packages/components/CHANGELOG.md | 10 +++++----- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/gutenberg.php b/gutenberg.php index 431607dcf051f..b71df9746d32e 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.3.0-rc.1 + * Version: 4.3.0 * Author: Gutenberg Team * * @package gutenberg diff --git a/package-lock.json b/package-lock.json index 9355492ccc340..f0115b86b1502 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "4.3.0-rc.1", + "version": "4.3.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 395b975363abf..94b27e29c9400 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "4.3.0-rc.1", + "version": "4.3.0", "private": true, "description": "A new WordPress editor experience", "repository": "git+https://github.com/WordPress/gutenberg.git", diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index b45cafa24f74e..b555b456549e2 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -1,13 +1,13 @@ ## 6.0.1 (Unreleased) -### Polish - -- Remove `<DateTimePicker />` obsolete `locale` prop (and pass-through to child components) and obsolete `is12Hour` prop pass through to `<DateTime />` [#11649](https://github.com/WordPress/gutenberg/pull/11649) - ### Bug Fixes - Avoid constantly recomputing the popover position. +### Polish + +- Remove `<DateTimePicker />` obsolete `locale` prop (and pass-through to child components) and obsolete `is12Hour` prop pass through to `<DateTime />` [#11649](https://github.com/WordPress/gutenberg/pull/11649) + ## 6.0.0 (2018-11-12) ### Breaking Change @@ -92,7 +92,7 @@ - `withAPIData` has been removed. Please use the Core Data module or `@wordpress/api-fetch` directly instead. - `Draggable` as a DOM node drag handler has been deprecated. Please, use `Draggable` as a wrap component for your DOM node drag handler. -- Change how required built-ins are polyfilled with Babel 7 ([#9171](https://github.com/WordPress/gutenberg/pull/9171)). If you're using an environment that has limited or no support for ES2015+ such as lower versions of IE then using [core-js](https://github.com/zloirock/core-js) or [@babel/polyfill](https://babeljs.io/docs/en/next/babel-polyfill) will add support for these methods. +- Change how required built-ins are polyfilled with Babel 7 ([#9171](https://github.com/WordPress/gutenberg/pull/9171)). If you're using an environment that has limited or no support for ES2015+ such as lower versions of IE then using [core-js](https://github.com/zloirock/core-js) or [@babel/polyfill](https://babeljs.io/docs/en/next/babel-polyfill) will add support for these methods. - `withContext` has been removed. Please use `wp.element.createContext` instead. See: https://reactjs.org/docs/context.html. ### New Feature From 42d1fe5cba17a111928cc3b6336790d7d72de52e Mon Sep 17 00:00:00 2001 From: Pinar Olguc <pinarolguc@gmail.com> Date: Mon, 12 Nov 2018 18:39:53 +0300 Subject: [PATCH 104/106] Add toolbar to image block (#11660) * Add toolbar to image block * Refactor edit.native.js to make it similar to web * Raplace BlockFormatControls usage with BlockControls --- .../block-library/src/image/edit.native.js | 20 ++++++++++++++++--- packages/components/src/index.native.js | 1 + 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/block-library/src/image/edit.native.js b/packages/block-library/src/image/edit.native.js index ab7af710e4e6d..fde82c5a71777 100644 --- a/packages/block-library/src/image/edit.native.js +++ b/packages/block-library/src/image/edit.native.js @@ -7,7 +7,9 @@ import RNReactNativeGutenbergBridge from 'react-native-gutenberg-bridge'; /** * Internal dependencies */ -import { MediaPlaceholder, RichText } from '@wordpress/editor'; +import { MediaPlaceholder, RichText, BlockControls } from '@wordpress/editor'; +import { Toolbar, IconButton } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; export default function ImageEdit( props ) { const { attributes, isSelected, setAttributes } = props; @@ -20,8 +22,6 @@ export default function ImageEdit( props ) { }; const onMediaLibraryPress = () => { - // Call onMediaLibraryPress from the Native<->RN bridge. It should trigger an image picker from - // the WordPress media library and call the provided callback to set the image URL. RNReactNativeGutenbergBridge.onMediaLibraryPress( ( mediaUrl ) => { if ( mediaUrl ) { setAttributes( { url: mediaUrl } ); @@ -38,8 +38,22 @@ export default function ImageEdit( props ) { ); } + const toolbarEditButton = ( + <Toolbar> + <IconButton + className="components-toolbar__control" + label={ __( 'Edit image' ) } + icon="edit" + onClick={ onMediaLibraryPress } + /> + </Toolbar> + ); + return ( <View style={ { flex: 1 } }> + <BlockControls> + { toolbarEditButton } + </BlockControls> <Image style={ { width: '100%', height: 200 } } resizeMethod="scale" diff --git a/packages/components/src/index.native.js b/packages/components/src/index.native.js index 2fd9eaa5e836e..47799cd872668 100644 --- a/packages/components/src/index.native.js +++ b/packages/components/src/index.native.js @@ -3,6 +3,7 @@ export * from './primitives'; export { default as Dashicon } from './dashicon'; export { default as Toolbar } from './toolbar'; export { default as withSpokenMessages } from './higher-order/with-spoken-messages'; +export { default as IconButton } from './icon-button'; export { createSlotFill, Slot, Fill, Provider as SlotFillProvider } from './slot-fill'; // Higher-Order Components From 77dc2841cae104dc142788dde1943f158aa7a327 Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Mon, 12 Nov 2018 16:52:03 +0100 Subject: [PATCH 105/106] chore(release): publish - @wordpress/block-library@2.2.3 - @wordpress/components@6.0.1 - @wordpress/edit-post@3.0.1 - @wordpress/editor@7.0.1 - @wordpress/format-library@1.2.1 - @wordpress/list-reusable-blocks@1.1.11 - @wordpress/nux@2.0.13 --- packages/block-library/package.json | 2 +- packages/components/package.json | 2 +- packages/edit-post/package.json | 2 +- packages/editor/package.json | 2 +- packages/format-library/package.json | 2 +- packages/list-reusable-blocks/package.json | 2 +- packages/nux/package.json | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/block-library/package.json b/packages/block-library/package.json index 9c3c5b8c785ac..2116ee1b135ca 100644 --- a/packages/block-library/package.json +++ b/packages/block-library/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-library", - "version": "2.2.2", + "version": "2.2.3", "description": "Block library for the WordPress editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/components/package.json b/packages/components/package.json index cd9718030c93c..3caadd539bfe2 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/components", - "version": "6.0.0", + "version": "6.0.1", "description": "UI components 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 bfbc8efdb363e..723690492de93 100644 --- a/packages/edit-post/package.json +++ b/packages/edit-post/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/edit-post", - "version": "3.0.0", + "version": "3.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 b68d654ca8f00..01d120ad12f8a 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/editor", - "version": "7.0.0", + "version": "7.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 db94fa6776d30..9910bd1147b89 100644 --- a/packages/format-library/package.json +++ b/packages/format-library/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/format-library", - "version": "1.2.0", + "version": "1.2.1", "description": "Format library for the WordPress editor.", "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 bfdc2b592ad48..19ed93bf185cd 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.10", + "version": "1.1.11", "description": "Adding Export/Import support to the reusable blocks listing.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/nux/package.json b/packages/nux/package.json index d7034a7469ba0..a424299d14b7e 100644 --- a/packages/nux/package.json +++ b/packages/nux/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/nux", - "version": "2.0.12", + "version": "2.0.13", "description": "NUX (New User eXperience) module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", From c1ae13ef2511768d2eec9fcb281f777e1d48e3d9 Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Mon, 12 Nov 2018 16:59:45 +0100 Subject: [PATCH 106/106] Update packages changelogs after release (#11766) --- packages/block-library/CHANGELOG.md | 4 ++-- packages/components/CHANGELOG.md | 2 +- packages/edit-post/CHANGELOG.md | 2 ++ packages/editor/CHANGELOG.md | 3 +-- packages/format-library/CHANGELOG.md | 2 ++ packages/list-reusable-blocks/CHANGELOG.md | 2 ++ packages/nux/CHANGELOG.md | 4 +++- 7 files changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/block-library/CHANGELOG.md b/packages/block-library/CHANGELOG.md index 4a86877ce74d4..251bb889d9c42 100644 --- a/packages/block-library/CHANGELOG.md +++ b/packages/block-library/CHANGELOG.md @@ -1,4 +1,4 @@ -## 2.2.3 (Unreleased) +## 2.2.3 (2018-11-12) ### Bug Fixes @@ -56,7 +56,7 @@ ### Breaking Change -- Change how required built-ins are polyfilled with Babel 7 ([#9171](https://github.com/WordPress/gutenberg/pull/9171)). If you're using an environment that has limited or no support for ES2015+ such as lower versions of IE then using [core-js](https://github.com/zloirock/core-js) or [@babel/polyfill](https://babeljs.io/docs/en/next/babel-polyfill) will add support for these methods. +- Change how required built-ins are polyfilled with Babel 7 ([#9171](https://github.com/WordPress/gutenberg/pull/9171)). If you're using an environment that has limited or no support for ES2015+ such as lower versions of IE then using [core-js](https://github.com/zloirock/core-js) or [@babel/polyfill](https://babeljs.io/docs/en/next/babel-polyfill) will add support for these methods. ### Deprecations diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index b555b456549e2..35a2b9e592d50 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -1,4 +1,4 @@ -## 6.0.1 (Unreleased) +## 6.0.1 (2018-11-12) ### Bug Fixes diff --git a/packages/edit-post/CHANGELOG.md b/packages/edit-post/CHANGELOG.md index 931e6e6b17a70..adec363e1f952 100644 --- a/packages/edit-post/CHANGELOG.md +++ b/packages/edit-post/CHANGELOG.md @@ -1,3 +1,5 @@ +## 3.0.1 (2018-11-12) + ## 3.0.0 (2018-11-12) ### Breaking Change diff --git a/packages/editor/CHANGELOG.md b/packages/editor/CHANGELOG.md index 42d702f72303f..bca353b25d5cc 100644 --- a/packages/editor/CHANGELOG.md +++ b/packages/editor/CHANGELOG.md @@ -1,10 +1,9 @@ -## 7.0.1 (Unreleased) +## 7.0.1 (2018-11-12) ### Polish - Remove unnecessary `locale` prop usage [#11649](https://github.com/WordPress/gutenberg/pull/11649) - ### Bug Fixes - Fix multi-selection triggering too often when using floated blocks. diff --git a/packages/format-library/CHANGELOG.md b/packages/format-library/CHANGELOG.md index 4a039dba8ecf2..e41d16d9c6109 100644 --- a/packages/format-library/CHANGELOG.md +++ b/packages/format-library/CHANGELOG.md @@ -1,3 +1,5 @@ +## 1.2.1 (2018-11-12) + ## 1.2.0 (2018-11-12) ## New Feature diff --git a/packages/list-reusable-blocks/CHANGELOG.md b/packages/list-reusable-blocks/CHANGELOG.md index 3a28915b10d6c..999793cb81171 100644 --- a/packages/list-reusable-blocks/CHANGELOG.md +++ b/packages/list-reusable-blocks/CHANGELOG.md @@ -1,3 +1,5 @@ +## 1.1.11 (2018-11-12) + ## 1.1.10 (2018-11-10) ## 1.1.9 (2018-11-09) diff --git a/packages/nux/CHANGELOG.md b/packages/nux/CHANGELOG.md index 3719df408a5d1..fb411f96fe44a 100644 --- a/packages/nux/CHANGELOG.md +++ b/packages/nux/CHANGELOG.md @@ -1,3 +1,5 @@ +## 2.0.13 (2018-11-12) + ## 2.0.12 (2018-11-12) ## 2.0.11 (2018-11-09) @@ -24,4 +26,4 @@ ### Breaking Change -- Change how required built-ins are polyfilled with Babel 7 ([#9171](https://github.com/WordPress/gutenberg/pull/9171)). If you're using an environment that has limited or no support for ES2015+ such as lower versions of IE then using [core-js](https://github.com/zloirock/core-js) or [@babel/polyfill](https://babeljs.io/docs/en/next/babel-polyfill) will add support for these methods. +- Change how required built-ins are polyfilled with Babel 7 ([#9171](https://github.com/WordPress/gutenberg/pull/9171)). If you're using an environment that has limited or no support for ES2015+ such as lower versions of IE then using [core-js](https://github.com/zloirock/core-js) or [@babel/polyfill](https://babeljs.io/docs/en/next/babel-polyfill) will add support for these methods.