From f3b4794c84b43044990abede0a9ed753552a2d23 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Fri, 16 Nov 2018 08:07:08 +0800 Subject: [PATCH 001/254] Fix escape key failing to close inline link popover (#11806) * Bind an onClose handler to handle use of escape key in InlineLinkUI * Add test case for escape key usage when the settings icon is focused --- packages/format-library/src/link/inline.js | 8 ++------ test/e2e/specs/links.test.js | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/packages/format-library/src/link/inline.js b/packages/format-library/src/link/inline.js index a9b8388513d605..350c94386ed857 100644 --- a/packages/format-library/src/link/inline.js +++ b/packages/format-library/src/link/inline.js @@ -14,7 +14,7 @@ import { ToggleControl, withSpokenMessages, } from '@wordpress/components'; -import { ESCAPE, LEFT, RIGHT, UP, DOWN, BACKSPACE, ENTER } from '@wordpress/keycodes'; +import { LEFT, RIGHT, UP, DOWN, BACKSPACE, ENTER } from '@wordpress/keycodes'; import { prependHTTP, safeDecodeURI, filterURLForDisplay } from '@wordpress/url'; import { create, @@ -156,11 +156,6 @@ class InlineLinkUI extends Component { } onKeyDown( event ) { - if ( event.keyCode === ESCAPE ) { - event.stopPropagation(); - this.resetState(); - } - if ( [ LEFT, DOWN, RIGHT, UP, BACKSPACE, ENTER ].indexOf( event.keyCode ) > -1 ) { // Stop the key event from propagating up to ObserveTyping.startTypingInTextField. event.stopPropagation(); @@ -247,6 +242,7 @@ class InlineLinkUI extends Component { > ( { // Expect the the escape key to dismiss the popover normally. await page.keyboard.press( 'Escape' ); expect( await page.$( '.editor-url-popover' ) ).toBeNull(); + + // Press Cmd+K to insert a link + await pressWithModifier( META_KEY, 'K' ); + + // Wait for the URL field to auto-focus + await waitForAutoFocus(); + expect( await page.$( '.editor-url-popover' ) ).not.toBeNull(); + + // Tab to the settings icon button. + await page.keyboard.press( 'Tab' ); + await page.keyboard.press( 'Tab' ); + + // Expect the the escape key to dismiss the popover normally. + await page.keyboard.press( 'Escape' ); + expect( await page.$( '.editor-url-popover' ) ).toBeNull(); } ); it( 'can be modified using the keyboard once a link has been set', async () => { From 2a0b617b784e9fc810fca05afb2167943447283e Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Fri, 16 Nov 2018 08:51:20 +0800 Subject: [PATCH 002/254] Fix url validation errors (#11835) * Fix issue where href validation would incorrectly consider any URL with a fragment to be invalid * Resolve issue where getFragment only matched to the last # encountered instead of the first * Add extra checks for http urls to ensure the right number of forward slashes are used * Update isValidProtocol to only consider the protocol as characters up to and including the colon * Update changelogs --- packages/format-library/CHANGELOG.md | 6 ++++++ packages/format-library/src/link/test/utils.js | 3 +++ packages/format-library/src/link/utils.js | 10 ++++++++-- packages/url/CHANGELOG.md | 6 ++++++ packages/url/src/index.js | 6 +++--- packages/url/src/test/index.test.js | 4 +--- 6 files changed, 27 insertions(+), 8 deletions(-) diff --git a/packages/format-library/CHANGELOG.md b/packages/format-library/CHANGELOG.md index e431bb5c30be87..eb34600a9949e1 100644 --- a/packages/format-library/CHANGELOG.md +++ b/packages/format-library/CHANGELOG.md @@ -1,3 +1,9 @@ +## 1.2.3 (Unreleased) + +### Bug fixes +- Link URL validation now works correctly when a URL includes a fragment. Previously any URL containing a fragment failed validation. +- Link URL validation catches an incorrect number of forward slashes following a url using the http protocol. + ## 1.2.2 (2018-11-15) ## 1.2.1 (2018-11-12) diff --git a/packages/format-library/src/link/test/utils.js b/packages/format-library/src/link/test/utils.js index 19feb519e6490e..aec4af14588885 100644 --- a/packages/format-library/src/link/test/utils.js +++ b/packages/format-library/src/link/test/utils.js @@ -26,6 +26,8 @@ describe( 'isValidHref', () => { 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#fragment' ) ).toBe( true ); + expect( isValidHref( 'http://test.com/path#fragment' ) ).toBe( true ); expect( isValidHref( 'http://test.com/with/path/separators' ) ).toBe( true ); expect( isValidHref( 'http://test.com/with?query=string¶ms' ) ).toBe( true ); } ); @@ -36,6 +38,7 @@ describe( 'isValidHref', () => { 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' ) ).toBe( false ); expect( isValidHref( 'http://test.com?double?params' ) ).toBe( false ); diff --git a/packages/format-library/src/link/utils.js b/packages/format-library/src/link/utils.js index 498d96e26d9a74..a516917886a951 100644 --- a/packages/format-library/src/link/utils.js +++ b/packages/format-library/src/link/utils.js @@ -37,13 +37,19 @@ export function isValidHref( href ) { return false; } - // Does the href start with something that looks like a url protocol? + // 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; } + // Add some extra checks for http(s) URIs, since these are the most common use-case. + // This ensures URIs with an http protocol have exactly two forward slashes following the protocol. + if ( startsWith( protocol, 'http' ) && ! /^https?:\/\/[^\/\s]/i.test( trimmedHref ) ) { + return false; + } + const authority = getAuthority( trimmedHref ); if ( ! isValidAuthority( authority ) ) { return false; @@ -60,7 +66,7 @@ export function isValidHref( href ) { } const fragment = getFragment( trimmedHref ); - if ( fragment && ! isValidFragment( trimmedHref ) ) { + if ( fragment && ! isValidFragment( fragment ) ) { return false; } } diff --git a/packages/url/CHANGELOG.md b/packages/url/CHANGELOG.md index f583459887f5aa..0ec863c87def5a 100644 --- a/packages/url/CHANGELOG.md +++ b/packages/url/CHANGELOG.md @@ -1,3 +1,9 @@ +## 2.3.1 (Unreleased) + +### Bug fixes +- The `isValidProtocol` function now correctly considers the protocol of the URL as only incoporating characters up to and including the colon (':'). +- `getFragment` is now greedier and matches fragments from the first occurence of the '#' symbol instead of the last. + ## 2.3.0 (2018-11-12) ### New Features diff --git a/packages/url/src/index.js b/packages/url/src/index.js index cd066baa2da436..2f399711cd637d 100644 --- a/packages/url/src/index.js +++ b/packages/url/src/index.js @@ -37,13 +37,13 @@ export function getProtocol( url ) { * * @param {string} protocol The url protocol. * - * @return {boolean} True if the argument is a valid protocol (e.g. http://, tel:). + * @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 ); + return /^[a-z\-.\+]+[0-9]*:$/i.test( protocol ); } /** @@ -138,7 +138,7 @@ export function isValidQueryString( queryString ) { * @return {?string} The fragment part of the URL. */ export function getFragment( url ) { - const matches = /^\S+(#[^\s\?]*)/.exec( url ); + const matches = /^\S+?(#[^\s\?]*)/.exec( url ); if ( matches ) { return matches[ 1 ]; } diff --git a/packages/url/src/test/index.test.js b/packages/url/src/test/index.test.js index cf4d644bfad681..eaf4287462acc9 100644 --- a/packages/url/src/test/index.test.js +++ b/packages/url/src/test/index.test.js @@ -78,9 +78,7 @@ describe( 'isValidProtocol', () => { 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( 'file:' ) ).toBe( true ); expect( isValidProtocol( 'test.protocol:' ) ).toBe( true ); expect( isValidProtocol( 'test-protocol:' ) ).toBe( true ); expect( isValidProtocol( 'test+protocol:' ) ).toBe( true ); From e747c53d47bbe0debaae34976bc09db50a23f478 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20=28Greg=29=20Zi=C3=B3=C5=82kowski?= Date: Fri, 16 Nov 2018 09:30:26 +0100 Subject: [PATCH 003/254] Update npm dependencies to fix found vulnerabilities (#11958) --- package-lock.json | 472 +++++++++++++--------- packages/jest-console/package.json | 2 +- packages/jest-preset-default/package.json | 2 +- packages/scripts/package.json | 2 +- 4 files changed, 276 insertions(+), 202 deletions(-) diff --git a/package-lock.json b/package-lock.json index 194f9cde20cf27..3225bfcd623077 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2285,12 +2285,6 @@ "lodash": "^4.17.10", "rememo": "^3.0.0", "uuid": "^3.3.2" - }, - "dependencies": { - "uuid": { - "version": "3.3.2", - "bundled": true - } } }, "@wordpress/api-fetch": { @@ -2647,7 +2641,7 @@ "dev": true, "requires": { "@babel/runtime": "^7.0.0", - "jest-matcher-utils": "^22.4.3", + "jest-matcher-utils": "^23.6.0", "lodash": "^4.17.10" } }, @@ -2656,7 +2650,7 @@ "dev": true, "requires": { "@wordpress/jest-console": "file:packages/jest-console", - "babel-jest": "^23.4.2", + "babel-jest": "^23.6.0", "enzyme": "^3.7.0", "enzyme-adapter-react-16": "^1.6.0", "jest-enzyme": "^6.0.2" @@ -2751,7 +2745,7 @@ "chalk": "^2.4.1", "cross-spawn": "^5.1.0", "eslint": "^4.19.1", - "jest": "^23.4.2", + "jest": "^23.6.0", "npm-package-json-lint": "^3.3.1", "read-pkg-up": "^1.0.1", "resolve-bin": "^0.4.0" @@ -3009,12 +3003,12 @@ "dev": true }, "append-transform": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-1.0.0.tgz", - "integrity": "sha512-P009oYkeHyU742iSZJzZZywj4QRJdnTWffaKuJQLablCZ1uz6/cW4yaRgcDaoQ+uwOxxnt0gRUcwfsNP2ri0gw==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-0.4.0.tgz", + "integrity": "sha1-126/jKlNJ24keja61EpLdKthGZE=", "dev": true, "requires": { - "default-require-extensions": "^2.0.0" + "default-require-extensions": "^1.0.0" } }, "aproba": { @@ -3680,9 +3674,9 @@ } }, "babel-jest": { - "version": "23.4.2", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-23.4.2.tgz", - "integrity": "sha512-wg1LJ2tzsafXqPFVgAsYsMCVD5U7kwJZAvbZIxVm27iOewsQw1BR7VZifDlMTEWVo3wasoPPyMdKXWCsfFPr3Q==", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-23.6.0.tgz", + "integrity": "sha512-lqKGG6LYXYu+DQh/slrQ8nxXQkEkhugdXsU6St7GmhVS7Ilc/22ArwqXNJrf0QaOBjZB0360qZMwXqDYQHXaew==", "dev": true, "requires": { "babel-plugin-istanbul": "^4.1.6", @@ -5702,12 +5696,6 @@ } } }, - "compare-versions": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.3.0.tgz", - "integrity": "sha512-MAAAIOdi2s4Gl6rZ76PNcUa9IOYB+5ICdT41o5uMRf09aEu/F9RK+qhe8RjXNPwcTjGV7KU7h2P/fljThFVqyQ==", - "dev": true - }, "component-emitter": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", @@ -6859,20 +6847,12 @@ "integrity": "sha512-95k0GDqvBjZavkuvzx/YqVLv/6YYa17fz6ILMSf7neqQITCPbnfEnQvEgMPNjH4kgobe7+WIL0yJEHku+H3qtQ==" }, "default-require-extensions": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-2.0.0.tgz", - "integrity": "sha1-9fj7sYp9bVCyH2QfZJ67Uiz+JPc=", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-1.0.0.tgz", + "integrity": "sha1-836hXT4T/9m0N9M+GnW1+5eHTLg=", "dev": true, "requires": { - "strip-bom": "^3.0.0" - }, - "dependencies": { - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true - } + "strip-bom": "^2.0.0" } }, "defaults": { @@ -7942,15 +7922,15 @@ } }, "expect": { - "version": "23.4.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-23.4.0.tgz", - "integrity": "sha1-baTsyZwUcSU+cogziYOtHrrbYMM=", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-23.6.0.tgz", + "integrity": "sha512-dgSoOHgmtn/aDGRVFWclQyPDKl2CQRq0hmIEoUAuQs/2rn2NcvCWcSCovm6BLeuB/7EZuLGu2QfnR+qRt5OM4w==", "dev": true, "requires": { "ansi-styles": "^3.2.0", - "jest-diff": "^23.2.0", + "jest-diff": "^23.6.0", "jest-get-type": "^22.1.0", - "jest-matcher-utils": "^23.2.0", + "jest-matcher-utils": "^23.6.0", "jest-message-util": "^23.4.0", "jest-regex-util": "^23.3.0" }, @@ -8000,14 +7980,14 @@ } }, "jest-matcher-utils": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-23.2.0.tgz", - "integrity": "sha1-TUmB8jIT6Tnjzt8j3DTHR7WuGRM=", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-23.6.0.tgz", + "integrity": "sha512-rosyCHQfBcol4NsckTn01cdelzWLU9Cq7aaigDf8VwwpIRvWE/9zLgX2bON+FkEW69/0UuYslUe22SOdEf2nog==", "dev": true, "requires": { "chalk": "^2.0.1", "jest-get-type": "^22.1.0", - "pretty-format": "^23.2.0" + "pretty-format": "^23.6.0" } }, "jest-message-util": { @@ -8054,9 +8034,9 @@ } }, "pretty-format": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.2.0.tgz", - "integrity": "sha1-OwqqY8AYpTWDNzwcs6XZbMXoMBc=", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.6.0.tgz", + "integrity": "sha512-zf9NV1NSlDLDjycnwm6hpFATCGl/K1lt0R/GdkAK2O5LN/rwJoB+Mh93gGJjut4YbmecbfgLWVGSTCr0Ewvvbw==", "dev": true, "requires": { "ansi-regex": "^3.0.0", @@ -11059,23 +11039,45 @@ "dev": true }, "istanbul-api": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/istanbul-api/-/istanbul-api-1.3.1.tgz", - "integrity": "sha512-duj6AlLcsWNwUpfyfHt0nWIeRiZpuShnP40YTxOGQgtaN8fd6JYSxsvxUphTDy8V5MfDXo4s/xVCIIvVCO808g==", + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/istanbul-api/-/istanbul-api-1.3.7.tgz", + "integrity": "sha512-4/ApBnMVeEPG3EkSzcw25wDe4N66wxwn+KKn6b47vyek8Xb3NBAcg4xfuQbS7BqcZuTX4wxfD5lVagdggR3gyA==", "dev": true, "requires": { "async": "^2.1.4", - "compare-versions": "^3.1.0", "fileset": "^2.0.2", - "istanbul-lib-coverage": "^1.2.0", - "istanbul-lib-hook": "^1.2.0", - "istanbul-lib-instrument": "^1.10.1", - "istanbul-lib-report": "^1.1.4", - "istanbul-lib-source-maps": "^1.2.4", - "istanbul-reports": "^1.3.0", + "istanbul-lib-coverage": "^1.2.1", + "istanbul-lib-hook": "^1.2.2", + "istanbul-lib-instrument": "^1.10.2", + "istanbul-lib-report": "^1.1.5", + "istanbul-lib-source-maps": "^1.2.6", + "istanbul-reports": "^1.5.1", "js-yaml": "^3.7.0", "mkdirp": "^0.5.1", "once": "^1.4.0" + }, + "dependencies": { + "istanbul-lib-coverage": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.1.tgz", + "integrity": "sha512-PzITeunAgyGbtY1ibVIUiV679EFChHjoMNRibEIobvmrCRaIgwLxNucOSimtNWUhEib/oO7QY2imD75JVgCJWQ==", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.2.tgz", + "integrity": "sha512-aWHxfxDqvh/ZlxR8BBaEPVSWDPUkGD63VjGQn3jcw8jCp7sHEMKcrj4xfJn/ABzdMEHiQNyvDQhqm5o8+SQg7A==", + "dev": true, + "requires": { + "babel-generator": "^6.18.0", + "babel-template": "^6.16.0", + "babel-traverse": "^6.18.0", + "babel-types": "^6.18.0", + "babylon": "^6.18.0", + "istanbul-lib-coverage": "^1.2.1", + "semver": "^5.3.0" + } + } } }, "istanbul-lib-coverage": { @@ -11085,12 +11087,12 @@ "dev": true }, "istanbul-lib-hook": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-1.2.1.tgz", - "integrity": "sha512-eLAMkPG9FU0v5L02lIkcj/2/Zlz9OuluaXikdr5iStk8FDbSwAixTK9TkYxbF0eNnzAJTwM2fkV2A1tpsIp4Jg==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-1.2.2.tgz", + "integrity": "sha512-/Jmq7Y1VeHnZEQ3TL10VHyb564mn6VrQXHchON9Jf/AEcmQ3ZIiyD1BVzNOKTZf/G3gE+kiGK6SmpF9y3qGPLw==", "dev": true, "requires": { - "append-transform": "^1.0.0" + "append-transform": "^0.4.0" } }, "istanbul-lib-instrument": { @@ -11109,12 +11111,12 @@ } }, "istanbul-lib-report": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-1.1.4.tgz", - "integrity": "sha512-Azqvq5tT0U09nrncK3q82e/Zjkxa4tkFZv7E6VcqP0QCPn6oNljDPfrZEC/umNXds2t7b8sRJfs6Kmpzt8m2kA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-1.1.5.tgz", + "integrity": "sha512-UsYfRMoi6QO/doUshYNqcKJqVmFe9w51GZz8BS3WB0lYxAllQYklka2wP9+dGZeHYaWIdcXUx8JGdbqaoXRXzw==", "dev": true, "requires": { - "istanbul-lib-coverage": "^1.2.0", + "istanbul-lib-coverage": "^1.2.1", "mkdirp": "^0.5.1", "path-parse": "^1.0.5", "supports-color": "^3.1.2" @@ -11126,6 +11128,12 @@ "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", "dev": true }, + "istanbul-lib-coverage": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.1.tgz", + "integrity": "sha512-PzITeunAgyGbtY1ibVIUiV679EFChHjoMNRibEIobvmrCRaIgwLxNucOSimtNWUhEib/oO7QY2imD75JVgCJWQ==", + "dev": true + }, "supports-color": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", @@ -11138,22 +11146,30 @@ } }, "istanbul-lib-source-maps": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.5.tgz", - "integrity": "sha512-8O2T/3VhrQHn0XcJbP1/GN7kXMiRAlPi+fj3uEHrjBD8Oz7Py0prSC25C09NuAZS6bgW1NNKAvCSHZXB0irSGA==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.6.tgz", + "integrity": "sha512-TtbsY5GIHgbMsMiRw35YBHGpZ1DVFEO19vxxeiDMYaeOFOCzfnYVxvl6pOUIZR4dtPhAGpSMup8OyF8ubsaqEg==", "dev": true, "requires": { "debug": "^3.1.0", - "istanbul-lib-coverage": "^1.2.0", + "istanbul-lib-coverage": "^1.2.1", "mkdirp": "^0.5.1", "rimraf": "^2.6.1", "source-map": "^0.5.3" + }, + "dependencies": { + "istanbul-lib-coverage": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.1.tgz", + "integrity": "sha512-PzITeunAgyGbtY1ibVIUiV679EFChHjoMNRibEIobvmrCRaIgwLxNucOSimtNWUhEib/oO7QY2imD75JVgCJWQ==", + "dev": true + } } }, "istanbul-reports": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-1.3.0.tgz", - "integrity": "sha512-y2Z2IMqE1gefWUaVjrBm0mSKvUkaBy9Vqz8iwr/r40Y9hBbIteH5wqHG/9DLTfJ9xUnUT2j7A3+VVJ6EaYBllA==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-1.5.1.tgz", + "integrity": "sha512-+cfoZ0UXzWjhAdzosCPP3AN8vvef8XDkWtTfgaN+7L3YTpNYITnCaEkceo5SEYy644VkHka/P1FvkWvrG/rrJw==", "dev": true, "requires": { "handlebars": "^4.0.3" @@ -11181,13 +11197,13 @@ } }, "jest": { - "version": "23.4.2", - "resolved": "https://registry.npmjs.org/jest/-/jest-23.4.2.tgz", - "integrity": "sha512-w10HGpVFWT1oN8B2coxeiMEsZoggkDaw3i26xHGLU+rsR+LYkBk8qpZCgi+1cD1S6ttPjZDL8E8M99lmNhgTeA==", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-23.6.0.tgz", + "integrity": "sha512-lWzcd+HSiqeuxyhG+EnZds6iO3Y3ZEnMrfZq/OTGvF/C+Z4fPMCdhWTGSAiO2Oym9rbEXfwddHhh6jqrTF3+Lw==", "dev": true, "requires": { "import-local": "^1.0.0", - "jest-cli": "^23.4.2" + "jest-cli": "^23.6.0" }, "dependencies": { "arr-diff": { @@ -11235,9 +11251,9 @@ } }, "jest-cli": { - "version": "23.4.2", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-23.4.2.tgz", - "integrity": "sha512-vaDzy0wRWrgSfz4ZImCqD2gtZqCSoEWp60y3USvGDxA2b4K0rGj2voru6a5scJFjDx5GCiNWKpz2E8IdWDVjdw==", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-23.6.0.tgz", + "integrity": "sha512-hgeD1zRUp1E1zsiyOXjEn4LzRLWdJBV//ukAHGlx6s5mfCNJTbhbHjgxnDUXA8fsKWN/HqFFF6X5XcCwC/IvYQ==", "dev": true, "requires": { "ansi-escapes": "^3.0.0", @@ -11252,18 +11268,18 @@ "istanbul-lib-instrument": "^1.10.1", "istanbul-lib-source-maps": "^1.2.4", "jest-changed-files": "^23.4.2", - "jest-config": "^23.4.2", + "jest-config": "^23.6.0", "jest-environment-jsdom": "^23.4.0", "jest-get-type": "^22.1.0", - "jest-haste-map": "^23.4.1", + "jest-haste-map": "^23.6.0", "jest-message-util": "^23.4.0", "jest-regex-util": "^23.3.0", - "jest-resolve-dependencies": "^23.4.2", - "jest-runner": "^23.4.2", - "jest-runtime": "^23.4.2", - "jest-snapshot": "^23.4.2", + "jest-resolve-dependencies": "^23.6.0", + "jest-runner": "^23.6.0", + "jest-runtime": "^23.6.0", + "jest-snapshot": "^23.6.0", "jest-util": "^23.4.0", - "jest-validate": "^23.4.0", + "jest-validate": "^23.6.0", "jest-watcher": "^23.4.0", "jest-worker": "^23.2.0", "micromatch": "^2.3.11", @@ -11324,6 +11340,18 @@ "source-map": "^0.6.0" } }, + "jest-validate": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-23.6.0.tgz", + "integrity": "sha512-OFKapYxe72yz7agrDAWi8v2WL8GIfVqcbKRCLbRG9PAxtzF9b1SEDdTpytNDN12z2fJynoBwpMpvj2R39plI2A==", + "dev": true, + "requires": { + "chalk": "^2.0.1", + "jest-get-type": "^22.1.0", + "leven": "^2.1.0", + "pretty-format": "^23.6.0" + } + }, "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", @@ -11362,7 +11390,7 @@ }, "yargs": { "version": "11.1.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz", + "resolved": "http://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz", "integrity": "sha512-NwW69J42EsCSanF8kyn5upxvjp5ds+t3+udGBeTbFnERA+lF541DDpMawzo4z6W/QrzNM18D+BPMiOBibnFV5A==", "dev": true, "requires": { @@ -11401,24 +11429,25 @@ } }, "jest-config": { - "version": "23.4.2", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-23.4.2.tgz", - "integrity": "sha512-CDJGO4H+7P+T6khaSHEjTxqVaIlmQMEFAyJFOVrAQeM+Xn12iZ+YY8Pluk1RDxi8Jqj9DoE09PHQzASCGePGtg==", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-23.6.0.tgz", + "integrity": "sha512-i8V7z9BeDXab1+VNo78WM0AtWpBRXJLnkT+lyT+Slx/cbP5sZJ0+NDuLcmBE5hXAoK0aUp7vI+MOxR+R4d8SRQ==", "dev": true, "requires": { "babel-core": "^6.0.0", - "babel-jest": "^23.4.2", + "babel-jest": "^23.6.0", "chalk": "^2.0.1", "glob": "^7.1.1", "jest-environment-jsdom": "^23.4.0", "jest-environment-node": "^23.4.0", "jest-get-type": "^22.1.0", - "jest-jasmine2": "^23.4.2", + "jest-jasmine2": "^23.6.0", "jest-regex-util": "^23.3.0", - "jest-resolve": "^23.4.1", + "jest-resolve": "^23.6.0", "jest-util": "^23.4.0", - "jest-validate": "^23.4.0", - "pretty-format": "^23.2.0" + "jest-validate": "^23.6.0", + "micromatch": "^2.3.11", + "pretty-format": "^23.6.0" }, "dependencies": { "arr-diff": { @@ -11463,6 +11492,16 @@ "source-map": "^0.5.7" } }, + "babel-jest": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-23.6.0.tgz", + "integrity": "sha512-lqKGG6LYXYu+DQh/slrQ8nxXQkEkhugdXsU6St7GmhVS7Ilc/22ArwqXNJrf0QaOBjZB0360qZMwXqDYQHXaew==", + "dev": true, + "requires": { + "babel-plugin-istanbul": "^4.1.6", + "babel-preset-jest": "^23.2.0" + } + }, "braces": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", @@ -11555,6 +11594,18 @@ } } }, + "jest-validate": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-23.6.0.tgz", + "integrity": "sha512-OFKapYxe72yz7agrDAWi8v2WL8GIfVqcbKRCLbRG9PAxtzF9b1SEDdTpytNDN12z2fJynoBwpMpvj2R39plI2A==", + "dev": true, + "requires": { + "chalk": "^2.0.1", + "jest-get-type": "^22.1.0", + "leven": "^2.1.0", + "pretty-format": "^23.6.0" + } + }, "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", @@ -11586,9 +11637,9 @@ } }, "pretty-format": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.2.0.tgz", - "integrity": "sha1-OwqqY8AYpTWDNzwcs6XZbMXoMBc=", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.6.0.tgz", + "integrity": "sha512-zf9NV1NSlDLDjycnwm6hpFATCGl/K1lt0R/GdkAK2O5LN/rwJoB+Mh93gGJjut4YbmecbfgLWVGSTCr0Ewvvbw==", "dev": true, "requires": { "ansi-regex": "^3.0.0", @@ -11662,21 +11713,21 @@ } }, "jest-diff": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-23.2.0.tgz", - "integrity": "sha1-nyz0tR4Sx5FVAgCrwWtHEwrxBio=", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-23.6.0.tgz", + "integrity": "sha512-Gz9l5Ov+X3aL5L37IT+8hoCUsof1CVYBb2QEkOupK64XyRR3h+uRpYIm97K7sY8diFxowR8pIGEdyfMKTixo3g==", "dev": true, "requires": { "chalk": "^2.0.1", "diff": "^3.2.0", "jest-get-type": "^22.1.0", - "pretty-format": "^23.2.0" + "pretty-format": "^23.6.0" }, "dependencies": { "pretty-format": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.2.0.tgz", - "integrity": "sha1-OwqqY8AYpTWDNzwcs6XZbMXoMBc=", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.6.0.tgz", + "integrity": "sha512-zf9NV1NSlDLDjycnwm6hpFATCGl/K1lt0R/GdkAK2O5LN/rwJoB+Mh93gGJjut4YbmecbfgLWVGSTCr0Ewvvbw==", "dev": true, "requires": { "ansi-regex": "^3.0.0", @@ -11695,19 +11746,19 @@ } }, "jest-each": { - "version": "23.4.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-23.4.0.tgz", - "integrity": "sha1-L6nt2J2qGk7cn/m/YGKja3E0UUM=", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-23.6.0.tgz", + "integrity": "sha512-x7V6M/WGJo6/kLoissORuvLIeAoyo2YqLOoCDkohgJ4XOXSqOtyvr8FbInlAWS77ojBsZrafbozWoKVRdtxFCg==", "dev": true, "requires": { "chalk": "^2.0.1", - "pretty-format": "^23.2.0" + "pretty-format": "^23.6.0" }, "dependencies": { "pretty-format": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.2.0.tgz", - "integrity": "sha1-OwqqY8AYpTWDNzwcs6XZbMXoMBc=", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.6.0.tgz", + "integrity": "sha512-zf9NV1NSlDLDjycnwm6hpFATCGl/K1lt0R/GdkAK2O5LN/rwJoB+Mh93gGJjut4YbmecbfgLWVGSTCr0Ewvvbw==", "dev": true, "requires": { "ansi-regex": "^3.0.0", @@ -11895,13 +11946,14 @@ "dev": true }, "jest-haste-map": { - "version": "23.4.1", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-23.4.1.tgz", - "integrity": "sha512-PGQxOEGAfRbTyJkmZeOKkVSs+KVeWgG625p89KUuq+sIIchY5P8iPIIc+Hw2tJJPBzahU3qopw1kF/qyhDdNBw==", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-23.6.0.tgz", + "integrity": "sha512-uyNhMyl6dr6HaXGHp8VF7cK6KpC6G9z9LiMNsst+rJIZ8l7wY0tk8qwjPmEghczojZ2/ZhtEdIabZ0OQRJSGGg==", "dev": true, "requires": { "fb-watchman": "^2.0.0", "graceful-fs": "^4.1.11", + "invariant": "^2.2.4", "jest-docblock": "^23.2.0", "jest-serializer": "^23.0.1", "jest-worker": "^23.2.0", @@ -11986,23 +12038,23 @@ } }, "jest-jasmine2": { - "version": "23.4.2", - "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-23.4.2.tgz", - "integrity": "sha512-MUoqn41XHMQe5u8QvRTH2HahpBNzImnnjS3pV/T7LvrCM6f2zfGdi1Pm+bRbFMLJROqR8VlK8HmsenL2WjrUIQ==", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-23.6.0.tgz", + "integrity": "sha512-pe2Ytgs1nyCs8IvsEJRiRTPC0eVYd8L/dXJGU08GFuBwZ4sYH/lmFDdOL3ZmvJR8QKqV9MFuwlsAi/EWkFUbsQ==", "dev": true, "requires": { "babel-traverse": "^6.0.0", "chalk": "^2.0.1", "co": "^4.6.0", - "expect": "^23.4.0", + "expect": "^23.6.0", "is-generator-fn": "^1.0.0", - "jest-diff": "^23.2.0", - "jest-each": "^23.4.0", - "jest-matcher-utils": "^23.2.0", + "jest-diff": "^23.6.0", + "jest-each": "^23.6.0", + "jest-matcher-utils": "^23.6.0", "jest-message-util": "^23.4.0", - "jest-snapshot": "^23.4.2", + "jest-snapshot": "^23.6.0", "jest-util": "^23.4.0", - "pretty-format": "^23.2.0" + "pretty-format": "^23.6.0" }, "dependencies": { "arr-diff": { @@ -12050,14 +12102,14 @@ } }, "jest-matcher-utils": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-23.2.0.tgz", - "integrity": "sha1-TUmB8jIT6Tnjzt8j3DTHR7WuGRM=", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-23.6.0.tgz", + "integrity": "sha512-rosyCHQfBcol4NsckTn01cdelzWLU9Cq7aaigDf8VwwpIRvWE/9zLgX2bON+FkEW69/0UuYslUe22SOdEf2nog==", "dev": true, "requires": { "chalk": "^2.0.1", "jest-get-type": "^22.1.0", - "pretty-format": "^23.2.0" + "pretty-format": "^23.6.0" } }, "jest-message-util": { @@ -12120,9 +12172,9 @@ } }, "pretty-format": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.2.0.tgz", - "integrity": "sha1-OwqqY8AYpTWDNzwcs6XZbMXoMBc=", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.6.0.tgz", + "integrity": "sha512-zf9NV1NSlDLDjycnwm6hpFATCGl/K1lt0R/GdkAK2O5LN/rwJoB+Mh93gGJjut4YbmecbfgLWVGSTCr0Ewvvbw==", "dev": true, "requires": { "ansi-regex": "^3.0.0", @@ -12138,18 +12190,18 @@ } }, "jest-leak-detector": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-23.2.0.tgz", - "integrity": "sha1-wonZYdxjjxQ1fU75bgQx7MGqN30=", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-23.6.0.tgz", + "integrity": "sha512-f/8zA04rsl1Nzj10HIyEsXvYlMpMPcy0QkQilVZDFOaPbv2ur71X5u2+C4ZQJGyV/xvVXtCCZ3wQ99IgQxftCg==", "dev": true, "requires": { - "pretty-format": "^23.2.0" + "pretty-format": "^23.6.0" }, "dependencies": { "pretty-format": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.2.0.tgz", - "integrity": "sha1-OwqqY8AYpTWDNzwcs6XZbMXoMBc=", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.6.0.tgz", + "integrity": "sha512-zf9NV1NSlDLDjycnwm6hpFATCGl/K1lt0R/GdkAK2O5LN/rwJoB+Mh93gGJjut4YbmecbfgLWVGSTCr0Ewvvbw==", "dev": true, "requires": { "ansi-regex": "^3.0.0", @@ -12159,14 +12211,14 @@ } }, "jest-matcher-utils": { - "version": "22.4.3", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-22.4.3.tgz", - "integrity": "sha512-lsEHVaTnKzdAPR5t4B6OcxXo9Vy4K+kRRbG5gtddY8lBEC+Mlpvm1CJcsMESRjzUhzkz568exMV1hTB76nAKbA==", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-23.6.0.tgz", + "integrity": "sha512-rosyCHQfBcol4NsckTn01cdelzWLU9Cq7aaigDf8VwwpIRvWE/9zLgX2bON+FkEW69/0UuYslUe22SOdEf2nog==", "dev": true, "requires": { "chalk": "^2.0.1", - "jest-get-type": "^22.4.3", - "pretty-format": "^22.4.3" + "jest-get-type": "^22.1.0", + "pretty-format": "^23.6.0" } }, "jest-message-util": { @@ -12281,9 +12333,9 @@ "dev": true }, "jest-resolve": { - "version": "23.4.1", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-23.4.1.tgz", - "integrity": "sha512-VNk4YRNR5gsHhNS0Lp46/DzTT11e+ecbUC61ikE593cKbtdrhrMO+zXkOJaE8YDD5sHxH9W6OfssNn4FkZBzZQ==", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-23.6.0.tgz", + "integrity": "sha512-XyoRxNtO7YGpQDmtQCmZjum1MljDqUCob7XlZ6jy9gsMugHdN2hY4+Acz9Qvjz2mSsOnPSH7skBmDYCHXVZqkA==", "dev": true, "requires": { "browser-resolve": "^1.11.3", @@ -12292,30 +12344,30 @@ } }, "jest-resolve-dependencies": { - "version": "23.4.2", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-23.4.2.tgz", - "integrity": "sha512-JUrU1/1mQAf0eKwKT4+RRnaqcw0UcRzRE38vyO+YnqoXUVidf646iuaKE+NG7E6Gb0+EVPOJ6TgqkaTPdQz78A==", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-23.6.0.tgz", + "integrity": "sha512-EkQWkFWjGKwRtRyIwRwI6rtPAEyPWlUC2MpzHissYnzJeHcyCn1Hc8j7Nn1xUVrS5C6W5+ZL37XTem4D4pLZdA==", "dev": true, "requires": { "jest-regex-util": "^23.3.0", - "jest-snapshot": "^23.4.2" + "jest-snapshot": "^23.6.0" } }, "jest-runner": { - "version": "23.4.2", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-23.4.2.tgz", - "integrity": "sha512-o+aEdDE7/Gyp8fLYEEf5B8aOUguz76AYcAMl5pueucey2Q50O8uUIXJ7zvt8O6OEJDztR3Kb+osMoh8MVIqgTw==", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-23.6.0.tgz", + "integrity": "sha512-kw0+uj710dzSJKU6ygri851CObtCD9cN8aNkg8jWJf4ewFyEa6kwmiH/r/M1Ec5IL/6VFa0wnAk6w+gzUtjJzA==", "dev": true, "requires": { "exit": "^0.1.2", "graceful-fs": "^4.1.11", - "jest-config": "^23.4.2", + "jest-config": "^23.6.0", "jest-docblock": "^23.2.0", - "jest-haste-map": "^23.4.1", - "jest-jasmine2": "^23.4.2", - "jest-leak-detector": "^23.2.0", + "jest-haste-map": "^23.6.0", + "jest-jasmine2": "^23.6.0", + "jest-leak-detector": "^23.6.0", "jest-message-util": "^23.4.0", - "jest-runtime": "^23.4.2", + "jest-runtime": "^23.6.0", "jest-util": "^23.4.0", "jest-worker": "^23.2.0", "source-map-support": "^0.5.6", @@ -12432,9 +12484,9 @@ "dev": true }, "source-map-support": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.6.tgz", - "integrity": "sha512-N4KXEz7jcKqPf2b2vZF11lQIz9W5ZMuUcIOGj243lduidkf2fjkVKJS9vNxVWn3u/uxX38AcE8U9nnH9FPcq+g==", + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.9.tgz", + "integrity": "sha512-gR6Rw4MvUlYy83vP0vxoVNzM6t8MUXqNuRsuBmBHQDu1Fh6X015FrLdgoDKcNdkwGubozq0P4N0Q37UyFVr1EA==", "dev": true, "requires": { "buffer-from": "^1.0.0", @@ -12444,9 +12496,9 @@ } }, "jest-runtime": { - "version": "23.4.2", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-23.4.2.tgz", - "integrity": "sha512-qaUDOi7tcdDe3MH5g5ycEslTpx0voPZvzIYbKjvWxCzCL2JEemLM+7IEe0BeLi2v5wvb/uh3dkb2wQI67uPtCw==", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-23.6.0.tgz", + "integrity": "sha512-ycnLTNPT2Gv+TRhnAYAQ0B3SryEXhhRj1kA6hBPSeZaNQkJ7GbZsxOLUkwg6YmvWGdX3BB3PYKFLDQCAE1zNOw==", "dev": true, "requires": { "babel-core": "^6.0.0", @@ -12456,14 +12508,14 @@ "exit": "^0.1.2", "fast-json-stable-stringify": "^2.0.0", "graceful-fs": "^4.1.11", - "jest-config": "^23.4.2", - "jest-haste-map": "^23.4.1", + "jest-config": "^23.6.0", + "jest-haste-map": "^23.6.0", "jest-message-util": "^23.4.0", "jest-regex-util": "^23.3.0", - "jest-resolve": "^23.4.1", - "jest-snapshot": "^23.4.2", + "jest-resolve": "^23.6.0", + "jest-snapshot": "^23.6.0", "jest-util": "^23.4.0", - "jest-validate": "^23.4.0", + "jest-validate": "^23.6.0", "micromatch": "^2.3.11", "realpath-native": "^1.0.0", "slash": "^1.0.0", @@ -12589,6 +12641,18 @@ } } }, + "jest-validate": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-23.6.0.tgz", + "integrity": "sha512-OFKapYxe72yz7agrDAWi8v2WL8GIfVqcbKRCLbRG9PAxtzF9b1SEDdTpytNDN12z2fJynoBwpMpvj2R39plI2A==", + "dev": true, + "requires": { + "chalk": "^2.0.1", + "jest-get-type": "^22.1.0", + "leven": "^2.1.0", + "pretty-format": "^23.6.0" + } + }, "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", @@ -12619,6 +12683,16 @@ "regex-cache": "^0.4.2" } }, + "pretty-format": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.6.0.tgz", + "integrity": "sha512-zf9NV1NSlDLDjycnwm6hpFATCGl/K1lt0R/GdkAK2O5LN/rwJoB+Mh93gGJjut4YbmecbfgLWVGSTCr0Ewvvbw==", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0", + "ansi-styles": "^3.2.0" + } + }, "strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -12627,7 +12701,7 @@ }, "yargs": { "version": "11.1.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz", + "resolved": "http://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz", "integrity": "sha512-NwW69J42EsCSanF8kyn5upxvjp5ds+t3+udGBeTbFnERA+lF541DDpMawzo4z6W/QrzNM18D+BPMiOBibnFV5A==", "dev": true, "requires": { @@ -12663,20 +12737,20 @@ "dev": true }, "jest-snapshot": { - "version": "23.4.2", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-23.4.2.tgz", - "integrity": "sha512-rCBxIURDlVEW1gJgJSpo8l2F2gFwp9U7Yb3CmcABUpmQ8NASpb6LJkEvtcQifAYSi22OL44TSuanq1G6x1GJwg==", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-23.6.0.tgz", + "integrity": "sha512-tM7/Bprftun6Cvj2Awh/ikS7zV3pVwjRYU2qNYS51VZHgaAMBs5l4o/69AiDHhQrj5+LA2Lq4VIvK7zYk/bswg==", "dev": true, "requires": { "babel-types": "^6.0.0", "chalk": "^2.0.1", - "jest-diff": "^23.2.0", - "jest-matcher-utils": "^23.2.0", + "jest-diff": "^23.6.0", + "jest-matcher-utils": "^23.6.0", "jest-message-util": "^23.4.0", - "jest-resolve": "^23.4.1", + "jest-resolve": "^23.6.0", "mkdirp": "^0.5.1", "natural-compare": "^1.4.0", - "pretty-format": "^23.2.0", + "pretty-format": "^23.6.0", "semver": "^5.5.0" }, "dependencies": { @@ -12725,14 +12799,14 @@ } }, "jest-matcher-utils": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-23.2.0.tgz", - "integrity": "sha1-TUmB8jIT6Tnjzt8j3DTHR7WuGRM=", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-23.6.0.tgz", + "integrity": "sha512-rosyCHQfBcol4NsckTn01cdelzWLU9Cq7aaigDf8VwwpIRvWE/9zLgX2bON+FkEW69/0UuYslUe22SOdEf2nog==", "dev": true, "requires": { "chalk": "^2.0.1", "jest-get-type": "^22.1.0", - "pretty-format": "^23.2.0" + "pretty-format": "^23.6.0" } }, "jest-message-util": { @@ -12779,9 +12853,9 @@ } }, "pretty-format": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.2.0.tgz", - "integrity": "sha1-OwqqY8AYpTWDNzwcs6XZbMXoMBc=", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.6.0.tgz", + "integrity": "sha512-zf9NV1NSlDLDjycnwm6hpFATCGl/K1lt0R/GdkAK2O5LN/rwJoB+Mh93gGJjut4YbmecbfgLWVGSTCr0Ewvvbw==", "dev": true, "requires": { "ansi-regex": "^3.0.0", @@ -13234,9 +13308,9 @@ "dev": true }, "kleur": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-2.0.1.tgz", - "integrity": "sha512-Zq/jyANIJ2uX8UZjWlqLwbyhcxSXJtT/Y89lClyeZd3l++3ztL1I5SSCYrbcbwSunTjC88N3WuMk0kRDQD6gzA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-2.0.2.tgz", + "integrity": "sha512-77XF9iTllATmG9lSlIv0qdQ2BQ/h9t0bJllHlbvsQ0zUWfU7Yi0S8L5JXzPZgkefIiajLmBJJ4BsMJmqcf7oxQ==", "dev": true }, "known-css-properties": { @@ -14522,9 +14596,9 @@ } }, "merge": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/merge/-/merge-1.2.0.tgz", - "integrity": "sha1-dTHjnUlJwoGma4xabgJl6LBYlNo=", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/merge/-/merge-1.2.1.tgz", + "integrity": "sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ==", "dev": true }, "merge-descriptors": { @@ -14977,13 +15051,13 @@ } }, "node-notifier": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.2.1.tgz", - "integrity": "sha512-MIBs+AAd6dJ2SklbbE8RUDRlIVhU8MaNLh1A9SUZDUHPiZkWLFde6UNwG41yQHZEToHgJMXqyVZ9UcS/ReOVTg==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.3.0.tgz", + "integrity": "sha512-AhENzCSGZnZJgBARsUjnQ7DnZbzyP+HxlVXuD0xqAnvL8q+OqtSX7lGg9e8nHzwXkMMXNdVeqq4E2M3EUAqX6Q==", "dev": true, "requires": { "growly": "^1.3.0", - "semver": "^5.4.1", + "semver": "^5.5.0", "shellwords": "^0.1.1", "which": "^1.3.0" } @@ -17400,9 +17474,9 @@ "dev": true }, "pretty-format": { - "version": "22.4.3", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-22.4.3.tgz", - "integrity": "sha512-S4oT9/sT6MN7/3COoOy+ZJeA92VmOnveLHgrwBE3Z1W5N9S2A1QGNYiE1z75DAENbJrXXUb+OWXhpJcg05QKQQ==", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.6.0.tgz", + "integrity": "sha512-zf9NV1NSlDLDjycnwm6hpFATCGl/K1lt0R/GdkAK2O5LN/rwJoB+Mh93gGJjut4YbmecbfgLWVGSTCr0Ewvvbw==", "dev": true, "requires": { "ansi-regex": "^3.0.0", @@ -18156,9 +18230,9 @@ } }, "realpath-native": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/realpath-native/-/realpath-native-1.0.1.tgz", - "integrity": "sha512-W14EcXuqUvKP8dkWkD7B95iMy77lpMnlFXbbk409bQtNCbeu0kvRE5reo+yIZ3JXxg6frbGsz2DLQ39lrCB40g==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/realpath-native/-/realpath-native-1.0.2.tgz", + "integrity": "sha512-+S3zTvVt9yTntFrBpm7TQmQ3tzpCrnA1a/y+3cUHAc9ZR6aIjG0WNLR+Rj79QpJktY+VeW/TQtFlQ1bzsehI8g==", "dev": true, "requires": { "util.promisify": "^1.0.0" diff --git a/packages/jest-console/package.json b/packages/jest-console/package.json index 1b94ffda29253c..a962af91ec62ff 100644 --- a/packages/jest-console/package.json +++ b/packages/jest-console/package.json @@ -26,7 +26,7 @@ "module": "build-module/index.js", "dependencies": { "@babel/runtime": "^7.0.0", - "jest-matcher-utils": "^22.4.3", + "jest-matcher-utils": "^23.6.0", "lodash": "^4.17.10" }, "peerDependencies": { diff --git a/packages/jest-preset-default/package.json b/packages/jest-preset-default/package.json index ce4dee0522c9fa..29402dfb79ec8c 100644 --- a/packages/jest-preset-default/package.json +++ b/packages/jest-preset-default/package.json @@ -27,7 +27,7 @@ "main": "index.js", "dependencies": { "@wordpress/jest-console": "file:../jest-console", - "babel-jest": "^23.4.2", + "babel-jest": "^23.6.0", "enzyme": "^3.7.0", "enzyme-adapter-react-16": "^1.6.0", "jest-enzyme": "^6.0.2" diff --git a/packages/scripts/package.json b/packages/scripts/package.json index faac1e7c8c7882..9656fa50392b1e 100644 --- a/packages/scripts/package.json +++ b/packages/scripts/package.json @@ -38,7 +38,7 @@ "chalk": "^2.4.1", "cross-spawn": "^5.1.0", "eslint": "^4.19.1", - "jest": "^23.4.2", + "jest": "^23.6.0", "npm-package-json-lint": "^3.3.1", "read-pkg-up": "^1.0.1", "resolve-bin": "^0.4.0" From c60894e71602842f98b68801ef00f1118f04a842 Mon Sep 17 00:00:00 2001 From: Jorge Date: Fri, 16 Nov 2018 10:13:49 +0000 Subject: [PATCH 004/254] Fix: Make it impossible to use the UI to add a reusable block inside the same reusable block. (#11316) It was possible to use the UI to add a reusable block inside itself. This commit addresses this problem. In order to address this problem, a new selector isAncestorOf is created. --- packages/editor/src/store/selectors.js | 27 ++++ packages/editor/src/store/test/selectors.js | 131 +++++++++++++++++++- 2 files changed, 152 insertions(+), 6 deletions(-) diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index f3e12d5485768e..85883b65c55c9b 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -1117,6 +1117,29 @@ export function getLastMultiSelectedBlockClientId( state ) { return last( getMultiSelectedBlockClientIds( state ) ) || null; } +/** + * Checks if possibleAncestorId is an ancestor of possibleDescendentId. + * + * @param {Object} state Editor state. + * @param {string} possibleAncestorId Possible ancestor client ID. + * @param {string} possibleDescendentId Possible descent client ID. + * + * @return {boolean} True if possibleAncestorId is an ancestor + * of possibleDescendentId, and false otherwise. + */ +const isAncestorOf = createSelector( + ( state, possibleAncestorId, possibleDescendentId ) => { + let idToCheck = possibleDescendentId; + while ( possibleAncestorId !== idToCheck && idToCheck ) { + idToCheck = getBlockRootClientId( state, idToCheck ); + } + return possibleAncestorId === idToCheck; + }, + ( state ) => [ + state.editor.present.blocks, + ], +); + /** * Returns true if a multi-selection exists, and the block corresponding to the * specified client ID is the first block of the multi-selection set, or false @@ -1747,6 +1770,10 @@ const canIncludeReusableBlockInInserter = ( state, reusableBlock, rootClientId ) return false; } + if ( isAncestorOf( state, reusableBlock.clientId, rootClientId ) ) { + return false; + } + return true; }; diff --git a/packages/editor/src/store/test/selectors.js b/packages/editor/src/store/test/selectors.js index 1470f2261db39c..9e7dcf6aa91072 100644 --- a/packages/editor/src/store/test/selectors.js +++ b/packages/editor/src/store/test/selectors.js @@ -4229,6 +4229,121 @@ describe( 'selectors', () => { } ); } ); + it( 'should not list a reusable block item if it is being inserted inside it self', () => { + const state = { + editor: { + present: { + blocks: { + byClientId: { + block1ref: { + name: 'core/block', + clientId: 'block1ref', + attributes: { + ref: 1, + }, + }, + itselfBlock1: { name: 'core/test-block-a' }, + itselfBlock2: { name: 'core/test-block-b' }, + }, + order: { + '': [ 'block1ref' ], + }, + }, + edits: {}, + }, + }, + initialEdits: {}, + reusableBlocks: { + data: { + 1: { clientId: 'itselfBlock1', title: 'Reusable Block 1' }, + 2: { clientId: 'itselfBlock2', title: 'Reusable Block 2' }, + }, + }, + currentPost: {}, + preferences: { + insertUsage: {}, + }, + blockListSettings: {}, + settings: {}, + }; + const items = getInserterItems( state, 'itselfBlock1' ); + const reusableBlockItems = filter( items, [ 'name', 'core/block' ] ); + expect( reusableBlockItems ).toHaveLength( 1 ); + expect( reusableBlockItems[ 0 ] ).toEqual( { + id: 'core/block/2', + name: 'core/block', + initialAttributes: { ref: 2 }, + title: 'Reusable Block 2', + icon: { + src: 'test', + }, + category: 'reusable', + keywords: [], + isDisabled: false, + utility: 0, + frecency: 0, + } ); + } ); + + it( 'should not list a reusable block item if it is being inserted inside a descendent', () => { + const state = { + editor: { + present: { + blocks: { + byClientId: { + block2ref: { + name: 'core/block', + clientId: 'block1ref', + attributes: { + ref: 2, + }, + }, + referredBlock1: { name: 'core/test-block-a' }, + referredBlock2: { name: 'core/test-block-b' }, + childReferredBlock2: { name: 'core/test-block-a' }, + grandchildReferredBlock2: { name: 'core/test-block-b' }, + }, + order: { + '': [ 'block2ref' ], + referredBlock2: [ 'childReferredBlock2' ], + childReferredBlock2: [ 'grandchildReferredBlock2' ], + }, + }, + edits: {}, + }, + }, + initialEdits: {}, + reusableBlocks: { + data: { + 1: { clientId: 'referredBlock1', title: 'Reusable Block 1' }, + 2: { clientId: 'referredBlock2', title: 'Reusable Block 2' }, + }, + }, + currentPost: {}, + preferences: { + insertUsage: {}, + }, + blockListSettings: {}, + settings: {}, + }; + const items = getInserterItems( state, 'grandchildReferredBlock2' ); + const reusableBlockItems = filter( items, [ 'name', 'core/block' ] ); + expect( reusableBlockItems ).toHaveLength( 1 ); + expect( reusableBlockItems[ 0 ] ).toEqual( { + id: 'core/block/1', + name: 'core/block', + initialAttributes: { ref: 1 }, + title: 'Reusable Block 1', + icon: { + src: 'test', + }, + category: 'reusable', + keywords: [], + isDisabled: false, + utility: 0, + frecency: 0, + } ); + } ); it( 'should order items by descending utility and frecency', () => { const state = { editor: { @@ -4278,8 +4393,12 @@ describe( 'selectors', () => { byClientId: { block1: { name: 'core/test-block-a' }, block2: { name: 'core/test-block-a' }, + block3: { name: 'core/test-block-a' }, + block4: { name: 'core/test-block-a' }, + }, + order: { + '': [ 'block3', 'block4' ], }, - order: {}, }, edits: {}, }, @@ -4302,14 +4421,14 @@ describe( 'selectors', () => { const stateSecondBlockRestricted = { ...state, blockListSettings: { - block2: { + block4: { allowedBlocks: [ 'core/test-block-b' ], }, }, }; - const firstBlockFirstCall = getInserterItems( state, 'block1' ); - const firstBlockSecondCall = getInserterItems( stateSecondBlockRestricted, 'block1' ); + const firstBlockFirstCall = getInserterItems( state, 'block3' ); + const firstBlockSecondCall = getInserterItems( stateSecondBlockRestricted, 'block3' ); expect( firstBlockFirstCall ).toBe( firstBlockSecondCall ); expect( firstBlockFirstCall.map( ( item ) => item.id ) ).toEqual( [ 'core/test-block-b', @@ -4319,8 +4438,8 @@ describe( 'selectors', () => { 'core/block/2', ] ); - const secondBlockFirstCall = getInserterItems( state, 'block2' ); - const secondBlockSecondCall = getInserterItems( stateSecondBlockRestricted, 'block2' ); + const secondBlockFirstCall = getInserterItems( state, 'block4' ); + const secondBlockSecondCall = getInserterItems( stateSecondBlockRestricted, 'block4' ); expect( secondBlockFirstCall.map( ( item ) => item.id ) ).toEqual( [ 'core/test-block-b', 'core/test-freeform', From f6351459ac82f8fdf5fd3cb6a0e72d3da9f2e32c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20Van=C2=A0Dorpe?= Date: Fri, 16 Nov 2018 13:02:30 +0100 Subject: [PATCH 005/254] RichText: fix caret position when deleting a selected word (#11965) * Add test case * Try native delete behaviour for uncollpased selection * Handle deletion if rich text is entirely selected * Remove unnecessary code --- packages/dom/src/dom.js | 16 +++- .../editor/src/components/rich-text/index.js | 10 +-- .../src/components/rich-text/tinymce.js | 9 +- .../__snapshots__/writing-flow.test.js.snap | 82 +++++++++++++++++++ test/e2e/specs/writing-flow.test.js | 60 +++++++++----- 5 files changed, 143 insertions(+), 34 deletions(-) diff --git a/packages/dom/src/dom.js b/packages/dom/src/dom.js index 9f65e47559ac33..c718cdbebe1907 100644 --- a/packages/dom/src/dom.js +++ b/packages/dom/src/dom.js @@ -497,11 +497,25 @@ export function isEntirelySelected( element ) { const { startContainer, endContainer, startOffset, endOffset } = range; - return ( + if ( startContainer === element && endContainer === element && startOffset === 0 && endOffset === element.childNodes.length + ) { + return true; + } + + const lastChild = element.lastChild; + const lastChildContentLength = lastChild.nodeType === TEXT_NODE ? + lastChild.data.length : + lastChild.childNodes.length; + + return ( + startContainer === element.firstChild && + endContainer === element.lastChild && + startOffset === 0 && + endOffset === lastChildContentLength ); } diff --git a/packages/editor/src/components/rich-text/index.js b/packages/editor/src/components/rich-text/index.js index 3bda199650d234..050421c57fdaa9 100644 --- a/packages/editor/src/components/rich-text/index.js +++ b/packages/editor/src/components/rich-text/index.js @@ -510,8 +510,8 @@ export class RichText extends Component { const start = getSelectionStart( value ); const end = getSelectionEnd( value ); - // Always handle uncollapsed selections ourselves. - if ( ! isCollapsed( value ) ) { + // Always handle full content deletion ourselves. + if ( start === 0 && end !== 0 && end === value.text.length ) { this.onChange( remove( value ) ); event.preventDefault(); return; @@ -600,12 +600,6 @@ export class RichText extends Component { * keyboard. */ onKeyUp( { keyCode } ) { - // The input event does not fire when the whole field is selected and - // BACKSPACE is pressed. - if ( keyCode === BACKSPACE ) { - this.onChange( this.createRecord() ); - } - // `scrollToRect` is called on `nodechange`, whereas calling it on // `keyup` *when* moving to a new RichText element results in incorrect // scrolling. Though the following allows false positives, it results diff --git a/packages/editor/src/components/rich-text/tinymce.js b/packages/editor/src/components/rich-text/tinymce.js index 91dc9612c9ae13..3a2fa697ec8a0c 100644 --- a/packages/editor/src/components/rich-text/tinymce.js +++ b/packages/editor/src/components/rich-text/tinymce.js @@ -10,6 +10,7 @@ import classnames from 'classnames'; */ import { Component, createElement } from '@wordpress/element'; import { BACKSPACE, DELETE, ENTER, LEFT, RIGHT } from '@wordpress/keycodes'; +import { isEntirelySelected } from '@wordpress/dom'; /** * Internal dependencies @@ -272,11 +273,13 @@ 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; + const isDelete = keyCode === DELETE || keyCode === BACKSPACE; // Disables TinyMCE behaviour. - if ( keyCode === ENTER || ( ! isCollapsed && ( keyCode === DELETE || keyCode === BACKSPACE ) ) ) { + if ( + keyCode === ENTER || + ( isDelete && isEntirelySelected( this.editorNode ) ) + ) { event.preventDefault(); // For some reason this is needed to also prevent the insertion of // line breaks. diff --git a/test/e2e/specs/__snapshots__/writing-flow.test.js.snap b/test/e2e/specs/__snapshots__/writing-flow.test.js.snap index ee6bcfc536604e..77e8a9596b7729 100644 --- a/test/e2e/specs/__snapshots__/writing-flow.test.js.snap +++ b/test/e2e/specs/__snapshots__/writing-flow.test.js.snap @@ -32,6 +32,52 @@ exports[`adding blocks should clean TinyMCE content 2`] = ` " `; +exports[`adding blocks should create valid paragraph blocks when rapidly pressing Enter 1`] = ` +" +

+ + + +

+ + + +

+ + + +

+ + + +

+ + + +

+ + + +

+ + + +

+ + + +

+ + + +

+ + + +

+" +`; + exports[`adding blocks should insert line break at end 1`] = ` "

a

@@ -75,3 +121,39 @@ exports[`adding blocks should navigate around inline boundaries 1`] = `

BeforeThird

" `; + +exports[`adding blocks should not delete surrounding space when deleting a selected word 1`] = ` +" +

alpha  gamma

+" +`; + +exports[`adding blocks should not delete surrounding space when deleting a selected word 2`] = ` +" +

alpha beta gamma

+" +`; + +exports[`adding blocks should not delete surrounding space when deleting a word with Alt+Backspace 1`] = ` +" +

alpha  gamma

+" +`; + +exports[`adding blocks should not delete surrounding space when deleting a word with Alt+Backspace 2`] = ` +" +

alpha beta gamma

+" +`; + +exports[`adding blocks should not delete surrounding space when deleting a word with Backspace 1`] = ` +" +

1  3

+" +`; + +exports[`adding blocks should not delete surrounding space when deleting a word with Backspace 2`] = ` +" +

1 2 3

+" +`; diff --git a/test/e2e/specs/writing-flow.test.js b/test/e2e/specs/writing-flow.test.js index 0da4e2f29ad8af..0da85a9ff4dbe7 100644 --- a/test/e2e/specs/writing-flow.test.js +++ b/test/e2e/specs/writing-flow.test.js @@ -224,42 +224,58 @@ describe( 'adding blocks', () => { expect( isInBlock ).toBe( true ); } ); - it( 'should not delete trailing spaces when deleting a word with backspace', async () => { + it( 'should not delete surrounding space when deleting a word with Backspace', async () => { await clickBlockAppender(); - await page.keyboard.type( '1 2 3 4' ); + await page.keyboard.type( '1 2 3' ); + await pressTimes( 'ArrowLeft', ' 3'.length ); 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' ); + + expect( await getEditedPostContent() ).toMatchSnapshot(); + + await page.keyboard.type( '2' ); + + expect( await getEditedPostContent() ).toMatchSnapshot(); } ); - it( 'should not delete trailing spaces when deleting a word with alt + backspace', async () => { + it( 'should not delete surrounding space when deleting a word with Alt+Backspace', async () => { await clickBlockAppender(); - await page.keyboard.type( 'alpha beta gamma delta' ); + await page.keyboard.type( 'alpha beta gamma' ); + await pressTimes( 'ArrowLeft', ' gamma'.length ); + 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' ); + + expect( await getEditedPostContent() ).toMatchSnapshot(); + + await page.keyboard.type( 'beta' ); + + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); + + it( 'should not delete surrounding space when deleting a selected word', async () => { + await clickBlockAppender(); + await page.keyboard.type( 'alpha beta gamma' ); + await pressTimes( 'ArrowLeft', ' gamma'.length ); + await page.keyboard.down( 'Shift' ); + await pressTimes( 'ArrowLeft', 'beta'.length ); + await page.keyboard.up( 'Shift' ); + await page.keyboard.press( 'Backspace' ); + + expect( await getEditedPostContent() ).toMatchSnapshot(); + + await page.keyboard.type( 'beta' ); + + expect( await getEditedPostContent() ).toMatchSnapshot(); } ); 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' ); + await pressTimes( 'Enter', 10 ); + // Check that none of the paragraph blocks have
in them. - const postContent = await getEditedPostContent(); - expect( postContent.indexOf( 'br' ) ).toBe( -1 ); + expect( await getEditedPostContent() ).toMatchSnapshot(); } ); } ); From 2b567e9ef2aafc92c5609c2f0c8c87c2541626a5 Mon Sep 17 00:00:00 2001 From: John Date: Fri, 16 Nov 2018 12:12:57 +0000 Subject: [PATCH 006/254] Remove absolute positions in link popover e2e test (#11968) --- test/e2e/specs/links.test.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/test/e2e/specs/links.test.js b/test/e2e/specs/links.test.js index c4379478792d85..0d2c29501103f9 100644 --- a/test/e2e/specs/links.test.js +++ b/test/e2e/specs/links.test.js @@ -450,14 +450,15 @@ describe( 'Links', () => { } ); it( 'link popover remains visible after a mouse drag event', async () => { - // Create some blocks so we components with event handlers on the page + // Create some blocks so we have components with event handlers on the page for ( let loop = 0; loop < 5; loop++ ) { await insertBlock( 'Paragraph' ); await page.keyboard.type( 'This is Gutenberg' ); } // Focus on first paragraph, so the link popover will appear over the subsequent ones - await page.mouse.click( 120, 280 ); + await page.click( '[aria-label="Block Navigation"]' ); + await page.click( '.editor-block-navigation__item button' ); // Select some text await pressWithModifier( SELECT_WORD_MODIFIER_KEYS, 'ArrowLeft' ); @@ -472,9 +473,12 @@ describe( 'Links', () => { await page.click( 'button[aria-label="Link Settings"]' ); // Move mouse over the 'open in new tab' section, then click and drag - await page.mouse.move( 50, 330 ); + const settings = await page.$( '.editor-url-popover__settings' ); + const bounds = await settings.boundingBox(); + + await page.mouse.move( bounds.x, bounds.y ); await page.mouse.down(); - await page.mouse.move( 100, 330, { steps: 10 } ); + await page.mouse.move( bounds.x + ( bounds.width / 2 ), bounds.y, { steps: 10 } ); await page.mouse.up(); // The link popover should still be visible From 68a6bd57e569b83acd8ea2c8f4a425df29565775 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20Van=C2=A0Dorpe?= Date: Fri, 16 Nov 2018 13:48:34 +0100 Subject: [PATCH 007/254] RichText: remove iOS scroll adjusting (#11971) * RichText: remove scroll adjusting * Remove isViewportSmall prop --- .../editor/src/components/rich-text/index.js | 77 +------------------ .../src/components/rich-text/tinymce.js | 2 - 2 files changed, 1 insertion(+), 78 deletions(-) diff --git a/packages/editor/src/components/rich-text/index.js b/packages/editor/src/components/rich-text/index.js index 050421c57fdaa9..ab2d64d111af1f 100644 --- a/packages/editor/src/components/rich-text/index.js +++ b/packages/editor/src/components/rich-text/index.js @@ -14,11 +14,7 @@ import memize from 'memize'; * WordPress dependencies */ import { Component, Fragment, RawHTML } from '@wordpress/element'; -import { - isHorizontalEdge, - getRectangleFromRange, - getScrollContainer, -} from '@wordpress/dom'; +import { isHorizontalEdge } from '@wordpress/dom'; import { createBlobURL } from '@wordpress/blob'; import { BACKSPACE, DELETE, ENTER } from '@wordpress/keycodes'; import { withDispatch, withSelect } from '@wordpress/data'; @@ -94,10 +90,8 @@ export class RichText extends Component { this.onSetup = this.onSetup.bind( this ); this.onFocus = this.onFocus.bind( this ); this.onChange = this.onChange.bind( this ); - this.onNodeChange = this.onNodeChange.bind( this ); this.onDeleteKeyDown = this.onDeleteKeyDown.bind( this ); this.onKeyDown = this.onKeyDown.bind( this ); - this.onKeyUp = this.onKeyUp.bind( this ); this.onPaste = this.onPaste.bind( this ); this.onCreateUndoLevel = this.onCreateUndoLevel.bind( this ); this.setFocusedElement = this.setFocusedElement.bind( this ); @@ -156,7 +150,6 @@ export class RichText extends Component { */ onSetup( editor ) { this.editor = editor; - editor.on( 'nodechange', this.onNodeChange ); } setFocusedElement() { @@ -593,48 +586,6 @@ export class RichText extends Component { } } - /** - * Handles a keyup event. - * - * @param {number} $1.keyCode The key code that has been pressed on the - * keyboard. - */ - onKeyUp( { keyCode } ) { - // `scrollToRect` is called on `nodechange`, whereas calling it on - // `keyup` *when* moving to a new RichText element results in incorrect - // scrolling. Though the following allows false positives, it results - // in much smoother scrolling. - if ( this.props.isViewportSmall && keyCode !== BACKSPACE && keyCode !== ENTER ) { - this.scrollToRect( getRectangleFromRange( this.editor.selection.getRng() ) ); - } - } - - scrollToRect( rect ) { - const { top: caretTop } = rect; - const container = getScrollContainer( this.editableRef ); - - if ( ! container ) { - return; - } - - // When scrolling, avoid positioning the caret at the very top of - // the viewport, providing some "air" and some textual context for - // the user, and avoiding toolbars. - const graceOffset = 100; - - // Avoid pointless scrolling by establishing a threshold under - // which scrolling should be skipped; - const epsilon = 10; - const delta = caretTop - graceOffset; - - if ( Math.abs( delta ) > epsilon ) { - container.scrollTo( - container.scrollLeft, - container.scrollTop + delta, - ); - } - } - /** * Splits the content at the location of the selection. * @@ -683,29 +634,6 @@ export class RichText extends Component { this.onSplit( before, after, ...blocks ); } - onNodeChange( { parents } ) { - if ( ! this.isActive() ) { - return; - } - - if ( this.props.isViewportSmall ) { - let rect; - const selectedAnchor = find( parents, ( node ) => node.tagName === 'A' ); - if ( selectedAnchor ) { - // If we selected a link, position the Link UI below the link - rect = selectedAnchor.getBoundingClientRect(); - } else { - // Otherwise, position the Link UI below the cursor or text selection - rect = getRectangleFromRange( this.editor.selection.getRng() ); - } - - // Originally called on `focusin`, that hook turned out to be - // premature. On `nodechange` we can work with the finalized TinyMCE - // instance and scroll to proper position. - this.scrollToRect( rect ); - } - } - componentDidUpdate( prevProps ) { const { tagName, value, isSelected } = this.props; @@ -901,7 +829,6 @@ export class RichText extends Component { onInput={ this.onInput } onCompositionEnd={ this.onCompositionEnd } onKeyDown={ this.onKeyDown } - onKeyUp={ this.onKeyUp } onFocus={ this.onFocus } multilineTag={ this.multilineTag } multilineWrapperTags={ this.multilineWrapperTags } @@ -956,11 +883,9 @@ const RichTextContainer = compose( [ }; } ), withSelect( ( select ) => { - const { isViewportMatch } = select( 'core/viewport' ); const { canUserUseUnfilteredHTML, isCaretWithinFormattedText } = select( 'core/editor' ); return { - isViewportSmall: isViewportMatch( '< small' ), canUserUseUnfilteredHTML: canUserUseUnfilteredHTML(), isCaretWithinFormattedText: isCaretWithinFormattedText(), }; diff --git a/packages/editor/src/components/rich-text/tinymce.js b/packages/editor/src/components/rich-text/tinymce.js index 3a2fa697ec8a0c..0085343bdbd27f 100644 --- a/packages/editor/src/components/rich-text/tinymce.js +++ b/packages/editor/src/components/rich-text/tinymce.js @@ -339,7 +339,6 @@ export default class TinyMCE extends Component { onPaste, onInput, onKeyDown, - onKeyUp, onCompositionEnd, } = this.props; @@ -369,7 +368,6 @@ export default class TinyMCE extends Component { onInput, onFocus: this.onFocus, onKeyDown, - onKeyUp, onCompositionEnd, } ); } From 9591c904b03c7a82dd06db210316360d5d138de2 Mon Sep 17 00:00:00 2001 From: Matias Ventura Date: Fri, 16 Nov 2018 10:41:50 -0300 Subject: [PATCH 008/254] Fix missing margin on placeholder with block icons. (#11947) --- packages/components/src/placeholder/style.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/components/src/placeholder/style.scss b/packages/components/src/placeholder/style.scss index 62d23e704bbd65..1d2f079a1dd37b 100644 --- a/packages/components/src/placeholder/style.scss +++ b/packages/components/src/placeholder/style.scss @@ -25,7 +25,8 @@ font-weight: 600; margin-bottom: 1em; - .dashicon { + .dashicon, + .editor-block-icon { margin-right: 1ch; } } From 1dfee170884746499aa07b80c78323abea1ba15f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20Van=C2=A0Dorpe?= Date: Fri, 16 Nov 2018 16:55:40 +0100 Subject: [PATCH 009/254] RichText: clean up: remove paste timeouts (#11978) --- packages/editor/src/components/rich-text/index.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/editor/src/components/rich-text/index.js b/packages/editor/src/components/rich-text/index.js index ab2d64d111af1f..e6722e86377eb4 100644 --- a/packages/editor/src/components/rich-text/index.js +++ b/packages/editor/src/components/rich-text/index.js @@ -263,24 +263,23 @@ export class RichText extends Component { window.console.log( 'Received item:\n\n', file ); if ( shouldReplace ) { - // Necessary to allow the paste bin to be removed without errors. - this.props.setTimeout( () => this.props.onReplace( content ) ); + this.props.onReplace( content ); } else if ( this.onSplit ) { - // Necessary to get the right range. - // Also done in the TinyMCE paste plugin. - this.props.setTimeout( () => this.splitContent( content ) ); + this.splitContent( content ); } return; } + const record = this.getRecord(); + // There is a selection, check if a URL is pasted. - if ( ! this.editor.selection.isCollapsed() ) { + if ( ! isCollapsed( record ) ) { const pastedText = ( html || plainText ).replace( /<[^>]+>/g, '' ).trim(); // A URL was pasted, turn the selection into a link if ( isURL( pastedText ) ) { - this.onChange( applyFormat( this.getRecord(), { + this.onChange( applyFormat( record, { type: 'a', attributes: { href: decodeEntities( pastedText ), @@ -314,7 +313,7 @@ export class RichText extends Component { if ( typeof content === 'string' ) { const recordToInsert = create( { html: content } ); - this.onChange( insert( this.getRecord(), recordToInsert ) ); + this.onChange( insert( record, recordToInsert ) ); } else if ( this.onSplit ) { if ( ! content.length ) { return; From 745eda13fc2475d73bb8ea623798c78df9df7820 Mon Sep 17 00:00:00 2001 From: Kjell Reigstad Date: Fri, 16 Nov 2018 12:24:48 -0500 Subject: [PATCH 010/254] Override our global aligncenter/alignleft/alignright display property for the cover block (#11990) Previously, center/left/right-aligned cover blocks were ignoring our intended display rule. This ensures they appear as intended. --- 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 3ef5a402056b09..806ed02fb2dc9b 100644 --- a/packages/block-library/src/cover/style.scss +++ b/packages/block-library/src/cover/style.scss @@ -95,6 +95,13 @@ max-width: $content-width / 2; width: 100%; } + + // Aligned cover blocks should not use our global alignment rules + &.aligncenter, + &.alignleft, + &.alignright { + display: flex; + } } .wp-block-cover__video-background { From daa2e8d4d6e2642fbab550f6a111e2a6e119f0e6 Mon Sep 17 00:00:00 2001 From: Joen Asmussen Date: Fri, 16 Nov 2018 18:57:31 +0100 Subject: [PATCH 011/254] Try: Only enforce embed min-width after mobile breakpoint (#11961) Maybe fixes #10310. If an instagram embed is smaller than 326px, it gets cropped. That's why we have a 360px min-width, which is a width that is friendly to most phones. However an editor style can be mobile-unfriendly, with wide left and right paddings. If you insert an embed in such a post, it's born with that 360px min-width and will zoom the browser. This PR makes a decision: It is better that the embed looks broken, than the editor breaks. As such, we allow embeds to be cropped on the mobile breakpoint. At least than a user can set that embed to be fullwide to give it extra space on mobile. --- packages/block-library/src/embed/editor.scss | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/block-library/src/embed/editor.scss b/packages/block-library/src/embed/editor.scss index f56a9f6b35a306..0b79d5b0414beb 100644 --- a/packages/block-library/src/embed/editor.scss +++ b/packages/block-library/src/embed/editor.scss @@ -7,7 +7,9 @@ // Apply a min-width, or the embed can collapse when floated. // Instagram widgets have a min-width of 326px, so go a bit beyond that. - min-width: 360px; + @include break-small() { + min-width: 360px; + } &.is-loading { display: flex; From f9ddd68820b60c98c8ca490e3bc2c185d76ad00d Mon Sep 17 00:00:00 2001 From: mzorz Date: Fri, 16 Nov 2018 15:07:33 -0300 Subject: [PATCH 012/254] [RNMobile] add isSelected prop to PlainText-backed blocks (#11989) * handling isSelected in PlainText primitive through a deferred call to focus() on React Native TextInput * adds isSelected prop support for Code block * added isSelected prop to More block * promoted PlainText native implentation to extend Component and reuse ReactNative lifecycle * fixed lint warnings * get style from props --- .../block-library/src/code/edit.native.js | 5 ++- .../block-library/src/more/edit.native.js | 4 +- .../src/components/plain-text/index.native.js | 37 ++++++++++++++----- 3 files changed, 34 insertions(+), 12 deletions(-) diff --git a/packages/block-library/src/code/edit.native.js b/packages/block-library/src/code/edit.native.js index b6f4a8e541b37b..40f1225b2ae755 100644 --- a/packages/block-library/src/code/edit.native.js +++ b/packages/block-library/src/code/edit.native.js @@ -20,7 +20,9 @@ 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 } ) { +export default function CodeEdit( props ) { + const { attributes, setAttributes, style } = props; + return ( setAttributes( { content } ) } placeholder={ __( 'Write code…' ) } aria-label={ __( 'Code' ) } + isSelected={ props.isSelected } /> </View> ); diff --git a/packages/block-library/src/more/edit.native.js b/packages/block-library/src/more/edit.native.js index c2e8d36eae0d36..2c4d8bf89a693a 100644 --- a/packages/block-library/src/more/edit.native.js +++ b/packages/block-library/src/more/edit.native.js @@ -14,7 +14,8 @@ import { __ } from '@wordpress/i18n'; import { PlainText } from '@wordpress/editor'; import styles from './editor.scss'; -export default function MoreEdit( { attributes, setAttributes } ) { +export default function MoreEdit( props ) { + const { attributes, setAttributes } = props; const { customText } = attributes; const defaultText = __( 'Read more' ); const value = customText !== undefined ? customText : defaultText; @@ -30,6 +31,7 @@ export default function MoreEdit( { attributes, setAttributes } ) { underlineColorAndroid="transparent" onChange={ ( newValue ) => setAttributes( { customText: newValue } ) } placeholder={ defaultText } + isSelected={ props.isSelected } /> <Text className={ styles[ 'block-library-more__right-marker' ] }>--&gt;</Text> </View> diff --git a/packages/editor/src/components/plain-text/index.native.js b/packages/editor/src/components/plain-text/index.native.js index c9ee8c69780716..ae98e6a3f9ce6a 100644 --- a/packages/editor/src/components/plain-text/index.native.js +++ b/packages/editor/src/components/plain-text/index.native.js @@ -3,19 +3,36 @@ */ import { TextInput } from 'react-native'; +/** + * WordPress dependencies + */ +import { Component } from '@wordpress/element'; + /** * Internal dependencies */ import styles from './style.scss'; -function PlainText( { onChange, className, ...props } ) { - return ( - <TextInput - className={ [ styles[ 'editor-plain-text' ], className ] } - onChangeText={ ( text ) => onChange( text ) } - { ...props } - /> - ); -} +export default class PlainText extends Component { + componentDidMount() { + // if isSelected is true, we should request the focus on this TextInput + if ( ( this._input.isFocused() === false ) && ( this._input.props.isSelected === true ) ) { + this.focus(); + } + } + + focus() { + this._input.focus(); + } -export default PlainText; + render() { + return ( + <TextInput + ref={ ( x ) => this._input = x } + className={ [ styles[ 'editor-plain-text' ], this.props.className ] } + onChangeText={ ( text ) => this.props.onChange( text ) } + { ...this.props } + /> + ); + } +} From a2ecd1f1834ded71f7effcfb35fa25f499994c9a Mon Sep 17 00:00:00 2001 From: Dixita Dusara <dixitadusara@gmail.com> Date: Sat, 17 Nov 2018 00:51:21 +0530 Subject: [PATCH 013/254] Fix: superfluous word in block description "Media & Text"(#11994) ## Description Fix issue: #11986 --- packages/block-library/src/media-text/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-library/src/media-text/index.js b/packages/block-library/src/media-text/index.js index 79e472456daff9..9a1f7151e7de55 100644 --- a/packages/block-library/src/media-text/index.js +++ b/packages/block-library/src/media-text/index.js @@ -27,7 +27,7 @@ export const name = 'core/media-text'; export const settings = { title: __( 'Media & Text' ), - description: __( 'Set media and words side-by-side media for a richer layout.' ), + description: __( 'Set media and words side-by-side for a richer layout.' ), icon: <SVG xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><Path d="M13 17h8v-2h-8v2zM3 19h8V5H3v14zM13 9h8V7h-8v2zm0 4h8v-2h-8v2z" /></SVG>, From 7e814e393b2f2a842c57980d2d09dc0f8d630722 Mon Sep 17 00:00:00 2001 From: Andrew Ozz <azaozz@users.noreply.github.com> Date: Fri, 16 Nov 2018 22:25:05 +0200 Subject: [PATCH 014/254] Add `wp-image-####` CSS class to images in the Media and Text block (#11922) * Add `wp-image-####` class name to images. Needed to add `srcset` and `sizes` attributes to the images on the front-end. --- .../block-library/src/media-text/index.js | 139 ++++++++++++------ .../fixtures/core__media-text.html | 2 +- .../fixtures/core__media-text.json | 2 +- .../fixtures/core__media-text.parsed.json | 4 +- .../fixtures/core__media-text.serialized.html | 2 +- .../core__media-text__image-alt-no-align.html | 2 +- .../core__media-text__image-alt-no-align.json | 2 +- ...media-text__image-alt-no-align.parsed.json | 4 +- ...a-text__image-alt-no-align.serialized.html | 2 +- 9 files changed, 104 insertions(+), 55 deletions(-) diff --git a/packages/block-library/src/media-text/index.js b/packages/block-library/src/media-text/index.js index 9a1f7151e7de55..281db991ae268a 100644 --- a/packages/block-library/src/media-text/index.js +++ b/packages/block-library/src/media-text/index.js @@ -24,6 +24,50 @@ const DEFAULT_MEDIA_WIDTH = 50; export const name = 'core/media-text'; +const blockAttributes = { + align: { + type: 'string', + default: 'wide', + }, + backgroundColor: { + type: 'string', + }, + customBackgroundColor: { + type: 'string', + }, + mediaAlt: { + type: 'string', + source: 'attribute', + selector: 'figure img', + attribute: 'alt', + default: '', + }, + mediaPosition: { + type: 'string', + default: 'left', + }, + mediaId: { + type: 'number', + }, + mediaUrl: { + type: 'string', + source: 'attribute', + selector: 'figure video,figure img', + attribute: 'src', + }, + mediaType: { + type: 'string', + }, + mediaWidth: { + type: 'number', + default: 50, + }, + isStackedOnMobile: { + type: 'boolean', + default: false, + }, +}; + export const settings = { title: __( 'Media & Text' ), @@ -35,49 +79,7 @@ export const settings = { keywords: [ __( 'image' ), __( 'video' ) ], - attributes: { - align: { - type: 'string', - default: 'wide', - }, - backgroundColor: { - type: 'string', - }, - customBackgroundColor: { - type: 'string', - }, - mediaAlt: { - type: 'string', - source: 'attribute', - selector: 'figure img', - attribute: 'alt', - default: '', - }, - mediaPosition: { - type: 'string', - default: 'left', - }, - mediaId: { - type: 'number', - }, - mediaUrl: { - type: 'string', - source: 'attribute', - selector: 'figure video,figure img', - attribute: 'src', - }, - mediaType: { - type: 'string', - }, - mediaWidth: { - type: 'number', - default: 50, - }, - isStackedOnMobile: { - type: 'boolean', - default: false, - }, - }, + attributes: blockAttributes, supports: { align: [ 'wide', 'full' ], @@ -152,12 +154,12 @@ export const settings = { mediaType, mediaUrl, mediaWidth, + mediaId, } = attributes; const mediaTypeRenders = { - image: () => <img src={ mediaUrl } alt={ mediaAlt } />, + image: () => <img src={ mediaUrl } alt={ mediaAlt } className={ ( mediaId && mediaType === 'image' ) ? `wp-image-${ mediaId }` : null } />, video: () => <video controls src={ mediaUrl } />, }; - const backgroundClass = getColorClassName( 'background-color', backgroundColor ); const className = classnames( { 'has-media-on-the-right': 'right' === mediaPosition, @@ -184,4 +186,51 @@ export const settings = { </div> ); }, + + deprecated: [ + { + attributes: blockAttributes, + save( { attributes } ) { + const { + backgroundColor, + customBackgroundColor, + isStackedOnMobile, + mediaAlt, + mediaPosition, + mediaType, + mediaUrl, + mediaWidth, + } = attributes; + const mediaTypeRenders = { + image: () => <img src={ mediaUrl } alt={ mediaAlt } />, + video: () => <video controls src={ mediaUrl } />, + }; + const backgroundClass = getColorClassName( 'background-color', backgroundColor ); + const className = classnames( { + 'has-media-on-the-right': 'right' === mediaPosition, + [ backgroundClass ]: backgroundClass, + 'is-stacked-on-mobile': isStackedOnMobile, + } ); + + let gridTemplateColumns; + if ( mediaWidth !== DEFAULT_MEDIA_WIDTH ) { + gridTemplateColumns = 'right' === mediaPosition ? `auto ${ mediaWidth }%` : `${ mediaWidth }% auto`; + } + const style = { + backgroundColor: backgroundClass ? undefined : customBackgroundColor, + gridTemplateColumns, + }; + return ( + <div className={ className } style={ style }> + <figure className="wp-block-media-text__media" > + { ( mediaTypeRenders[ mediaType ] || noop )() } + </figure> + <div className="wp-block-media-text__content"> + <InnerBlocks.Content /> + </div> + </div> + ); + }, + }, + ], }; diff --git a/test/integration/full-content/fixtures/core__media-text.html b/test/integration/full-content/fixtures/core__media-text.html index 63d279b9db438c..ec516a98eb94df 100644 --- a/test/integration/full-content/fixtures/core__media-text.html +++ b/test/integration/full-content/fixtures/core__media-text.html @@ -1,7 +1,7 @@ <!-- wp:media-text {"mediaId":17985,"mediaType":"image"} --> <div class="wp-block-media-text alignwide"> <figure class="wp-block-media-text__media"> - <img src="http://localhost/wp-content/uploads/2018/09/1600px-Mount_Everest_as_seen_from_Drukair2_PLW_edit.jpg" alt=""/> + <img src="http://localhost/wp-content/uploads/2018/09/1600px-Mount_Everest_as_seen_from_Drukair2_PLW_edit.jpg" alt="" class="wp-image-17985"/> </figure> <div class="wp-block-media-text__content"> <!-- wp:paragraph {"placeholder":"Content…","fontSize":"large"} --> diff --git a/test/integration/full-content/fixtures/core__media-text.json b/test/integration/full-content/fixtures/core__media-text.json index 2b892048ecdde3..09e4f0c6f68f38 100644 --- a/test/integration/full-content/fixtures/core__media-text.json +++ b/test/integration/full-content/fixtures/core__media-text.json @@ -28,6 +28,6 @@ "originalContent": "<p class=\"has-large-font-size\">My Content</p>" } ], - "originalContent": "<div class=\"wp-block-media-text alignwide\">\n\t<figure class=\"wp-block-media-text__media\">\n\t\t<img src=\"http://localhost/wp-content/uploads/2018/09/1600px-Mount_Everest_as_seen_from_Drukair2_PLW_edit.jpg\" alt=\"\"/>\n\t</figure>\n\t<div class=\"wp-block-media-text__content\">\n\t\t\n\t</div>\n</div>" + "originalContent": "<div class=\"wp-block-media-text alignwide\">\n\t<figure class=\"wp-block-media-text__media\">\n\t\t<img src=\"http://localhost/wp-content/uploads/2018/09/1600px-Mount_Everest_as_seen_from_Drukair2_PLW_edit.jpg\" alt=\"\" class=\"wp-image-17985\"/>\n\t</figure>\n\t<div class=\"wp-block-media-text__content\">\n\t\t\n\t</div>\n</div>" } ] 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 ec0bf62e613174..7c758d02aa73a7 100644 --- a/test/integration/full-content/fixtures/core__media-text.parsed.json +++ b/test/integration/full-content/fixtures/core__media-text.parsed.json @@ -19,9 +19,9 @@ ] } ], - "innerHTML": "\n<div class=\"wp-block-media-text alignwide\">\n\t<figure class=\"wp-block-media-text__media\">\n\t\t<img src=\"http://localhost/wp-content/uploads/2018/09/1600px-Mount_Everest_as_seen_from_Drukair2_PLW_edit.jpg\" alt=\"\"/>\n\t</figure>\n\t<div class=\"wp-block-media-text__content\">\n\t\t\n\t</div>\n</div>\n", + "innerHTML": "\n<div class=\"wp-block-media-text alignwide\">\n\t<figure class=\"wp-block-media-text__media\">\n\t\t<img src=\"http://localhost/wp-content/uploads/2018/09/1600px-Mount_Everest_as_seen_from_Drukair2_PLW_edit.jpg\" alt=\"\" class=\"wp-image-17985\"/>\n\t</figure>\n\t<div class=\"wp-block-media-text__content\">\n\t\t\n\t</div>\n</div>\n", "innerContent": [ - "\n<div class=\"wp-block-media-text alignwide\">\n\t<figure class=\"wp-block-media-text__media\">\n\t\t<img src=\"http://localhost/wp-content/uploads/2018/09/1600px-Mount_Everest_as_seen_from_Drukair2_PLW_edit.jpg\" alt=\"\"/>\n\t</figure>\n\t<div class=\"wp-block-media-text__content\">\n\t\t", + "\n<div class=\"wp-block-media-text alignwide\">\n\t<figure class=\"wp-block-media-text__media\">\n\t\t<img src=\"http://localhost/wp-content/uploads/2018/09/1600px-Mount_Everest_as_seen_from_Drukair2_PLW_edit.jpg\" alt=\"\" class=\"wp-image-17985\"/>\n\t</figure>\n\t<div class=\"wp-block-media-text__content\">\n\t\t", null, "\n\t</div>\n</div>\n" ] diff --git a/test/integration/full-content/fixtures/core__media-text.serialized.html b/test/integration/full-content/fixtures/core__media-text.serialized.html index 5e70ba8e96eaa3..51d3cce84b74e6 100644 --- a/test/integration/full-content/fixtures/core__media-text.serialized.html +++ b/test/integration/full-content/fixtures/core__media-text.serialized.html @@ -1,5 +1,5 @@ <!-- wp:media-text {"mediaId":17985,"mediaType":"image"} --> -<div class="wp-block-media-text alignwide"><figure class="wp-block-media-text__media"><img src="http://localhost/wp-content/uploads/2018/09/1600px-Mount_Everest_as_seen_from_Drukair2_PLW_edit.jpg" alt=""/></figure><div class="wp-block-media-text__content"><!-- wp:paragraph {"placeholder":"Content…","fontSize":"large"} --> +<div class="wp-block-media-text alignwide"><figure class="wp-block-media-text__media"><img src="http://localhost/wp-content/uploads/2018/09/1600px-Mount_Everest_as_seen_from_Drukair2_PLW_edit.jpg" alt="" class="wp-image-17985"/></figure><div class="wp-block-media-text__content"><!-- wp:paragraph {"placeholder":"Content…","fontSize":"large"} --> <p class="has-large-font-size">My Content</p> <!-- /wp:paragraph --></div></div> <!-- /wp:media-text --> diff --git a/test/integration/full-content/fixtures/core__media-text__image-alt-no-align.html b/test/integration/full-content/fixtures/core__media-text__image-alt-no-align.html index 66e424e3b148a3..0c383e4537a57b 100644 --- a/test/integration/full-content/fixtures/core__media-text__image-alt-no-align.html +++ b/test/integration/full-content/fixtures/core__media-text__image-alt-no-align.html @@ -1,7 +1,7 @@ <!-- wp:media-text {"align":"none","mediaId":17985,"mediaType":"image"} --> <div class="wp-block-media-text"> <figure class="wp-block-media-text__media"> - <img src="http://localhost/wp-content/uploads/2018/09/1600px-Mount_Everest_as_seen_from_Drukair2_PLW_edit.jpg" alt="my alt"/> + <img src="http://localhost/wp-content/uploads/2018/09/1600px-Mount_Everest_as_seen_from_Drukair2_PLW_edit.jpg" alt="my alt" class="wp-image-17985" /> </figure> <div class="wp-block-media-text__content"> <!-- wp:paragraph {"placeholder":"Content…","fontSize":"large"} --> diff --git a/test/integration/full-content/fixtures/core__media-text__image-alt-no-align.json b/test/integration/full-content/fixtures/core__media-text__image-alt-no-align.json index 548f2dbd28f18d..0efacb35ee5b02 100644 --- a/test/integration/full-content/fixtures/core__media-text__image-alt-no-align.json +++ b/test/integration/full-content/fixtures/core__media-text__image-alt-no-align.json @@ -28,6 +28,6 @@ "originalContent": "<p class=\"has-large-font-size\">Content</p>" } ], - "originalContent": "<div class=\"wp-block-media-text\">\n\t<figure class=\"wp-block-media-text__media\">\n\t\t<img src=\"http://localhost/wp-content/uploads/2018/09/1600px-Mount_Everest_as_seen_from_Drukair2_PLW_edit.jpg\" alt=\"my alt\"/>\n\t</figure>\n\t<div class=\"wp-block-media-text__content\">\n\t\t\n\t</div>\n</div>" + "originalContent": "<div class=\"wp-block-media-text\">\n\t<figure class=\"wp-block-media-text__media\">\n\t\t<img src=\"http://localhost/wp-content/uploads/2018/09/1600px-Mount_Everest_as_seen_from_Drukair2_PLW_edit.jpg\" alt=\"my alt\" class=\"wp-image-17985\" />\n\t</figure>\n\t<div class=\"wp-block-media-text__content\">\n\t\t\n\t</div>\n</div>" } ] 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 e078f9c6ae589d..9068878ecf502e 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 @@ -20,9 +20,9 @@ ] } ], - "innerHTML": "\n<div class=\"wp-block-media-text\">\n\t<figure class=\"wp-block-media-text__media\">\n\t\t<img src=\"http://localhost/wp-content/uploads/2018/09/1600px-Mount_Everest_as_seen_from_Drukair2_PLW_edit.jpg\" alt=\"my alt\"/>\n\t</figure>\n\t<div class=\"wp-block-media-text__content\">\n\t\t\n\t</div>\n</div>\n", + "innerHTML": "\n<div class=\"wp-block-media-text\">\n\t<figure class=\"wp-block-media-text__media\">\n\t\t<img src=\"http://localhost/wp-content/uploads/2018/09/1600px-Mount_Everest_as_seen_from_Drukair2_PLW_edit.jpg\" alt=\"my alt\" class=\"wp-image-17985\" />\n\t</figure>\n\t<div class=\"wp-block-media-text__content\">\n\t\t\n\t</div>\n</div>\n", "innerContent": [ - "\n<div class=\"wp-block-media-text\">\n\t<figure class=\"wp-block-media-text__media\">\n\t\t<img src=\"http://localhost/wp-content/uploads/2018/09/1600px-Mount_Everest_as_seen_from_Drukair2_PLW_edit.jpg\" alt=\"my alt\"/>\n\t</figure>\n\t<div class=\"wp-block-media-text__content\">\n\t\t", + "\n<div class=\"wp-block-media-text\">\n\t<figure class=\"wp-block-media-text__media\">\n\t\t<img src=\"http://localhost/wp-content/uploads/2018/09/1600px-Mount_Everest_as_seen_from_Drukair2_PLW_edit.jpg\" alt=\"my alt\" class=\"wp-image-17985\" />\n\t</figure>\n\t<div class=\"wp-block-media-text__content\">\n\t\t", null, "\n\t</div>\n</div>\n" ] diff --git a/test/integration/full-content/fixtures/core__media-text__image-alt-no-align.serialized.html b/test/integration/full-content/fixtures/core__media-text__image-alt-no-align.serialized.html index 3a28587c29bafc..9e00e7ce93ca1a 100644 --- a/test/integration/full-content/fixtures/core__media-text__image-alt-no-align.serialized.html +++ b/test/integration/full-content/fixtures/core__media-text__image-alt-no-align.serialized.html @@ -1,5 +1,5 @@ <!-- wp:media-text {"align":"none","mediaId":17985,"mediaType":"image"} --> -<div class="wp-block-media-text"><figure class="wp-block-media-text__media"><img src="http://localhost/wp-content/uploads/2018/09/1600px-Mount_Everest_as_seen_from_Drukair2_PLW_edit.jpg" alt="my alt"/></figure><div class="wp-block-media-text__content"><!-- wp:paragraph {"placeholder":"Content…","fontSize":"large"} --> +<div class="wp-block-media-text"><figure class="wp-block-media-text__media"><img src="http://localhost/wp-content/uploads/2018/09/1600px-Mount_Everest_as_seen_from_Drukair2_PLW_edit.jpg" alt="my alt" class="wp-image-17985"/></figure><div class="wp-block-media-text__content"><!-- wp:paragraph {"placeholder":"Content…","fontSize":"large"} --> <p class="has-large-font-size">Content</p> <!-- /wp:paragraph --></div></div> <!-- /wp:media-text --> From 1b6cf07d2266bd16b1e858b4f7e8589106ad0b87 Mon Sep 17 00:00:00 2001 From: etoledom <etoledom@icloud.com> Date: Fri, 16 Nov 2018 19:22:19 -0300 Subject: [PATCH 015/254] RNMobile: Register default block (#11988) --- packages/block-library/src/index.native.js | 3 +++ packages/blocks/src/api/index.native.js | 2 ++ 2 files changed, 5 insertions(+) diff --git a/packages/block-library/src/index.native.js b/packages/block-library/src/index.native.js index 3da15242c7cfbd..a1bd0b0d2f4f8c 100644 --- a/packages/block-library/src/index.native.js +++ b/packages/block-library/src/index.native.js @@ -3,6 +3,7 @@ */ import { registerBlockType, + setDefaultBlockName, } from '@wordpress/blocks'; /** @@ -27,3 +28,5 @@ export const registerCoreBlocks = () => { registerBlockType( name, settings ); } ); }; + +setDefaultBlockName( paragraph.name ); diff --git a/packages/blocks/src/api/index.native.js b/packages/blocks/src/api/index.native.js index ac675f1f21870a..aa1056f46631e5 100644 --- a/packages/blocks/src/api/index.native.js +++ b/packages/blocks/src/api/index.native.js @@ -20,6 +20,8 @@ export { getBlockTypes, hasBlockSupport, isReusableBlock, + setDefaultBlockName, + getDefaultBlockName, } from './registration'; export { getPhrasingContentSchema } from './raw-handling'; export { default as children } from './children'; From 88b5033ffc9ec02ec4db6a5f419125f3c144ef19 Mon Sep 17 00:00:00 2001 From: imath <imathi.eu@outlook.fr> Date: Sat, 17 Nov 2018 11:38:21 +0100 Subject: [PATCH 016/254] Use the MediaUploadCheck component before each Upload component (#11924) * Use the MediaUploadCheck component before each Upload component This PR should fix #11910 1. It adds a MediaUploadCheck component before the forgotten MediaUpload components used directly in some blocks : - within the edit toolbar buttons of the cover, file and image blocks, - within the Poster image setting of the video block. 2. It also adds a MediaUploadCheck component before rendering the Block Drop Zone. * Edit the MediaUpload documentation about Upload Permissions Include some informations about how to make sure the current user has upload permissions (by wrapping the MediaUpload component into the MediaUploadCheck one) --- packages/block-library/src/cover/index.js | 33 +++++++------ packages/block-library/src/file/edit.js | 31 ++++++------ packages/block-library/src/image/edit.js | 33 +++++++------ packages/block-library/src/video/edit.js | 49 ++++++++++--------- .../src/components/block-drop-zone/index.js | 23 ++++++--- .../src/components/media-upload/README.md | 25 +++++----- 6 files changed, 108 insertions(+), 86 deletions(-) diff --git a/packages/block-library/src/cover/index.js b/packages/block-library/src/cover/index.js index 8a8808327ec97b..8dff936dc440d1 100644 --- a/packages/block-library/src/cover/index.js +++ b/packages/block-library/src/cover/index.js @@ -26,6 +26,7 @@ import { BlockAlignmentToolbar, MediaPlaceholder, MediaUpload, + MediaUploadCheck, AlignmentToolbar, PanelColorSettings, RichText, @@ -259,21 +260,23 @@ export const settings = { setAttributes( { contentAlign: nextAlign } ); } } /> - <Toolbar> - <MediaUpload - onSelect={ onSelectMedia } - allowedTypes={ ALLOWED_MEDIA_TYPES } - value={ id } - render={ ( { open } ) => ( - <IconButton - className="components-toolbar__control" - label={ __( 'Edit media' ) } - icon="edit" - onClick={ open } - /> - ) } - /> - </Toolbar> + <MediaUploadCheck> + <Toolbar> + <MediaUpload + onSelect={ onSelectMedia } + allowedTypes={ ALLOWED_MEDIA_TYPES } + value={ id } + render={ ( { open } ) => ( + <IconButton + className="components-toolbar__control" + label={ __( 'Edit media' ) } + icon="edit" + onClick={ open } + /> + ) } + /> + </Toolbar> + </MediaUploadCheck> </Fragment> ) } </BlockControls> diff --git a/packages/block-library/src/file/edit.js b/packages/block-library/src/file/edit.js index 5d1dc30cf9e365..eb311fb48a73a9 100644 --- a/packages/block-library/src/file/edit.js +++ b/packages/block-library/src/file/edit.js @@ -19,6 +19,7 @@ import { Component, Fragment } from '@wordpress/element'; import { MediaUpload, MediaPlaceholder, + MediaUploadCheck, BlockControls, RichText, mediaUpload, @@ -165,20 +166,22 @@ class FileEdit extends Component { } } /> <BlockControls> - <Toolbar> - <MediaUpload - onSelect={ this.onSelectFile } - value={ id } - render={ ( { open } ) => ( - <IconButton - className="components-toolbar__control" - label={ __( 'Edit file' ) } - onClick={ open } - icon="edit" - /> - ) } - /> - </Toolbar> + <MediaUploadCheck> + <Toolbar> + <MediaUpload + onSelect={ this.onSelectFile } + value={ id } + render={ ( { open } ) => ( + <IconButton + className="components-toolbar__control" + label={ __( 'Edit file' ) } + onClick={ open } + icon="edit" + /> + ) } + /> + </Toolbar> + </MediaUploadCheck> </BlockControls> <div className={ classes }> <div className={ `${ className }__content-wrapper` }> diff --git a/packages/block-library/src/image/edit.js b/packages/block-library/src/image/edit.js index 7a2c77298918f6..f7dd63a22db4ac 100644 --- a/packages/block-library/src/image/edit.js +++ b/packages/block-library/src/image/edit.js @@ -39,6 +39,7 @@ import { InspectorControls, MediaPlaceholder, MediaUpload, + MediaUploadCheck, BlockAlignmentToolbar, mediaUpload, } from '@wordpress/editor'; @@ -328,21 +329,23 @@ class ImageEdit extends Component { ); } else { toolbarEditButton = ( - <Toolbar> - <MediaUpload - onSelect={ this.onSelectImage } - allowedTypes={ ALLOWED_MEDIA_TYPES } - value={ id } - render={ ( { open } ) => ( - <IconButton - className="components-toolbar__control" - label={ __( 'Edit image' ) } - icon="edit" - onClick={ open } - /> - ) } - /> - </Toolbar> + <MediaUploadCheck> + <Toolbar> + <MediaUpload + onSelect={ this.onSelectImage } + allowedTypes={ ALLOWED_MEDIA_TYPES } + value={ id } + render={ ( { open } ) => ( + <IconButton + className="components-toolbar__control" + label={ __( 'Edit image' ) } + icon="edit" + onClick={ open } + /> + ) } + /> + </Toolbar> + </MediaUploadCheck> ); } } diff --git a/packages/block-library/src/video/edit.js b/packages/block-library/src/video/edit.js index 26039e79c85c3a..118bd117606685 100644 --- a/packages/block-library/src/video/edit.js +++ b/packages/block-library/src/video/edit.js @@ -19,6 +19,7 @@ import { InspectorControls, MediaPlaceholder, MediaUpload, + MediaUploadCheck, RichText, mediaUpload, } from '@wordpress/editor'; @@ -207,30 +208,32 @@ class VideoEdit extends Component { { value: 'none', label: __( 'None' ) }, ] } /> - <BaseControl - className="editor-video-poster-control" - label={ __( 'Poster Image' ) } - > - <MediaUpload - title={ __( 'Select Poster Image' ) } - onSelect={ this.onSelectPoster } - allowedTypes={ VIDEO_POSTER_ALLOWED_MEDIA_TYPES } - render={ ( { open } ) => ( - <Button - isDefault - onClick={ open } - ref={ this.posterImageButton } - > - { ! this.props.attributes.poster ? __( 'Select Poster Image' ) : __( 'Replace image' ) } + <MediaUploadCheck> + <BaseControl + className="editor-video-poster-control" + label={ __( 'Poster Image' ) } + > + <MediaUpload + title={ __( 'Select Poster Image' ) } + onSelect={ this.onSelectPoster } + allowedTypes={ VIDEO_POSTER_ALLOWED_MEDIA_TYPES } + render={ ( { open } ) => ( + <Button + isDefault + onClick={ open } + ref={ this.posterImageButton } + > + { ! this.props.attributes.poster ? __( 'Select Poster Image' ) : __( 'Replace image' ) } + </Button> + ) } + /> + { !! this.props.attributes.poster && + <Button onClick={ this.onRemovePoster } isLink isDestructive> + { __( 'Remove Poster Image' ) } </Button> - ) } - /> - { !! this.props.attributes.poster && - <Button onClick={ this.onRemovePoster } isLink isDestructive> - { __( 'Remove Poster Image' ) } - </Button> - } - </BaseControl> + } + </BaseControl> + </MediaUploadCheck> </PanelBody> </InspectorControls> <figure className={ className }> diff --git a/packages/editor/src/components/block-drop-zone/index.js b/packages/editor/src/components/block-drop-zone/index.js index 3af8b0cff3a6f7..3e9c0db427e58a 100644 --- a/packages/editor/src/components/block-drop-zone/index.js +++ b/packages/editor/src/components/block-drop-zone/index.js @@ -19,6 +19,11 @@ import { Component } from '@wordpress/element'; import { withDispatch, withSelect } from '@wordpress/data'; import { compose } from '@wordpress/compose'; +/** + * Internal dependencies + */ +import MediaUploadCheck from '../media-upload/check'; + const parseDropEvent = ( event ) => { let result = { srcRootClientId: null, @@ -111,14 +116,16 @@ class BlockDropZone extends Component { const isAppender = index === undefined; return ( - <DropZone - className={ classnames( 'editor-block-drop-zone', { - 'is-appender': isAppender, - } ) } - onFilesDrop={ this.onFilesDrop } - onHTMLDrop={ this.onHTMLDrop } - onDrop={ this.onDrop } - /> + <MediaUploadCheck> + <DropZone + className={ classnames( 'editor-block-drop-zone', { + 'is-appender': isAppender, + } ) } + onFilesDrop={ this.onFilesDrop } + onHTMLDrop={ this.onHTMLDrop } + onDrop={ this.onDrop } + /> + </MediaUploadCheck> ); } } diff --git a/packages/editor/src/components/media-upload/README.md b/packages/editor/src/components/media-upload/README.md index ae670b227ea925..1dc11cb2a37ed3 100644 --- a/packages/editor/src/components/media-upload/README.md +++ b/packages/editor/src/components/media-upload/README.md @@ -24,25 +24,28 @@ You can check how this component is implemented for the edit post page using `wp ## Usage +To make sure the current user has Upload permissions, you need to wrap the MediaUpload component into the MediaUploadCheck one. ```jsx import { Button } from '@wordpress/components'; -import { MediaUpload } from '@wordpress/editor'; +import { MediaUpload, MediaUploadCheck } from '@wordpress/editor'; const ALLOWED_MEDIA_TYPES = [ 'audio' ]; function MyMediaUploader() { return ( - <MediaUpload - onSelect={ ( media ) => console.log( 'selected ' + media.length ) } - allowedTypes={ ALLOWED_MEDIA_TYPES } - value={ mediaId } - render={ ( { open } ) => ( - <Button onClick={ open }> - Open Media Library - </Button> - ) } - /> + <MediaUploadCheck> + <MediaUpload + onSelect={ ( media ) => console.log( 'selected ' + media.length ) } + allowedTypes={ ALLOWED_MEDIA_TYPES } + value={ mediaId } + render={ ( { open } ) => ( + <Button onClick={ open }> + Open Media Library + </Button> + ) } + /> + </MediaUploadCheck> ); } ``` From 3b16e5fc7baceb64b0d1384e234a6077403dcb70 Mon Sep 17 00:00:00 2001 From: Vadim Nicolai <nicolai.vadim@gmail.com> Date: Sat, 17 Nov 2018 16:49:06 +0200 Subject: [PATCH 017/254] Keycodes package modifiers usage and adding missing JSDocs. (#11855) * Replaced pressWithModifier with rawShortcut. * Replaced SELECT_WORD_MODIFIER_KEYS, META_KEY and ACCESS_MODIFIER_KEYS with rawShortcuts. * Replaced key strings with constants. * Replaced rawShortcut usage. TODO: Apply DRY. * Adjusted failing tests and overwritten some modifiers. * Removed META_KEY and SELECT_WORD_MODIFIER_KEYS added after syncing with mster. * Make sure that the check for OS is executed in e2e tests * Another try to properly call the check for OS in e2e tests --- packages/keycodes/src/index.js | 2 +- test/e2e/specs/a11y.test.js | 11 ++--- test/e2e/specs/block-deletion.test.js | 5 +- .../specs/block-hierarchy-navigation.test.js | 14 +++--- test/e2e/specs/block-icons.test.js | 3 +- test/e2e/specs/blocks/list.test.js | 2 +- test/e2e/specs/change-detection.test.js | 19 ++++---- .../e2e/specs/deprecated-node-matcher.test.js | 3 +- test/e2e/specs/links.test.js | 48 +++++++++---------- test/e2e/specs/multi-block-selection.test.js | 11 ++--- test/e2e/specs/navigable-toolbar.test.js | 2 +- test/e2e/specs/publish-panel.test.js | 2 +- test/e2e/specs/reusable-blocks.test.js | 5 +- test/e2e/specs/rich-text.test.js | 14 +++--- test/e2e/specs/shortcut-help.test.js | 5 +- test/e2e/specs/sidebar.test.js | 8 ++-- test/e2e/specs/splitting-merging.test.js | 7 ++- test/e2e/specs/templates.test.js | 3 +- test/e2e/specs/undo.test.js | 19 ++++---- test/e2e/specs/writing-flow.test.js | 23 +++++---- test/e2e/support/utils/index.js | 3 +- test/e2e/support/utils/login.js | 8 ++-- test/e2e/support/utils/meta-key.js | 8 ---- test/e2e/support/utils/press-with-modifier.js | 39 ++++++++------- 24 files changed, 122 insertions(+), 142 deletions(-) delete mode 100644 test/e2e/support/utils/meta-key.js diff --git a/packages/keycodes/src/index.js b/packages/keycodes/src/index.js index 611fc58c62279e..45e5dba7076743 100644 --- a/packages/keycodes/src/index.js +++ b/packages/keycodes/src/index.js @@ -43,7 +43,7 @@ export const CTRL = 'ctrl'; export const COMMAND = 'meta'; export const SHIFT = 'shift'; -const modifiers = { +export const modifiers = { primary: ( _isApple ) => _isApple() ? [ COMMAND ] : [ CTRL ], primaryShift: ( _isApple ) => _isApple() ? [ SHIFT, COMMAND ] : [ CTRL, SHIFT ], primaryAlt: ( _isApple ) => _isApple() ? [ ALT, COMMAND ] : [ CTRL, ALT ], diff --git a/test/e2e/specs/a11y.test.js b/test/e2e/specs/a11y.test.js index 3495df65a90a42..19403fe69e9e4f 100644 --- a/test/e2e/specs/a11y.test.js +++ b/test/e2e/specs/a11y.test.js @@ -2,7 +2,6 @@ * Internal dependencies */ import { - ACCESS_MODIFIER_KEYS, newPost, pressWithModifier, } from '../support/utils'; @@ -19,7 +18,7 @@ describe( 'a11y', () => { } ); it( 'tabs header bar', async () => { - await pressWithModifier( 'Control', '~' ); + await pressWithModifier( 'ctrl', '~' ); await page.keyboard.press( 'Tab' ); @@ -32,7 +31,7 @@ describe( 'a11y', () => { it( 'constrains focus to a modal when tabbing', async () => { // Open keyboard help modal. - await pressWithModifier( ACCESS_MODIFIER_KEYS, 'h' ); + await pressWithModifier( 'access', 'h' ); // The close button should not be focused by default; this is a strange UX // experience. @@ -46,7 +45,7 @@ describe( 'a11y', () => { } ); it( 'returns focus to the first tabbable in a modal after blurring a tabbable', async () => { - await pressWithModifier( ACCESS_MODIFIER_KEYS, 'h' ); + await pressWithModifier( 'access', 'h' ); // Click to move focus to an element after the last tabbable within the // modal. @@ -58,13 +57,13 @@ describe( 'a11y', () => { } ); it( 'returns focus to the last tabbable in a modal after blurring a tabbable and tabbing in reverse direction', async () => { - await pressWithModifier( ACCESS_MODIFIER_KEYS, 'h' ); + await pressWithModifier( 'access', 'h' ); // Click to move focus to an element before the first tabbable within // the modal. await page.click( '.components-modal__header-heading' ); - await pressWithModifier( 'Shift', 'Tab' ); + await pressWithModifier( 'shift', 'Tab' ); expect( await isCloseButtonFocused() ).toBe( true ); } ); diff --git a/test/e2e/specs/block-deletion.test.js b/test/e2e/specs/block-deletion.test.js index 7da0695f1ab436..6b8c97fc12dcee 100644 --- a/test/e2e/specs/block-deletion.test.js +++ b/test/e2e/specs/block-deletion.test.js @@ -6,7 +6,6 @@ import { getEditedPostContent, newPost, pressWithModifier, - ACCESS_MODIFIER_KEYS, } from '../support/utils'; const addThreeParagraphsToNewPost = async () => { @@ -50,7 +49,7 @@ describe( 'block deletion -', () => { it( 'results in two remaining blocks and positions the caret at the end of the second block', async () => { // Type some text to assert that the shortcut also deletes block content. await page.keyboard.type( 'this is block 2' ); - await pressWithModifier( ACCESS_MODIFIER_KEYS, 'z' ); + await pressWithModifier( 'access', 'z' ); expect( await getEditedPostContent() ).toMatchSnapshot(); // Type additional text and assert that caret position is correct by comparing to snapshot. @@ -98,7 +97,7 @@ describe( 'block deletion -', () => { await page.keyboard.press( 'Enter' ); // Press the up arrow once to select the third and fourth blocks. - await pressWithModifier( 'Shift', 'ArrowUp' ); + await pressWithModifier( 'shift', 'ArrowUp' ); // Now that the block wrapper is selected, press backspace to delete it. await page.keyboard.press( 'Backspace' ); diff --git a/test/e2e/specs/block-hierarchy-navigation.test.js b/test/e2e/specs/block-hierarchy-navigation.test.js index 82b4a53fa01936..970a6fb140ef52 100644 --- a/test/e2e/specs/block-hierarchy-navigation.test.js +++ b/test/e2e/specs/block-hierarchy-navigation.test.js @@ -2,8 +2,6 @@ * Internal dependencies */ import { - ACCESS_MODIFIER_KEYS, - META_KEY, newPost, insertBlock, getEditedPostContent, @@ -12,7 +10,7 @@ import { } from '../support/utils'; async function openBlockNavigator() { - return pressWithModifier( ACCESS_MODIFIER_KEYS, 'o' ); + return pressWithModifier( 'access', 'o' ); } describe( 'Navigating the block hierarchy', () => { @@ -63,10 +61,10 @@ describe( 'Navigating the block hierarchy', () => { await page.keyboard.press( 'Enter' ); // Move focus to the sidebar area. - await pressWithModifier( 'Control', '`' ); - await pressWithModifier( 'Control', '`' ); - await pressWithModifier( 'Control', '`' ); - await pressWithModifier( 'Control', '`' ); + await pressWithModifier( 'ctrl', '`' ); + await pressWithModifier( 'ctrl', '`' ); + await pressWithModifier( 'ctrl', '`' ); + await pressWithModifier( 'ctrl', '`' ); await pressTimes( 'Tab', 4 ); // Tweak the columns count by increasing it by one. @@ -99,7 +97,7 @@ describe( 'Navigating the block hierarchy', () => { await page.keyboard.press( 'Space' ); // Replace its content. - await pressWithModifier( META_KEY, 'A' ); + await pressWithModifier( 'primary', 'A' ); await page.keyboard.type( 'and I say hello' ); expect( await getEditedPostContent() ).toMatchSnapshot(); diff --git a/test/e2e/specs/block-icons.test.js b/test/e2e/specs/block-icons.test.js index 0e44cddb124f44..4042b05a6f7003 100644 --- a/test/e2e/specs/block-icons.test.js +++ b/test/e2e/specs/block-icons.test.js @@ -2,7 +2,6 @@ * Internal dependencies */ import { - ACCESS_MODIFIER_KEYS, pressWithModifier, newPost, insertBlock, @@ -36,7 +35,7 @@ async function getFirstInserterIcon() { } async function selectFirstBlock() { - await pressWithModifier( ACCESS_MODIFIER_KEYS, 'o' ); + await pressWithModifier( 'access', 'o' ); const navButtons = await page.$$( '.editor-block-navigation__item-button' ); await navButtons[ 0 ].click(); } diff --git a/test/e2e/specs/blocks/list.test.js b/test/e2e/specs/blocks/list.test.js index f28196cfbdffab..80d8297acdc7ac 100644 --- a/test/e2e/specs/blocks/list.test.js +++ b/test/e2e/specs/blocks/list.test.js @@ -80,7 +80,7 @@ describe( 'List', () => { it( 'can be created by converting a paragraph with line breaks', async () => { await clickBlockAppender(); await page.keyboard.type( 'one' ); - await pressWithModifier( 'Shift', 'Enter' ); + await pressWithModifier( 'shift', 'Enter' ); await page.keyboard.type( 'two' ); await convertBlock( 'List' ); diff --git a/test/e2e/specs/change-detection.test.js b/test/e2e/specs/change-detection.test.js index 9ac12abbf5b097..8efe2fc1b00313 100644 --- a/test/e2e/specs/change-detection.test.js +++ b/test/e2e/specs/change-detection.test.js @@ -7,7 +7,6 @@ import { pressWithModifier, ensureSidebarOpened, publishPost, - META_KEY, } from '../support/utils'; describe( 'Change detection', () => { @@ -69,7 +68,7 @@ describe( 'Change detection', () => { await interceptSave(); // Keyboard shortcut Ctrl+S save. - await pressWithModifier( META_KEY, 'S' ); + await pressWithModifier( 'primary', 'S' ); expect( hadInterceptedSave ).toBe( false ); } ); @@ -164,7 +163,7 @@ describe( 'Change detection', () => { page.waitForSelector( '.editor-post-saved-state.is-saved' ), // Keyboard shortcut Ctrl+S save. - pressWithModifier( META_KEY, 'S' ), + pressWithModifier( 'primary', 'S' ), ] ); await assertIsDirty( false ); @@ -178,13 +177,13 @@ describe( 'Change detection', () => { page.waitForSelector( '.editor-post-saved-state.is-saved' ), // Keyboard shortcut Ctrl+S save. - pressWithModifier( META_KEY, 'S' ), + pressWithModifier( 'primary', 'S' ), ] ); await interceptSave(); // Keyboard shortcut Ctrl+S save. - await pressWithModifier( META_KEY, 'S' ); + await pressWithModifier( 'primary', 'S' ); expect( hadInterceptedSave ).toBe( false ); } ); @@ -196,7 +195,7 @@ describe( 'Change detection', () => { await Promise.all( [ // Keyboard shortcut Ctrl+S save. - pressWithModifier( META_KEY, 'S' ), + pressWithModifier( 'primary', 'S' ), // Ensure save update fails and presents button. page.waitForXPath( @@ -222,7 +221,7 @@ describe( 'Change detection', () => { await interceptSave(); // Keyboard shortcut Ctrl+S save. - await pressWithModifier( META_KEY, 'S' ); + await pressWithModifier( 'primary', 'S' ); await releaseSaveIntercept(); @@ -238,7 +237,7 @@ describe( 'Change detection', () => { await interceptSave(); // Keyboard shortcut Ctrl+S save. - await pressWithModifier( META_KEY, 'S' ); + await pressWithModifier( 'primary', 'S' ); await page.type( '.editor-post-title__input', '!' ); @@ -255,7 +254,7 @@ describe( 'Change detection', () => { await interceptSave(); // Keyboard shortcut Ctrl+S save. - await pressWithModifier( META_KEY, 'S' ); + await pressWithModifier( 'primary', 'S' ); // Dirty post while save is in-flight. await page.type( '.editor-post-title__input', '!' ); @@ -277,7 +276,7 @@ describe( 'Change detection', () => { await interceptSave(); // Keyboard shortcut Ctrl+S save. - await pressWithModifier( META_KEY, 'S' ); + await pressWithModifier( 'primary', 'S' ); await clickBlockAppender(); diff --git a/test/e2e/specs/deprecated-node-matcher.test.js b/test/e2e/specs/deprecated-node-matcher.test.js index 885ab8c9d4d6c0..a93f23f0de8837 100644 --- a/test/e2e/specs/deprecated-node-matcher.test.js +++ b/test/e2e/specs/deprecated-node-matcher.test.js @@ -5,7 +5,6 @@ import { newPost, insertBlock, getEditedPostContent, - META_KEY, pressWithModifier, } from '../support/utils'; import { activatePlugin, deactivatePlugin } from '../support/plugins'; @@ -38,7 +37,7 @@ describe( 'Deprecated Node Matcher', () => { await page.keyboard.down( 'Shift' ); await page.keyboard.press( 'ArrowLeft' ); await page.keyboard.up( 'Shift' ); - await pressWithModifier( META_KEY, 'b' ); + await pressWithModifier( 'primary', 'b' ); expect( await getEditedPostContent() ).toMatchSnapshot(); } ); } ); diff --git a/test/e2e/specs/links.test.js b/test/e2e/specs/links.test.js index 0d2c29501103f9..a59041f4f78259 100644 --- a/test/e2e/specs/links.test.js +++ b/test/e2e/specs/links.test.js @@ -2,7 +2,6 @@ * Internal dependencies */ import { - META_KEY, clickBlockAppender, getEditedPostContent, newPost, @@ -16,7 +15,6 @@ import { * * @type {string} */ -const SELECT_WORD_MODIFIER_KEYS = process.platform === 'darwin' ? [ 'Shift', 'Alt' ] : [ 'Shift', 'Control' ]; describe( 'Links', () => { beforeEach( async () => { @@ -38,7 +36,7 @@ describe( 'Links', () => { await page.keyboard.type( 'This is Gutenberg' ); // Select some text - await pressWithModifier( SELECT_WORD_MODIFIER_KEYS, 'ArrowLeft' ); + await pressWithModifier( 'shiftAlt', 'ArrowLeft' ); // Click on the Link button await page.click( 'button[aria-label="Link"]' ); @@ -62,10 +60,10 @@ describe( 'Links', () => { await page.keyboard.type( 'This is Gutenberg' ); // Select some text - await pressWithModifier( SELECT_WORD_MODIFIER_KEYS, 'ArrowLeft' ); + await pressWithModifier( 'shiftAlt', 'ArrowLeft' ); // Press Cmd+K to insert a link - await pressWithModifier( META_KEY, 'K' ); + await pressWithModifier( 'primary', 'K' ); // Wait for the URL field to auto-focus await waitForAutoFocus(); @@ -89,7 +87,7 @@ describe( 'Links', () => { await moveMouse(); // Press Cmd+K to insert a link - await pressWithModifier( META_KEY, 'K' ); + await pressWithModifier( 'primary', 'K' ); // Wait for the URL field to auto-focus await waitForAutoFocus(); @@ -110,10 +108,10 @@ describe( 'Links', () => { await page.keyboard.type( 'This is Gutenberg: https://wordpress.org/gutenberg' ); // Select the URL - await pressWithModifier( SELECT_WORD_MODIFIER_KEYS, 'ArrowLeft' ); - await pressWithModifier( SELECT_WORD_MODIFIER_KEYS, 'ArrowLeft' ); - await pressWithModifier( SELECT_WORD_MODIFIER_KEYS, 'ArrowLeft' ); - await pressWithModifier( SELECT_WORD_MODIFIER_KEYS, 'ArrowLeft' ); + await pressWithModifier( 'shiftAlt', 'ArrowLeft' ); + await pressWithModifier( 'shiftAlt', 'ArrowLeft' ); + await pressWithModifier( 'shiftAlt', 'ArrowLeft' ); + await pressWithModifier( 'shiftAlt', 'ArrowLeft' ); // Click on the Link button await page.click( 'button[aria-label="Link"]' ); @@ -128,7 +126,7 @@ describe( 'Links', () => { await page.keyboard.type( 'This is Gutenberg' ); // Select some text - await pressWithModifier( SELECT_WORD_MODIFIER_KEYS, 'ArrowLeft' ); + await pressWithModifier( 'shiftAlt', 'ArrowLeft' ); // Click on the Link button await page.click( 'button[aria-label="Link"]' ); @@ -149,7 +147,7 @@ describe( 'Links', () => { await page.keyboard.type( 'This is Gutenberg' ); // Select some text - await pressWithModifier( SELECT_WORD_MODIFIER_KEYS, 'ArrowLeft' ); + await pressWithModifier( 'shiftAlt', 'ArrowLeft' ); // Click on the Link button await page.click( 'button[aria-label="Link"]' ); @@ -284,7 +282,7 @@ describe( 'Links', () => { await newPost(); await clickBlockAppender(); await page.keyboard.type( 'This is Gutenberg' ); - await pressWithModifier( SELECT_WORD_MODIFIER_KEYS, 'ArrowLeft' ); + await pressWithModifier( 'shiftAlt', 'ArrowLeft' ); await page.click( 'button[aria-label="Link"]' ); // Wait for the URL field to auto-focus @@ -324,10 +322,10 @@ describe( 'Links', () => { // Now in a new post and try to create a link from an autocomplete suggestion using the keyboard. await page.keyboard.type( 'This is Gutenberg' ); - await pressWithModifier( SELECT_WORD_MODIFIER_KEYS, 'ArrowLeft' ); + await pressWithModifier( 'shiftAlt', 'ArrowLeft' ); // Press Cmd+K to insert a link - await pressWithModifier( META_KEY, 'K' ); + await pressWithModifier( 'primary', 'K' ); // Wait for the URL field to auto-focus await waitForAutoFocus(); @@ -360,10 +358,10 @@ describe( 'Links', () => { // Now in a new post and try to create a link from an autocomplete suggestion using the keyboard. await page.keyboard.type( 'This is Gutenberg' ); - await pressWithModifier( SELECT_WORD_MODIFIER_KEYS, 'ArrowLeft' ); + await pressWithModifier( 'shiftAlt', 'ArrowLeft' ); // Press Cmd+K to insert a link - await pressWithModifier( META_KEY, 'K' ); + await pressWithModifier( 'primary', 'K' ); // Wait for the URL field to auto-focus await waitForAutoFocus(); @@ -379,7 +377,7 @@ describe( 'Links', () => { expect( await page.$( '.editor-url-popover' ) ).toBeNull(); // Press Cmd+K to insert a link - await pressWithModifier( META_KEY, 'K' ); + await pressWithModifier( 'primary', 'K' ); // Wait for the URL field to auto-focus await waitForAutoFocus(); @@ -390,7 +388,7 @@ describe( 'Links', () => { expect( await page.$( '.editor-url-popover' ) ).toBeNull(); // Press Cmd+K to insert a link - await pressWithModifier( META_KEY, 'K' ); + await pressWithModifier( 'primary', 'K' ); // Wait for the URL field to auto-focus await waitForAutoFocus(); @@ -411,8 +409,8 @@ describe( 'Links', () => { // Create a block with some text and format it as a link. await clickBlockAppender(); await page.keyboard.type( 'This is Gutenberg' ); - await pressWithModifier( SELECT_WORD_MODIFIER_KEYS, 'ArrowLeft' ); - await pressWithModifier( META_KEY, 'K' ); + await pressWithModifier( 'shiftAlt', 'ArrowLeft' ); + await pressWithModifier( 'primary', 'K' ); await waitForAutoFocus(); await page.keyboard.type( URL ); await page.keyboard.press( 'Enter' ); @@ -429,7 +427,7 @@ describe( 'Links', () => { // Press Cmd+K to edit the link and the url-input should become // focused with the value previously inserted. - await pressWithModifier( META_KEY, 'K' ); + await pressWithModifier( 'primary', 'K' ); await waitForAutoFocus(); const activeElementParentClasses = await page.evaluate( () => Object.values( document.activeElement.parentElement.classList ) ); expect( activeElementParentClasses ).toContain( 'editor-url-input' ); @@ -440,8 +438,8 @@ describe( 'Links', () => { 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 pressWithModifier( 'shiftAlt', 'ArrowLeft' ); + await pressWithModifier( 'primary', 'K' ); await waitForAutoFocus(); await page.keyboard.type( 'http://#test.com' ); await page.keyboard.press( 'Enter' ); @@ -461,7 +459,7 @@ describe( 'Links', () => { await page.click( '.editor-block-navigation__item button' ); // Select some text - await pressWithModifier( SELECT_WORD_MODIFIER_KEYS, 'ArrowLeft' ); + await pressWithModifier( 'shiftAlt', 'ArrowLeft' ); // Click on the Link button await page.click( 'button[aria-label="Link"]' ); diff --git a/test/e2e/specs/multi-block-selection.test.js b/test/e2e/specs/multi-block-selection.test.js index bd07dc8b099f87..959fc5bc447907 100644 --- a/test/e2e/specs/multi-block-selection.test.js +++ b/test/e2e/specs/multi-block-selection.test.js @@ -6,7 +6,6 @@ import { insertBlock, newPost, pressWithModifier, - META_KEY, } from '../support/utils'; describe( 'Multi-block selection', () => { @@ -60,7 +59,7 @@ describe( 'Multi-block selection', () => { // Multiselect via keyboard await page.click( 'body' ); - await pressWithModifier( META_KEY, 'a' ); + await pressWithModifier( 'primary', 'a' ); // Verify selection await expectMultiSelected( blocks, true ); @@ -73,8 +72,8 @@ describe( 'Multi-block selection', () => { // Select all via double shortcut. await page.click( firstBlockSelector ); - await pressWithModifier( META_KEY, 'a' ); - await pressWithModifier( META_KEY, 'a' ); + await pressWithModifier( 'primary', 'a' ); + await pressWithModifier( 'primary', 'a' ); await expectMultiSelected( blocks, true ); } ); @@ -127,8 +126,8 @@ describe( 'Multi-block selection', () => { await page.keyboard.type( 'Third Paragraph' ); // Multiselect via keyboard. - await pressWithModifier( META_KEY, 'a' ); - await pressWithModifier( META_KEY, 'a' ); + await pressWithModifier( 'primary', 'a' ); + await pressWithModifier( 'primary', '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 diff --git a/test/e2e/specs/navigable-toolbar.test.js b/test/e2e/specs/navigable-toolbar.test.js index aa781f0fa57358..e62753149ace2e 100644 --- a/test/e2e/specs/navigable-toolbar.test.js +++ b/test/e2e/specs/navigable-toolbar.test.js @@ -44,7 +44,7 @@ describe( 'block toolbar', () => { await page.keyboard.type( 'Example' ); // Upward - await pressWithModifier( 'Alt', 'F10' ); + await pressWithModifier( 'alt', 'F10' ); expect( await isInBlockToolbar() ).toBe( true ); // Downward diff --git a/test/e2e/specs/publish-panel.test.js b/test/e2e/specs/publish-panel.test.js index 9a3a6eaee63e5b..27842682577c78 100644 --- a/test/e2e/specs/publish-panel.test.js +++ b/test/e2e/specs/publish-panel.test.js @@ -52,7 +52,7 @@ describe( 'PostPublishPanel', () => { it( 'should retain focus within the panel', async () => { await page.type( '.editor-post-title__input', 'E2E Test Post' ); await openPublishPanel(); - await pressWithModifier( 'Shift', 'Tab' ); + await pressWithModifier( 'shift', 'Tab' ); const focusedElementClassList = await page.$eval( ':focus', ( focusedElement ) => { return Object.values( focusedElement.classList ); diff --git a/test/e2e/specs/reusable-blocks.test.js b/test/e2e/specs/reusable-blocks.test.js index 06e5a6e3df8ee4..64ce2e938fcdad 100644 --- a/test/e2e/specs/reusable-blocks.test.js +++ b/test/e2e/specs/reusable-blocks.test.js @@ -7,7 +7,6 @@ import { pressWithModifier, searchForBlock, getEditedPostContent, - META_KEY, } from '../support/utils'; function waitForAndAcceptDialog() { @@ -215,8 +214,8 @@ describe( 'Reusable Blocks', () => { await page.keyboard.type( 'Second paragraph' ); // Select all the blocks - await pressWithModifier( META_KEY, 'a' ); - await pressWithModifier( META_KEY, 'a' ); + await pressWithModifier( 'primary', 'a' ); + await pressWithModifier( 'primary', 'a' ); // Trigger isTyping = false await page.mouse.move( 200, 300, { steps: 10 } ); diff --git a/test/e2e/specs/rich-text.test.js b/test/e2e/specs/rich-text.test.js index 9203ec32c303dd..4bf15fd9fcd582 100644 --- a/test/e2e/specs/rich-text.test.js +++ b/test/e2e/specs/rich-text.test.js @@ -7,8 +7,6 @@ import { insertBlock, clickBlockAppender, pressWithModifier, - META_KEY, - ACCESS_MODIFIER_KEYS, } from '../support/utils'; describe( 'RichText', () => { @@ -31,8 +29,8 @@ describe( 'RichText', () => { it( 'should apply formatting with access shortcut', async () => { await clickBlockAppender(); await page.keyboard.type( 'test' ); - await pressWithModifier( META_KEY, 'a' ); - await pressWithModifier( ACCESS_MODIFIER_KEYS, 'd' ); + await pressWithModifier( 'primary', 'a' ); + await pressWithModifier( 'access', 'd' ); expect( await getEditedPostContent() ).toMatchSnapshot(); } ); @@ -40,8 +38,8 @@ describe( 'RichText', () => { it( 'should apply formatting with primary shortcut', async () => { await clickBlockAppender(); await page.keyboard.type( 'test' ); - await pressWithModifier( META_KEY, 'a' ); - await pressWithModifier( META_KEY, 'b' ); + await pressWithModifier( 'primary', 'a' ); + await pressWithModifier( 'primary', 'b' ); expect( await getEditedPostContent() ).toMatchSnapshot(); } ); @@ -50,10 +48,10 @@ describe( 'RichText', () => { await clickBlockAppender(); await page.keyboard.type( 'Some ' ); // All following characters should now be bold. - await pressWithModifier( META_KEY, 'b' ); + await pressWithModifier( 'primary', 'b' ); await page.keyboard.type( 'bold' ); // All following characters should no longer be bold. - await pressWithModifier( META_KEY, 'b' ); + await pressWithModifier( 'primary', 'b' ); await page.keyboard.type( '.' ); expect( await getEditedPostContent() ).toMatchSnapshot(); diff --git a/test/e2e/specs/shortcut-help.test.js b/test/e2e/specs/shortcut-help.test.js index b7f7ea7491fd22..ea69639fe36229 100644 --- a/test/e2e/specs/shortcut-help.test.js +++ b/test/e2e/specs/shortcut-help.test.js @@ -6,7 +6,6 @@ import { clickOnMoreMenuItem, clickOnCloseModalButton, pressWithModifier, - ACCESS_MODIFIER_KEYS, } from '../support/utils'; describe( 'keyboard shortcut help modal', () => { @@ -27,13 +26,13 @@ describe( 'keyboard shortcut help modal', () => { } ); it( 'displays the shortcut help modal when opened using the shortcut key (access+h)', async () => { - await pressWithModifier( ACCESS_MODIFIER_KEYS, 'h' ); + await pressWithModifier( 'access', 'h' ); const shortcutHelpModalElements = await page.$$( '.edit-post-keyboard-shortcut-help' ); expect( shortcutHelpModalElements ).toHaveLength( 1 ); } ); it( 'closes the shortcut help modal when the shortcut key (access+h) is pressed again', async () => { - await pressWithModifier( ACCESS_MODIFIER_KEYS, 'h' ); + await pressWithModifier( 'access', 'h' ); const shortcutHelpModalElements = await page.$$( '.edit-post-keyboard-shortcut-help' ); expect( shortcutHelpModalElements ).toHaveLength( 0 ); } ); diff --git a/test/e2e/specs/sidebar.test.js b/test/e2e/specs/sidebar.test.js index 69c36fcf6ff293..3b5c9b92716673 100644 --- a/test/e2e/specs/sidebar.test.js +++ b/test/e2e/specs/sidebar.test.js @@ -82,10 +82,10 @@ describe( 'Sidebar', () => { await newPost(); // Region navigate to Sidebar. - await pressWithModifier( 'Control', '`' ); - await pressWithModifier( 'Control', '`' ); - await pressWithModifier( 'Control', '`' ); - await pressWithModifier( 'Control', '`' ); + await pressWithModifier( 'ctrl', '`' ); + await pressWithModifier( 'ctrl', '`' ); + await pressWithModifier( 'ctrl', '`' ); + await pressWithModifier( 'ctrl', '`' ); // Tab lands at first (presumed selected) option "Document". await page.keyboard.press( 'Tab' ); diff --git a/test/e2e/specs/splitting-merging.test.js b/test/e2e/specs/splitting-merging.test.js index d059fbf42ea63d..f6456dfa1e3429 100644 --- a/test/e2e/specs/splitting-merging.test.js +++ b/test/e2e/specs/splitting-merging.test.js @@ -7,7 +7,6 @@ import { getEditedPostContent, pressTimes, pressWithModifier, - META_KEY, } from '../support/utils'; describe( 'splitting and merging blocks', () => { @@ -44,7 +43,7 @@ describe( 'splitting and merging blocks', () => { await page.keyboard.down( 'Shift' ); await pressTimes( 'ArrowRight', 5 ); await page.keyboard.up( 'Shift' ); - await pressWithModifier( META_KEY, 'b' ); + await pressWithModifier( 'primary', 'b' ); // Collapse selection, still within inline boundary. await page.keyboard.press( 'ArrowRight' ); await page.keyboard.press( 'Enter' ); @@ -57,7 +56,7 @@ describe( 'splitting and merging blocks', () => { // Regression Test: Caret should reset to end of inline boundary when // backspacing to delete second paragraph. await insertBlock( 'Paragraph' ); - await pressWithModifier( META_KEY, 'b' ); + await pressWithModifier( 'primary', 'b' ); await page.keyboard.type( 'Foo' ); await page.keyboard.press( 'Enter' ); await page.keyboard.press( 'Backspace' ); @@ -129,7 +128,7 @@ describe( 'splitting and merging blocks', () => { await page.keyboard.down( 'Shift' ); await pressTimes( 'ArrowLeft', 3 ); await page.keyboard.up( 'Shift' ); - await pressWithModifier( META_KEY, 'b' ); + await pressWithModifier( 'primary', 'b' ); await page.keyboard.press( 'ArrowRight' ); await page.keyboard.press( 'Enter' ); await page.keyboard.press( 'Enter' ); diff --git a/test/e2e/specs/templates.test.js b/test/e2e/specs/templates.test.js index 4becad767d74b5..9b5d22080e0d42 100644 --- a/test/e2e/specs/templates.test.js +++ b/test/e2e/specs/templates.test.js @@ -2,7 +2,6 @@ * Internal dependencies */ import { - META_KEY, newPost, getEditedPostContent, saveDraft, @@ -49,7 +48,7 @@ describe( 'templates', () => { // re-added after saving and reloading the editor. await page.type( '.editor-post-title__input', 'My Empty Book' ); await page.keyboard.press( 'ArrowDown' ); - await pressWithModifier( META_KEY, 'A' ); + await pressWithModifier( 'primary', 'A' ); await page.keyboard.press( 'Backspace' ); await saveDraft(); await page.reload(); diff --git a/test/e2e/specs/undo.test.js b/test/e2e/specs/undo.test.js index 1ef7b9b32b88c4..fe67a73dfb680a 100644 --- a/test/e2e/specs/undo.test.js +++ b/test/e2e/specs/undo.test.js @@ -6,7 +6,6 @@ import { getEditedPostContent, newPost, pressWithModifier, - META_KEY, } from '../support/utils'; describe( 'undo', () => { @@ -23,7 +22,7 @@ describe( 'undo', () => { expect( await getEditedPostContent() ).toMatchSnapshot(); - await pressWithModifier( META_KEY, 'z' ); + await pressWithModifier( 'primary', 'z' ); expect( await getEditedPostContent() ).toMatchSnapshot(); } ); @@ -32,12 +31,12 @@ describe( 'undo', () => { await clickBlockAppender(); await page.keyboard.type( 'before keyboard ' ); - await pressWithModifier( META_KEY, 'b' ); + await pressWithModifier( 'primary', 'b' ); await page.keyboard.type( 'after keyboard' ); expect( await getEditedPostContent() ).toMatchSnapshot(); - await pressWithModifier( META_KEY, 'z' ); + await pressWithModifier( 'primary', 'z' ); expect( await getEditedPostContent() ).toMatchSnapshot(); } ); @@ -53,12 +52,12 @@ describe( 'undo', () => { expect( await getEditedPostContent() ).toMatchSnapshot(); - await pressWithModifier( META_KEY, 'z' ); // Undo 3rd paragraph text. - await pressWithModifier( META_KEY, 'z' ); // Undo 3rd block. - await pressWithModifier( META_KEY, 'z' ); // Undo 2nd paragraph text. - await pressWithModifier( META_KEY, 'z' ); // Undo 2nd block. - await pressWithModifier( META_KEY, 'z' ); // Undo 1st paragraph text. - await pressWithModifier( META_KEY, 'z' ); // Undo 1st block. + await pressWithModifier( 'primary', 'z' ); // Undo 3rd paragraph text. + await pressWithModifier( 'primary', 'z' ); // Undo 3rd block. + await pressWithModifier( 'primary', 'z' ); // Undo 2nd paragraph text. + await pressWithModifier( 'primary', 'z' ); // Undo 2nd block. + await pressWithModifier( 'primary', 'z' ); // Undo 1st paragraph text. + await pressWithModifier( 'primary', 'z' ); // Undo 1st block. expect( await getEditedPostContent() ).toBe( '' ); // After undoing every action, there should be no more undo history. diff --git a/test/e2e/specs/writing-flow.test.js b/test/e2e/specs/writing-flow.test.js index 0da85a9ff4dbe7..bcad1f3d55bb0c 100644 --- a/test/e2e/specs/writing-flow.test.js +++ b/test/e2e/specs/writing-flow.test.js @@ -7,7 +7,6 @@ import { newPost, pressTimes, pressWithModifier, - META_KEY, } from '../support/utils'; describe( 'adding blocks', () => { @@ -89,7 +88,7 @@ describe( 'adding blocks', () => { await page.keyboard.down( 'Shift' ); await pressTimes( 'ArrowLeft', 6 ); await page.keyboard.up( 'Shift' ); - await pressWithModifier( META_KEY, 'b' ); + await pressWithModifier( 'primary', 'b' ); // Arrow left from selected bold should collapse to before the inline // boundary. Arrow once more to traverse into first paragraph. @@ -146,7 +145,7 @@ describe( 'adding blocks', () => { // Ensure no zero-width space character. Notably, this can occur when // save occurs while at an inline boundary edge. await clickBlockAppender(); - await pressWithModifier( META_KEY, 'b' ); + await pressWithModifier( 'primary', 'b' ); expect( await getEditedPostContent() ).toMatchSnapshot(); // Backspace to remove the content in this block, resetting it. @@ -154,7 +153,7 @@ describe( 'adding blocks', () => { // Ensure no data-mce-selected. Notably, this can occur when content // is saved while typing within an inline boundary. - await pressWithModifier( META_KEY, 'b' ); + await pressWithModifier( 'primary', 'b' ); await page.keyboard.type( 'Inside' ); expect( await getEditedPostContent() ).toMatchSnapshot(); } ); @@ -162,14 +161,14 @@ describe( 'adding blocks', () => { it( 'should insert line break at end', async () => { await clickBlockAppender(); await page.keyboard.type( 'a' ); - await pressWithModifier( 'Shift', 'Enter' ); + await pressWithModifier( 'shift', 'Enter' ); expect( await getEditedPostContent() ).toMatchSnapshot(); } ); it( 'should insert line break at end and continue writing', async () => { await clickBlockAppender(); await page.keyboard.type( 'a' ); - await pressWithModifier( 'Shift', 'Enter' ); + await pressWithModifier( 'shift', 'Enter' ); await page.keyboard.type( 'b' ); expect( await getEditedPostContent() ).toMatchSnapshot(); } ); @@ -178,7 +177,7 @@ describe( 'adding blocks', () => { await clickBlockAppender(); await page.keyboard.type( 'ab' ); await page.keyboard.press( 'ArrowLeft' ); - await pressWithModifier( 'Shift', 'Enter' ); + await pressWithModifier( 'shift', 'Enter' ); expect( await getEditedPostContent() ).toMatchSnapshot(); } ); @@ -186,13 +185,13 @@ describe( 'adding blocks', () => { await clickBlockAppender(); await page.keyboard.type( 'a' ); await page.keyboard.press( 'ArrowLeft' ); - await pressWithModifier( 'Shift', 'Enter' ); + await pressWithModifier( 'shift', 'Enter' ); expect( await getEditedPostContent() ).toMatchSnapshot(); } ); it( 'should insert line break in empty container', async () => { await clickBlockAppender(); - await pressWithModifier( 'Shift', 'Enter' ); + await pressWithModifier( 'shift', 'Enter' ); expect( await getEditedPostContent() ).toMatchSnapshot(); } ); @@ -210,7 +209,7 @@ describe( 'adding blocks', () => { expect( isInTitle ).toBe( true ); // Should remain in title upon modifier + ArrowDown: - await pressWithModifier( META_KEY, 'ArrowDown' ); + await pressWithModifier( 'primary', 'ArrowDown' ); isInTitle = await page.evaluate( () => ( !! document.activeElement.closest( '.editor-post-title' ) ) ); @@ -243,9 +242,9 @@ describe( 'adding blocks', () => { await pressTimes( 'ArrowLeft', ' gamma'.length ); if ( process.platform === 'darwin' ) { - await pressWithModifier( 'Alt', 'Backspace' ); + await pressWithModifier( 'alt', 'Backspace' ); } else { - await pressWithModifier( META_KEY, 'Backspace' ); + await pressWithModifier( 'primary', 'Backspace' ); } expect( await getEditedPostContent() ).toMatchSnapshot(); diff --git a/test/e2e/support/utils/index.js b/test/e2e/support/utils/index.js index 693bf990d74e9f..a3f52f91ad79d4 100644 --- a/test/e2e/support/utils/index.js +++ b/test/e2e/support/utils/index.js @@ -16,14 +16,13 @@ export { insertBlock } from './insert-block'; export { isEmbedding } from './is-embedding'; export { JSONResponse } from './json-response'; export { matchURL } from './match-url'; -export { META_KEY } from './meta-key'; export { mockOrTransform } from './mock-or-transform'; export { newPost } from './new-post'; export { observeFocusLoss } from './observe-focus-loss'; export { openDocumentSettingsSidebar } from './open-document-settings-sidebar'; export { openPublishPanel } from './open-publish-panel'; export { pressTimes } from './press-times'; -export { ACCESS_MODIFIER_KEYS, pressWithModifier } from './press-with-modifier'; +export { pressWithModifier } from './press-with-modifier'; export { publishPost } from './publish-post'; export { publishPostWithoutPrePublishChecks } from './publish-post-without-pre-publish-checks'; export { saveDraft } from './save-draft'; diff --git a/test/e2e/support/utils/login.js b/test/e2e/support/utils/login.js index 1b843b637469e3..19b3e99d727271 100644 --- a/test/e2e/support/utils/login.js +++ b/test/e2e/support/utils/login.js @@ -1,16 +1,16 @@ + /** * Internal dependencies */ -import { pressWithModifier } from './press-with-modifier'; -import { META_KEY } from './meta-key'; import { WP_USERNAME, WP_PASSWORD } from './config'; +import { pressWithModifier } from './press-with-modifier'; export async function login( username = WP_USERNAME, password = WP_PASSWORD ) { await page.focus( '#user_login' ); - await pressWithModifier( META_KEY, 'a' ); + await pressWithModifier( 'primary', 'a' ); await page.type( '#user_login', username ); await page.focus( '#user_pass' ); - await pressWithModifier( META_KEY, 'a' ); + await pressWithModifier( 'primary', 'a' ); await page.type( '#user_pass', password ); await Promise.all( [ page.waitForNavigation(), page.click( '#wp-submit' ) ] ); diff --git a/test/e2e/support/utils/meta-key.js b/test/e2e/support/utils/meta-key.js deleted file mode 100644 index 1728d48687e55b..00000000000000 --- a/test/e2e/support/utils/meta-key.js +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Platform-specific meta key. - * - * @see pressWithModifier - * - * @type {string} - */ -export const META_KEY = process.platform === 'darwin' ? 'Meta' : 'Control'; diff --git a/test/e2e/support/utils/press-with-modifier.js b/test/e2e/support/utils/press-with-modifier.js index d25a8fa0f2bae7..ec274dfd39766b 100644 --- a/test/e2e/support/utils/press-with-modifier.js +++ b/test/e2e/support/utils/press-with-modifier.js @@ -1,35 +1,42 @@ /** * External dependencies */ -import { castArray } from 'lodash'; +import { capitalize } from 'lodash'; /** - * Platform-specific modifier for the access key chord. - * - * @see pressWithModifier - * - * @type {string} + * WordPress dependencies */ -export const ACCESS_MODIFIER_KEYS = - process.platform === 'darwin' ? [ 'Control', 'Alt' ] : [ 'Shift', 'Alt' ]; +import { modifiers, SHIFT, ALT, CTRL } from '@wordpress/keycodes'; /** - * Performs a key press with modifier (Shift, Control, Meta, Mod), where "Mod" - * is normalized to platform-specific modifier (Meta in MacOS, else Control). + * Performs a key press with modifier (Shift, Control, Meta, Alt), where each modifier + * is normalized to platform-specific modifier. * - * @param {string|Array} modifiers Modifier key or array of modifier keys. - * @param {string} key Key to press while modifier held. + * @param {string} modifier Modifier key. + * @param {string} key Key to press while modifier held. */ -export async function pressWithModifier( modifiers, key ) { - const modifierKeys = castArray( modifiers ); +export async function pressWithModifier( modifier, key ) { + const isAppleOS = () => process.platform === 'darwin'; + const overWrittenModifiers = { + ...modifiers, + shiftAlt: ( _isApple ) => _isApple() ? [ SHIFT, ALT ] : [ SHIFT, CTRL ], + }; + const mappedModifiers = overWrittenModifiers[ modifier ]( isAppleOS ); + const ctrlSwap = ( mod ) => mod === CTRL ? 'control' : mod; await Promise.all( - modifierKeys.map( async ( modifier ) => page.keyboard.down( modifier ) ) + mappedModifiers.map( async ( mod ) => { + const capitalizedMod = capitalize( ctrlSwap( mod ) ); + return page.keyboard.down( capitalizedMod ); + } ) ); await page.keyboard.press( key ); await Promise.all( - modifierKeys.map( async ( modifier ) => page.keyboard.up( modifier ) ) + mappedModifiers.map( async ( mod ) => { + const capitalizedMod = capitalize( ctrlSwap( mod ) ); + return page.keyboard.up( capitalizedMod ); + } ) ); } From d1d478632814c590a06e1265abde7b3611a76d98 Mon Sep 17 00:00:00 2001 From: Dixita Dusara <dixitadusara@gmail.com> Date: Sat, 17 Nov 2018 20:43:25 +0530 Subject: [PATCH 018/254] Translate URL to Classic Editor plugin (#12013) Fixes #11983. --- gutenberg.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gutenberg.php b/gutenberg.php index 762707d1755746..e76f15561139ef 100644 --- a/gutenberg.php +++ b/gutenberg.php @@ -41,9 +41,9 @@ function the_gutenberg_project() { ); } else { // Using Gutenberg in Core. printf( - // Translators: link is for Classic Editor plugin. + /* translators: %s: https://wordpress.org/plugins/classic-editor/ */ __( 'The Block Editor requires JavaScript. Please try the <a href="%s">Classic Editor plugin</a>.', 'gutenberg' ), - 'https://wordpress.org/plugins/classic-editor/' + __( 'https://wordpress.org/plugins/classic-editor/', 'gutenberg' ) ); } ?> From 454415ced9d5a265535f82d7562282f1cd1c668e Mon Sep 17 00:00:00 2001 From: Pascal Birchler <pascal.birchler@gmail.com> Date: Sun, 18 Nov 2018 15:26:37 +0100 Subject: [PATCH 019/254] Use correct translation function for assistive text (#12011) --- packages/editor/src/store/effects.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/editor/src/store/effects.js b/packages/editor/src/store/effects.js index d54120ac961608..7c05730540dbe5 100644 --- a/packages/editor/src/store/effects.js +++ b/packages/editor/src/store/effects.js @@ -14,7 +14,7 @@ import { doBlocksMatchTemplate, synchronizeBlocksWithTemplate, } from '@wordpress/blocks'; -import { __, sprintf } from '@wordpress/i18n'; +import { _n, sprintf } from '@wordpress/i18n'; /** * Internal dependencies @@ -267,6 +267,7 @@ export default { MULTI_SELECT: ( action, { getState } ) => { const blockCount = getSelectedBlockCount( getState() ); - speak( sprintf( __( '%s blocks selected.' ), blockCount ), 'assertive' ); + /* translators: %s: number of selected blocks */ + speak( sprintf( _n( '%s block selected.', '%s blocks selected.', blockCount ), blockCount ), 'assertive' ); }, }; From 78a736462f8d1d70b838125b493918d472353384 Mon Sep 17 00:00:00 2001 From: Robert Anderson <robert@noisysocks.com> Date: Mon, 19 Nov 2018 10:54:14 +1100 Subject: [PATCH 020/254] Mobile: Fix post visibility popover not appearing (#11954) --- assets/stylesheets/_z-index.scss | 1 + .../src/components/post-visibility/style.scss | 4 +++ test/e2e/specs/post-visibility.test.js | 31 +++++++++++++++++++ .../utils/open-document-settings-sidebar.js | 2 +- 4 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 test/e2e/specs/post-visibility.test.js diff --git a/assets/stylesheets/_z-index.scss b/assets/stylesheets/_z-index.scss index 39e1bb461d46b1..85b0099665715d 100644 --- a/assets/stylesheets/_z-index.scss +++ b/assets/stylesheets/_z-index.scss @@ -85,6 +85,7 @@ $z-layers: ( // Shows above edit post sidebar; Specificity needs to be higher than 3 classes. ".block-editor__container .components-popover.components-color-palette__picker.is-bottom": 100001, + ".edit-post-post-visibility__dialog.components-popover.is-bottom": 100001, ".components-autocomplete__results": 1000000, diff --git a/packages/editor/src/components/post-visibility/style.scss b/packages/editor/src/components/post-visibility/style.scss index ad02ee0f7cbb94..6efc2afc59ea1e 100644 --- a/packages/editor/src/components/post-visibility/style.scss +++ b/packages/editor/src/components/post-visibility/style.scss @@ -33,3 +33,7 @@ margin-left: 28px; } } + +.edit-post-post-visibility__dialog.components-popover.is-bottom { + z-index: z-index(".edit-post-post-visibility__dialog.components-popover.is-bottom"); +} diff --git a/test/e2e/specs/post-visibility.test.js b/test/e2e/specs/post-visibility.test.js new file mode 100644 index 00000000000000..392fd3f9c6efdf --- /dev/null +++ b/test/e2e/specs/post-visibility.test.js @@ -0,0 +1,31 @@ +/** + * Internal dependencies + */ +import { + setViewport, + newPost, + openDocumentSettingsSidebar, +} from '../support/utils'; + +describe( 'Post visibility', () => { + [ 'large', 'small' ].forEach( ( viewport ) => { + it( `can be changed when the viewport is ${ viewport }`, async () => { + await setViewport( viewport ); + + await newPost(); + + await openDocumentSettingsSidebar(); + + await page.click( '.edit-post-post-visibility__toggle' ); + + const [ privateLabel ] = await page.$x( '//label[text()="Private"]' ); + await privateLabel.click(); + + const currentStatus = await page.evaluate( () => { + return wp.data.select( 'core/editor' ).getEditedPostAttribute( 'status' ); + } ); + + expect( currentStatus ).toBe( 'private' ); + } ); + } ); +} ); diff --git a/test/e2e/support/utils/open-document-settings-sidebar.js b/test/e2e/support/utils/open-document-settings-sidebar.js index 3e85a58e32974e..ab6f797a4bacca 100644 --- a/test/e2e/support/utils/open-document-settings-sidebar.js +++ b/test/e2e/support/utils/open-document-settings-sidebar.js @@ -7,6 +7,6 @@ export async function openDocumentSettingsSidebar() { ); if ( openButton ) { - await page.click( openButton ); + await openButton.click(); } } From 9622659b57c46d85ae2e7e30acb02d1a69ace34f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20=28Greg=29=20Zi=C3=B3=C5=82kowski?= <grzegorz@gziolo.pl> Date: Mon, 19 Nov 2018 09:28:21 +0100 Subject: [PATCH 021/254] Config: Mirror packages dependencies registration with core (#11637) * Config: Mirror packages dependencies registration with core * Introduce package dependencies file * Automate Webpack config by fetching list of packages from package.json file * Make phpcs happy * Sort packages dependencies alphabetically * Update @since comment for gutenberg_register_packages_scripts * Address feedback from the review --- lib/client-assets.php | 451 ++-------------------------------- lib/packages-dependencies.php | 235 ++++++++++++++++++ package-lock.json | 12 + package.json | 1 + webpack.config.js | 52 +--- 5 files changed, 285 insertions(+), 466 deletions(-) create mode 100644 lib/packages-dependencies.php diff --git a/lib/client-assets.php b/lib/client-assets.php index 7f4acff26619e4..f08e2cdad84ff2 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -133,6 +133,29 @@ function gutenberg_override_style( $handle, $src, $deps = array(), $ver = false, wp_register_style( $handle, $src, $deps, $ver, $media ); } +/** + * Registers all the WordPress packages scripts that are in the standardized + * `build/` location. + * + * @since 4.5.0 + */ +function gutenberg_register_packages_scripts() { + $packages_dependencies = include dirname( __FILE__ ) . '/packages-dependencies.php'; + + foreach ( $packages_dependencies as $handle => $dependencies ) { + // Remove `wp-` prefix from the handle to get the package's name. + $package_name = strpos( $handle, 'wp-' ) === 0 ? substr( $handle, 3 ) : $handle; + $path = "build/$package_name/index.js"; + gutenberg_override_script( + $handle, + gutenberg_url( $path ), + array_merge( $dependencies, array( 'wp-polyfill' ) ), + filemtime( gutenberg_dir_path() . $path ), + true + ); + } +} + /** * Registers common scripts and styles to be used as dependencies of the editor * and plugins. @@ -156,78 +179,10 @@ function gutenberg_register_scripts_and_styles() { ) ) ); - gutenberg_override_script( - 'wp-url', - gutenberg_url( 'build/url/index.js' ), - array( 'wp-polyfill' ), - filemtime( gutenberg_dir_path() . 'build/url/index.js' ), - true - ); - gutenberg_override_script( - 'wp-autop', - gutenberg_url( 'build/autop/index.js' ), - array( 'wp-polyfill' ), - filemtime( gutenberg_dir_path() . 'build/autop/index.js' ), - true - ); - gutenberg_override_script( - 'wp-wordcount', - gutenberg_url( 'build/wordcount/index.js' ), - array( 'wp-polyfill' ), - filemtime( gutenberg_dir_path() . 'build/wordcount/index.js' ), - true - ); - gutenberg_override_script( - 'wp-dom-ready', - gutenberg_url( 'build/dom-ready/index.js' ), - array( 'wp-polyfill' ), - filemtime( gutenberg_dir_path() . 'build/dom-ready/index.js' ), - true - ); - gutenberg_override_script( - 'wp-a11y', - gutenberg_url( 'build/a11y/index.js' ), - array( 'wp-dom-ready', 'wp-polyfill' ), - filemtime( gutenberg_dir_path() . 'build/a11y/index.js' ), - true - ); - gutenberg_override_script( - 'wp-hooks', - gutenberg_url( 'build/hooks/index.js' ), - array( 'wp-polyfill' ), - filemtime( gutenberg_dir_path() . 'build/hooks/index.js' ), - true - ); - gutenberg_override_script( - 'wp-i18n', - gutenberg_url( 'build/i18n/index.js' ), - array( 'wp-polyfill' ), - filemtime( gutenberg_dir_path() . 'build/i18n/index.js' ), - true - ); - gutenberg_override_script( - 'wp-is-shallow-equal', - gutenberg_url( 'build/is-shallow-equal/index.js' ), - array( 'wp-polyfill' ), - filemtime( gutenberg_dir_path() . 'build/is-shallow-equal/index.js' ), - true - ); - gutenberg_override_script( - 'wp-token-list', - gutenberg_url( 'build/token-list/index.js' ), - array( 'lodash', 'wp-polyfill' ), - filemtime( gutenberg_dir_path() . 'build/token-list/index.js' ), - true - ); - // Editor Scripts. - gutenberg_override_script( - 'wp-api-fetch', - gutenberg_url( 'build/api-fetch/index.js' ), - array( 'wp-polyfill', 'wp-hooks', 'wp-i18n', 'wp-url' ), - filemtime( gutenberg_dir_path() . 'build/api-fetch/index.js' ), - true - ); + gutenberg_register_packages_scripts(); + + // Inline scripts. wp_add_inline_script( 'wp-api-fetch', sprintf( @@ -244,61 +199,6 @@ function gutenberg_register_scripts_and_styles() { ), 'after' ); - - gutenberg_override_script( - 'wp-deprecated', - gutenberg_url( 'build/deprecated/index.js' ), - array( 'wp-polyfill', 'wp-hooks' ), - filemtime( gutenberg_dir_path() . 'build/deprecated/index.js' ), - true - ); - gutenberg_override_script( - 'wp-blob', - gutenberg_url( 'build/blob/index.js' ), - array( 'wp-polyfill' ), - filemtime( gutenberg_dir_path() . 'build/blob/index.js' ), - true - ); - gutenberg_override_script( - 'wp-compose', - gutenberg_url( 'build/compose/index.js' ), - array( - 'lodash', - 'wp-element', - 'wp-is-shallow-equal', - 'wp-polyfill', - ), - filemtime( gutenberg_dir_path() . 'build/compose/index.js' ), - true - ); - gutenberg_override_script( - 'wp-keycodes', - gutenberg_url( 'build/keycodes/index.js' ), - array( 'lodash', 'wp-polyfill' ), - filemtime( gutenberg_dir_path() . 'build/keycodes/index.js' ), - true - ); - gutenberg_override_script( - 'wp-html-entities', - gutenberg_url( 'build/html-entities/index.js' ), - array( 'wp-polyfill' ), - filemtime( gutenberg_dir_path() . 'build/html-entities/index.js' ), - true - ); - gutenberg_override_script( - 'wp-data', - gutenberg_url( 'build/data/index.js' ), - array( - 'lodash', - 'wp-compose', - 'wp-element', - 'wp-is-shallow-equal', - 'wp-polyfill', - 'wp-redux-routine', - ), - filemtime( gutenberg_dir_path() . 'build/data/index.js' ), - true - ); wp_add_inline_script( 'wp-data', implode( @@ -314,62 +214,6 @@ 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' ), - array( 'wp-data', 'wp-api-fetch', 'wp-polyfill', 'wp-url', 'lodash' ), - filemtime( gutenberg_dir_path() . 'build/core-data/index.js' ), - true - ); - gutenberg_override_script( - 'wp-dom', - gutenberg_url( 'build/dom/index.js' ), - array( 'lodash', 'wp-polyfill', 'wp-tinymce' ), - filemtime( gutenberg_dir_path() . 'build/dom/index.js' ), - true - ); - gutenberg_override_script( - 'wp-block-serialization-default-parser', - gutenberg_url( 'build/block-serialization-default-parser/index.js' ), - array(), - filemtime( gutenberg_dir_path() . 'build/block-serialization-default-parser/index.js' ), - true - ); - gutenberg_override_script( - 'wp-block-serialization-spec-parser', - gutenberg_url( 'build/block-serialization-spec-parser/index.js' ), - array( 'wp-polyfill' ), - filemtime( gutenberg_dir_path() . 'build/block-serialization-spec-parser/index.js' ), - true - ); - gutenberg_override_script( - 'wp-shortcode', - gutenberg_url( 'build/shortcode/index.js' ), - array( 'wp-polyfill', 'lodash' ), - filemtime( gutenberg_dir_path() . 'build/shortcode/index.js' ), - true - ); - gutenberg_override_script( - 'wp-redux-routine', - gutenberg_url( 'build/redux-routine/index.js' ), - array( 'wp-polyfill' ), - filemtime( gutenberg_dir_path() . 'build/redux-routine/index.js' ), - true - ); - gutenberg_override_script( - 'wp-date', - gutenberg_url( 'build/date/index.js' ), - array( 'moment', 'wp-polyfill' ), - filemtime( gutenberg_dir_path() . 'build/date/index.js' ), - true - ); global $wp_locale; wp_add_inline_script( 'wp-date', @@ -406,163 +250,6 @@ function gutenberg_register_scripts_and_styles() { ), 'after' ); - gutenberg_override_script( - 'wp-element', - gutenberg_url( 'build/element/index.js' ), - array( 'wp-polyfill', 'react', 'react-dom', 'lodash', 'wp-escape-html' ), - filemtime( gutenberg_dir_path() . 'build/element/index.js' ), - true - ); - gutenberg_override_script( - 'wp-escape-html', - gutenberg_url( 'build/escape-html/index.js' ), - array( 'wp-polyfill' ), - filemtime( gutenberg_dir_path() . 'build/element/index.js' ), - true - ); - gutenberg_override_script( - 'wp-rich-text', - gutenberg_url( 'build/rich-text/index.js' ), - array( - 'lodash', - 'wp-polyfill', - 'wp-data', - 'wp-escape-html', - ), - filemtime( gutenberg_dir_path() . 'build/rich-text/index.js' ), - true - ); - gutenberg_override_script( - 'wp-components', - gutenberg_url( 'build/components/index.js' ), - array( - 'lodash', - 'moment', - 'wp-a11y', - 'wp-api-fetch', - 'wp-compose', - 'wp-deprecated', - 'wp-dom', - 'wp-element', - 'wp-hooks', - 'wp-html-entities', - 'wp-i18n', - 'wp-is-shallow-equal', - 'wp-keycodes', - 'wp-polyfill', - 'wp-rich-text', - 'wp-url', - ), - filemtime( gutenberg_dir_path() . 'build/components/index.js' ), - true - ); - gutenberg_override_script( - 'wp-blocks', - gutenberg_url( 'build/blocks/index.js' ), - array( - 'wp-autop', - 'wp-blob', - 'wp-block-serialization-default-parser', - 'wp-data', - 'wp-dom', - 'wp-element', - 'wp-hooks', - 'wp-html-entities', - 'wp-i18n', - 'wp-is-shallow-equal', - 'wp-polyfill', - 'wp-shortcode', - 'lodash', - ), - filemtime( gutenberg_dir_path() . 'build/blocks/index.js' ), - true - ); - gutenberg_override_script( - 'wp-notices', - gutenberg_url( 'build/notices/index.js' ), - array( - 'lodash', - 'wp-a11y', - 'wp-data', - 'wp-polyfill', - ), - filemtime( gutenberg_dir_path() . 'build/notices/index.js' ), - true - ); - gutenberg_override_script( - 'wp-viewport', - gutenberg_url( 'build/viewport/index.js' ), - array( 'wp-polyfill', 'wp-element', 'wp-data', 'wp-compose', 'lodash' ), - filemtime( gutenberg_dir_path() . 'build/viewport/index.js' ), - true - ); - gutenberg_override_script( - 'wp-block-library', - gutenberg_url( 'build/block-library/index.js' ), - array( - 'editor', - 'lodash', - 'moment', - 'wp-api-fetch', - 'wp-autop', - 'wp-blob', - 'wp-blocks', - 'wp-components', - 'wp-compose', - 'wp-core-data', - 'wp-data', - 'wp-date', - 'wp-editor', - 'wp-element', - 'wp-html-entities', - 'wp-i18n', - 'wp-keycodes', - 'wp-polyfill', - 'wp-url', - 'wp-viewport', - 'wp-rich-text', - ), - filemtime( gutenberg_dir_path() . 'build/block-library/index.js' ), - true - ); - gutenberg_override_script( - 'wp-format-library', - gutenberg_url( 'build/format-library/index.js' ), - array( - 'wp-components', - 'wp-dom', - 'wp-editor', - 'wp-element', - 'wp-i18n', - 'wp-keycodes', - 'wp-polyfill', - 'wp-rich-text', - 'wp-url', - ), - filemtime( gutenberg_dir_path() . 'build/format-library/index.js' ), - true - ); - gutenberg_override_script( - 'wp-nux', - gutenberg_url( 'build/nux/index.js' ), - array( - 'wp-element', - 'wp-components', - 'wp-compose', - 'wp-data', - 'wp-i18n', - 'wp-polyfill', - 'lodash', - ), - filemtime( gutenberg_dir_path() . 'build/nux/index.js' ), - true - ); - gutenberg_override_script( - 'wp-plugins', - gutenberg_url( 'build/plugins/index.js' ), - array( 'lodash', 'wp-compose', 'wp-element', 'wp-hooks', 'wp-polyfill' ), - filemtime( gutenberg_dir_path() . 'build/plugins/index.js' ) - ); // Loading the old editor and its config to ensure the classic block works as expected. wp_add_inline_script( 'editor', @@ -666,92 +353,6 @@ function gutenberg_register_scripts_and_styles() { ) ); - gutenberg_override_script( - 'wp-editor', - gutenberg_url( 'build/editor/index.js' ), - array( - 'jquery', - 'lodash', - 'tinymce-latest-lists', - 'wp-a11y', - 'wp-api-fetch', - 'wp-blob', - 'wp-blocks', - 'wp-components', - 'wp-compose', - 'wp-core-data', - 'wp-data', - 'wp-date', - 'wp-deprecated', - 'wp-dom', - 'wp-element', - 'wp-hooks', - 'wp-html-entities', - 'wp-i18n', - 'wp-is-shallow-equal', - 'wp-keycodes', - 'wp-notices', - 'wp-nux', - 'wp-polyfill', - 'wp-tinymce', - 'wp-token-list', - 'wp-url', - 'wp-viewport', - 'wp-wordcount', - 'wp-rich-text', - ), - filemtime( gutenberg_dir_path() . 'build/editor/index.js' ) - ); - - gutenberg_override_script( - 'wp-edit-post', - gutenberg_url( 'build/edit-post/index.js' ), - array( - 'jquery', - 'lodash', - 'postbox', - 'media-models', - 'media-views', - 'wp-a11y', - 'wp-api-fetch', - 'wp-block-library', - 'wp-blocks', - 'wp-components', - 'wp-compose', - 'wp-core-data', - 'wp-data', - 'wp-dom-ready', - 'wp-editor', - 'wp-element', - 'wp-embed', - 'wp-i18n', - 'wp-keycodes', - 'wp-nux', - 'wp-plugins', - 'wp-polyfill', - 'wp-url', - 'wp-viewport', - ), - filemtime( gutenberg_dir_path() . 'build/edit-post/index.js' ), - true - ); - - gutenberg_override_script( - 'wp-list-reusable-blocks', - gutenberg_url( 'build/list-reusable-blocks/index.js' ), - array( - 'lodash', - 'wp-api-fetch', - 'wp-components', - 'wp-compose', - 'wp-element', - 'wp-i18n', - 'wp-polyfill', - ), - filemtime( gutenberg_dir_path() . 'build/list-reusable-blocks/index.js' ), - true - ); - // Editor Styles. // This empty stylesheet is defined to ensure backwards compatibility. gutenberg_override_style( 'wp-blocks', false ); diff --git a/lib/packages-dependencies.php b/lib/packages-dependencies.php new file mode 100644 index 00000000000000..d729cca109f38c --- /dev/null +++ b/lib/packages-dependencies.php @@ -0,0 +1,235 @@ +<?php +/** + * Returns array with the list of WordPress packages and their dependencies. + * + * @package gutenberg + */ + +return array( + 'wp-a11y' => array( + 'wp-dom-ready', + ), + 'wp-annotations' => array( + 'wp-data', + 'wp-hooks', + 'wp-i18n', + 'wp-rich-text', + ), + 'wp-api-fetch' => array( + 'wp-hooks', + 'wp-i18n', + 'wp-url', + ), + 'wp-autop' => array(), + 'wp-blob' => array(), + 'wp-block-library' => array( + 'editor', + 'lodash', + 'moment', + 'wp-api-fetch', + 'wp-autop', + 'wp-blob', + 'wp-blocks', + 'wp-components', + 'wp-compose', + 'wp-core-data', + 'wp-data', + 'wp-date', + 'wp-editor', + 'wp-element', + 'wp-html-entities', + 'wp-i18n', + 'wp-keycodes', + 'wp-rich-text', + 'wp-url', + 'wp-viewport', + ), + 'wp-block-serialization-default-parser' => array(), + 'wp-block-serialization-spec-parser' => array(), + 'wp-blocks' => array( + 'lodash', + 'wp-autop', + 'wp-blob', + 'wp-block-serialization-default-parser', + 'wp-data', + 'wp-dom', + 'wp-element', + 'wp-hooks', + 'wp-html-entities', + 'wp-i18n', + 'wp-is-shallow-equal', + 'wp-shortcode', + ), + 'wp-components' => array( + 'lodash', + 'moment', + 'wp-a11y', + 'wp-api-fetch', + 'wp-compose', + 'wp-deprecated', + 'wp-dom', + 'wp-element', + 'wp-hooks', + 'wp-html-entities', + 'wp-i18n', + 'wp-is-shallow-equal', + 'wp-keycodes', + 'wp-rich-text', + 'wp-url', + ), + 'wp-compose' => array( + 'lodash', + 'wp-element', + 'wp-is-shallow-equal', + ), + 'wp-core-data' => array( + 'lodash', + 'wp-api-fetch', + 'wp-data', + 'wp-url', + ), + 'wp-data' => array( + 'lodash', + 'wp-compose', + 'wp-element', + 'wp-is-shallow-equal', + 'wp-redux-routine', + ), + 'wp-date' => array( + 'moment', + ), + 'wp-deprecated' => array( + 'wp-hooks', + ), + 'wp-dom' => array( + 'lodash', + 'wp-tinymce', + ), + 'wp-dom-ready' => array(), + 'wp-edit-post' => array( + 'jquery', + 'lodash', + 'postbox', + 'media-models', + 'media-views', + 'wp-a11y', + 'wp-api-fetch', + 'wp-block-library', + 'wp-blocks', + 'wp-components', + 'wp-compose', + 'wp-core-data', + 'wp-data', + 'wp-dom-ready', + 'wp-editor', + 'wp-element', + 'wp-embed', + 'wp-i18n', + 'wp-keycodes', + 'wp-nux', + 'wp-plugins', + 'wp-url', + 'wp-viewport', + ), + 'wp-editor' => array( + 'jquery', + 'lodash', + 'tinymce-latest-lists', + 'wp-a11y', + 'wp-api-fetch', + 'wp-blob', + 'wp-blocks', + 'wp-components', + 'wp-compose', + 'wp-core-data', + 'wp-data', + 'wp-date', + 'wp-deprecated', + 'wp-dom', + 'wp-element', + 'wp-hooks', + 'wp-html-entities', + 'wp-i18n', + 'wp-is-shallow-equal', + 'wp-keycodes', + 'wp-notices', + 'wp-nux', + 'wp-rich-text', + 'wp-tinymce', + 'wp-token-list', + 'wp-url', + 'wp-viewport', + 'wp-wordcount', + ), + 'wp-element' => array( + 'lodash', + 'react', + 'react-dom', + 'wp-escape-html', + ), + 'wp-escape-html' => array(), + 'wp-format-library' => array( + 'wp-components', + 'wp-dom', + 'wp-editor', + 'wp-element', + 'wp-i18n', + 'wp-keycodes', + 'wp-rich-text', + 'wp-url', + ), + 'wp-hooks' => array(), + 'wp-html-entities' => array(), + 'wp-i18n' => array(), + 'wp-is-shallow-equal' => array(), + 'wp-keycodes' => array( + 'lodash', + ), + 'wp-list-reusable-blocks' => array( + 'lodash', + 'wp-api-fetch', + 'wp-components', + 'wp-compose', + 'wp-element', + 'wp-i18n', + ), + 'wp-notices' => array( + 'lodash', + 'wp-a11y', + 'wp-data', + ), + 'wp-nux' => array( + 'lodash', + 'wp-components', + 'wp-compose', + 'wp-data', + 'wp-element', + 'wp-i18n', + ), + 'wp-plugins' => array( + 'lodash', + 'wp-compose', + 'wp-element', + 'wp-hooks', + ), + 'wp-redux-routine' => array(), + 'wp-rich-text' => array( + 'lodash', + 'wp-data', + 'wp-escape-html', + ), + 'wp-shortcode' => array( + 'lodash', + ), + 'wp-token-list' => array( + 'lodash', + ), + 'wp-url' => array(), + 'wp-viewport' => array( + 'lodash', + 'wp-compose', + 'wp-data', + 'wp-element', + ), + 'wp-wordcount' => array(), +); diff --git a/package-lock.json b/package-lock.json index 3225bfcd623077..5d995bca4758f9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2672,6 +2672,18 @@ "webpack-sources": "^1.1.0" } }, + "@wordpress/list-reusable-blocks": { + "version": "file:packages/list-reusable-blocks", + "requires": { + "@babel/runtime": "^7.0.0", + "@wordpress/api-fetch": "file:packages/api-fetch", + "@wordpress/components": "file:packages/components", + "@wordpress/compose": "file:packages/compose", + "@wordpress/element": "file:packages/element", + "@wordpress/i18n": "file:packages/i18n", + "lodash": "^4.17.10" + } + }, "@wordpress/notices": { "version": "file:packages/notices", "requires": { diff --git a/package.json b/package.json index fba02b0a957f67..19548299b7e193 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@wordpress/i18n": "file:packages/i18n", "@wordpress/is-shallow-equal": "file:packages/is-shallow-equal", "@wordpress/keycodes": "file:packages/keycodes", + "@wordpress/list-reusable-blocks": "file:packages/list-reusable-blocks", "@wordpress/notices": "file:packages/notices", "@wordpress/nux": "file:packages/nux", "@wordpress/plugins": "file:packages/plugins", diff --git a/webpack.config.js b/webpack.config.js index acf041927ea485..0f6062429151e6 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -16,6 +16,13 @@ const CustomTemplatedPathPlugin = require( '@wordpress/custom-templated-path-web const LibraryExportDefaultPlugin = require( '@wordpress/library-export-default-webpack-plugin' ); const { BundleAnalyzerPlugin } = require( 'webpack-bundle-analyzer' ); +/** + * Internal dependencies + */ +const { dependencies } = require( './package' ); + +const WORDPRESS_NAMESPACE = '@wordpress/'; + /** * Given a string, returns a new string with dash separators converted to * camelCase equivalent. This is not as aggressive as `_.camelCase` in @@ -33,46 +40,9 @@ function camelCaseDash( string ) { ); } -const gutenbergPackages = [ - 'a11y', - 'annotations', - 'api-fetch', - 'autop', - 'blob', - 'blocks', - 'block-library', - 'block-serialization-default-parser', - 'block-serialization-spec-parser', - 'components', - 'compose', - 'core-data', - 'data', - 'date', - 'deprecated', - 'dom', - 'dom-ready', - 'edit-post', - 'editor', - 'element', - 'escape-html', - 'format-library', - 'hooks', - 'html-entities', - 'i18n', - 'is-shallow-equal', - 'keycodes', - 'list-reusable-blocks', - 'notices', - 'nux', - 'plugins', - 'redux-routine', - 'rich-text', - 'shortcode', - 'token-list', - 'url', - 'viewport', - 'wordcount', -]; +const gutenbergPackages = Object.keys( dependencies ) + .filter( ( packageName ) => packageName.startsWith( WORDPRESS_NAMESPACE ) ) + .map( ( packageName ) => packageName.replace( WORDPRESS_NAMESPACE, '' ) ); const externals = { react: 'React', @@ -85,7 +55,7 @@ const externals = { }; gutenbergPackages.forEach( ( name ) => { - externals[ `@wordpress/${ name }` ] = { + externals[ WORDPRESS_NAMESPACE + name ] = { this: [ 'wp', camelCaseDash( name ) ], }; } ); From 08fe16a13c37e0ab7870fc123b66244fdd537f7b Mon Sep 17 00:00:00 2001 From: Andrew Duthie <andrew@andrewduthie.com> Date: Mon, 19 Nov 2018 03:49:51 -0500 Subject: [PATCH 022/254] Build Tooling: Avoid `.default` on browser global assignments (#12006) * Core Data: Avoid exporting default * Token List: Assign exported default to global --- packages/core-data/src/index.js | 4 +--- webpack.config.js | 1 + 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/core-data/src/index.js b/packages/core-data/src/index.js index 1d8aa2fab6cb3d..b0c5718ab9b888 100644 --- a/packages/core-data/src/index.js +++ b/packages/core-data/src/index.js @@ -41,12 +41,10 @@ const entityActions = defaultEntities.reduce( ( result, entity ) => { return result; }, {} ); -const store = registerStore( REDUCER_KEY, { +registerStore( REDUCER_KEY, { reducer, controls, actions: { ...actions, ...entityActions }, selectors: { ...selectors, ...entitySelectors }, resolvers: { ...resolvers, ...entityResolvers }, } ); - -export default store; diff --git a/webpack.config.js b/webpack.config.js index 0f6062429151e6..7d9877d7c15e3e 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -137,6 +137,7 @@ const config = { 'deprecated', 'dom-ready', 'redux-routine', + 'token-list', ].map( camelCaseDash ) ), new CopyWebpackPlugin( gutenbergPackages.map( ( packageName ) => ( { From 763bb6b8ae926c088d80e7e66e7f258b5b32195b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Wrede?= <soerenwrede@gmail.com> Date: Mon, 19 Nov 2018 09:51:40 +0100 Subject: [PATCH 023/254] Update More block description (#11587) * Update More block description * Update packages/block-library/src/more/index.js Co-Authored-By: Soean <soerenwrede@gmail.com> --- 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 0aab7198df8f02..f51b9ef971e455 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 homepage? Use this block to define where you want the separation.' ), + description: __( 'Mark the excerpt of this content. Content before this block will be shown in the excerpt on your archives page.' ), 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 56b9553e67f99e2dd51cbb680a9c664397717c72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milan=20Dini=C4=87?= <milan@srpski.biz> Date: Mon, 19 Nov 2018 10:20:02 +0100 Subject: [PATCH 024/254] Improve post locking modal (#10886) * Use post type label instead of hardcoded one. * Link to post type screen for editing all posts. * Use button in both cases when linking to screen with all posts. * Add translators comments about placeholder in string. Remove translators comments explaining post type. * Add dot at the end of strings with sentences for consistency. * Use apostrophe instead of single-quote character. --- .../src/components/post-locked-modal/index.js | 36 +++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/packages/editor/src/components/post-locked-modal/index.js b/packages/editor/src/components/post-locked-modal/index.js index 420be8d2e895e6..db8de6f00579c2 100644 --- a/packages/editor/src/components/post-locked-modal/index.js +++ b/packages/editor/src/components/post-locked-modal/index.js @@ -2,6 +2,7 @@ * External dependencies */ import jQuery from 'jquery'; +import { get } from 'lodash'; /** * WordPress dependencies @@ -118,7 +119,7 @@ class PostLockedModal extends Component { } render() { - const { user, postId, isLocked, isTakeover, postLockUtils } = this.props; + const { user, postId, isLocked, isTakeover, postLockUtils, postType } = this.props; if ( ! isLocked ) { return null; } @@ -133,7 +134,10 @@ class PostLockedModal extends Component { action: 'edit', _wpnonce: postLockUtils.nonce, } ); - const allPosts = getWPAdminURL( 'edit.php' ); + const allPostsUrl = getWPAdminURL( 'edit.php', { + post_type: get( postType, [ 'slug' ] ), + } ); + const allPostsLabel = get( postType, [ 'labels', 'all_items' ] ); return ( <Modal title={ isTakeover ? __( 'Someone else has taken over this post.' ) : __( 'This post is already being edited.' ) } @@ -155,19 +159,19 @@ class PostLockedModal extends Component { <div> { userDisplayName ? sprintf( - /* translators: 'post' is generic and may be of any type (post, page, etc.). */ - __( '%s now has editing control of this post. Don\'t worry, your changes up to this moment have been saved.' ), + /* translators: %s: user's display name */ + __( '%s now has editing control of this post. Don’t worry, your changes up to this moment have been saved.' ), userDisplayName ) : - /* translators: 'post' is generic and may be of any type (post, page, etc.). */ - __( 'Another user now has editing control of this post. Don\'t worry, your changes up to this moment have been saved.' ) + __( 'Another user now has editing control of this post. Don’t worry, your changes up to this moment have been saved.' ) } </div> - <p> - <a href={ allPosts }> - { __( 'View all posts' ) } - </a> - </p> + + <div className="editor-post-locked-modal__buttons"> + <Button isPrimary isLarge href={ allPostsUrl }> + { allPostsLabel } + </Button> + </div> </div> ) } { ! isTakeover && ( @@ -175,18 +179,17 @@ class PostLockedModal extends Component { <div> { userDisplayName ? sprintf( - /* translators: 'post' is generic and may be of any type (post, page, etc.). */ + /* translators: %s: user's display name */ __( '%s is currently working on this post, which means you cannot make changes, unless you take over.' ), userDisplayName ) : - /* translators: 'post' is generic and may be of any type (post, page, etc.). */ __( 'Another user is currently working on this post, which means you cannot make changes, unless you take over.' ) } </div> <div className="editor-post-locked-modal__buttons"> - <Button isDefault isLarge href={ allPosts }> - { __( 'All Posts' ) } + <Button isDefault isLarge href={ allPostsUrl }> + { allPostsLabel } </Button> <PostPreviewButton /> <Button isPrimary isLarge href={ unlockUrl }> @@ -209,7 +212,9 @@ export default compose( getPostLockUser, getCurrentPostId, getActivePostLock, + getEditedPostAttribute, } = select( 'core/editor' ); + const { getPostType } = select( 'core' ); return { isLocked: isPostLocked(), isTakeover: isPostLockTakeover(), @@ -217,6 +222,7 @@ export default compose( postId: getCurrentPostId(), postLockUtils: getEditorSettings().postLockUtils, activePostLock: getActivePostLock(), + postType: getPostType( getEditedPostAttribute( 'type' ) ), }; } ), withDispatch( ( dispatch ) => { From c5e146e75ff249b5ecfe5665c0076074172e3e91 Mon Sep 17 00:00:00 2001 From: Jon Desrosiers <desrosj@users.noreply.github.com> Date: Mon, 19 Nov 2018 05:13:03 -0500 Subject: [PATCH 025/254] Fix the Privacy Policy help notice (#11999) * Display the privacy policy help notice with the `admin_notices` action hook. The new editor does not support the `edit_form_after_title` action hook. Because WordPress Core uses this hook to output the notice, it is not printed to the screen. After #11604 is merged, legacy style admin notices (`<div class="notice">...notice content...</div>`) will be consumed by the Notices API and displayed. This change ensures that when #11604 is merged the privacy policy notice will appear again when editing the privacy policy page. * Use `get_post()` instead of `global $post`. * Add missing `@since` documentation tag. * Only display the notice if the function is actually hooked to `edit_form_after_title`. This prevents the notice from displaying twice if the notice has already been moved to the `admin_notices` hook (as would happen in https://core.trac.wordpress.org/attachment/ticket/45057/45057.diff). --- lib/compat.php | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/lib/compat.php b/lib/compat.php index ff0938c496828a..88b417491a89e9 100644 --- a/lib/compat.php +++ b/lib/compat.php @@ -116,7 +116,6 @@ function gutenberg_wpautop( $content ) { remove_filter( 'the_content', 'wpautop' ); add_filter( 'the_content', 'gutenberg_wpautop', 6 ); - /** * Check if we need to load the block warning in the Classic Editor. * @@ -304,3 +303,22 @@ function gutenberg_warn_classic_about_blocks() { </script> <?php } + +/** + * Display the privacy policy help notice. + * + * In Gutenberg, the `edit_form_after_title` hook is not supported. Because + * WordPress Core uses this hook to display this notice, it never displays. + * Outputting the notice on the `admin_notices` hook allows Gutenberg to + * consume the notice and display it with the Notices API. + * + * @since 4.5.0 + */ +function gutenberg_show_privacy_policy_help_text() { + if ( is_gutenberg_page() && has_action( 'edit_form_after_title', array( 'WP_Privacy_Policy_Content', 'notice' ) ) ) { + remove_action( 'edit_form_after_title', array( 'WP_Privacy_Policy_Content', 'notice' ) ); + + WP_Privacy_Policy_Content::notice( get_post() ); + } +} +add_action( 'admin_notices', 'gutenberg_show_privacy_policy_help_text' ); From 66b95afaa5f812d754fe2df56b4ad008846468c8 Mon Sep 17 00:00:00 2001 From: Joen Asmussen <joen@automattic.com> Date: Mon, 19 Nov 2018 11:17:19 +0100 Subject: [PATCH 026/254] Fix regression with Sibling Inserter. (#11969) * Fix regression with Sibling Inserter. The sibling inserter is the plus you see when you hover between two blocks. When you have no blocks selected, or use the unified toolbar mode. you can access the sibling inserter between any two blocks. When you have a block selected, and have the block-affixed toolbars, only the sibling inserter below the selected block should be availabl e. Though it should still be keyboard accessible. This PR fixes that. * Fix for focus mode. --- packages/editor/src/components/block-list/style.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/editor/src/components/block-list/style.scss b/packages/editor/src/components/block-list/style.scss index 879e47b7a2cb52..e6787f1128204f 100644 --- a/packages/editor/src/components/block-list/style.scss +++ b/packages/editor/src/components/block-list/style.scss @@ -686,7 +686,8 @@ // 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-inserter { + .is-selected > .editor-block-list__insertion-point > .editor-block-list__insertion-point-inserter, + .is-focused > .editor-block-list__insertion-point > .editor-block-list__insertion-point-inserter { opacity: 0; pointer-events: none; From c7caf4f5e4ea089891da17f21b6e17d480128146 Mon Sep 17 00:00:00 2001 From: Robert Anderson <robert@noisysocks.com> Date: Mon, 19 Nov 2018 21:18:24 +1100 Subject: [PATCH 027/254] Exclude reusable blocks from the global block count (#11787) Modifies getGlobalBlockCount() to exclude reusable blocks from its count. This fixes the block count including reusable blocks that have been fetched and parsed but not inserted into the post or page. --- packages/editor/src/store/selectors.js | 14 +++--- packages/editor/src/store/test/selectors.js | 50 +++++++++------------ 2 files changed, 27 insertions(+), 37 deletions(-) diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index 85883b65c55c9b..3c8d2d121acd92 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -15,7 +15,6 @@ import { map, orderBy, reduce, - size, some, } from 'lodash'; import createSelector from 'rememo'; @@ -739,16 +738,17 @@ export const getClientIdsWithDescendants = createSelector( */ export const getGlobalBlockCount = createSelector( ( state, blockName ) => { + const clientIds = getClientIdsWithDescendants( state ); if ( ! blockName ) { - return size( state.editor.present.blocks.byClientId ); + return clientIds.length; } - return reduce( - state.editor.present.blocks.byClientId, - ( count, block ) => block.name === blockName ? count + 1 : count, - 0 - ); + return reduce( clientIds, ( count, clientId ) => { + const block = state.editor.present.blocks.byClientId[ clientId ]; + return block.name === blockName ? count + 1 : count; + }, 0 ); }, ( state ) => [ + state.editor.present.blocks.order, state.editor.present.blocks.byClientId, ] ); diff --git a/packages/editor/src/store/test/selectors.js b/packages/editor/src/store/test/selectors.js index 9e7dcf6aa91072..751bfccc2945f0 100644 --- a/packages/editor/src/store/test/selectors.js +++ b/packages/editor/src/store/test/selectors.js @@ -2511,54 +2511,44 @@ describe( 'selectors', () => { } ); describe( 'getGlobalBlockCount', () => { - it( 'should return the global number of top-level blocks in the post', () => { - const state = { - editor: { - present: { - blocks: { - byClientId: { - 23: { clientId: 23, name: 'core/heading', attributes: {} }, - 123: { clientId: 123, name: 'core/paragraph', attributes: {} }, - }, + const state = { + editor: { + present: { + blocks: { + byClientId: { + 123: { clientId: 123, name: 'core/heading', attributes: {} }, + 456: { clientId: 456, name: 'core/paragraph', attributes: {} }, + 789: { clientId: 789, name: 'core/paragraph', attributes: {} }, + }, + order: { + '': [ 123, 456 ], }, }, }, - }; + }, + }; + it( 'should return the global number of blocks in the post', () => { expect( getGlobalBlockCount( state ) ).toBe( 2 ); } ); - it( 'should return the global umber of blocks of a given type', () => { - const state = { - editor: { - present: { - blocks: { - byClientId: { - 123: { clientId: 123, name: 'core/columns', attributes: {} }, - 456: { clientId: 456, name: 'core/paragraph', attributes: {} }, - 789: { clientId: 789, name: 'core/paragraph', attributes: {} }, - 124: { clientId: 123, name: 'core/heading', attributes: {} }, - }, - }, - }, - }, - }; - - expect( getGlobalBlockCount( state, 'core/heading' ) ).toBe( 1 ); + it( 'should return the global number of blocks in the post of a given type', () => { + expect( getGlobalBlockCount( state, 'core/paragraph' ) ).toBe( 1 ); } ); it( 'should return 0 if no blocks exist', () => { - const state = { + const emptyState = { editor: { present: { blocks: { byClientId: {}, + order: {}, }, }, }, }; - expect( getGlobalBlockCount( state ) ).toBe( 0 ); - expect( getGlobalBlockCount( state, 'core/heading' ) ).toBe( 0 ); + expect( getGlobalBlockCount( emptyState ) ).toBe( 0 ); + expect( getGlobalBlockCount( emptyState, 'core/heading' ) ).toBe( 0 ); } ); } ); From 3726c56fd2a8bd47d6f0aae9f5d4c3ba15a221f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s?= <nosolosw@users.noreply.github.com> Date: Mon, 19 Nov 2018 11:23:14 +0100 Subject: [PATCH 028/254] Fix editor break on Unified toolbar (#11996) some interactions may cause no block being selected. --- .../block-settings-menu/plugin-block-settings-menu-group.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/edit-post/src/components/block-settings-menu/plugin-block-settings-menu-group.js b/packages/edit-post/src/components/block-settings-menu/plugin-block-settings-menu-group.js index 24859cb92fcc3c..a216f4d1bd3288 100644 --- a/packages/edit-post/src/components/block-settings-menu/plugin-block-settings-menu-group.js +++ b/packages/edit-post/src/components/block-settings-menu/plugin-block-settings-menu-group.js @@ -13,6 +13,9 @@ import { withSelect } from '@wordpress/data'; const { Fill: PluginBlockSettingsMenuGroup, Slot } = createSlotFill( 'PluginBlockSettingsMenuGroup' ); const PluginBlockSettingsMenuGroupSlot = ( { fillProps, selectedBlocks } ) => { + if ( !! selectedBlocks ) { + return null; + } selectedBlocks = map( selectedBlocks, ( block ) => block.name ); return ( <Slot fillProps={ { ...fillProps, selectedBlocks } } > From 8585f150022f01db120a1321a3a3755b6666ec61 Mon Sep 17 00:00:00 2001 From: Joen Asmussen <joen@automattic.com> Date: Mon, 19 Nov 2018 11:25:13 +0100 Subject: [PATCH 029/254] Fix small visual regression with button variation preview. (#12051) In https://github.com/WordPress/gutenberg/pull/11757/files#diff-9bc45fdf28d434e26e152546f25fd08eR101, a change was introduced to how custom placeholder text works. Custom placeholder text is basically a duplicate version of the text, which holds the visual placeholder. The height change as linked above was added to make sure the two elements are in sync, height-wise. Which is important for when the placeholder text wraps. The Button styles this placeholder directly, which means it changes things sligtly in this regard. The long term solution would be to refactor this slightly, so that we don't style this placeholder, but instead style a parent element in the editor. But in the mean time, this PR fixes the current markup and CSS. --- packages/block-library/src/button/editor.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/block-library/src/button/editor.scss b/packages/block-library/src/button/editor.scss index c4e960352c80c5..f20d565964dbcd 100644 --- a/packages/block-library/src/button/editor.scss +++ b/packages/block-library/src/button/editor.scss @@ -31,6 +31,11 @@ $blocks-button__line-height: $big-font-size + 6px; opacity: 0.8; } + // Polish the empty placeholder text for the button in variation previews. + .editor-rich-text__tinymce[data-is-placeholder-visible="true"] { + height: auto; + } + // Don't let the placeholder text wrap in the variation preview. .editor-block-preview__content & { max-width: 100%; From 095d18fe5f1b55ab243cdba947740542e5b0fe47 Mon Sep 17 00:00:00 2001 From: Marcus Kazmierczak <marcus@mkaz.com> Date: Mon, 19 Nov 2018 02:27:28 -0800 Subject: [PATCH 030/254] Honor the Disable Visual Editor setting (#12000) * Do not override user_can_richedit option * Add checks for user_can_richedit() to show plain-text editor Honors the user setting for `Disable the visual editor when writing` If the user checks the option to disable visual editor: - Returns from Gutenberg init - Removes the links to Classic Editor in post list - Removes the Gutenberg menu item When editing a post, it will load the plain text editor. * Update user_can_richedit added to filter Moves the user_can_richedit to attach just on the filters and not spread through out different fucntions. add_filter( 'gutenberg_can_edit_post_type', 'user_can_richedit', 5 ); add_filter( 'gutenberg_can_edit_post', 'user_can_richedit', 5 ); add_filter( 'use_block_editor_for_post', 'user_can_richedit', 5 ); * Update with just plugin changes * Don't include core function use_block_editor_for_post * Don't show Gutenberg menu in admin sidebar, since Demo link will not load block editor, still plain text editor * Fix comment --- gutenberg.php | 10 ++++++++++ lib/client-assets.php | 6 ------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/gutenberg.php b/gutenberg.php index e76f15561139ef..4756b68584b930 100644 --- a/gutenberg.php +++ b/gutenberg.php @@ -67,6 +67,9 @@ function the_gutenberg_project() { * @since 0.1.0 */ function gutenberg_menu() { + if ( ! gutenberg_can_edit_post( $post ) ) { + return; + } global $submenu; add_menu_page( @@ -200,6 +203,13 @@ function gutenberg_pre_init() { add_filter( 'replace_editor', 'gutenberg_init', 10, 2 ); } +/** + * Enable Gutenberg based on user_can_richedit setting. + * Set gutenberg_can_edit_post based on user setting for disable visual editor. + */ +add_filter( 'gutenberg_can_edit_post_type', 'user_can_richedit', 5 ); +add_filter( 'gutenberg_can_edit_post', 'user_can_richedit', 5 ); + /** * Initialize Gutenberg. * diff --git a/lib/client-assets.php b/lib/client-assets.php index f08e2cdad84ff2..2b78d56cf2f09c 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -1020,12 +1020,6 @@ function gutenberg_editor_scripts_and_styles( $hook ) { 'after' ); - // Ignore Classic Editor's `rich_editing` user option, aka "Disable visual - // editor". Forcing this to be true guarantees that TinyMCE and its plugins - // are available in Gutenberg. Fixes - // https://github.com/WordPress/gutenberg/issues/5667. - add_filter( 'user_can_richedit', '__return_true' ); - wp_enqueue_script( 'wp-edit-post' ); wp_enqueue_script( 'wp-format-library' ); wp_enqueue_style( 'wp-format-library' ); From dfa4e0429a360cb850988b07ba3cd92d37e3cd1a Mon Sep 17 00:00:00 2001 From: Ned Zimmerman <ned@bight.ca> Date: Mon, 19 Nov 2018 05:46:31 -0500 Subject: [PATCH 031/254] Add link rel and link class to image block inspector (see #7504) (#7771) * Add link classes and rel attribute to image block (see #7504) * Make labels consistent with classic editor. * Fix coding standards error * Add tests * Consolidate check for presence of image link * Rebase, incorporate upstream changes * Changes as per review * Fix tests * Resolve final review comment * Update CONTRIBUTORS.md --- CONTRIBUTORS.md | 2 + packages/block-library/src/image/edit.js | 56 ++++++++++++++++++- packages/block-library/src/image/index.js | 55 ++++++++++++++++-- .../core__image__custom-link-class.html | 3 + .../core__image__custom-link-class.json | 17 ++++++ ...core__image__custom-link-class.parsed.json | 22 ++++++++ ...__image__custom-link-class.serialized.html | 3 + .../core__image__custom-link-rel.html | 3 + .../core__image__custom-link-rel.json | 17 ++++++ .../core__image__custom-link-rel.parsed.json | 22 ++++++++ ...re__image__custom-link-rel.serialized.html | 3 + 11 files changed, 197 insertions(+), 6 deletions(-) create mode 100644 test/integration/full-content/fixtures/core__image__custom-link-class.html create mode 100644 test/integration/full-content/fixtures/core__image__custom-link-class.json create mode 100644 test/integration/full-content/fixtures/core__image__custom-link-class.parsed.json create mode 100644 test/integration/full-content/fixtures/core__image__custom-link-class.serialized.html create mode 100644 test/integration/full-content/fixtures/core__image__custom-link-rel.html create mode 100644 test/integration/full-content/fixtures/core__image__custom-link-rel.json create mode 100644 test/integration/full-content/fixtures/core__image__custom-link-rel.parsed.json create mode 100644 test/integration/full-content/fixtures/core__image__custom-link-rel.serialized.html diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 15f1331b338a0f..3a130f10d4ddf2 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -117,3 +117,5 @@ This list is manually curated to include valuable contributions by volunteers th | @amdrew | @sumobi | | @MaedahBatool | @MaedahBatool | | @luehrsen | @luehrsen | +| @getsource | @mikeschroder | +| @greatislander | @greatislander | diff --git a/packages/block-library/src/image/edit.js b/packages/block-library/src/image/edit.js index f7dd63a22db4ac..c708c1a485e401 100644 --- a/packages/block-library/src/image/edit.js +++ b/packages/block-library/src/image/edit.js @@ -59,6 +59,7 @@ const LINK_DESTINATION_NONE = 'none'; const LINK_DESTINATION_MEDIA = 'media'; const LINK_DESTINATION_ATTACHMENT = 'attachment'; const LINK_DESTINATION_CUSTOM = 'custom'; +const NEW_TAB_REL = 'noreferrer noopener'; const ALLOWED_MEDIA_TYPES = [ 'image' ]; export const pickRelevantMediaFiles = ( image ) => { @@ -103,7 +104,10 @@ class ImageEdit extends Component { this.updateHeight = this.updateHeight.bind( this ); this.updateDimensions = this.updateDimensions.bind( this ); this.onSetCustomHref = this.onSetCustomHref.bind( this ); + this.onSetLinkClass = this.onSetLinkClass.bind( this ); + this.onSetLinkRel = this.onSetLinkRel.bind( this ); this.onSetLinkDestination = this.onSetLinkDestination.bind( this ); + this.onSetNewTab = this.onSetNewTab.bind( this ); this.getFilename = this.getFilename.bind( this ); this.toggleIsEditing = this.toggleIsEditing.bind( this ); this.onUploadError = this.onUploadError.bind( this ); @@ -216,6 +220,31 @@ class ImageEdit extends Component { this.props.setAttributes( { href: value } ); } + onSetLinkClass( value ) { + this.props.setAttributes( { linkClass: value } ); + } + + onSetLinkRel( value ) { + this.props.setAttributes( { rel: value } ); + } + + onSetNewTab( value ) { + const { rel } = this.props.attributes; + const linkTarget = value ? '_blank' : undefined; + + let updatedRel = rel; + if ( linkTarget && ! rel ) { + updatedRel = NEW_TAB_REL; + } else if ( ! linkTarget && rel === NEW_TAB_REL ) { + updatedRel = undefined; + } + + this.props.setAttributes( { + linkTarget, + rel: updatedRel, + } ); + } + onFocusCaption() { if ( ! this.state.captionFocused ) { this.setState( { @@ -310,7 +339,20 @@ class ImageEdit extends Component { toggleSelection, isRTL, } = this.props; - const { url, alt, caption, align, id, href, linkDestination, width, height, linkTarget } = attributes; + const { + url, + alt, + caption, + align, + id, + href, + rel, + linkClass, + linkDestination, + width, + height, + linkTarget, + } = attributes; const isExternal = isExternalImage( id, url ); const imageSizeOptions = this.getImageSizeOptions(); @@ -480,8 +522,18 @@ class ImageEdit extends Component { /> <ToggleControl label={ __( 'Open in New Tab' ) } - onChange={ () => setAttributes( { linkTarget: ! linkTarget ? '_blank' : undefined } ) } + onChange={ this.onSetNewTab } checked={ linkTarget === '_blank' } /> + <TextControl + label={ __( 'Link CSS Class' ) } + value={ linkClass || '' } + onChange={ this.onSetLinkClass } + /> + <TextControl + label={ __( 'Link Rel' ) } + value={ rel || '' } + onChange={ this.onSetLinkRel } + /> </Fragment> ) } </PanelBody> diff --git a/packages/block-library/src/image/index.js b/packages/block-library/src/image/index.js index 4f6619a6c6e527..0a5ca7618c4aa9 100644 --- a/packages/block-library/src/image/index.js +++ b/packages/block-library/src/image/index.js @@ -52,6 +52,18 @@ const blockAttributes = { selector: 'figure > a', attribute: 'href', }, + rel: { + type: 'string', + source: 'attribute', + selector: 'figure > a', + attribute: 'rel', + }, + linkClass: { + type: 'string', + source: 'attribute', + selector: 'figure > a', + attribute: 'class', + }, id: { type: 'number', }, @@ -89,7 +101,7 @@ const schema = { children: { ...imageSchema, a: { - attributes: [ 'href', 'target' ], + attributes: [ 'href', 'rel', 'target' ], children: imageSchema, }, figcaption: { @@ -132,7 +144,9 @@ export const settings = { const anchorElement = node.querySelector( 'a' ); const linkDestination = anchorElement && anchorElement.href ? 'custom' : undefined; const href = anchorElement && anchorElement.href ? anchorElement.href : undefined; - const attributes = getBlockAttributes( 'core/image', node.outerHTML, { align, id, linkDestination, href } ); + const rel = anchorElement && anchorElement.rel ? anchorElement.rel : undefined; + const linkClass = anchorElement && anchorElement.className ? anchorElement.className : undefined; + const attributes = getBlockAttributes( 'core/image', node.outerHTML, { align, id, linkDestination, href, rel, linkClass } ); return createBlock( 'core/image', attributes ); }, }, @@ -181,6 +195,18 @@ export const settings = { attribute: 'href', selector: 'a', }, + rel: { + type: 'string', + source: 'attribute', + attribute: 'rel', + selector: 'a', + }, + linkClass: { + type: 'string', + source: 'attribute', + attribute: 'class', + selector: 'a', + }, id: { type: 'number', shortcode: ( { named: { id } } ) => { @@ -212,7 +238,19 @@ export const settings = { edit, save( { attributes } ) { - const { url, alt, caption, align, href, width, height, id, linkTarget } = attributes; + const { + url, + alt, + caption, + align, + href, + rel, + linkClass, + width, + height, + id, + linkTarget, + } = attributes; const classes = classnames( { [ `align${ align }` ]: align, @@ -231,7 +269,16 @@ export const settings = { const figure = ( <Fragment> - { href ? <a href={ href } target={ linkTarget } rel={ linkTarget === '_blank' ? 'noreferrer noopener' : undefined }>{ image }</a> : image } + { href ? ( + <a + className={ linkClass } + href={ href } + target={ linkTarget } + rel={ rel } + > + { image } + </a> + ) : image } { ! RichText.isEmpty( caption ) && <RichText.Content tagName="figcaption" value={ caption } /> } </Fragment> ); diff --git a/test/integration/full-content/fixtures/core__image__custom-link-class.html b/test/integration/full-content/fixtures/core__image__custom-link-class.html new file mode 100644 index 00000000000000..57cc46c9c39bbe --- /dev/null +++ b/test/integration/full-content/fixtures/core__image__custom-link-class.html @@ -0,0 +1,3 @@ +<!-- wp:core/image {"linkDestination":"custom"} --> +<figure class="wp-block-image"><a class="custom-link" href="https://wordpress.org/"><img src="https://cldup.com/uuUqE_dXzy.jpg" alt="" /></a></figure> +<!-- /wp:core/image --> diff --git a/test/integration/full-content/fixtures/core__image__custom-link-class.json b/test/integration/full-content/fixtures/core__image__custom-link-class.json new file mode 100644 index 00000000000000..7009c3f2dede0c --- /dev/null +++ b/test/integration/full-content/fixtures/core__image__custom-link-class.json @@ -0,0 +1,17 @@ +[ + { + "clientId": "_clientId_0", + "name": "core/image", + "isValid": true, + "attributes": { + "url": "https://cldup.com/uuUqE_dXzy.jpg", + "alt": "", + "caption": "", + "href": "https://wordpress.org/", + "linkDestination": "custom", + "linkClass": "custom-link" + }, + "innerBlocks": [], + "originalContent": "<figure class=\"wp-block-image\"><a class=\"custom-link\" href=\"https://wordpress.org/\"><img src=\"https://cldup.com/uuUqE_dXzy.jpg\" alt=\"\" /></a></figure>" + } +] diff --git a/test/integration/full-content/fixtures/core__image__custom-link-class.parsed.json b/test/integration/full-content/fixtures/core__image__custom-link-class.parsed.json new file mode 100644 index 00000000000000..73638304d13812 --- /dev/null +++ b/test/integration/full-content/fixtures/core__image__custom-link-class.parsed.json @@ -0,0 +1,22 @@ +[ + { + "blockName": "core/image", + "attrs": { + "linkDestination": "custom" + }, + "innerBlocks": [], + "innerHTML": "\n<figure class=\"wp-block-image\"><a class=\"custom-link\" href=\"https://wordpress.org/\"><img src=\"https://cldup.com/uuUqE_dXzy.jpg\" alt=\"\" /></a></figure>\n", + "innerContent": [ + "\n<figure class=\"wp-block-image\"><a class=\"custom-link\" href=\"https://wordpress.org/\"><img src=\"https://cldup.com/uuUqE_dXzy.jpg\" alt=\"\" /></a></figure>\n" + ] + }, + { + "attrs": {}, + "blockName": null, + "innerBlocks": [], + "innerHTML": "\n", + "innerContent": [ + "\n" + ] + } +] diff --git a/test/integration/full-content/fixtures/core__image__custom-link-class.serialized.html b/test/integration/full-content/fixtures/core__image__custom-link-class.serialized.html new file mode 100644 index 00000000000000..9ac1f7a7a914e9 --- /dev/null +++ b/test/integration/full-content/fixtures/core__image__custom-link-class.serialized.html @@ -0,0 +1,3 @@ +<!-- wp:image {"linkDestination":"custom"} --> +<figure class="wp-block-image"><a class="custom-link" href="https://wordpress.org/"><img src="https://cldup.com/uuUqE_dXzy.jpg" alt=""/></a></figure> +<!-- /wp:image --> diff --git a/test/integration/full-content/fixtures/core__image__custom-link-rel.html b/test/integration/full-content/fixtures/core__image__custom-link-rel.html new file mode 100644 index 00000000000000..3424ed3fff3d70 --- /dev/null +++ b/test/integration/full-content/fixtures/core__image__custom-link-rel.html @@ -0,0 +1,3 @@ +<!-- wp:core/image {"linkDestination":"custom"} --> +<figure class="wp-block-image"><a href="https://wordpress.org/" rel="external"><img src="https://cldup.com/uuUqE_dXzy.jpg" alt="" /></a></figure> +<!-- /wp:core/image --> diff --git a/test/integration/full-content/fixtures/core__image__custom-link-rel.json b/test/integration/full-content/fixtures/core__image__custom-link-rel.json new file mode 100644 index 00000000000000..3826afc9861b8e --- /dev/null +++ b/test/integration/full-content/fixtures/core__image__custom-link-rel.json @@ -0,0 +1,17 @@ +[ + { + "clientId": "_clientId_0", + "name": "core/image", + "isValid": true, + "attributes": { + "url": "https://cldup.com/uuUqE_dXzy.jpg", + "alt": "", + "caption": "", + "href": "https://wordpress.org/", + "linkDestination": "custom", + "rel": "external" + }, + "innerBlocks": [], + "originalContent": "<figure class=\"wp-block-image\"><a href=\"https://wordpress.org/\" rel=\"external\"><img src=\"https://cldup.com/uuUqE_dXzy.jpg\" alt=\"\" /></a></figure>" + } +] diff --git a/test/integration/full-content/fixtures/core__image__custom-link-rel.parsed.json b/test/integration/full-content/fixtures/core__image__custom-link-rel.parsed.json new file mode 100644 index 00000000000000..a1c7d028f72c9d --- /dev/null +++ b/test/integration/full-content/fixtures/core__image__custom-link-rel.parsed.json @@ -0,0 +1,22 @@ +[ + { + "blockName": "core/image", + "attrs": { + "linkDestination": "custom" + }, + "innerBlocks": [], + "innerHTML": "\n<figure class=\"wp-block-image\"><a href=\"https://wordpress.org/\" rel=\"external\"><img src=\"https://cldup.com/uuUqE_dXzy.jpg\" alt=\"\" /></a></figure>\n", + "innerContent": [ + "\n<figure class=\"wp-block-image\"><a href=\"https://wordpress.org/\" rel=\"external\"><img src=\"https://cldup.com/uuUqE_dXzy.jpg\" alt=\"\" /></a></figure>\n" + ] + }, + { + "attrs": {}, + "blockName": null, + "innerBlocks": [], + "innerHTML": "\n", + "innerContent": [ + "\n" + ] + } +] diff --git a/test/integration/full-content/fixtures/core__image__custom-link-rel.serialized.html b/test/integration/full-content/fixtures/core__image__custom-link-rel.serialized.html new file mode 100644 index 00000000000000..92702548c11d9d --- /dev/null +++ b/test/integration/full-content/fixtures/core__image__custom-link-rel.serialized.html @@ -0,0 +1,3 @@ +<!-- wp:image {"linkDestination":"custom"} --> +<figure class="wp-block-image"><a href="https://wordpress.org/" rel="external"><img src="https://cldup.com/uuUqE_dXzy.jpg" alt=""/></a></figure> +<!-- /wp:image --> From b198e5c4791b2282332def0de89841cd64d75f8e Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Mon, 19 Nov 2018 12:21:17 +0100 Subject: [PATCH 032/254] Add a minimal multi-selection block panel (#12050) * Add a minimal multi-selection block panel * Polish a little bit. * Address feedback. * Add missing style for icon. * Change the block tab. --- .../sidebar/settings-header/index.js | 45 +++++++------------ .../src/components/block-inspector/index.js | 3 +- .../src/components/block-inspector/style.scss | 6 +-- .../multi-selection-inspector/index.js | 41 +++++++++++++++++ .../multi-selection-inspector/style.scss | 27 +++++++++++ packages/editor/src/style.scss | 1 + 6 files changed, 89 insertions(+), 34 deletions(-) create mode 100644 packages/editor/src/components/multi-selection-inspector/index.js create mode 100644 packages/editor/src/components/multi-selection-inspector/style.scss diff --git a/packages/edit-post/src/components/sidebar/settings-header/index.js b/packages/edit-post/src/components/sidebar/settings-header/index.js index 35d731881b5673..33a3bbc27e29ab 100644 --- a/packages/edit-post/src/components/sidebar/settings-header/index.js +++ b/packages/edit-post/src/components/sidebar/settings-header/index.js @@ -1,22 +1,16 @@ /** * WordPress dependencies */ -import { compose } from '@wordpress/compose'; -import { __, _n, sprintf } from '@wordpress/i18n'; -import { withDispatch, withSelect } from '@wordpress/data'; +import { __ } from '@wordpress/i18n'; +import { withDispatch } from '@wordpress/data'; /** * Internal dependencies */ import SidebarHeader from '../sidebar-header'; -const SettingsHeader = ( { count, openDocumentSettings, openBlockSettings, sidebarName } ) => { - // Do not display "0 Blocks". - const blockCount = count === 0 ? 1 : count; - const blockLabel = blockCount === 1 ? - __( 'Block' ) : - sprintf( _n( '%d Block', '%d Blocks', blockCount ), blockCount ); - +const SettingsHeader = ( { openDocumentSettings, openBlockSettings, sidebarName } ) => { + const blockLabel = __( 'Block' ); const [ documentAriaLabel, documentActiveClass ] = sidebarName === 'edit-post/document' ? // translators: ARIA label for the Document Settings sidebar tab, selected. [ __( 'Document settings (selected)' ), 'is-active' ] : @@ -61,21 +55,16 @@ const SettingsHeader = ( { count, openDocumentSettings, openBlockSettings, sideb ); }; -export default compose( - withSelect( ( select ) => ( { - count: select( 'core/editor' ).getSelectedBlockCount(), - } ) ), - withDispatch( ( dispatch ) => { - const { openGeneralSidebar } = dispatch( 'core/edit-post' ); - const { clearSelectedBlock } = dispatch( 'core/editor' ); - return { - openDocumentSettings() { - openGeneralSidebar( 'edit-post/document' ); - clearSelectedBlock(); - }, - openBlockSettings() { - openGeneralSidebar( 'edit-post/block' ); - }, - }; - } ), -)( SettingsHeader ); +export default withDispatch( ( dispatch ) => { + const { openGeneralSidebar } = dispatch( 'core/edit-post' ); + const { clearSelectedBlock } = dispatch( 'core/editor' ); + return { + openDocumentSettings() { + openGeneralSidebar( 'edit-post/document' ); + clearSelectedBlock(); + }, + openBlockSettings() { + openGeneralSidebar( 'edit-post/block' ); + }, + }; +} )( SettingsHeader ); diff --git a/packages/editor/src/components/block-inspector/index.js b/packages/editor/src/components/block-inspector/index.js index 1a515efa20bda4..737b0863cbc2ef 100644 --- a/packages/editor/src/components/block-inspector/index.js +++ b/packages/editor/src/components/block-inspector/index.js @@ -20,10 +20,11 @@ import BlockIcon from '../block-icon'; import InspectorControls from '../inspector-controls'; import InspectorAdvancedControls from '../inspector-advanced-controls'; import BlockStyles from '../block-styles'; +import MultiSelectionInspector from '../multi-selection-inspector'; const BlockInspector = ( { selectedBlockClientId, selectedBlockName, blockType, count, hasBlockStyles } ) => { if ( count > 1 ) { - return <span className="editor-block-inspector__multi-blocks">{ __( 'Coming Soon' ) }</span>; + return <MultiSelectionInspector />; } const isSelectedBlockUnregistered = selectedBlockName === getUnregisteredTypeHandlerName(); diff --git a/packages/editor/src/components/block-inspector/style.scss b/packages/editor/src/components/block-inspector/style.scss index ce9497de9ef65b..e238a085616846 100644 --- a/packages/editor/src/components/block-inspector/style.scss +++ b/packages/editor/src/components/block-inspector/style.scss @@ -1,5 +1,4 @@ -.editor-block-inspector__no-blocks, -.editor-block-inspector__multi-blocks { +.editor-block-inspector__no-blocks { display: block; font-size: $default-font-size; background: $white; @@ -7,9 +6,6 @@ text-align: center; } -.editor-block-inspector__multi-blocks { - border-bottom: $border-width solid $light-gray-500; -} .editor-block-inspector__card { display: flex; diff --git a/packages/editor/src/components/multi-selection-inspector/index.js b/packages/editor/src/components/multi-selection-inspector/index.js new file mode 100644 index 00000000000000..3b569f86495dfa --- /dev/null +++ b/packages/editor/src/components/multi-selection-inspector/index.js @@ -0,0 +1,41 @@ +/** + * WordPress dependencies + */ +import { sprintf, __ } from '@wordpress/i18n'; +import { withSelect } from '@wordpress/data'; +import { serialize } from '@wordpress/blocks'; +import { count as wordCount } from '@wordpress/wordcount'; +import { + Path, + SVG, +} from '@wordpress/components'; + +/** + * Internal Dependencies + */ +import BlockIcon from '../block-icon'; + +function MultiSelectionInspector( { blocks } ) { + return ( + <div className="editor-multi-selection-inspector__card"> + <BlockIcon icon={ + <SVG xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><Path d="M3 5H1v16c0 1.1.9 2 2 2h16v-2H3V5zm18-4H7c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V3c0-1.1-.9-2-2-2zm0 16H7V3h14v14z" /></SVG> + } showColors /> + <div className="editor-multi-selection-inspector__card-content"> + <div className="editor-multi-selection-inspector__card-title"> + { sprintf( __( '%d blocks' ), blocks.length ) } + </div> + <div className="editor-multi-selection-inspector__card-description"> + { sprintf( __( '%d words.' ), wordCount( serialize( blocks ), 'words' ) ) } + </div> + </div> + </div> + ); +} + +export default withSelect( ( select ) => { + const { getMultiSelectedBlocks } = select( 'core/editor' ); + return { + blocks: getMultiSelectedBlocks(), + }; +} )( MultiSelectionInspector ); diff --git a/packages/editor/src/components/multi-selection-inspector/style.scss b/packages/editor/src/components/multi-selection-inspector/style.scss new file mode 100644 index 00000000000000..36023adaef78d5 --- /dev/null +++ b/packages/editor/src/components/multi-selection-inspector/style.scss @@ -0,0 +1,27 @@ +.editor-multi-selection-inspector__card { + display: flex; + align-items: flex-start; + margin: -16px; + padding: 16px; +} + +.editor-multi-selection-inspector__card-content { + flex-grow: 1; +} + +.editor-multi-selection-inspector__card-title { + font-weight: 500; + margin-bottom: 5px; +} + +.editor-multi-selection-inspector__card-description { + font-size: $default-font-size; +} + +.editor-multi-selection-inspector__card .editor-block-icon { + margin-left: -2px; + margin-right: 10px; + padding: 0 3px; + width: $icon-button-size; + height: $icon-button-size-small; +} diff --git a/packages/editor/src/style.scss b/packages/editor/src/style.scss index 972322a31a8b6c..5b79e1b52407ac 100644 --- a/packages/editor/src/style.scss +++ b/packages/editor/src/style.scss @@ -23,6 +23,7 @@ @import "./components/inserter/style.scss"; @import "./components/inserter-list-item/style.scss"; @import "./components/media-placeholder/style.scss"; +@import "./components/multi-selection-inspector/style.scss"; @import "./components/page-attributes/style.scss"; @import "./components/panel-color-settings/style.scss"; @import "./components/plain-text/style.scss"; From 558f3f508423788eab3ff43cb80d41fdd3ff77fc Mon Sep 17 00:00:00 2001 From: antpb <itartist.pdf@gmail.com> Date: Mon, 19 Nov 2018 05:41:24 -0600 Subject: [PATCH 033/254] Fix/media library collections #4612 (#10843) * removes destroyCollection and moves refresh to `onOpen` * Removes file saved from vim-ing/rebasing weird. :) * remove leftovers from rebase --- .../hooks/components/media-upload/index.js | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 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 be68a9e2ce9600..ed6e15dad52461 100644 --- a/packages/edit-post/src/hooks/components/media-upload/index.js +++ b/packages/edit-post/src/hooks/components/media-upload/index.js @@ -163,6 +163,8 @@ class MediaUpload extends Component { } onOpen() { + this.updateCollection(); + if ( ! this.props.value ) { return; } @@ -178,12 +180,27 @@ class MediaUpload extends Component { onClose() { const { onClose } = this.props; - if ( onClose ) { onClose(); } } + updateCollection() { + const frameContent = this.frame.content.get(); + if ( frameContent ) { + const collection = frameContent.collection; + + // clean all attachments we have in memory. + collection.toArray().forEach( ( model ) => model.trigger( 'destroy', model ) ); + + // reset has more flag, if library had small amount of items all items may have been loaded before. + collection.mirroring._hasMore = true; + + // request items + collection.more(); + } + } + openModal() { this.frame.open(); } From eee99f1ed711542301f630598e3847d5e6f33f46 Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Mon, 19 Nov 2018 12:52:28 +0100 Subject: [PATCH 034/254] Revert "Fix editor break on Unified toolbar (#11996)" This reverts commit 3726c56fd2a8bd47d6f0aae9f5d4c3ba15a221f1. --- .../block-settings-menu/plugin-block-settings-menu-group.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/edit-post/src/components/block-settings-menu/plugin-block-settings-menu-group.js b/packages/edit-post/src/components/block-settings-menu/plugin-block-settings-menu-group.js index a216f4d1bd3288..24859cb92fcc3c 100644 --- a/packages/edit-post/src/components/block-settings-menu/plugin-block-settings-menu-group.js +++ b/packages/edit-post/src/components/block-settings-menu/plugin-block-settings-menu-group.js @@ -13,9 +13,6 @@ import { withSelect } from '@wordpress/data'; const { Fill: PluginBlockSettingsMenuGroup, Slot } = createSlotFill( 'PluginBlockSettingsMenuGroup' ); const PluginBlockSettingsMenuGroupSlot = ( { fillProps, selectedBlocks } ) => { - if ( !! selectedBlocks ) { - return null; - } selectedBlocks = map( selectedBlocks, ( block ) => block.name ); return ( <Slot fillProps={ { ...fillProps, selectedBlocks } } > From a6a9a8f0b6a38253ae786a56a9a21686e9545b47 Mon Sep 17 00:00:00 2001 From: Pascal Birchler <pascal.birchler@gmail.com> Date: Mon, 19 Nov 2018 13:01:26 +0100 Subject: [PATCH 035/254] Improve i18n negligence in MultiSelectionInspector (#12054) Addresses an issue introduced in #12050. --- .../components/multi-selection-inspector/index.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/editor/src/components/multi-selection-inspector/index.js b/packages/editor/src/components/multi-selection-inspector/index.js index 3b569f86495dfa..5378c5fe6f539f 100644 --- a/packages/editor/src/components/multi-selection-inspector/index.js +++ b/packages/editor/src/components/multi-selection-inspector/index.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { sprintf, __ } from '@wordpress/i18n'; +import { sprintf, _n } from '@wordpress/i18n'; import { withSelect } from '@wordpress/data'; import { serialize } from '@wordpress/blocks'; import { count as wordCount } from '@wordpress/wordcount'; @@ -16,6 +16,8 @@ import { import BlockIcon from '../block-icon'; function MultiSelectionInspector( { blocks } ) { + const words = wordCount( serialize( blocks ), 'words' ); + return ( <div className="editor-multi-selection-inspector__card"> <BlockIcon icon={ @@ -23,10 +25,16 @@ function MultiSelectionInspector( { blocks } ) { } showColors /> <div className="editor-multi-selection-inspector__card-content"> <div className="editor-multi-selection-inspector__card-title"> - { sprintf( __( '%d blocks' ), blocks.length ) } + { + /* translators: %d: number of blocks */ + sprintf( _n( '%d block', '%d blocks', blocks.length ), blocks.length ) + } </div> <div className="editor-multi-selection-inspector__card-description"> - { sprintf( __( '%d words.' ), wordCount( serialize( blocks ), 'words' ) ) } + { + /* translators: %d: number of words */ + sprintf( _n( '%d word', '%d words', words ), words ) + } </div> </div> </div> From defff25c1f29f9978ab42343293dc85acee698c6 Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Mon, 19 Nov 2018 14:30:29 +0100 Subject: [PATCH 036/254] Revert "Fix/media library collections #4612 (#10843)" (#12064) This reverts commit 558f3f508423788eab3ff43cb80d41fdd3ff77fc. --- .../hooks/components/media-upload/index.js | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) 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 ed6e15dad52461..be68a9e2ce9600 100644 --- a/packages/edit-post/src/hooks/components/media-upload/index.js +++ b/packages/edit-post/src/hooks/components/media-upload/index.js @@ -163,8 +163,6 @@ class MediaUpload extends Component { } onOpen() { - this.updateCollection(); - if ( ! this.props.value ) { return; } @@ -180,27 +178,12 @@ class MediaUpload extends Component { onClose() { const { onClose } = this.props; + if ( onClose ) { onClose(); } } - updateCollection() { - const frameContent = this.frame.content.get(); - if ( frameContent ) { - const collection = frameContent.collection; - - // clean all attachments we have in memory. - collection.toArray().forEach( ( model ) => model.trigger( 'destroy', model ) ); - - // reset has more flag, if library had small amount of items all items may have been loaded before. - collection.mirroring._hasMore = true; - - // request items - collection.more(); - } - } - openModal() { this.frame.open(); } From 41a5f2e91c29fd57ea3605cad5c87f0e7dbfaed7 Mon Sep 17 00:00:00 2001 From: Andrew Duthie <andrew@andrewduthie.com> Date: Mon, 19 Nov 2018 08:32:33 -0500 Subject: [PATCH 037/254] Token List: Add missing stringifier and iterator (#11825) * Token List: Add stringifier * Token List: Add iterator * Token List: Add tests for array-inherited methods --- packages/token-list/CHANGELOG.md | 7 ++++ packages/token-list/src/index.js | 23 ++++++++++ packages/token-list/src/test/index.js | 60 +++++++++++++++++++++++++++ 3 files changed, 90 insertions(+) diff --git a/packages/token-list/CHANGELOG.md b/packages/token-list/CHANGELOG.md index 25a7fe2de7b84a..8bf612a4669aa9 100644 --- a/packages/token-list/CHANGELOG.md +++ b/packages/token-list/CHANGELOG.md @@ -1,3 +1,10 @@ +## 1.1.0 (Unreleased) + +### Enhancements + +- Implements missing stringification behavior (i.e. `toString`), as prescribed in the standard to be the `value` property (the interface's `stringifier`) +- Implements missing iterator behavior + ## 1.0.0 (2018-09-05) - Initial release diff --git a/packages/token-list/src/index.js b/packages/token-list/src/index.js index 21d06edcc8cab9..b959c331e79af7 100644 --- a/packages/token-list/src/index.js +++ b/packages/token-list/src/index.js @@ -59,6 +59,29 @@ export default class TokenList { return this._valueAsArray.length; } + /** + * Returns the stringified form of the TokenList. + * + * @link https://dom.spec.whatwg.org/#DOMTokenList-stringification-behavior + * @link https://www.ecma-international.org/ecma-262/9.0/index.html#sec-tostring + * + * @return {string} Token set as string. + */ + toString() { + return this.value; + } + + /** + * Returns an iterator for the TokenList, iterating items of the set. + * + * @link https://dom.spec.whatwg.org/#domtokenlist + * + * @return {Generator} TokenList iterator. + */ + * [ Symbol.iterator ]() { + return yield* this._valueAsArray; + } + /** * Returns the token with index `index`. * diff --git a/packages/token-list/src/test/index.js b/packages/token-list/src/test/index.js index 708b0ebfda333d..d221abd620370e 100644 --- a/packages/token-list/src/test/index.js +++ b/packages/token-list/src/test/index.js @@ -25,15 +25,75 @@ describe( 'token-list', () => { expect( list.value ).toBe( 'abc' ); expect( list ).toHaveLength( 1 ); } ); + + describe( 'array method inheritence', () => { + it( 'entries', () => { + const list = new TokenList( 'abc ' ); + + expect( [ ...list.entries() ] ).toEqual( [ [ 0, 'abc' ] ] ); + } ); + + it( 'forEach', () => { + expect.assertions( 1 ); + + const list = new TokenList( 'abc ' ); + + list.forEach( ( item ) => expect( item ).toBe( 'abc' ) ); + } ); + + it( 'values', () => { + const list = new TokenList( 'abc ' ); + + expect( [ ...list.values() ] ).toEqual( [ 'abc' ] ); + } ); + + it( 'keys', () => { + const list = new TokenList( 'abc ' ); + + expect( [ ...list.keys() ] ).toEqual( [ 0 ] ); + } ); + } ); } ); describe( 'value', () => { + it( 'gets the stringified value', () => { + const list = new TokenList( 'abc ' ); + + expect( list.value ).toBe( 'abc' ); + } ); + it( 'sets to stringified value', () => { const list = new TokenList(); list.value = undefined; expect( list.value ).toBe( 'undefined' ); } ); + + it( 'is the stringifier of the instance', () => { + const list = new TokenList( 'abc ' ); + + expect( String( list ) ).toBe( 'abc' ); + } ); + } ); + + describe( 'Symbol.iterator', () => { + it( 'returns a generator', () => { + const list = new TokenList(); + + expect( list[ Symbol.iterator ]().next ).toEqual( expect.any( Function ) ); + } ); + + it( 'yields entries', () => { + expect.assertions( 2 ); + + const classes = [ 'abc', 'def' ]; + const list = new TokenList( classes.join( ' ' ) ); + + let i = 0; + for ( const item of list ) { + expect( item ).toBe( classes[ i++ ] ); + } + } ); } ); describe( 'item', () => { From 229ed1c7dcdffa651d293ada25a9afaf2af2a708 Mon Sep 17 00:00:00 2001 From: Andrew Duthie <andrew@andrewduthie.com> Date: Mon, 19 Nov 2018 08:45:43 -0500 Subject: [PATCH 038/254] Data: Avoid responding to `componentDidUpdate` in `withDispatch` (#11866) * Data: Avoid responding to `componentDidUpdate` in `withDispatch` * Data: Update withSelect test case for ignored select props changes --- .eslintrc.js | 4 + packages/data/README.md | 2 + .../src/components/with-dispatch/index.js | 94 +++++++++---------- .../src/components/with-select/test/index.js | 5 +- 4 files changed, 52 insertions(+), 53 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 4f4bfa86ffb2ba..45fc4c94140f1c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -165,6 +165,10 @@ module.exports = { selector: 'CallExpression[callee.object.name="Math"][callee.property.name="random"]', message: 'Do not use Math.random() to generate unique IDs; use withInstanceId instead. (If you’re not generating unique IDs: ignore this message.)', }, + { + selector: 'CallExpression[callee.name="withDispatch"] > :function > BlockStatement > :not(VariableDeclaration,ReturnStatement)', + message: 'withDispatch must return an object with consistent keys. Avoid performing logic in `mapDispatchToProps`.', + }, ], 'react/forbid-elements': [ 'error', { forbid: [ diff --git a/packages/data/README.md b/packages/data/README.md index dfe9783d830d6d..f6ca4c665c8fff 100644 --- a/packages/data/README.md +++ b/packages/data/README.md @@ -286,6 +286,8 @@ const SaleButton = withDispatch( ( dispatch, ownProps ) => { // <SaleButton>Start Sale!</SaleButton> ``` +*Note:* It is important that the `mapDispatchToProps` function always returns an object with the same keys. For example, it should not contain conditions under which a different value would be returned. + ## Generic Stores The `@wordpress/data` module offers a more advanced and generic interface for the purposes of integrating other data systems and situations where more direct control over a data system is needed. In this case, a data store will need to be implemented outside of `@wordpress/data` and then plugged in via three functions: diff --git a/packages/data/src/components/with-dispatch/index.js b/packages/data/src/components/with-dispatch/index.js index 85a7b0961a1aab..3c302c557187fa 100644 --- a/packages/data/src/components/with-dispatch/index.js +++ b/packages/data/src/components/with-dispatch/index.js @@ -7,7 +7,7 @@ import { mapValues } from 'lodash'; * WordPress dependencies */ import { Component } from '@wordpress/element'; -import { pure, compose, createHigherOrderComponent } from '@wordpress/compose'; +import { createHigherOrderComponent } from '@wordpress/compose'; /** * Internal dependencies @@ -27,63 +27,57 @@ import { RegistryConsumer } from '../registry-provider'; * @return {Component} Enhanced component with merged dispatcher props. */ const withDispatch = ( mapDispatchToProps ) => createHigherOrderComponent( - compose( [ - pure, - ( WrappedComponent ) => { - class ComponentWithDispatch extends Component { - constructor( props ) { - super( ...arguments ); + ( WrappedComponent ) => { + class ComponentWithDispatch extends Component { + constructor( props ) { + super( ...arguments ); - this.proxyProps = {}; - this.setProxyProps( props ); - } + this.proxyProps = {}; - componentDidUpdate() { - this.setProxyProps( this.props ); - } + this.setProxyProps( props ); + } - proxyDispatch( propName, ...args ) { - // Original dispatcher is a pre-bound (dispatching) action creator. - mapDispatchToProps( this.props.registry.dispatch, this.props.ownProps )[ propName ]( ...args ); - } + proxyDispatch( propName, ...args ) { + // Original dispatcher is a pre-bound (dispatching) action creator. + mapDispatchToProps( this.props.registry.dispatch, this.props.ownProps )[ propName ]( ...args ); + } - setProxyProps( props ) { - // Assign as instance property so that in subsequent render - // reconciliation, the prop values are referentially equal. - // Importantly, note that while `mapDispatchToProps` is - // called, it is done only to determine the keys for which - // proxy functions should be created. The actual registry - // dispatch does not occur until the function is called. - const propsToDispatchers = mapDispatchToProps( this.props.registry.dispatch, props.ownProps ); - this.proxyProps = mapValues( propsToDispatchers, ( dispatcher, propName ) => { - // Prebind with prop name so we have reference to the original - // dispatcher to invoke. Track between re-renders to avoid - // creating new function references every render. - if ( this.proxyProps.hasOwnProperty( propName ) ) { - return this.proxyProps[ propName ]; - } + setProxyProps( props ) { + // Assign as instance property so that in subsequent render + // reconciliation, the prop values are referentially equal. + // Importantly, note that while `mapDispatchToProps` is + // called, it is done only to determine the keys for which + // proxy functions should be created. The actual registry + // dispatch does not occur until the function is called. + const propsToDispatchers = mapDispatchToProps( this.props.registry.dispatch, props.ownProps ); + this.proxyProps = mapValues( propsToDispatchers, ( dispatcher, propName ) => { + // Prebind with prop name so we have reference to the original + // dispatcher to invoke. Track between re-renders to avoid + // creating new function references every render. + if ( this.proxyProps.hasOwnProperty( propName ) ) { + return this.proxyProps[ propName ]; + } - return this.proxyDispatch.bind( this, propName ); - } ); - } + return this.proxyDispatch.bind( this, propName ); + } ); + } - render() { - return <WrappedComponent { ...this.props.ownProps } { ...this.proxyProps } />; - } + render() { + return <WrappedComponent { ...this.props.ownProps } { ...this.proxyProps } />; } + } - return ( ownProps ) => ( - <RegistryConsumer> - { ( registry ) => ( - <ComponentWithDispatch - ownProps={ ownProps } - registry={ registry } - /> - ) } - </RegistryConsumer> - ); - }, - ] ), + return ( ownProps ) => ( + <RegistryConsumer> + { ( registry ) => ( + <ComponentWithDispatch + ownProps={ ownProps } + registry={ registry } + /> + ) } + </RegistryConsumer> + ); + }, 'withDispatch' ); diff --git a/packages/data/src/components/with-select/test/index.js b/packages/data/src/components/with-select/test/index.js index 4a874872df032d..f42926e02266f7 100644 --- a/packages/data/src/components/with-select/test/index.js +++ b/packages/data/src/components/with-select/test/index.js @@ -115,11 +115,10 @@ describe( 'withSelect', () => { testInstance.findByType( 'button' ).props.onClick(); expect( testInstance.findByType( 'button' ).props.children ).toBe( 1 ); - // 3 times = + // 2 times = // 1. Initial mount // 2. When click handler is called - // 3. After select updates its merge props - expect( mapDispatchToProps ).toHaveBeenCalledTimes( 3 ); + expect( mapDispatchToProps ).toHaveBeenCalledTimes( 2 ); expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 2 ); } ); From 2aea727bf2c86f30c24713bfcbc0bcd37494ad98 Mon Sep 17 00:00:00 2001 From: William Earnhardt <wearnhardt@gmail.com> Date: Mon, 19 Nov 2018 08:51:27 -0500 Subject: [PATCH 039/254] Fix undefined variable warning in gutenberg.php (#12053) * Fix undefined variable warning * Remove the gutenberg menu check --- gutenberg.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/gutenberg.php b/gutenberg.php index 4756b68584b930..4c99e466cc6b6a 100644 --- a/gutenberg.php +++ b/gutenberg.php @@ -67,9 +67,6 @@ function the_gutenberg_project() { * @since 0.1.0 */ function gutenberg_menu() { - if ( ! gutenberg_can_edit_post( $post ) ) { - return; - } global $submenu; add_menu_page( From 6ae6285600fee4c184a8f3adc7de87f4ca852a52 Mon Sep 17 00:00:00 2001 From: William Earnhardt <wearnhardt@gmail.com> Date: Mon, 19 Nov 2018 08:58:50 -0500 Subject: [PATCH 040/254] Recursively step through edits to track individually changed post meta (#10827) * Only send edited meta values as state to editPost * Recursively step through edits to granularly update state on EDIT_POST * Don't recurse for arrays * Editor: Deeply diff/merge edited property edits/updates * Editor: Pass meta updates from block as patch * Editor: Shallow merge on whitelisted post property edits --- .../editor/src/components/block-list/block.js | 7 +- packages/editor/src/store/reducer.js | 21 ++++- packages/editor/src/store/test/reducer.js | 84 +++++++++++++++++++ 3 files changed, 104 insertions(+), 8 deletions(-) diff --git a/packages/editor/src/components/block-list/block.js b/packages/editor/src/components/block-list/block.js index 614fbe9f762f07..1263bbe9e40bc5 100644 --- a/packages/editor/src/components/block-list/block.js +++ b/packages/editor/src/components/block-list/block.js @@ -179,10 +179,7 @@ export class BlockListBlock extends Component { }, {} ); if ( size( metaAttributes ) ) { - this.props.onMetaChange( { - ...this.props.meta, - ...metaAttributes, - } ); + this.props.onMetaChange( metaAttributes ); } } @@ -646,7 +643,6 @@ const applyWithSelect = withSelect( ( select, { clientId, rootClientId, isLargeV isTyping, isCaretWithinFormattedText, getBlockIndex, - getEditedPostAttribute, getBlockMode, isSelectionEnabled, getSelectedBlocksInitialCaretPosition, @@ -673,7 +669,6 @@ const applyWithSelect = withSelect( ( select, { clientId, rootClientId, isLargeV isTypingWithinBlock: ( isSelected || isParentOfSelectedBlock ) && isTyping(), isCaretWithinFormattedText: isCaretWithinFormattedText(), order: getBlockIndex( clientId, rootClientId ), - meta: getEditedPostAttribute( 'meta' ), mode: getBlockMode( clientId ), isSelectionEnabled: isSelectionEnabled(), initialPosition: getSelectedBlocksInitialCaretPosition(), diff --git a/packages/editor/src/store/reducer.js b/packages/editor/src/store/reducer.js index 6bfb468cd71578..5396186a9be44d 100644 --- a/packages/editor/src/store/reducer.js +++ b/packages/editor/src/store/reducer.js @@ -35,6 +35,16 @@ import { } from './defaults'; import { insertAt, moveTo } from './array'; +/** + * Set of post properties for which edits should assume a merging behavior, + * assuming an object value. + * + * @type {Set} + */ +const EDIT_MERGE_PROPERTIES = new Set( [ + 'meta', +] ); + /** * Returns a post attribute value, flattening nested rendered content using its * raw value in place of its original object form. @@ -244,7 +254,14 @@ export const editor = flow( [ // Only assign into result if not already same value if ( value !== state[ key ] ) { result = getMutateSafeObject( state, result ); - result[ key ] = value; + + if ( EDIT_MERGE_PROPERTIES.has( key ) ) { + // Merge properties should assign to current value. + result[ key ] = { ...result[ key ], ...value }; + } else { + // Otherwise override. + result[ key ] = value; + } } return result; @@ -264,7 +281,7 @@ export const editor = flow( [ ( key ) => getPostRawValue( action.post[ key ] ); return reduce( state, ( result, value, key ) => { - if ( value !== getCanonicalValue( key ) ) { + if ( ! isEqual( value, getCanonicalValue( key ) ) ) { return result; } diff --git a/packages/editor/src/store/test/reducer.js b/packages/editor/src/store/test/reducer.js index d40b07b0b36bd7..e088a2a67012ac 100644 --- a/packages/editor/src/store/test/reducer.js +++ b/packages/editor/src/store/test/reducer.js @@ -1040,6 +1040,90 @@ describe( 'state', () => { } ); } ); + it( 'should merge object values', () => { + const original = editor( undefined, { + type: 'EDIT_POST', + edits: { + meta: { + a: 1, + }, + }, + } ); + + const state = editor( original, { + type: 'EDIT_POST', + edits: { + meta: { + b: 2, + }, + }, + } ); + + expect( state.present.edits ).toEqual( { + meta: { + a: 1, + b: 2, + }, + } ); + } ); + + it( 'return state by reference on unchanging update', () => { + const original = editor( undefined, {} ); + + const state = editor( original, { + type: 'UPDATE_POST', + edits: {}, + } ); + + expect( state.present.edits ).toBe( original.present.edits ); + } ); + + it( 'unset reset post values which match by canonical value', () => { + const original = editor( undefined, { + type: 'EDIT_POST', + edits: { + title: 'modified title', + }, + } ); + + const state = editor( original, { + type: 'RESET_POST', + post: { + title: { + raw: 'modified title', + }, + }, + } ); + + expect( state.present.edits ).toEqual( {} ); + } ); + + it( 'unset reset post values by deep match', () => { + const original = editor( undefined, { + type: 'EDIT_POST', + edits: { + title: 'modified title', + meta: { + a: 1, + b: 2, + }, + }, + } ); + + const state = editor( original, { + type: 'UPDATE_POST', + edits: { + title: 'modified title', + meta: { + a: 1, + b: 2, + }, + }, + } ); + + expect( state.present.edits ).toEqual( {} ); + } ); + 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 From 3a57743b8bec37e392c9f81549330888614d7634 Mon Sep 17 00:00:00 2001 From: Andrea Fercia <a.fercia@gmail.com> Date: Mon, 19 Nov 2018 15:15:38 +0100 Subject: [PATCH 041/254] Sidebar permalink panel a11y improvements (#12030) * Use ExternalLink for links with target=_blank. * Adjust font-weight values accordingly to current CSS coding standards. --- .../src/components/sidebar/post-link/index.js | 10 +++++----- .../src/components/sidebar/post-link/style.scss | 7 +------ 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/packages/edit-post/src/components/sidebar/post-link/index.js b/packages/edit-post/src/components/sidebar/post-link/index.js index 5d9c38bd80a1a3..744d96a6ec37aa 100644 --- a/packages/edit-post/src/components/sidebar/post-link/index.js +++ b/packages/edit-post/src/components/sidebar/post-link/index.js @@ -8,7 +8,7 @@ import { get } from 'lodash'; */ import { Fragment } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; -import { PanelBody, TextControl } from '@wordpress/components'; +import { PanelBody, TextControl, ExternalLink } from '@wordpress/components'; import { withSelect, withDispatch } from '@wordpress/data'; import { compose, ifCondition, withState } from '@wordpress/compose'; import { addQueryArgs } from '@wordpress/url'; @@ -77,7 +77,7 @@ function PostLink( { <p className="edit-post-post-link__preview-label"> { __( 'Preview' ) } </p> - <a + <ExternalLink className="edit-post-post-link__link" href={ postLink } target="_blank" @@ -88,14 +88,14 @@ function PostLink( { </Fragment> ) : postLink } - </a> - <a + </ExternalLink> + <ExternalLink className="edit-post-post-link__permalink-settings" href={ addQueryArgs( 'options-permalink.php' ) } target="_blank" > { __( 'Permalink Settings' ) } - </a> + </ExternalLink> </PanelBody> ); } diff --git a/packages/edit-post/src/components/sidebar/post-link/style.scss b/packages/edit-post/src/components/sidebar/post-link/style.scss index a6dd413a3ef51c..d32fba6567c2c9 100644 --- a/packages/edit-post/src/components/sidebar/post-link/style.scss +++ b/packages/edit-post/src/components/sidebar/post-link/style.scss @@ -1,9 +1,5 @@ -.edit-post-post-link__link { - font-weight: 600; -} - .edit-post-post-link__link-post-name { - font-weight: 800; + font-weight: 600; } .edit-post-post-link__preview-label { @@ -16,6 +12,5 @@ .edit-post-post-link__permalink-settings { margin-top: 1em; - font-weight: 500; display: block; } From 76aa6adcdc9a204db7652a326d928479b26f7969 Mon Sep 17 00:00:00 2001 From: Bernie Reiter <ockham@raz.or.at> Date: Mon, 19 Nov 2018 15:30:45 +0100 Subject: [PATCH 042/254] Editor Store: Have refreshPost() effect bypass caching (#12060) * Editor Store: Have refreshPost() effect bypass caching * Address feedback --- packages/editor/src/store/effects/posts.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/editor/src/store/effects/posts.js b/packages/editor/src/store/effects/posts.js index 3635ca37641f1e..af722dd4512bd8 100644 --- a/packages/editor/src/store/effects/posts.js +++ b/packages/editor/src/store/effects/posts.js @@ -312,7 +312,8 @@ export const refreshPost = async ( action, store ) => { const postTypeSlug = getCurrentPostType( getState() ); const postType = await resolveSelector( 'core', 'getPostType', postTypeSlug ); const newPost = await apiFetch( { - path: `/wp/v2/${ postType.rest_base }/${ post.id }?context=edit`, + // Timestamp arg allows caller to bypass browser caching, which is expected for this specific function. + path: `/wp/v2/${ postType.rest_base }/${ post.id }?context=edit&_timestamp=${ Date.now() }`, } ); dispatch( resetPost( newPost ) ); }; From e601299f807263ac1f0e6030ba485f154ddca4aa Mon Sep 17 00:00:00 2001 From: Miguel Fonseca <miguelcsf@gmail.com> Date: Mon, 19 Nov 2018 16:28:52 +0000 Subject: [PATCH 043/254] Gallery: Duplicate image IDs into easy-to-parse attribute (#11540) * [TRY] Gallery: Duplicate images' IDs into easy-to-parse attribute * typo fix * Fixed a check in shortcode transform; Handled the file transform. * Corrected fixtures and snapshots. * Improved error message. * Update packages/block-library/src/gallery/index.js Co-Authored-By: jorgefilipecosta <jorge.costa@developer.pt> * Update packages/block-library/src/gallery/index.js Co-Authored-By: jorgefilipecosta <jorge.costa@developer.pt> * Update packages/block-library/src/gallery/index.js Co-Authored-By: jorgefilipecosta <jorge.costa@developer.pt> * Update packages/block-library/src/gallery/index.js Co-Authored-By: jorgefilipecosta <jorge.costa@developer.pt> * Add deprecation logic. * update fixtures --- packages/block-library/src/gallery/edit.js | 34 +++++-- packages/block-library/src/gallery/index.js | 97 +++++++++++++++++-- post-content.php | 4 +- .../blocks-raw-handling.spec.js.snap | 2 +- test/integration/fixtures/wordpress-out.html | 2 +- .../full-content/fixtures/core__gallery.html | 2 +- .../full-content/fixtures/core__gallery.json | 4 + .../fixtures/core__gallery.parsed.json | 7 +- .../fixtures/core__gallery.serialized.html | 2 +- .../fixtures/core__gallery__columns.html | 2 +- .../fixtures/core__gallery__columns.json | 4 + .../core__gallery__columns.parsed.json | 4 + .../core__gallery__columns.serialized.html | 2 +- 13 files changed, 141 insertions(+), 25 deletions(-) diff --git a/packages/block-library/src/gallery/edit.js b/packages/block-library/src/gallery/edit.js index b9a125db38be3a..003570d3bdf7d1 100644 --- a/packages/block-library/src/gallery/edit.js +++ b/packages/block-library/src/gallery/edit.js @@ -1,7 +1,7 @@ /** * External Dependencies */ -import { filter, pick, get } from 'lodash'; +import { filter, pick, map, get } from 'lodash'; /** * WordPress dependencies @@ -63,12 +63,28 @@ class GalleryEdit extends Component { this.setImageAttributes = this.setImageAttributes.bind( this ); this.addFiles = this.addFiles.bind( this ); this.uploadFromFiles = this.uploadFromFiles.bind( this ); + this.setAttributes = this.setAttributes.bind( this ); this.state = { selectedImage: null, }; } + setAttributes( attributes ) { + if ( attributes.ids ) { + throw new Error( 'The "ids" attribute should not be changed directly. It is managed automatically when "images" attribute changes' ); + } + + if ( attributes.images ) { + attributes = { + ...attributes, + ids: map( attributes.images, 'id' ), + }; + } + + this.props.setAttributes( attributes ); + } + onSelectImage( index ) { return () => { if ( this.state.selectedImage !== index ) { @@ -84,7 +100,7 @@ class GalleryEdit extends Component { const images = filter( this.props.attributes.images, ( img, i ) => index !== i ); const { columns } = this.props.attributes; this.setState( { selectedImage: null } ); - this.props.setAttributes( { + this.setAttributes( { images, columns: columns ? Math.min( images.length, columns ) : columns, } ); @@ -92,21 +108,21 @@ class GalleryEdit extends Component { } onSelectImages( images ) { - this.props.setAttributes( { + this.setAttributes( { images: images.map( ( image ) => pickRelevantMediaFiles( image ) ), } ); } setLinkTo( value ) { - this.props.setAttributes( { linkTo: value } ); + this.setAttributes( { linkTo: value } ); } setColumnsNumber( value ) { - this.props.setAttributes( { columns: value } ); + this.setAttributes( { columns: value } ); } toggleImageCrop() { - this.props.setAttributes( { imageCrop: ! this.props.attributes.imageCrop } ); + this.setAttributes( { imageCrop: ! this.props.attributes.imageCrop } ); } getImageCropHelp( checked ) { @@ -114,7 +130,8 @@ class GalleryEdit extends Component { } setImageAttributes( index, attributes ) { - const { attributes: { images }, setAttributes } = this.props; + const { attributes: { images } } = this.props; + const { setAttributes } = this; if ( ! images[ index ] ) { return; } @@ -136,7 +153,8 @@ class GalleryEdit extends Component { addFiles( files ) { const currentImages = this.props.attributes.images || []; - const { noticeOperations, setAttributes } = this.props; + const { noticeOperations } = this.props; + const { setAttributes } = this; mediaUpload( { allowedTypes: ALLOWED_MEDIA_TYPES, filesList: files, diff --git a/packages/block-library/src/gallery/index.js b/packages/block-library/src/gallery/index.js index e347c806d2af37..080da725b8d92e 100644 --- a/packages/block-library/src/gallery/index.js +++ b/packages/block-library/src/gallery/index.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { filter, every } from 'lodash'; +import { filter, every, map, some } from 'lodash'; /** * WordPress dependencies @@ -52,6 +52,10 @@ const blockAttributes = { }, }, }, + ids: { + type: 'array', + default: [], + }, columns: { type: 'number', }, @@ -67,6 +71,16 @@ const blockAttributes = { export const name = 'core/gallery'; +const parseShortcodeIds = ( ids ) => { + if ( ! ids ) { + return []; + } + + return ids.split( ',' ).map( ( id ) => ( + parseInt( id, 10 ) + ) ); +}; + export const settings = { title: __( 'Gallery' ), description: __( 'Display multiple images in a rich gallery.' ), @@ -89,6 +103,7 @@ export const settings = { if ( validImages.length > 0 ) { return createBlock( 'core/gallery', { images: validImages.map( ( { id, url, alt, caption } ) => ( { id, url, alt, caption } ) ), + ids: validImages.map( ( { id } ) => id ), } ); } return createBlock( 'core/gallery' ); @@ -101,15 +116,17 @@ export const settings = { images: { type: 'array', shortcode: ( { named: { ids } } ) => { - if ( ! ids ) { - return []; - } - - return ids.split( ',' ).map( ( id ) => ( { - id: parseInt( id, 10 ), + return parseShortcodeIds( ids ).map( ( id ) => ( { + id, } ) ); }, }, + ids: { + type: 'array', + shortcode: ( { named: { ids } } ) => { + return parseShortcodeIds( ids ); + }, + }, columns: { type: 'number', shortcode: ( { named: { columns = '3' } } ) => { @@ -139,8 +156,12 @@ export const settings = { mediaUpload( { filesList: files, onFileChange: ( images ) => { + const imagesAttr = images.map( + pickRelevantMediaFiles + ); onChange( block.clientId, { - images: images.map( ( image ) => pickRelevantMediaFiles( image ) ), + ids: map( imagesAttr, 'id' ), + images: imagesAttr, } ); }, allowedTypes: [ 'image' ], @@ -199,6 +220,66 @@ export const settings = { }, deprecated: [ + { + attributes: blockAttributes, + isEligible( { images, ids } ) { + return images && + images.length > 0 && + ( + ( ! ids && images ) || + ( ids && images && ids.length !== images.length ) || + some( images, ( id, index ) => { + if ( ! id && ids[ index ] !== null ) { + return true; + } + return parseInt( id, 10 ) !== ids[ index ]; + } ) + ); + }, + migrate( attributes ) { + return { + ...attributes, + ids: map( attributes.images, ( { id } ) => { + if ( ! id ) { + return null; + } + return parseInt( id, 10 ); + } ), + }; + }, + save( { attributes } ) { + const { images, columns = defaultColumnsNumber( attributes ), imageCrop, linkTo } = attributes; + return ( + <ul className={ `columns-${ columns } ${ imageCrop ? 'is-cropped' : '' }` } > + { images.map( ( image ) => { + let href; + + switch ( linkTo ) { + case 'media': + href = image.url; + break; + case 'attachment': + href = image.link; + break; + } + + const img = <img src={ image.url } alt={ image.alt } data-id={ image.id } data-link={ image.link } className={ image.id ? `wp-image-${ image.id }` : null } />; + + return ( + <li key={ image.id || image.url } className="blocks-gallery-item"> + <figure> + { href ? <a href={ href }>{ img }</a> : img } + { image.caption && image.caption.length > 0 && ( + <RichText.Content tagName="figcaption" value={ image.caption } /> + ) } + </figure> + </li> + ); + } ) } + </ul> + ); + }, + }, { attributes: blockAttributes, save( { attributes } ) { diff --git a/post-content.php b/post-content.php index 7dac5620a8efd6..f34262d1eed4c8 100644 --- a/post-content.php +++ b/post-content.php @@ -88,7 +88,7 @@ <p><?php _e( 'Blocks can be anything you need. For instance, you may want to add a subdued quote as part of the composition of your text, or you may prefer to display a giant stylized one. All of these options are available in the inserter.', 'gutenberg' ); ?></p> <!-- /wp:paragraph --> -<!-- wp:gallery {"columns":2} --> +<!-- wp:gallery {"ids":[null,null,null],"columns":2} --> <ul class="wp-block-gallery columns-2 is-cropped"> <li class="blocks-gallery-item"><figure><img src="https://cldup.com/n0g6ME5VKC.jpg" alt="" /></figure></li> <li class="blocks-gallery-item"><figure><img src="https://cldup.com/ZjESfxPI3R.jpg" alt="" /></figure></li> @@ -116,7 +116,7 @@ <p><?php _e( 'Sure, the full-wide image can be pretty big. But sometimes the image is worth it.', 'gutenberg' ); ?></p> <!-- /wp:paragraph --> -<!-- wp:gallery {"align":"wide","images":[{"url":"https://cldup.com/_rSwtEeDGD.jpg","alt":""},{"url":"https://cldup.com/L-cC3qX2DN.jpg","alt":""}]} --> +<!-- wp:gallery {"ids":[null,null],"align":"wide","images":[{"url":"https://cldup.com/_rSwtEeDGD.jpg","alt":""},{"url":"https://cldup.com/L-cC3qX2DN.jpg","alt":""}]} --> <ul class="wp-block-gallery alignwide columns-2 is-cropped"> <li class="blocks-gallery-item"><figure><img src="https://cldup.com/_rSwtEeDGD.jpg" alt="" /></figure></li> <li class="blocks-gallery-item"><figure><img src="https://cldup.com/L-cC3qX2DN.jpg" alt="" /></figure></li> diff --git a/test/integration/__snapshots__/blocks-raw-handling.spec.js.snap b/test/integration/__snapshots__/blocks-raw-handling.spec.js.snap index 84028b7c5f539b..f92c383e637686 100644 --- a/test/integration/__snapshots__/blocks-raw-handling.spec.js.snap +++ b/test/integration/__snapshots__/blocks-raw-handling.spec.js.snap @@ -29,7 +29,7 @@ exports[`Blocks raw handling rawHandler should convert HTML post to blocks with <h3>Shortcode</h3> <!-- /wp:heading --> -<!-- wp:gallery {\\"columns\\":3,\\"linkTo\\":\\"attachment\\"} --> +<!-- wp:gallery {\\"ids\\":[1],\\"columns\\":3,\\"linkTo\\":\\"attachment\\"} --> <ul class=\\"wp-block-gallery columns-3 is-cropped\\"><li class=\\"blocks-gallery-item\\"><figure><img data-id=\\"1\\" class=\\"wp-image-1\\"/></figure></li></ul> <!-- /wp:gallery -->" `; diff --git a/test/integration/fixtures/wordpress-out.html b/test/integration/fixtures/wordpress-out.html index b1d5977395c22f..e179380c983daa 100644 --- a/test/integration/fixtures/wordpress-out.html +++ b/test/integration/fixtures/wordpress-out.html @@ -18,6 +18,6 @@ <h3>More tag</h3> <h3>Shortcode</h3> <!-- /wp:heading --> -<!-- wp:gallery {"columns":3,"linkTo":"attachment"} --> +<!-- wp:gallery {"ids":[1],"columns":3,"linkTo":"attachment"} --> <ul class="wp-block-gallery columns-3 is-cropped"><li class="blocks-gallery-item"><figure><img data-id="1" class="wp-image-1"/></figure></li></ul> <!-- /wp:gallery --> diff --git a/test/integration/full-content/fixtures/core__gallery.html b/test/integration/full-content/fixtures/core__gallery.html index 5e48c7e66351fb..a5842fd7dd581b 100644 --- a/test/integration/full-content/fixtures/core__gallery.html +++ b/test/integration/full-content/fixtures/core__gallery.html @@ -1,4 +1,4 @@ -<!-- wp:core/gallery --> +<!-- wp:core/gallery {"ids":[null,null]} --> <ul class="wp-block-gallery columns-2 is-cropped"> <li class="blocks-gallery-item"> <figure> diff --git a/test/integration/full-content/fixtures/core__gallery.json b/test/integration/full-content/fixtures/core__gallery.json index 8cfcc26d3b37a0..1e87ee42c8702f 100644 --- a/test/integration/full-content/fixtures/core__gallery.json +++ b/test/integration/full-content/fixtures/core__gallery.json @@ -16,6 +16,10 @@ "caption": "" } ], + "ids": [ + null, + null + ], "imageCrop": true, "linkTo": "none" }, diff --git a/test/integration/full-content/fixtures/core__gallery.parsed.json b/test/integration/full-content/fixtures/core__gallery.parsed.json index fc5c9e17d6890b..e070b1f70b5f63 100644 --- a/test/integration/full-content/fixtures/core__gallery.parsed.json +++ b/test/integration/full-content/fixtures/core__gallery.parsed.json @@ -1,7 +1,12 @@ [ { "blockName": "core/gallery", - "attrs": {}, + "attrs": { + "ids": [ + null, + null + ] + }, "innerBlocks": [], "innerHTML": "\n<ul class=\"wp-block-gallery columns-2 is-cropped\">\n\t<li class=\"blocks-gallery-item\">\n\t\t<figure>\n\t\t\t<img src=\"https://cldup.com/uuUqE_dXzy.jpg\" alt=\"title\" />\n\t\t</figure>\n\t</li>\n\t<li class=\"blocks-gallery-item\">\n\t\t<figure>\n\t\t\t<img src=\"http://google.com/hi.png\" alt=\"title\" />\n\t\t</figure>\n\t</li>\n</ul>\n", "innerContent": [ diff --git a/test/integration/full-content/fixtures/core__gallery.serialized.html b/test/integration/full-content/fixtures/core__gallery.serialized.html index 5bf6ce819f9767..55a1e6bab03770 100644 --- a/test/integration/full-content/fixtures/core__gallery.serialized.html +++ b/test/integration/full-content/fixtures/core__gallery.serialized.html @@ -1,3 +1,3 @@ -<!-- wp:gallery --> +<!-- wp:gallery {"ids":[null,null]} --> <ul class="wp-block-gallery columns-2 is-cropped"><li class="blocks-gallery-item"><figure><img src="https://cldup.com/uuUqE_dXzy.jpg" alt="title"/></figure></li><li class="blocks-gallery-item"><figure><img src="http://google.com/hi.png" alt="title"/></figure></li></ul> <!-- /wp:gallery --> diff --git a/test/integration/full-content/fixtures/core__gallery__columns.html b/test/integration/full-content/fixtures/core__gallery__columns.html index cf1f1bb43fa3f4..6493c70309dad2 100644 --- a/test/integration/full-content/fixtures/core__gallery__columns.html +++ b/test/integration/full-content/fixtures/core__gallery__columns.html @@ -1,4 +1,4 @@ -<!-- wp:core/gallery {"columns":1} --> +<!-- wp:core/gallery {"ids":[null,null],"columns":1} --> <ul class="wp-block-gallery columns-1 is-cropped"> <li class="blocks-gallery-item"> <figure> diff --git a/test/integration/full-content/fixtures/core__gallery__columns.json b/test/integration/full-content/fixtures/core__gallery__columns.json index b3daaa05f6e8a0..d96473e9d8d041 100644 --- a/test/integration/full-content/fixtures/core__gallery__columns.json +++ b/test/integration/full-content/fixtures/core__gallery__columns.json @@ -16,6 +16,10 @@ "caption": "" } ], + "ids": [ + null, + null + ], "columns": 1, "imageCrop": true, "linkTo": "none" 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 6f6e4b856d7ece..6a2b71ac242f19 100644 --- a/test/integration/full-content/fixtures/core__gallery__columns.parsed.json +++ b/test/integration/full-content/fixtures/core__gallery__columns.parsed.json @@ -2,6 +2,10 @@ { "blockName": "core/gallery", "attrs": { + "ids": [ + null, + null + ], "columns": 1 }, "innerBlocks": [], diff --git a/test/integration/full-content/fixtures/core__gallery__columns.serialized.html b/test/integration/full-content/fixtures/core__gallery__columns.serialized.html index 183e484ec42b6d..7c5c369cc4199b 100644 --- a/test/integration/full-content/fixtures/core__gallery__columns.serialized.html +++ b/test/integration/full-content/fixtures/core__gallery__columns.serialized.html @@ -1,3 +1,3 @@ -<!-- wp:gallery {"columns":1} --> +<!-- wp:gallery {"ids":[null,null],"columns":1} --> <ul class="wp-block-gallery columns-1 is-cropped"><li class="blocks-gallery-item"><figure><img src="https://cldup.com/uuUqE_dXzy.jpg" alt="title"/></figure></li><li class="blocks-gallery-item"><figure><img src="http://google.com/hi.png" alt="title"/></figure></li></ul> <!-- /wp:gallery --> From 88112df49f530137d47a705efd011de3950fc8b3 Mon Sep 17 00:00:00 2001 From: Robert Anderson <robert@noisysocks.com> Date: Tue, 20 Nov 2018 03:46:01 +1100 Subject: [PATCH 044/254] Prevent RESET_BLOCKS from affecting reusable blocks (#11746) * Prevent RESET_BLOCKS from affecting reusable blocks Changes the editor reducer so that RESET_BLOCKS will only remove blocks that are actually in the post, i.e. blocks that are a descendent of the `''` `blocks.order` key. This fixes a bug where editing a post in Text Mode would break any reusable blocks in the post. * Augment RESET_BLOCKS action instead of replacing it Keep the RESET_BLOCKS logic in the reducer by instead using a higher-order reducer to augment RESET_BLOCKS with a list of client IDs that should remain in state as they are referenced by a reusable block. * Revert "Augment RESET_BLOCKS action instead of replacing it" This reverts commit 7d3d116dff60ccf2379cbdbb809724228bb4ee98. * Use reduce() to simplify getNestedBlockClientIds() --- packages/editor/src/store/reducer.js | 55 ++++++++++++++++++++++- packages/editor/src/store/test/reducer.js | 52 +++++++++++++++++++++ 2 files changed, 105 insertions(+), 2 deletions(-) diff --git a/packages/editor/src/store/reducer.js b/packages/editor/src/store/reducer.js index 5396186a9be44d..cc7cdeedb55f3c 100644 --- a/packages/editor/src/store/reducer.js +++ b/packages/editor/src/store/reducer.js @@ -111,6 +111,28 @@ function getFlattenedBlocks( blocks ) { return flattenedBlocks; } +/** + * Given a block order map object, returns *all* of the block client IDs that are + * a descendant of the given root client ID. + * + * Calling this with `rootClientId` set to `''` results in a list of client IDs + * that are in the post. That is, it excludes blocks like fetched reusable + * blocks which are stored into state but not visible. + * + * @param {Object} blocksOrder Object that maps block client IDs to a list of + * nested block client IDs. + * @param {?string} rootClientId The root client ID to search. Defaults to ''. + * + * @return {Array} List of descendant client IDs. + */ +function getNestedBlockClientIds( blocksOrder, rootClientId = '' ) { + return reduce( blocksOrder[ rootClientId ], ( result, clientId ) => [ + ...result, + clientId, + ...getNestedBlockClientIds( blocksOrder, clientId ), + ], [] ); +} + /** * Returns an object against which it is safe to perform mutating operations, * given the original object and its current working copy. @@ -221,6 +243,35 @@ const withInnerBlocksRemoveCascade = ( reducer ) => ( state, action ) => { return reducer( state, action ); }; +/** + * Higher-order reducer which targets the combined blocks reducer and handles + * the `RESET_BLOCKS` action. When dispatched, this action will replace all + * blocks that exist in the post, leaving blocks that exist only in state (e.g. + * reusable blocks) alone. + * + * @param {Function} reducer Original reducer function. + * + * @return {Function} Enhanced reducer function. + */ +const withBlockReset = ( reducer ) => ( state, action ) => { + if ( state && action.type === 'RESET_BLOCKS' ) { + const visibleClientIds = getNestedBlockClientIds( state.order ); + return { + ...state, + byClientId: { + ...omit( state.byClientId, visibleClientIds ), + ...getFlattenedBlocks( action.blocks ), + }, + order: { + ...omit( state.order, visibleClientIds ), + ...mapBlockOrder( action.blocks ), + }, + }; + } + + return reducer( state, action ); +}; + /** * Undoable reducer returning the editor post state, including blocks parsed * from current HTML markup. @@ -297,6 +348,8 @@ export const editor = flow( [ blocks: flow( [ combineReducers, + withBlockReset, + // Track whether changes exist, resetting at each post save. Relies on // editor initialization firing post reset as an effect. withChangeDetection( { @@ -306,7 +359,6 @@ export const editor = flow( [ ] )( { byClientId( state = {}, action ) { switch ( action.type ) { - case 'RESET_BLOCKS': case 'SETUP_EDITOR_STATE': return getFlattenedBlocks( action.blocks ); @@ -409,7 +461,6 @@ export const editor = flow( [ order( state = {}, action ) { switch ( action.type ) { - case 'RESET_BLOCKS': case 'SETUP_EDITOR_STATE': return mapBlockOrder( action.blocks ); diff --git a/packages/editor/src/store/test/reducer.js b/packages/editor/src/store/test/reducer.js index e088a2a67012ac..99ac0c6e369967 100644 --- a/packages/editor/src/store/test/reducer.js +++ b/packages/editor/src/store/test/reducer.js @@ -1158,6 +1158,58 @@ describe( 'state', () => { } ); describe( 'blocks', () => { + it( 'should not reset any blocks that are not in the post', () => { + const actions = [ + { + type: 'RESET_BLOCKS', + blocks: [ + { + clientId: 'block1', + innerBlocks: [ + { clientId: 'block11', innerBlocks: [] }, + { clientId: 'block12', innerBlocks: [] }, + ], + }, + ], + }, + { + type: 'RECEIVE_BLOCKS', + blocks: [ + { + clientId: 'block2', + innerBlocks: [ + { clientId: 'block21', innerBlocks: [] }, + { clientId: 'block22', innerBlocks: [] }, + ], + }, + ], + }, + ]; + const original = deepFreeze( actions.reduce( editor, undefined ) ); + + const state = editor( original, { + type: 'RESET_BLOCKS', + blocks: [ + { + clientId: 'block3', + innerBlocks: [ + { clientId: 'block31', innerBlocks: [] }, + { clientId: 'block32', innerBlocks: [] }, + ], + }, + ], + } ); + + expect( state.present.blocks.byClientId ).toEqual( { + block2: { clientId: 'block2' }, + block21: { clientId: 'block21' }, + block22: { clientId: 'block22' }, + block3: { clientId: 'block3' }, + block31: { clientId: 'block31' }, + block32: { clientId: 'block32' }, + } ); + } ); + describe( 'byClientId', () => { it( 'should return with attribute block updates', () => { const original = deepFreeze( editor( undefined, { From 2be9a298b9c3a63fdd31231964f4dc194321b5c4 Mon Sep 17 00:00:00 2001 From: Jorge <jorge.costa@developer.pt> Date: Mon, 19 Nov 2018 17:04:19 +0000 Subject: [PATCH 045/254] Fix: missing imports on end 2 end tests. (#12070) --- test/e2e/support/utils/get-url.js | 1 + test/e2e/support/utils/is-wp-path.js | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/test/e2e/support/utils/get-url.js b/test/e2e/support/utils/get-url.js index 9ac210e4d1f559..edd5801036978a 100644 --- a/test/e2e/support/utils/get-url.js +++ b/test/e2e/support/utils/get-url.js @@ -2,6 +2,7 @@ * Node dependencies */ import { join } from 'path'; +import { URL } from 'url'; import { WP_BASE_URL } from './config'; diff --git a/test/e2e/support/utils/is-wp-path.js b/test/e2e/support/utils/is-wp-path.js index 3e0351d0a181ee..ecd1695fb52b66 100644 --- a/test/e2e/support/utils/is-wp-path.js +++ b/test/e2e/support/utils/is-wp-path.js @@ -1,3 +1,8 @@ +/** + * Node dependencies + */ +import { URL } from 'url'; + /** * Internal dependencies */ From 221afa1de83132bf5ab6d6dadca09e03c85b2f5e Mon Sep 17 00:00:00 2001 From: Anton Timmermans <email@atimmer.com> Date: Mon, 19 Nov 2018 19:14:00 +0100 Subject: [PATCH 046/254] Fix annotations text tracking (#11861) * Allow filtering formatToValue and valueToFormat This makes it possible to have an editor-only format that stays in position when typing inside `RichText`. Apply this to annotations, because it is necessary there. * Update annotations based on changes in `RichText` When the user types before or in an annotation we want the annotation to be adjusted accordingly. We can determine the location of the mark elements in the `RichText` based on the value passed to `__experimentalCreateValueToFormat` so we can update the annotations based on the values expressed there. * Annotations: Refactor RichText APIs Instead of having APIs for `formatToValue` and `valueToFormat`, create an `__experimentalCreateOnChangeEditableValue` function that is called when the editable value changes. The annotations API can use this to update its state. * Fix and add annotations reducer tests * Move annotations file to separate file. * Add e2e tests for annotation text tracking * Change e2e test annotations sidebar name to be unique * Update docs --- docs/data/data-core-annotations.md | 10 + package-lock.json | 1 + packages/annotations/src/format/annotation.js | 102 +++++++++-- packages/annotations/src/store/actions.js | 18 ++ packages/annotations/src/store/reducer.js | 70 ++++--- packages/annotations/src/store/selectors.js | 20 +- .../annotations/src/store/test/reducer.js | 89 +++++---- .../editor/src/components/rich-text/index.js | 73 ++++++++ packages/rich-text/package.json | 1 + packages/rich-text/src/create.js | 4 - .../rich-text/src/register-format-type.js | 107 ++++++++--- .../__snapshots__/plugins-api.test.js.snap | 2 +- test/e2e/specs/annotations.test.js | 173 ++++++++++++++++++ test/e2e/specs/plugins-api.test.js | 40 ---- test/e2e/test-plugins/plugins-api.php | 18 ++ .../plugins-api/annotations-sidebar.js | 121 ++++++++++++ test/e2e/test-plugins/plugins-api/sidebar.js | 18 -- 17 files changed, 694 insertions(+), 173 deletions(-) create mode 100644 test/e2e/specs/annotations.test.js create mode 100644 test/e2e/test-plugins/plugins-api/annotations-sidebar.js diff --git a/docs/data/data-core-annotations.md b/docs/data/data-core-annotations.md index f4f7d8cb5ff072..6600938219e0e9 100644 --- a/docs/data/data-core-annotations.md +++ b/docs/data/data-core-annotations.md @@ -75,6 +75,16 @@ Removes an annotation with a specific ID. * annotationId: The annotation to remove. +### __experimentalUpdateAnnotationRange + +Updates the range of an annotation. + +*Parameters* + + * annotationId: ID of the annotation to update. + * start: The start of the new range. + * end: The end of the new range. + ### __experimentalRemoveAnnotationsBySource Removes all annotations of a specific source. diff --git a/package-lock.json b/package-lock.json index 5d995bca4758f9..ba5ee04bb7d65f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2740,6 +2740,7 @@ "version": "file:packages/rich-text", "requires": { "@babel/runtime": "^7.0.0", + "@wordpress/compose": "file:packages/compose", "@wordpress/data": "file:packages/data", "@wordpress/escape-html": "file:packages/escape-html", "lodash": "^4.17.10", diff --git a/packages/annotations/src/format/annotation.js b/packages/annotations/src/format/annotation.js index b052de27f335fd..a2f6f2973c7268 100644 --- a/packages/annotations/src/format/annotation.js +++ b/packages/annotations/src/format/annotation.js @@ -2,13 +2,12 @@ * WordPress dependencies */ import { __ } from '@wordpress/i18n'; +import { applyFormat, removeFormat } from '@wordpress/rich-text'; -const name = 'core/annotation'; +const FORMAT_NAME = 'core/annotation'; -/** - * WordPress dependencies - */ -import { applyFormat, removeFormat } from '@wordpress/rich-text'; +const ANNOTATION_ATTRIBUTE_PREFIX = 'annotation-text-'; +const STORE_KEY = 'core/annotations'; /** * Applies given annotations to the given record. @@ -29,11 +28,17 @@ export function applyAnnotations( record, annotations = [] ) { end = record.text.length; } - const className = 'annotation-text-' + annotation.source; + const className = ANNOTATION_ATTRIBUTE_PREFIX + annotation.source; + const id = ANNOTATION_ATTRIBUTE_PREFIX + annotation.id; record = applyFormat( record, - { type: 'core/annotation', attributes: { className } }, + { + type: FORMAT_NAME, attributes: { + className, + id, + }, + }, start, end ); @@ -52,31 +57,104 @@ export function removeAnnotations( record ) { return removeFormat( record, 'core/annotation', 0, record.text.length ); } +/** + * Retrieves the positions of annotations inside an array of formats. + * + * @param {Array} formats Formats with annotations in there. + * @return {Object} ID keyed positions of annotations. + */ +function retrieveAnnotationPositions( formats ) { + const positions = {}; + + formats.forEach( ( characterFormats, i ) => { + characterFormats = characterFormats || []; + characterFormats = characterFormats.filter( ( format ) => format.type === FORMAT_NAME ); + characterFormats.forEach( ( format ) => { + let { id } = format.attributes; + id = id.replace( ANNOTATION_ATTRIBUTE_PREFIX, '' ); + + if ( ! positions.hasOwnProperty( id ) ) { + positions[ id ] = { + start: i, + }; + } + + // Annotations refer to positions between characters. + // Formats refer to the character themselves. + // So we need to adjust for that here. + positions[ id ].end = i + 1; + } ); + } ); + + return positions; +} + +/** + * Updates annotations in the state based on positions retrieved from RichText. + * + * @param {Array} annotations The annotations that are currently applied. + * @param {Array} positions The current positions of the given annotations. + * @param {Function} removeAnnotation Function to remove an annotation from the state. + * @param {Function} updateAnnotationRange Function to update an annotation range in the state. + */ +function updateAnnotationsWithPositions( annotations, positions, { removeAnnotation, updateAnnotationRange } ) { + annotations.forEach( ( currentAnnotation ) => { + const position = positions[ currentAnnotation.id ]; + // If we cannot find an annotation, delete it. + if ( ! position ) { + // Apparently the annotation has been removed, so remove it from the state: + // Remove... + removeAnnotation( currentAnnotation.id ); + return; + } + + const { start, end } = currentAnnotation; + if ( start !== position.start || end !== position.end ) { + updateAnnotationRange( currentAnnotation.id, position.start, position.end ); + } + } ); +} + export const annotation = { - name, + name: FORMAT_NAME, title: __( 'Annotation' ), tagName: 'mark', className: 'annotation-text', attributes: { className: 'class', + id: 'id', }, edit() { return null; }, __experimentalGetPropsForEditableTreePreparation( select, { richTextIdentifier, blockClientId } ) { return { - annotations: select( 'core/annotations' ).__experimentalGetAnnotationsForRichText( blockClientId, richTextIdentifier ), + annotations: select( STORE_KEY ).__experimentalGetAnnotationsForRichText( blockClientId, richTextIdentifier ), }; }, - __experimentalCreatePrepareEditableTree( props ) { + __experimentalCreatePrepareEditableTree( { annotations } ) { return ( formats, text ) => { - if ( props.annotations.length === 0 ) { + if ( annotations.length === 0 ) { return formats; } let record = { formats, text }; - record = applyAnnotations( record, props.annotations ); + record = applyAnnotations( record, annotations ); return record.formats; }; }, + __experimentalGetPropsForEditableTreeChangeHandler( dispatch ) { + return { + removeAnnotation: dispatch( STORE_KEY ).__experimentalRemoveAnnotation, + updateAnnotationRange: dispatch( STORE_KEY ).__experimentalUpdateAnnotationRange, + }; + }, + __experimentalCreateOnChangeEditableValue( props ) { + return ( formats ) => { + const positions = retrieveAnnotationPositions( formats ); + const { removeAnnotation, updateAnnotationRange, annotations } = props; + + updateAnnotationsWithPositions( annotations, positions, { removeAnnotation, updateAnnotationRange } ); + }; + }, }; diff --git a/packages/annotations/src/store/actions.js b/packages/annotations/src/store/actions.js index 73f8c9e1fe381c..96598022d5a7c1 100644 --- a/packages/annotations/src/store/actions.js +++ b/packages/annotations/src/store/actions.js @@ -57,6 +57,24 @@ export function __experimentalRemoveAnnotation( annotationId ) { }; } +/** + * Updates the range of an annotation. + * + * @param {string} annotationId ID of the annotation to update. + * @param {number} start The start of the new range. + * @param {number} end The end of the new range. + * + * @return {Object} Action object. + */ +export function __experimentalUpdateAnnotationRange( annotationId, start, end ) { + return { + type: 'ANNOTATION_UPDATE_RANGE', + annotationId, + start, + end, + }; +} + /** * Removes all annotations of a specific source. * diff --git a/packages/annotations/src/store/reducer.js b/packages/annotations/src/store/reducer.js index cb14165a5d6bdd..1f768a78ad2157 100644 --- a/packages/annotations/src/store/reducer.js +++ b/packages/annotations/src/store/reducer.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { isNumber, mapValues } from 'lodash'; +import { get, isNumber, mapValues } from 'lodash'; /** * Filters an array based on the predicate, but keeps the reference the same if @@ -38,7 +38,7 @@ function isValidAnnotationRange( annotation ) { * * @return {Array} Updated state. */ -export function annotations( state = { all: [], byBlockClientId: {} }, action ) { +export function annotations( state = {}, action ) { switch ( action.type ) { case 'ANNOTATION_ADD': const blockClientId = action.blockClientId; @@ -55,52 +55,48 @@ export function annotations( state = { all: [], byBlockClientId: {} }, action ) return state; } - const previousAnnotationsForBlock = state.byBlockClientId[ blockClientId ] || []; + const previousAnnotationsForBlock = get( state, blockClientId, [] ); return { - all: [ - ...state.all, - newAnnotation, - ], - byBlockClientId: { - ...state.byBlockClientId, - [ blockClientId ]: [ ...previousAnnotationsForBlock, action.id ], - }, + ...state, + [ blockClientId ]: [ ...previousAnnotationsForBlock, newAnnotation ], }; case 'ANNOTATION_REMOVE': - return { - all: state.all.filter( ( annotation ) => annotation.id !== action.annotationId ), + return mapValues( state, ( annotationsForBlock ) => { + return filterWithReference( annotationsForBlock, ( annotation ) => { + return 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_UPDATE_RANGE': + return mapValues( state, ( annotationsForBlock ) => { + let hasChangedRange = false; - case 'ANNOTATION_REMOVE_SOURCE': - const idsToRemove = []; + const newAnnotations = annotationsForBlock.map( ( annotation ) => { + if ( annotation.id === action.annotationId ) { + hasChangedRange = true; + return { + ...annotation, + range: { + start: action.start, + end: action.end, + }, + }; + } - const allAnnotations = state.all.filter( ( annotation ) => { - if ( annotation.source === action.source ) { - idsToRemove.push( annotation.id ); - return false; - } + return annotation; + } ); - return true; + return hasChangedRange ? newAnnotations : annotationsForBlock; } ); - return { - all: allAnnotations, - byBlockClientId: mapValues( state.byBlockClientId, ( annotationForBlock ) => { - return filterWithReference( annotationForBlock, ( annotationId ) => { - return ! idsToRemove.includes( annotationId ); - } ); - } ), - }; + case 'ANNOTATION_REMOVE_SOURCE': + return mapValues( state, ( annotationsForBlock ) => { + return filterWithReference( annotationsForBlock, ( annotation ) => { + return annotation.source !== action.source; + } ); + } ); } return state; diff --git a/packages/annotations/src/store/selectors.js b/packages/annotations/src/store/selectors.js index 659b83e83e30d1..ca6fcb64796d5f 100644 --- a/packages/annotations/src/store/selectors.js +++ b/packages/annotations/src/store/selectors.js @@ -2,6 +2,7 @@ * External dependencies */ import createSelector from 'rememo'; +import { get, flatMap } from 'lodash'; /** * Returns the annotations for a specific client ID. @@ -13,15 +14,19 @@ import createSelector from 'rememo'; */ export const __experimentalGetAnnotationsForBlock = createSelector( ( state, blockClientId ) => { - return state.all.filter( ( annotation ) => { - return annotation.selector === 'block' && annotation.blockClientId === blockClientId; + return get( state, blockClientId, [] ).filter( ( annotation ) => { + return annotation.selector === 'block'; } ); }, ( state, blockClientId ) => [ - state.byBlockClientId[ blockClientId ], + get( state, blockClientId, [] ), ] ); +export const __experimentalGetAllAnnotationsForBlock = function( state, blockClientId ) { + return get( state, blockClientId, [] ); +}; + /** * Returns the annotations that apply to the given RichText instance. * @@ -36,9 +41,8 @@ export const __experimentalGetAnnotationsForBlock = createSelector( */ export const __experimentalGetAnnotationsForRichText = createSelector( ( state, blockClientId, richTextIdentifier ) => { - return state.all.filter( ( annotation ) => { + return get( state, blockClientId, [] ).filter( ( annotation ) => { return annotation.selector === 'range' && - annotation.blockClientId === blockClientId && richTextIdentifier === annotation.richTextIdentifier; } ).map( ( annotation ) => { const { range, ...other } = annotation; @@ -50,7 +54,7 @@ export const __experimentalGetAnnotationsForRichText = createSelector( } ); }, ( state, blockClientId ) => [ - state.byBlockClientId[ blockClientId ], + get( state, blockClientId, [] ), ] ); @@ -61,5 +65,7 @@ export const __experimentalGetAnnotationsForRichText = createSelector( * @return {Array} All annotations currently applied. */ export function __experimentalGetAnnotations( state ) { - return state.all; + return flatMap( state, ( annotations ) => { + return annotations; + } ); } diff --git a/packages/annotations/src/store/test/reducer.js b/packages/annotations/src/store/test/reducer.js index a1dba8db8c8ac4..190795bd8a98cf 100644 --- a/packages/annotations/src/store/test/reducer.js +++ b/packages/annotations/src/store/test/reducer.js @@ -4,12 +4,12 @@ import { annotations } from '../reducer'; describe( 'annotations', () => { - const initialState = { all: [], byBlockClientId: {} }; + const initialState = {}; it( 'returns all annotations and annotation IDs per block', () => { const state = annotations( undefined, {} ); - expect( state ).toEqual( { all: [], byBlockClientId: {} } ); + expect( state ).toEqual( initialState ); } ); it( 'returns a state with an annotation that has been added', () => { @@ -23,24 +23,19 @@ describe( 'annotations', () => { } ); expect( state ).toEqual( { - all: [ - { - id: 'annotationId', - blockClientId: 'blockClientId', - richTextIdentifier: 'identifier', - source: 'default', - selector: 'block', - }, - ], - byBlockClientId: { - blockClientId: [ 'annotationId' ], - }, + blockClientId: [ { + id: 'annotationId', + blockClientId: 'blockClientId', + richTextIdentifier: 'identifier', + source: 'default', + selector: 'block', + } ], } ); } ); it( 'allows an annotation to be removed', () => { const state = annotations( { - all: [ + blockClientId: [ { id: 'annotationId', blockClientId: 'blockClientId', @@ -49,15 +44,12 @@ describe( 'annotations', () => { selector: 'block', }, ], - byBlockClientId: { - blockClientId: [ 'annotationId' ], - }, }, { type: 'ANNOTATION_REMOVE', annotationId: 'annotationId', } ); - expect( state ).toEqual( { all: [], byBlockClientId: { blockClientId: [] } } ); + expect( state ).toEqual( { blockClientId: [] } ); } ); it( 'allows an annotation to be removed by its source', () => { @@ -76,25 +68,20 @@ describe( 'annotations', () => { selector: 'block', }; const state = annotations( { - all: [ + blockClientId: [ annotation1, + ], + blockClientId2: [ annotation2, ], - byBlockClientId: { - blockClientId: [ 'annotationId' ], - blockClientId2: [ 'annotationId2' ], - }, }, { type: 'ANNOTATION_REMOVE_SOURCE', source: 'default', } ); expect( state ).toEqual( { - all: [ annotation2 ], - byBlockClientId: { - blockClientId: [], - blockClientId2: [ 'annotationId2' ], - }, + blockClientId: [], + blockClientId2: [ annotation2 ], } ); } ); @@ -113,7 +100,7 @@ describe( 'annotations', () => { } ); expect( state ).toEqual( { - all: [ + blockClientId: [ { id: 'annotationId', blockClientId: 'blockClientId', @@ -126,9 +113,45 @@ describe( 'annotations', () => { }, }, ], - byBlockClientId: { - blockClientId: [ 'annotationId' ], - }, + } ); + } ); + + it( 'moves annotations when said action is dispatched', () => { + const state = annotations( { + blockClientId: [ + { + id: 'annotationId', + blockClientId: 'blockClientId', + richTextIdentifier: 'identifier', + source: 'default', + selector: 'range', + range: { + start: 0, + end: 100, + }, + }, + ], + }, { + type: 'ANNOTATION_UPDATE_RANGE', + annotationId: 'annotationId', + start: 50, + end: 75, + } ); + + expect( state ).toEqual( { + blockClientId: [ + { + id: 'annotationId', + blockClientId: 'blockClientId', + richTextIdentifier: 'identifier', + source: 'default', + selector: 'range', + range: { + start: 50, + end: 75, + }, + }, + ], } ); } ); diff --git a/packages/editor/src/components/rich-text/index.js b/packages/editor/src/components/rich-text/index.js index e6722e86377eb4..6167269d600edc 100644 --- a/packages/editor/src/components/rich-text/index.js +++ b/packages/editor/src/components/rich-text/index.js @@ -7,6 +7,9 @@ import { isNil, isEqual, omit, + pickBy, + get, + isPlainObject, } from 'lodash'; import memize from 'memize'; @@ -36,6 +39,7 @@ import { getSelectionStart, getSelectionEnd, remove, + removeFormat, isCollapsed, LINE_SEPARATOR, charAt, @@ -406,6 +410,18 @@ export class RichText extends Component { } } + /** + * Calls all registered onChangeEditableValue handlers. + * + * @param {Array} formats The formats of the latest rich-text value. + * @param {string} text The text of the latest rich-text value. + */ + onChangeEditableValue( { formats, text } ) { + get( this.props, [ 'onChangeEditableValue' ], [] ).forEach( ( eventHandler ) => { + eventHandler( formats, text ); + } ); + } + /** * Sync the value to global state. The node tree and selection will also be * updated if differences are found. @@ -420,6 +436,8 @@ export class RichText extends Component { const { start, end } = record; + this.onChangeEditableValue( record ); + this.savedContent = this.valueToFormat( record ); this.props.onChange( this.savedContent ); this.setState( { start, end } ); @@ -682,6 +700,11 @@ export class RichText extends Component { return false; } + // Allow primitives and arrays: + if ( ! isPlainObject( this.props[ name ] ) ) { + return this.props[ name ] !== prevProps[ name ]; + } + return Object.keys( this.props[ name ] ).some( ( subName ) => { return this.props[ name ][ subName ] !== prevProps[ name ][ subName ]; } ); @@ -689,10 +712,30 @@ export class RichText extends Component { if ( shouldReapply ) { const record = this.formatToValue( value ); + + // Maintain the previous selection: + record.start = this.state.start; + record.end = this.state.end; + this.applyRecord( record ); } } + /** + * Get props that are provided by formats to modify RichText. + * + * @return {Object} Props that start with 'format_'. + */ + getFormatProps() { + return pickBy( this.props, ( propValue, name ) => name.startsWith( 'format_' ) ); + } + + /** + * Converts the outside data structure to our internal representation. + * + * @param {*} value The outside value, data type depends on props. + * @return {Object} An internal rich-text value. + */ formatToValue( value ) { // Handle deprecated `children` and `node` sources. if ( Array.isArray( value ) ) { @@ -734,7 +777,35 @@ export class RichText extends Component { } ).body.innerHTML; } + /** + * Removes editor only formats from the value. + * + * Editor only formats are applied using `prepareEditableTree`, so we need to + * remove them before converting the internal state + * + * @param {Object} value The internal rich-text value. + * @return {Object} A new rich-text value. + */ + removeEditorOnlyFormats( value ) { + this.props.formatTypes.forEach( ( formatType ) => { + // Remove formats created by prepareEditableTree, because they are editor only. + if ( formatType.__experimentalCreatePrepareEditableTree ) { + value = removeFormat( value, formatType.name, 0, value.text.length ); + } + } ); + + return value; + } + + /** + * Converts the internal value to the external data format. + * + * @param {Object} value The internal rich-text value. + * @return {*} The external data format, data type depends on props. + */ valueToFormat( value ) { + value = this.removeEditorOnlyFormats( value ); + // Handle deprecated `children` and `node` sources. if ( this.usedDeprecatedChildrenSource ) { return children.fromDOM( unstableToDom( { @@ -883,10 +954,12 @@ const RichTextContainer = compose( [ } ), withSelect( ( select ) => { const { canUserUseUnfilteredHTML, isCaretWithinFormattedText } = select( 'core/editor' ); + const { getFormatTypes } = select( 'core/rich-text' ); return { canUserUseUnfilteredHTML: canUserUseUnfilteredHTML(), isCaretWithinFormattedText: isCaretWithinFormattedText(), + formatTypes: getFormatTypes(), }; } ), withDispatch( ( dispatch ) => { diff --git a/packages/rich-text/package.json b/packages/rich-text/package.json index bad5ebed5555bb..e14fcf4f8f5145 100644 --- a/packages/rich-text/package.json +++ b/packages/rich-text/package.json @@ -21,6 +21,7 @@ "react-native": "src/index", "dependencies": { "@babel/runtime": "^7.0.0", + "@wordpress/compose": "file:../compose", "@wordpress/data": "file:../data", "@wordpress/escape-html": "file:../escape-html", "lodash": "^4.17.10", diff --git a/packages/rich-text/src/create.js b/packages/rich-text/src/create.js index 5ab6a34e6c903e..fdbbde943aef1a 100644 --- a/packages/rich-text/src/create.js +++ b/packages/rich-text/src/create.js @@ -57,10 +57,6 @@ function toFormat( { type, attributes } ) { return attributes ? { type, attributes } : { type }; } - if ( formatType.__experimentalCreatePrepareEditableTree ) { - return null; - } - if ( ! attributes ) { return { type: formatType.name }; } diff --git a/packages/rich-text/src/register-format-type.js b/packages/rich-text/src/register-format-type.js index 38af31b958e90c..8b61fc3a4308b1 100644 --- a/packages/rich-text/src/register-format-type.js +++ b/packages/rich-text/src/register-format-type.js @@ -1,8 +1,14 @@ +/** + * External dependencies + */ +import { mapKeys } from 'lodash'; + /** * WordPress dependencies */ -import { select, dispatch, withSelect } from '@wordpress/data'; +import { select, dispatch, withSelect, withDispatch } from '@wordpress/data'; import { addFilter } from '@wordpress/hooks'; +import { compose } from '@wordpress/compose'; /** * Registers a new format provided a unique name and an object defining its @@ -114,30 +120,89 @@ 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, + let Component = OriginalComponent; + if ( + settings.__experimentalCreatePrepareEditableTree || + settings.__experimentalCreateFormatToValue || + settings.__experimentalCreateValueToFormat + ) { + Component = ( props ) => { + const additionalProps = {}; + + if ( settings.__experimentalCreatePrepareEditableTree ) { + additionalProps.prepareEditableTree = [ + ...( props.prepareEditableTree || [] ), + settings.__experimentalCreatePrepareEditableTree( props[ `format_${ name }` ], { + richTextIdentifier: props.identifier, + blockClientId: props.clientId, + } ), + ]; } - ), - } ) )( ( props ) => ( - <OriginalComponent - { ...props } - prepareEditableTree={ [ - ...( props.prepareEditableTree || [] ), - settings.__experimentalCreatePrepareEditableTree( props[ `format_${ name }` ], { - richTextIdentifier: props.identifier, - blockClientId: props.clientId, - } ), - ] } - /> - ) ); + + if ( settings.__experimentalCreateOnChangeEditableValue ) { + const dispatchProps = Object.keys( props ).reduce( ( accumulator, propKey ) => { + const propValue = props[ propKey ]; + const keyPrefix = `format_${ name }_dispatch_`; + if ( propKey.startsWith( keyPrefix ) ) { + const realKey = propKey.replace( keyPrefix, '' ); + + accumulator[ realKey ] = propValue; + } + + return accumulator; + }, {} ); + + additionalProps.onChangeEditableValue = [ + ...( props.onChangeEditableValue || [] ), + settings.__experimentalCreateOnChangeEditableValue( { + ...props[ `format_${ name }` ], + ...dispatchProps, + }, { + richTextIdentifier: props.identifier, + blockClientId: props.clientId, + } ), + ]; + } + + return <OriginalComponent + { ...props } + { ...additionalProps } + />; + }; + } + + const hocs = [ + withSelect( ( sel, { clientId, identifier } ) => ( { + [ `format_${ name }` ]: settings.__experimentalGetPropsForEditableTreePreparation( + sel, + { + richTextIdentifier: identifier, + blockClientId: clientId, + } + ), + } ) ), + ]; + + if ( settings.__experimentalGetPropsForEditableTreeChangeHandler ) { + hocs.push( withDispatch( ( disp, { clientId, identifier } ) => { + const dispatchProps = settings.__experimentalGetPropsForEditableTreeChangeHandler( + disp, + { + richTextIdentifier: identifier, + blockClientId: clientId, + } + ); + + return mapKeys( dispatchProps, ( value, key ) => { + return `format_${ name }_dispatch_${ key }`; + } ); + } ) ); + } + + return compose( hocs )( Component ); } ); } diff --git a/test/e2e/specs/__snapshots__/plugins-api.test.js.snap b/test/e2e/specs/__snapshots__/plugins-api.test.js.snap index 5916648d1dd2b5..0b62fc522bf7ef 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><button type=\\"button\\" class=\\"components-button is-button is-primary\\">Add annotation</button></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></div></div>"`; diff --git a/test/e2e/specs/annotations.test.js b/test/e2e/specs/annotations.test.js new file mode 100644 index 00000000000000..3fa1eefdec9dda --- /dev/null +++ b/test/e2e/specs/annotations.test.js @@ -0,0 +1,173 @@ +/** + * Internal dependencies + */ +import { + clickOnMoreMenuItem, + newPost, +} 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' ); + } ); + + afterAll( async () => { + await deactivatePlugin( 'gutenberg-test-plugin-plugins-api' ); + } ); + + beforeEach( async () => { + await newPost(); + } ); + + /** + * Annotates the text in the first block from start to end. + * + * @param {number} start Position to start the annotation. + * @param {number} end Position to end the annotation. + * + * @return {void} + */ + async function annotateFirstBlock( start, end ) { + await page.focus( '#annotations-tests-range-start' ); + await page.keyboard.press( 'Backspace' ); + await page.keyboard.type( start + '' ); + await page.focus( '#annotations-tests-range-end' ); + await page.keyboard.press( 'Backspace' ); + await page.keyboard.type( end + '' ); + + // Click add annotation button. + const addAnnotationButton = ( await page.$x( "//button[contains(text(), 'Add annotation')]" ) )[ 0 ]; + await addAnnotationButton.click(); + } + + /** + * Presses the button that removes all annotations. + * + * @return {void} + */ + async function removeAnnotations() { + // Click remove annotations button. + const addAnnotationButton = ( await page.$x( "//button[contains(text(), 'Remove annotations')]" ) )[ 0 ]; + await addAnnotationButton.click(); + } + + /** + * Returns the inner text of the first text annotation on the page. + * + * @return {Promise<string>} The annotated text. + */ + async function getAnnotatedText() { + const annotations = await page.$$( ANNOTATIONS_SELECTOR ); + + const annotation = annotations[ 0 ]; + + return await page.evaluate( ( el ) => el.innerText, annotation ); + } + + /** + * Returns the inner HTML of the first RichText in the page. + * + * @return {Promise<string>} Inner HTML. + */ + async function getRichTextInnerHTML() { + const htmlContent = await page.$$( '.editor-rich-text__tinymce' ); + return await page.evaluate( ( el ) => { + return el.innerHTML; + }, htmlContent[ 0 ] ); + } + + describe( 'Annotations', () => { + it( 'Allows a block to be annotated', async () => { + await page.keyboard.type( 'Title' + '\n' + 'Paragraph to annotate' ); + + await clickOnMoreMenuItem( 'Annotations Sidebar' ); + + let annotations = await page.$$( ANNOTATIONS_SELECTOR ); + expect( annotations ).toHaveLength( 0 ); + + await annotateFirstBlock( 9, 13 ); + + annotations = await page.$$( ANNOTATIONS_SELECTOR ); + expect( annotations ).toHaveLength( 1 ); + + const text = await getAnnotatedText(); + 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;' ); + } ); + + it( 'Keeps the cursor in the same location when applying annotation', async () => { + await page.keyboard.type( 'Title' + '\n' + 'ABC' ); + await clickOnMoreMenuItem( 'Annotations Sidebar' ); + + await annotateFirstBlock( 1, 2 ); + + // The selection should still be at the end, so test that by typing: + await page.keyboard.type( 'D' ); + + await removeAnnotations(); + const htmlContent = await page.$$( '.editor-rich-text__tinymce' ); + const html = await page.evaluate( ( el ) => { + return el.innerHTML; + }, htmlContent[ 0 ] ); + + expect( html ).toBe( 'ABCD' ); + } ); + + it( 'Moves when typing before it', async () => { + await page.keyboard.type( 'Title' + '\n' + 'ABC' ); + await clickOnMoreMenuItem( 'Annotations Sidebar' ); + + await annotateFirstBlock( 1, 2 ); + + await page.keyboard.press( 'ArrowLeft' ); + await page.keyboard.press( 'ArrowLeft' ); + + // Put an 1 after the A, it should not be annotated. + await page.keyboard.type( '1' ); + + const annotatedText = await getAnnotatedText(); + expect( annotatedText ).toBe( 'B' ); + + await removeAnnotations(); + const blockText = await getRichTextInnerHTML(); + expect( blockText ).toBe( 'A1BC' ); + } ); + + it( 'Grows when typing inside it', async () => { + await page.keyboard.type( 'Title' + '\n' + 'ABC' ); + await clickOnMoreMenuItem( 'Annotations Sidebar' ); + + await annotateFirstBlock( 1, 2 ); + + await page.keyboard.press( 'ArrowLeft' ); + + // Put an 1 after the A, it should not be annotated. + await page.keyboard.type( '2' ); + + const annotatedText = await getAnnotatedText(); + expect( annotatedText ).toBe( 'B2' ); + + await removeAnnotations(); + const blockText = await getRichTextInnerHTML(); + expect( blockText ).toBe( 'AB2C' ); + } ); + } ); +} ); diff --git a/test/e2e/specs/plugins-api.test.js b/test/e2e/specs/plugins-api.test.js index bc841eb9e52f67..d149cf492a5a33 100644 --- a/test/e2e/specs/plugins-api.test.js +++ b/test/e2e/specs/plugins-api.test.js @@ -11,14 +11,6 @@ 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' ); @@ -83,36 +75,4 @@ 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 d219eab684f955..64a8cb23644480 100644 --- a/test/e2e/test-plugins/plugins-api.php +++ b/test/e2e/test-plugins/plugins-api.php @@ -50,3 +50,21 @@ filemtime( plugin_dir_path( __FILE__ ) . 'plugins-api/sidebar.js' ), true ); + +wp_enqueue_script( + 'gutenberg-test-annotations-sidebar', + plugins_url( 'plugins-api/annotations-sidebar.js', __FILE__ ), + array( + 'wp-components', + 'wp-compose', + 'wp-data', + 'wp-edit-post', + 'wp-editor', + 'wp-element', + 'wp-i18n', + 'wp-plugins', + 'wp-annotations', + ), + filemtime( plugin_dir_path( __FILE__ ) . 'plugins-api/annotations-sidebar.js' ), + true +); diff --git a/test/e2e/test-plugins/plugins-api/annotations-sidebar.js b/test/e2e/test-plugins/plugins-api/annotations-sidebar.js new file mode 100644 index 00000000000000..5dfa0e6a52888b --- /dev/null +++ b/test/e2e/test-plugins/plugins-api/annotations-sidebar.js @@ -0,0 +1,121 @@ +( function() { + var Button = wp.components.Button; + var PanelBody = wp.components.PanelBody; + var PanelRow = wp.components.PanelRow; + 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; + var Component = wp.element.Component; + var __ = wp.i18n.__; + var registerPlugin = wp.plugins.registerPlugin; + var PluginSidebar = wp.editPost.PluginSidebar; + var PluginSidebarMoreMenuItem = wp.editPost.PluginSidebarMoreMenuItem; + + class SidebarContents extends Component { + constructor( props ) { + super( props ); + + this.state = { + start: 0, + end: 0, + } + } + + render() { + return el( + PanelBody, + {}, + el( + 'input', + { + type: 'number', + id: 'annotations-tests-range-start', + onChange: ( reactEvent ) => { + this.setState( { + start: reactEvent.target.value, + } ); + }, + value: this.state.start, + } + ), + el( + 'input', + { + type: 'number', + id: 'annotations-tests-range-end', + onChange: ( reactEvent ) => { + this.setState( { + end: reactEvent.target.value, + } ); + }, + value: this.state.end, + } + ), + el( + Button, + { + isPrimary: true, + onClick: () => { + dispatch( 'core/annotations' ).__experimentalAddAnnotation( { + source: 'e2e-tests', + blockClientId: select( 'core/editor' ).getBlockOrder()[ 0 ], + richTextIdentifier: 'content', + range: { + start: parseInt( this.state.start, 10 ), + end: parseInt( this.state.end, 10 ), + }, + } ); + }, + }, + __( 'Add annotation' ) + ), + el( + Button, + { + isPrimary: true, + onClick: () => { + dispatch( 'core/annotations' ).__experimentalRemoveAnnotationsBySource( 'e2e-tests' ); + } + }, + + __( 'Remove annotations' ) + ) + ); + } + } + + function AnnotationsSidebar() { + return el( + Fragment, + {}, + el( + PluginSidebar, + { + name: 'annotations-sidebar', + title: __( 'Annotations Sidebar' ) + }, + el( + SidebarContents, + {} + ) + ), + el( + PluginSidebarMoreMenuItem, + { + target: 'annotations-sidebar' + }, + __( 'Annotations Sidebar' ) + ) + ); + } + + registerPlugin( 'annotations-sidebar', { + icon: 'text', + render: AnnotationsSidebar + } ); +} )(); diff --git a/test/e2e/test-plugins/plugins-api/sidebar.js b/test/e2e/test-plugins/plugins-api/sidebar.js index c97d29c754f23a..22038c81c4c928 100644 --- a/test/e2e/test-plugins/plugins-api/sidebar.js +++ b/test/e2e/test-plugins/plugins-api/sidebar.js @@ -50,24 +50,6 @@ }, __( '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' ) ) ); } From 5dbc6414f1f365b4b76e69fa35134365e1291e40 Mon Sep 17 00:00:00 2001 From: Andrew Duthie <andrew@andrewduthie.com> Date: Mon, 19 Nov 2018 13:17:25 -0500 Subject: [PATCH 047/254] Compat: Upgrade admin notices to use Notices module at runtime (#11604) * Notices: Normalize notice type to use WP prefix * Notices: Support Notice object as argument of createNotice * Edit Post: Upgrade admin notices to notice module * Notices: Extract default status to constant * Docs: Add unstable API convention to coding guidelines See: https://wordpress.slack.com/archives/C5UNMSU4R/p1541690775295000 (Registration: https://make.wordpress.org/chat/) * Notices: Support __unstableHTML action property * Components: Pass through __unstableHTML as Notice RawHTML * Edit Post: Pass through notice HTML from admin notices * Notices: Enforce string-y content by cast * Notices: Add speak option * Edit Post: Add missing AdminNotices classes * Edit Post: Derive all AdminNotices upgraded notice children * Edit Post: Fix AdminNotices missing text nodes content * Edit Post: Reverse order of AdminNotice upgraded notices * Notices: Mark as __unstableHTML via option Content is used for both * Edit Post: Limit upgraded notices to wpbody-content ID --- docs/data/data-core-notices.md | 5 + docs/reference/coding-guidelines.md | 10 +- lib/packages-dependencies.php | 1 + packages/components/src/notice/index.js | 6 + packages/components/src/notice/list.js | 6 +- packages/edit-post/CHANGELOG.md | 6 + .../src/components/admin-notices/index.js | 105 ++++++++++++++++++ .../components/admin-notices/test/index.js | 61 ++++++++++ .../edit-post/src/components/layout/index.js | 2 + packages/edit-post/src/index.js | 1 + packages/notices/CHANGELOG.md | 11 ++ packages/notices/src/store/actions.js | 21 +++- packages/notices/src/store/constants.js | 7 ++ packages/notices/src/store/selectors.js | 9 +- packages/notices/src/store/test/actions.js | 57 +++++++++- 15 files changed, 294 insertions(+), 14 deletions(-) create mode 100644 packages/edit-post/src/components/admin-notices/index.js create mode 100644 packages/edit-post/src/components/admin-notices/test/index.js diff --git a/docs/data/data-core-notices.md b/docs/data/data-core-notices.md index b7ea283f02687f..5ae7f1fb000679 100644 --- a/docs/data/data-core-notices.md +++ b/docs/data/data-core-notices.md @@ -32,6 +32,11 @@ Yields action objects used in signalling that a notice is to be created. * options.isDismissible: Whether the notice can be dismissed by user. Defaults to `true`. + * options.speak: Whether the notice + content should be + announced to screen + readers. Defaults to + `true`. * options.actions: User actions to be presented with notice. diff --git a/docs/reference/coding-guidelines.md b/docs/reference/coding-guidelines.md index dcc943b3288ae6..8264e765231048 100644 --- a/docs/reference/coding-guidelines.md +++ b/docs/reference/coding-guidelines.md @@ -93,9 +93,13 @@ Exposed APIs that are still being tested, discussed and are subject to change sh Example: ```js -export { - internalApi as __experimentalExposedApi -} from './internalApi.js'; +export { __experimentalDoAction } from './api'; +``` + +If an API must be exposed but is clearly not intended to be supported into the future, you may also use `__unstable` as a prefix to differentiate it from an experimental API. Unstable APIs should serve an immediate and temporary purpose. They should _never_ be used by plugin developers as they can be removed at any point without notice, and thus should be omitted from public-facing documentation. The inline code documentation should clearly caution their use. + +```js +export { __unstableDoAction } from './api'; ``` ### Variable Naming diff --git a/lib/packages-dependencies.php b/lib/packages-dependencies.php index d729cca109f38c..f30b3c59a36531 100644 --- a/lib/packages-dependencies.php +++ b/lib/packages-dependencies.php @@ -126,6 +126,7 @@ 'wp-embed', 'wp-i18n', 'wp-keycodes', + 'wp-notices', 'wp-nux', 'wp-plugins', 'wp-url', diff --git a/packages/components/src/notice/index.js b/packages/components/src/notice/index.js index 616774ec99915c..90b2d908deee92 100644 --- a/packages/components/src/notice/index.js +++ b/packages/components/src/notice/index.js @@ -8,6 +8,7 @@ import classnames from 'classnames'; * WordPress dependencies */ import { __ } from '@wordpress/i18n'; +import { RawHTML } from '@wordpress/element'; /** * Internal dependencies @@ -21,11 +22,16 @@ function Notice( { onRemove = noop, isDismissible = true, actions = [], + __unstableHTML, } ) { const classes = classnames( className, 'components-notice', 'is-' + status, { 'is-dismissible': isDismissible, } ); + if ( __unstableHTML ) { + children = <RawHTML>{ children }</RawHTML>; + } + return ( <div className={ classes }> <div className="components-notice__content"> diff --git a/packages/components/src/notice/list.js b/packages/components/src/notice/list.js index a9e72c546deb6e..4ff12e2a451181 100644 --- a/packages/components/src/notice/list.js +++ b/packages/components/src/notice/list.js @@ -25,7 +25,11 @@ function NoticeList( { notices, onRemove = noop, className = 'components-notice- <div className={ className }> { children } { [ ...notices ].reverse().map( ( notice ) => ( - <Notice { ...omit( notice, 'content' ) } key={ notice.id } onRemove={ removeNotice( notice.id ) }> + <Notice + { ...omit( notice, [ 'content' ] ) } + key={ notice.id } + onRemove={ removeNotice( notice.id ) } + > { notice.content } </Notice> ) ) } diff --git a/packages/edit-post/CHANGELOG.md b/packages/edit-post/CHANGELOG.md index 6d0aae67aa6984..4eeda8d66d7502 100644 --- a/packages/edit-post/CHANGELOG.md +++ b/packages/edit-post/CHANGELOG.md @@ -1,3 +1,9 @@ +## 3.1.0 (Unreleased) + +### New Feature + +- The new `AdminNotices` component will transparently upgrade any `.notice` elements on the page to the equivalent `@wordpress/notices` module notice state. + ## 3.0.2 (2018-11-15) ## 3.0.1 (2018-11-12) diff --git a/packages/edit-post/src/components/admin-notices/index.js b/packages/edit-post/src/components/admin-notices/index.js new file mode 100644 index 00000000000000..af04efaa9953a3 --- /dev/null +++ b/packages/edit-post/src/components/admin-notices/index.js @@ -0,0 +1,105 @@ +/** + * WordPress dependencies + */ +import { Component } from '@wordpress/element'; +import { withDispatch } from '@wordpress/data'; + +/** + * Mapping of server-supported notice class names to an equivalent notices + * module status. + * + * @type {Map} + */ +const NOTICE_CLASS_STATUSES = { + 'notice-success': 'success', + updated: 'success', + 'notice-warning': 'warning', + 'notice-error': 'error', + error: 'error', + 'notice-info': 'info', +}; + +/** + * Returns an array of admin notice Elements. + * + * @return {Element[]} Admin notice elements. + */ +function getAdminNotices() { + // The order is reversed to match expectations of rendered order, since a + // NoticesList is itself rendered in reverse order (newest to oldest). + return [ ...document.querySelectorAll( '#wpbody-content > .notice' ) ].reverse(); +} + +/** + * Given an admin notice Element, returns the relevant notice content HTML. + * + * @param {Element} element Admin notice element. + * + * @return {Element} Upgraded notice HTML. + */ +function getNoticeHTML( element ) { + const fragments = []; + + for ( const child of element.childNodes ) { + if ( child.nodeType !== window.Node.ELEMENT_NODE ) { + const value = child.nodeValue.trim(); + if ( value ) { + fragments.push( child.nodeValue ); + } + } else if ( ! child.classList.contains( 'notice-dismiss' ) ) { + fragments.push( child.outerHTML ); + } + } + + return fragments.join( '' ); +} + +/** + * Given an admin notice Element, returns the upgraded status type, or + * undefined if one cannot be determined (i.e. one is not assigned). + * + * @param {Element} element Admin notice element. + * + * @return {?string} Upgraded status type. + */ +function getNoticeStatus( element ) { + for ( const className of element.classList ) { + if ( NOTICE_CLASS_STATUSES.hasOwnProperty( className ) ) { + return NOTICE_CLASS_STATUSES[ className ]; + } + } +} + +export class AdminNotices extends Component { + componentDidMount() { + this.convertNotices(); + } + + convertNotices() { + const { createNotice } = this.props; + getAdminNotices().forEach( ( element ) => { + // Convert and create. + const status = getNoticeStatus( element ); + const content = getNoticeHTML( element ); + const isDismissible = element.classList.contains( 'is-dismissible' ); + createNotice( status, content, { + speak: false, + __unstableHTML: true, + isDismissible, + } ); + + // Remove (now-redundant) admin notice element. + element.parentNode.removeChild( element ); + } ); + } + + render() { + return null; + } +} + +export default withDispatch( ( dispatch ) => { + const { createNotice } = dispatch( 'core/notices' ); + + return { createNotice }; +} )( AdminNotices ); diff --git a/packages/edit-post/src/components/admin-notices/test/index.js b/packages/edit-post/src/components/admin-notices/test/index.js new file mode 100644 index 00000000000000..1dcda9a00a2755 --- /dev/null +++ b/packages/edit-post/src/components/admin-notices/test/index.js @@ -0,0 +1,61 @@ +/** + * External dependencies + */ +import renderer from 'react-test-renderer'; + +/** + * Internal dependencies + */ +import { AdminNotices } from '../'; + +describe( 'AdminNotices', () => { + beforeEach( () => { + // The superfluous whitespace is intentional in verifying expected + // outputs of (a) non-element first child of the element (whitespace + // text node) and (b) untrimmed content. + document.body.innerHTML = ` + <div id="wpbody-content"> + <div class="notice updated is-dismissible"> + <p>My <strong>notice</strong> text</p> + <p>My second line of text</p> + <button type="button" class="notice-dismiss"> + <span class="screen-reader-text">Dismiss this notice.</span> + </button> + </div> + <div class="notice notice-warning">Warning</div> + <aside class="elsewhere"> + <div class="notice">Ignore me</div> + </aside> + </div> + `; + } ); + + it( 'should upgrade notices', () => { + const createNotice = jest.fn(); + + renderer.create( <AdminNotices createNotice={ createNotice } /> ); + + expect( createNotice ).toHaveBeenCalledTimes( 2 ); + expect( createNotice.mock.calls[ 0 ] ).toEqual( [ + 'warning', + 'Warning', + { + speak: false, + __unstableHTML: true, + isDismissible: false, + }, + ] ); + expect( createNotice.mock.calls[ 1 ] ).toEqual( [ + 'success', + '<p>My <strong>notice</strong> text</p><p>My second line of text</p>', + { + speak: false, + __unstableHTML: true, + isDismissible: true, + }, + ] ); + + // Verify all but `<aside>` are removed. + expect( document.getElementById( 'wpbody-content' ).childElementCount ).toBe( 1 ); + } ); +} ); diff --git a/packages/edit-post/src/components/layout/index.js b/packages/edit-post/src/components/layout/index.js index aa43ceaba75d0a..ed5773e5182f5b 100644 --- a/packages/edit-post/src/components/layout/index.js +++ b/packages/edit-post/src/components/layout/index.js @@ -37,6 +37,7 @@ import Sidebar from '../sidebar'; import PluginPostPublishPanel from '../sidebar/plugin-post-publish-panel'; import PluginPrePublishPanel from '../sidebar/plugin-pre-publish-panel'; import FullscreenMode from '../fullscreen-mode'; +import AdminNotices from '../admin-notices'; function Layout( { mode, @@ -69,6 +70,7 @@ function Layout( { <BrowserURL /> <UnsavedChangesWarning /> <AutosaveMonitor /> + <AdminNotices /> <Header /> <div className="edit-post-layout__content" diff --git a/packages/edit-post/src/index.js b/packages/edit-post/src/index.js index 0832cbd141f697..26a2c71ea446c6 100644 --- a/packages/edit-post/src/index.js +++ b/packages/edit-post/src/index.js @@ -5,6 +5,7 @@ import '@wordpress/core-data'; import '@wordpress/editor'; import '@wordpress/nux'; import '@wordpress/viewport'; +import '@wordpress/notices'; import { registerCoreBlocks } from '@wordpress/block-library'; import { render, unmountComponentAtNode } from '@wordpress/element'; import { dispatch } from '@wordpress/data'; diff --git a/packages/notices/CHANGELOG.md b/packages/notices/CHANGELOG.md index ea9d88d2ece178..6a38ce167d73af 100644 --- a/packages/notices/CHANGELOG.md +++ b/packages/notices/CHANGELOG.md @@ -1,3 +1,14 @@ +## 1.1.0 (Unreleased) + +### New Feature + +- The `createNotice` can now optionally accept a WPNotice object as the sole argument. +- New option `speak` enables control as to whether the notice content is announced to screen readers (defaults to `true`) + +### Bug Fixes + +- While `createNotice` only explicitly supported content of type `string`, it was not previously enforced. This has been corrected. + ## 1.0.5 (2018-11-15) ## 1.0.4 (2018-11-09) diff --git a/packages/notices/src/store/actions.js b/packages/notices/src/store/actions.js index 20d31d028c00b3..af74f5b6116fea 100644 --- a/packages/notices/src/store/actions.js +++ b/packages/notices/src/store/actions.js @@ -6,7 +6,7 @@ import { uniqueId } from 'lodash'; /** * Internal dependencies */ -import { DEFAULT_CONTEXT } from './constants'; +import { DEFAULT_CONTEXT, DEFAULT_STATUS } from './constants'; /** * Yields action objects used in signalling that a notice is to be created. @@ -23,18 +23,32 @@ import { DEFAULT_CONTEXT } from './constants'; * @param {?boolean} options.isDismissible Whether the notice can * be dismissed by user. * Defaults to `true`. + * @param {?boolean} options.speak Whether the notice + * content should be + * announced to screen + * readers. Defaults to + * `true`. * @param {?Array<WPNoticeAction>} options.actions User actions to be * presented with notice. */ -export function* createNotice( status = 'info', content, options = {} ) { +export function* createNotice( status = DEFAULT_STATUS, content, options = {} ) { const { + speak = true, isDismissible = true, context = DEFAULT_CONTEXT, id = uniqueId( context ), actions = [], + __unstableHTML, } = options; - yield { type: 'SPEAK', message: content }; + // The supported value shape of content is currently limited to plain text + // strings. To avoid setting expectation that e.g. a WPElement could be + // supported, cast to a string. + content = String( content ); + + if ( speak ) { + yield { type: 'SPEAK', message: content }; + } yield { type: 'CREATE_NOTICE', @@ -43,6 +57,7 @@ export function* createNotice( status = 'info', content, options = {} ) { id, status, content, + __unstableHTML, isDismissible, actions, }, diff --git a/packages/notices/src/store/constants.js b/packages/notices/src/store/constants.js index 4c6ded8a4a3084..2949bde0577db2 100644 --- a/packages/notices/src/store/constants.js +++ b/packages/notices/src/store/constants.js @@ -6,3 +6,10 @@ * @type {string} */ export const DEFAULT_CONTEXT = 'global'; + +/** + * Default notice status. + * + * @type {string} + */ +export const DEFAULT_STATUS = 'info'; diff --git a/packages/notices/src/store/selectors.js b/packages/notices/src/store/selectors.js index 743d9f23ff623f..9ba3cec0e63a06 100644 --- a/packages/notices/src/store/selectors.js +++ b/packages/notices/src/store/selectors.js @@ -22,11 +22,16 @@ const DEFAULT_NOTICES = []; * `info`, `error`, or `warning`. Defaults * to `info`. * @property {string} content Notice message. + * @property {string} __unstableHTML Notice message as raw HTML. Intended to + * serve primarily for compatibility of + * server-rendered notices, and SHOULD NOT + * be used for notices. It is subject to + * removal without notice. * @property {boolean} isDismissible Whether the notice can be dismissed by * user. Defaults to `true`. * @property {WPNoticeAction[]} actions User actions to present with notice. * - * @typedef {Notice} + * @typedef {WPNotice} */ /** @@ -48,7 +53,7 @@ const DEFAULT_NOTICES = []; * @param {Object} state Notices state. * @param {?string} context Optional grouping context. * - * @return {Notice[]} Array of notices. + * @return {WPNotice[]} Array of notices. */ export function getNotices( state, context = DEFAULT_CONTEXT ) { return state[ context ] || DEFAULT_NOTICES; diff --git a/packages/notices/src/store/test/actions.js b/packages/notices/src/store/test/actions.js index e20ba54e7c6a78..ac6bc522f273ac 100644 --- a/packages/notices/src/store/test/actions.js +++ b/packages/notices/src/store/test/actions.js @@ -9,14 +9,15 @@ import { createWarningNotice, removeNotice, } from '../actions'; -import { DEFAULT_CONTEXT } from '../constants'; +import { DEFAULT_CONTEXT, DEFAULT_STATUS } from '../constants'; describe( 'actions', () => { describe( 'createNotice', () => { + const id = 'my-id'; const status = 'status'; const content = 'my message'; - it( 'should yields actions when options is empty', () => { + it( 'yields actions when options is empty', () => { const result = createNotice( status, content ); expect( result.next().value ).toMatchObject( { @@ -37,8 +38,28 @@ describe( 'actions', () => { } ); } ); - it( 'should yields actions when options passed', () => { - const id = 'my-id'; + it( 'normalizes content to string', () => { + const result = createNotice( status, <strong>Hello</strong> ); + + expect( result.next().value ).toMatchObject( { + type: 'SPEAK', + message: expect.any( String ), + } ); + + expect( result.next().value ).toMatchObject( { + type: 'CREATE_NOTICE', + context: DEFAULT_CONTEXT, + notice: { + status, + content: expect.any( String ), + isDismissible: true, + id: expect.any( String ), + actions: [], + }, + } ); + } ); + + it( 'yields actions when options passed', () => { const context = 'foo'; const options = { id, @@ -65,6 +86,32 @@ describe( 'actions', () => { }, } ); } ); + + it( 'yields action when speak disabled', () => { + const result = createNotice( + undefined, + 'my <strong>message</strong>', + { + id, + __unstableHTML: true, + isDismissible: false, + speak: false, + } + ); + + expect( result.next().value ).toEqual( { + type: 'CREATE_NOTICE', + context: DEFAULT_CONTEXT, + notice: { + id, + status: DEFAULT_STATUS, + content: 'my <strong>message</strong>', + __unstableHTML: true, + isDismissible: false, + actions: [], + }, + } ); + } ); } ); describe( 'createSuccessNotice', () => { @@ -106,7 +153,7 @@ describe( 'actions', () => { type: 'CREATE_NOTICE', context: DEFAULT_CONTEXT, notice: { - status: 'info', + status: DEFAULT_STATUS, content, isDismissible: true, id: expect.any( String ), From d941a07447880e08c6de2d590e522d0a6cfaf1a3 Mon Sep 17 00:00:00 2001 From: Jorge <jorge.costa@developer.pt> Date: Mon, 19 Nov 2018 18:45:01 +0000 Subject: [PATCH 048/254] Fix/media library collections - try 2 (#12074) --- .../src/hooks/components/media-upload/index.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) 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 be68a9e2ce9600..9160f4f84c15b5 100644 --- a/packages/edit-post/src/hooks/components/media-upload/index.js +++ b/packages/edit-post/src/hooks/components/media-upload/index.js @@ -163,6 +163,8 @@ class MediaUpload extends Component { } onOpen() { + this.updateCollection(); + if ( ! this.props.value ) { return; } @@ -184,6 +186,22 @@ class MediaUpload extends Component { } } + updateCollection() { + const frameContent = this.frame.content.get(); + if ( frameContent && frameContent.collection ) { + const collection = frameContent.collection; + + // clean all attachments we have in memory. + collection.toArray().forEach( ( model ) => model.trigger( 'destroy', model ) ); + + // reset has more flag, if library had small amount of items all items may have been loaded before. + collection.mirroring._hasMore = true; + + // request items + collection.more(); + } + } + openModal() { this.frame.open(); } From 794ac97863333e4b72162ac6dec3030773c68227 Mon Sep 17 00:00:00 2001 From: Andrew Duthie <andrew@andrewduthie.com> Date: Mon, 19 Nov 2018 14:00:06 -0500 Subject: [PATCH 049/254] Plugin: Remove deprecations slated for 4.5 removal (#12077) --- lib/packages-dependencies.php | 1 - package-lock.json | 1 - packages/components/CHANGELOG.md | 6 + packages/components/package.json | 1 - packages/components/src/dropdown/index.js | 16 --- .../components/src/tab-panel/test/index.js | 5 - packages/editor/CHANGELOG.md | 16 ++- packages/editor/src/components/index.js | 1 - .../post-publish-panel/test/toggle.js | 112 ------------------ .../components/post-publish-panel/toggle.js | 69 ----------- 10 files changed, 17 insertions(+), 211 deletions(-) delete mode 100644 packages/editor/src/components/post-publish-panel/test/toggle.js delete mode 100644 packages/editor/src/components/post-publish-panel/toggle.js diff --git a/lib/packages-dependencies.php b/lib/packages-dependencies.php index f30b3c59a36531..ee99db32c2005b 100644 --- a/lib/packages-dependencies.php +++ b/lib/packages-dependencies.php @@ -66,7 +66,6 @@ 'wp-a11y', 'wp-api-fetch', 'wp-compose', - 'wp-deprecated', 'wp-dom', 'wp-element', 'wp-hooks', diff --git a/package-lock.json b/package-lock.json index ba5ee04bb7d65f..5dca87bdac5845 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2410,7 +2410,6 @@ "@wordpress/a11y": "file:packages/a11y", "@wordpress/api-fetch": "file:packages/api-fetch", "@wordpress/compose": "file:packages/compose", - "@wordpress/deprecated": "file:packages/deprecated", "@wordpress/dom": "file:packages/dom", "@wordpress/element": "file:packages/element", "@wordpress/hooks": "file:packages/hooks", diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 188749366d50d0..81726381c57f13 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -1,3 +1,9 @@ +## 7.0.0 (Unreleased) + +### Breaking Change + +- `Dropdown.refresh()` has been removed. The contained `Popover` is now automatically refreshed. + ## 6.0.2 (2018-11-15) ## 6.0.1 (2018-11-12) diff --git a/packages/components/package.json b/packages/components/package.json index 77877a993d5515..24ad3e68fbd39d 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -24,7 +24,6 @@ "@wordpress/a11y": "file:../a11y", "@wordpress/api-fetch": "file:../api-fetch", "@wordpress/compose": "file:../compose", - "@wordpress/deprecated": "file:../deprecated", "@wordpress/dom": "file:../dom", "@wordpress/element": "file:../element", "@wordpress/hooks": "file:../hooks", diff --git a/packages/components/src/dropdown/index.js b/packages/components/src/dropdown/index.js index 71042b40bd7efe..81ebe65da6534d 100644 --- a/packages/components/src/dropdown/index.js +++ b/packages/components/src/dropdown/index.js @@ -1,7 +1,6 @@ /** * WordPress dependencies */ -import deprecated from '@wordpress/deprecated'; import { Component, createRef } from '@wordpress/element'; /** @@ -16,7 +15,6 @@ 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(); @@ -41,20 +39,6 @@ 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() { - deprecated( 'Dropdown.refresh()', { - plugin: 'Gutenberg', - version: '4.5', - hint: 'Popover is now automatically re-rendered without needing to execute "refresh"', - } ); - } - toggle() { this.setState( ( state ) => ( { isOpen: ! state.isOpen, diff --git a/packages/components/src/tab-panel/test/index.js b/packages/components/src/tab-panel/test/index.js index 7c45af6b45c636..153441cd195316 100644 --- a/packages/components/src/tab-panel/test/index.js +++ b/packages/components/src/tab-panel/test/index.js @@ -13,11 +13,6 @@ import TabPanel from '../'; */ import { Component } from '@wordpress/element'; -/** - * Mock Functions - */ -jest.mock( '@wordpress/deprecated', () => jest.fn() ); - describe( 'TabPanel', () => { const getElementByClass = ( wrapper, className ) => { return TestUtils.findRenderedDOMComponentWithClass( wrapper, className ); diff --git a/packages/editor/CHANGELOG.md b/packages/editor/CHANGELOG.md index 9e917cbd9fd84c..65eb76bccf8f7b 100644 --- a/packages/editor/CHANGELOG.md +++ b/packages/editor/CHANGELOG.md @@ -1,3 +1,9 @@ +## 9.0.0 (Unreleased) + +### Breaking Changes + +- `PostPublishPanelToggle` has been removed. Use `PostPublishButton` instead. + ## 8.0.0 (2018-11-15) ### Breaking Changes @@ -40,7 +46,7 @@ ### Deprecations -- `wp.editor.PostPublishPanelToggle` has been deprecated in favor of `wp.editor.PostPublishButton`. +- `PostPublishPanelToggle` has been deprecated in favor of `PostPublishButton`. ### Polish @@ -106,7 +112,7 @@ ### Deprecations -- `wp.editor.PanelColor` has been deprecated in favor of `wp.editor.PanelColorSettings`. +- `PanelColor` has been deprecated in favor of `PanelColorSettings`. ### New Features @@ -123,7 +129,7 @@ - The `value` property in color objects passed by `withColors` has been removed. Use `color` property instead. - `RichText` `getSettings` prop has been removed. The `unstableGetSettings` prop is available if continued use is required. Unstable APIs are strongly discouraged to be used, and are subject to removal without notice, even as part of a minor release. - `RichText` `onSetup` prop has been removed. The `unstableOnSetup` prop is available if continued use is required. Unstable APIs are strongly discouraged to be used, and are subject to removal without notice, even as part of a minor release. -- `wp.editor.RichTextProvider` has been removed. Please use `wp.data.select( 'core/editor' )` methods instead. +- `RichTextProvider` has been removed. Please use `wp.data.select( 'core/editor' )` methods instead. ### Deprecations @@ -151,12 +157,12 @@ - `getSharedBlocks` selector has been removed. Use `getReusableBlocks` instead. - `editorMediaUpload` has been removed. Use `mediaUpload` instead. - 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. -- `wp.editor.DocumentTitle` component has been removed. +- `DocumentTitle` component has been removed. - `getDocumentTitle` selector (`core/editor`) has been removed. ### Deprecations -- `wp.editor.RichTextProvider` flagged for deprecation. Please use `wp.data.select( 'core/editor' )` methods instead. +- `RichTextProvider` flagged for deprecation. Please use `wp.data.select( 'core/editor' )` methods instead. ### Bug Fixes diff --git a/packages/editor/src/components/index.js b/packages/editor/src/components/index.js index e5f5aaf4478d01..ebd0a59331198c 100644 --- a/packages/editor/src/components/index.js +++ b/packages/editor/src/components/index.js @@ -63,7 +63,6 @@ export { default as PostPreviewButton } from './post-preview-button'; export { default as PostPublishButton } from './post-publish-button'; export { default as PostPublishButtonLabel } from './post-publish-button/label'; export { default as PostPublishPanel } from './post-publish-panel'; -export { default as PostPublishPanelToggle } from './post-publish-panel/toggle'; export { default as PostSavedState } from './post-saved-state'; export { default as PostSchedule } from './post-schedule'; export { default as PostScheduleCheck } from './post-schedule/check'; diff --git a/packages/editor/src/components/post-publish-panel/test/toggle.js b/packages/editor/src/components/post-publish-panel/test/toggle.js deleted file mode 100644 index 1f1a4971dbd35b..00000000000000 --- a/packages/editor/src/components/post-publish-panel/test/toggle.js +++ /dev/null @@ -1,112 +0,0 @@ -/** - * External dependencies - */ -import { shallow } from 'enzyme'; - -/** - * Internal dependencies - */ -import { PostPublishPanelToggle } from '../toggle'; - -describe( 'PostPublishPanelToggle', () => { - describe( 'disabled', () => { - it( 'should be disabled if post is currently saving', () => { - const wrapper = shallow( - <PostPublishPanelToggle - isPublishable - isSaveable - isSaving - /> - ); - - 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', () => { - const wrapper = shallow( - <PostPublishPanelToggle - isPublishable - isSaveable - forceIsSaving - /> - ); - - expect( wrapper.prop( 'disabled' ) ).toBe( true ); - } ); - - it( 'should be disabled if post is not publishable and not forceIsDirty', () => { - const wrapper = shallow( - <PostPublishPanelToggle - isSaveable - isPublishable={ false } - forceIsDirty={ false } - /> - ); - - expect( wrapper.prop( 'disabled' ) ).toBe( true ); - } ); - - it( 'should be disabled if post is not saveable', () => { - const wrapper = shallow( - <PostPublishPanelToggle - isSaveable={ false } - isPublishable - /> - ); - - expect( wrapper.prop( 'disabled' ) ).toBe( true ); - } ); - - it( 'should be disabled if post is published', () => { - const wrapper = shallow( - <PostPublishPanelToggle - isSaveable - isPublishable - isPublished - /> - ); - - expect( wrapper.prop( 'disabled' ) ).toBe( true ); - } ); - - it( 'should be enabled if post is saveable but not publishable and forceIsDirty is true', () => { - const wrapper = shallow( - <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 ); - } ); - - it( 'should display Schedule… if able to be scheduled', () => { - const wrapper = shallow( - <PostPublishPanelToggle isPublishable isSaveable isBeingScheduled /> - ); - expect( wrapper.childAt( 0 ).text() ).toBe( 'Schedule…' ); - } ); - - it( 'should display Publish… if able to be published', () => { - const wrapper = shallow( - <PostPublishPanelToggle isPublishable isSaveable hasPublishAction /> - ); - expect( wrapper.childAt( 0 ).text() ).toBe( 'Publish…' ); - } ); - } ); -} ); diff --git a/packages/editor/src/components/post-publish-panel/toggle.js b/packages/editor/src/components/post-publish-panel/toggle.js deleted file mode 100644 index 0cf8b56019cb47..00000000000000 --- a/packages/editor/src/components/post-publish-panel/toggle.js +++ /dev/null @@ -1,69 +0,0 @@ -/** - * WordPress Dependencies - */ -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'; - -export function PostPublishPanelToggle( { - isSaving, - isPublishable, - isSaveable, - isPublished, - isBeingScheduled, - onToggle, - isOpen, - forceIsSaving, - forceIsDirty, -} ) { - const isButtonDisabled = - isPublished || - isSaving || - forceIsSaving || - ! isSaveable || - ( ! isPublishable && ! forceIsDirty ); - - deprecated( 'PostPublishPanelToggle', { - version: '4.5', - alternative: 'PostPublishButton', - plugin: 'Gutenberg', - } ); - - return ( - <Button - className="editor-post-publish-panel__toggle" - isPrimary - onClick={ onToggle } - aria-expanded={ isOpen } - disabled={ isButtonDisabled } - isBusy={ isSaving && isPublished } - > - { isBeingScheduled ? __( 'Schedule…' ) : __( 'Publish…' ) } - <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> - ); -} - -export default compose( [ - withSelect( ( select ) => { - const { - isSavingPost, - isEditedPostSaveable, - isEditedPostPublishable, - isCurrentPostPublished, - isEditedPostBeingScheduled, - } = select( 'core/editor' ); - return { - isSaving: isSavingPost(), - isSaveable: isEditedPostSaveable(), - isPublishable: isEditedPostPublishable(), - isPublished: isCurrentPostPublished(), - isBeingScheduled: isEditedPostBeingScheduled(), - }; - } ), -] )( PostPublishPanelToggle ); From 25b834e2063f3e88080999743c08a2f8433a363d Mon Sep 17 00:00:00 2001 From: Matias Ventura <mv@matiasventura.com> Date: Mon, 19 Nov 2018 16:41:15 -0300 Subject: [PATCH 050/254] Update "Unified Toolbar" name mode to "Top Toolbar" for clarity. (#12076) --- .../edit-post/src/components/header/header-toolbar/index.js | 4 ++-- .../edit-post/src/components/header/writing-menu/index.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/edit-post/src/components/header/header-toolbar/index.js b/packages/edit-post/src/components/header/header-toolbar/index.js index 9334d7663a2147..e5e92af1f2a645 100644 --- a/packages/edit-post/src/components/header/header-toolbar/index.js +++ b/packages/edit-post/src/components/header/header-toolbar/index.js @@ -23,9 +23,9 @@ import FullscreenModeClose from '../fullscreen-mode-close'; function HeaderToolbar( { hasFixedToolbar, isLargeViewport, mode } ) { const toolbarAriaLabel = hasFixedToolbar ? - /* translators: accessibility text for the editor toolbar when Unified Toolbar is on */ + /* translators: accessibility text for the editor toolbar when Top Toolbar is on */ __( 'Document and block tools' ) : - /* translators: accessibility text for the editor toolbar when Unified Toolbar is off */ + /* translators: accessibility text for the editor toolbar when Top Toolbar is off */ __( 'Document tools' ); return ( diff --git a/packages/edit-post/src/components/header/writing-menu/index.js b/packages/edit-post/src/components/header/writing-menu/index.js index 3b2ff3e47f5f31..e7b9c3dc791ade 100644 --- a/packages/edit-post/src/components/header/writing-menu/index.js +++ b/packages/edit-post/src/components/header/writing-menu/index.js @@ -17,7 +17,7 @@ function WritingMenu( { onClose } ) { > <FeatureToggle feature="fixedToolbar" - label={ __( 'Unified Toolbar' ) } + label={ __( 'Top Toolbar' ) } info={ __( 'Access all block and document tools in a single place' ) } onToggle={ onClose } /> <FeatureToggle From bad6c5a707f904ba3012b886bdf815d97ad269ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20Van=C2=A0Dorpe?= <iseulde@automattic.com> Date: Mon, 19 Nov 2018 20:42:29 +0100 Subject: [PATCH 051/254] Make it possible to undo prefix transforms (#11497) * Make it possible to undo prefix transforms * Same for backticks * Tests: Refactor tests to stop using META_KEY --- docs/block-api.md | 57 ++++++++++++--- packages/block-library/src/code/index.js | 3 +- packages/block-library/src/heading/index.js | 12 ++-- packages/block-library/src/list/index.js | 20 +++--- packages/block-library/src/quote/index.js | 6 +- packages/block-library/src/separator/index.js | 3 +- .../editor/src/components/rich-text/index.js | 8 +-- .../src/components/rich-text/patterns.js | 71 +++++++++++-------- .../__snapshots__/rich-text.test.js.snap | 12 ++++ .../blocks/__snapshots__/code.test.js.snap | 7 ++ .../blocks/__snapshots__/heading.test.js.snap | 13 ++++ .../blocks/__snapshots__/list.test.js.snap | 6 ++ .../__snapshots__/separator.test.js.snap | 7 ++ test/e2e/specs/blocks/code.test.js | 23 ++++++ test/e2e/specs/blocks/heading.test.js | 30 ++++++++ test/e2e/specs/blocks/list.test.js | 8 +++ test/e2e/specs/blocks/separator.test.js | 22 ++++++ test/e2e/specs/rich-text.test.js | 11 +++ 18 files changed, 251 insertions(+), 68 deletions(-) create mode 100644 test/e2e/specs/blocks/__snapshots__/code.test.js.snap create mode 100644 test/e2e/specs/blocks/__snapshots__/heading.test.js.snap create mode 100644 test/e2e/specs/blocks/__snapshots__/separator.test.js.snap create mode 100644 test/e2e/specs/blocks/code.test.js create mode 100644 test/e2e/specs/blocks/heading.test.js create mode 100644 test/e2e/specs/blocks/separator.test.js diff --git a/docs/block-api.md b/docs/block-api.md index 01b2295b46de0b..155bd417d21a27 100644 --- a/docs/block-api.md +++ b/docs/block-api.md @@ -124,18 +124,18 @@ Block styles can be used to provide alternative styles to block. It works by add // Register block styles. styles: [ // Mark style as default. - { - name: 'default', - label: __( 'Rounded' ), - isDefault: true + { + name: 'default', + label: __( 'Rounded' ), + isDefault: true }, - { - name: 'outline', - label: __( 'Outline' ) + { + name: 'outline', + label: __( 'Outline' ) }, - { - name: 'squared', - label: __( 'Squared' ) + { + name: 'squared', + label: __( 'Squared' ) }, ], ``` @@ -413,6 +413,43 @@ transforms: { ``` {% end %} +A prefix transform is a transform that will be applied if the user prefixes some text in e.g. the paragraph block with a given pattern and a trailing space. + +{% codetabs %} +{% ES5 %} +```js +transforms: { + from: [ + { + type: 'prefix', + prefix: '?', + transform: function( content ) { + return createBlock( 'my-plugin/question', { + content, + } ); + }, + }, + ] +} +``` +{% ESNext %} +```js +transforms: { + from: [ + { + type: 'prefix', + prefix: '?', + transform( content ) { + return createBlock( 'my-plugin/question', { + content, + } ); + }, + }, + ] +} +``` +{% end %} + #### parent (optional) diff --git a/packages/block-library/src/code/index.js b/packages/block-library/src/code/index.js index cb304079e1e2c7..ef893570bac1b8 100644 --- a/packages/block-library/src/code/index.js +++ b/packages/block-library/src/code/index.js @@ -39,8 +39,7 @@ export const settings = { transforms: { from: [ { - type: 'pattern', - trigger: 'enter', + type: 'enter', regExp: /^```$/, transform: () => createBlock( 'core/code' ), }, diff --git a/packages/block-library/src/heading/index.js b/packages/block-library/src/heading/index.js index 148d0629340744..288a72d35bc1be 100644 --- a/packages/block-library/src/heading/index.js +++ b/packages/block-library/src/heading/index.js @@ -107,18 +107,16 @@ export const settings = { } ); }, }, - { - type: 'pattern', - regExp: /^(#{2,6})\s/, - transform: ( { content, match } ) => { - const level = match[ 1 ].length; - + ...[ 2, 3, 4, 5, 6 ].map( ( level ) => ( { + type: 'prefix', + prefix: Array( level + 1 ).join( '#' ), + transform( content ) { return createBlock( 'core/heading', { level, content, } ); }, - }, + } ) ), ], to: [ { diff --git a/packages/block-library/src/list/index.js b/packages/block-library/src/list/index.js index 7d59ed853720e1..e7a6174ef25084 100644 --- a/packages/block-library/src/list/index.js +++ b/packages/block-library/src/list/index.js @@ -110,25 +110,25 @@ export const settings = { } ); }, }, - { - type: 'pattern', - regExp: /^[*-]\s/, - transform: ( { content } ) => { + ...[ '*', '-' ].map( ( prefix ) => ( { + type: 'prefix', + prefix, + transform( content ) { return createBlock( 'core/list', { values: `<li>${ content }</li>`, } ); }, - }, - { - type: 'pattern', - regExp: /^1[.)]\s/, - transform: ( { content } ) => { + } ) ), + ...[ '1.', '1)' ].map( ( prefix ) => ( { + type: 'prefix', + prefix, + transform( content ) { return createBlock( 'core/list', { ordered: true, values: `<li>${ content }</li>`, } ); }, - }, + } ) ), ], to: [ { diff --git a/packages/block-library/src/quote/index.js b/packages/block-library/src/quote/index.js index dd60905cded3b0..78e53875b771db 100644 --- a/packages/block-library/src/quote/index.js +++ b/packages/block-library/src/quote/index.js @@ -90,9 +90,9 @@ export const settings = { } ), }, { - type: 'pattern', - regExp: /^>\s/, - transform: ( { content } ) => { + type: 'prefix', + prefix: '>', + transform: ( content ) => { return createBlock( 'core/quote', { value: `<p>${ content }</p>`, } ); diff --git a/packages/block-library/src/separator/index.js b/packages/block-library/src/separator/index.js index f077d6533413de..adcc34297d2ff1 100644 --- a/packages/block-library/src/separator/index.js +++ b/packages/block-library/src/separator/index.js @@ -27,8 +27,7 @@ export const settings = { transforms: { from: [ { - type: 'pattern', - trigger: 'enter', + type: 'enter', regExp: /^-{3,}$/, transform: () => createBlock( 'core/separator' ), }, diff --git a/packages/editor/src/components/rich-text/index.js b/packages/editor/src/components/rich-text/index.js index 6167269d600edc..43df39403e3bbd 100644 --- a/packages/editor/src/components/rich-text/index.js +++ b/packages/editor/src/components/rich-text/index.js @@ -115,12 +115,12 @@ export class RichText extends Component { this.savedContent = value; this.patterns = getPatterns( { onReplace, - multilineTag: this.multilineTag, + onCreateUndoLevel: this.onCreateUndoLevel, valueToFormat: this.valueToFormat, + onChange: this.onChange, } ); - this.enterPatterns = getBlockTransforms( 'from' ).filter( ( { type, trigger } ) => - type === 'pattern' && trigger === 'enter' - ); + this.enterPatterns = getBlockTransforms( 'from' ) + .filter( ( { type } ) => type === 'enter' ); this.state = {}; diff --git a/packages/editor/src/components/rich-text/patterns.js b/packages/editor/src/components/rich-text/patterns.js index 5cb16b5fe68b6e..a9e9a8010149d7 100644 --- a/packages/editor/src/components/rich-text/patterns.js +++ b/packages/editor/src/components/rich-text/patterns.js @@ -1,18 +1,18 @@ -/** - * External dependencies - */ -import { filter } from 'lodash'; - /** * WordPress dependencies */ import { getBlockTransforms, findTransform } from '@wordpress/blocks'; -import { remove, applyFormat, getTextContent } from '@wordpress/rich-text'; - -export function getPatterns( { onReplace, multiline, valueToFormat } ) { - const patterns = filter( getBlockTransforms( 'from' ), ( { type, trigger } ) => { - return type === 'pattern' && trigger === undefined; - } ); +import { + remove, + applyFormat, + getTextContent, + getSelectionStart, + slice, +} from '@wordpress/rich-text'; + +export function getPatterns( { onReplace, valueToFormat, onCreateUndoLevel, onChange } ) { + const prefixTransforms = getBlockTransforms( 'from' ) + .filter( ( { type } ) => type === 'prefix' ); return [ ( record ) => { @@ -20,50 +20,61 @@ export function getPatterns( { onReplace, multiline, valueToFormat } ) { return record; } + const start = getSelectionStart( record ); const text = getTextContent( record ); - const transformation = findTransform( patterns, ( item ) => { - return item.regExp.test( text ); + const characterBefore = text.slice( start - 1, start ); + + if ( ! /\s/.test( characterBefore ) ) { + return record; + } + + const trimmedTextBefore = text.slice( 0, start ).trim(); + const transformation = findTransform( prefixTransforms, ( { prefix } ) => { + return trimmedTextBefore === prefix; } ); if ( ! transformation ) { return record; } - const result = text.match( transformation.regExp ); - - const block = transformation.transform( { - content: valueToFormat( remove( record, 0, result[ 0 ].length ) ), - match: result, - } ); + const content = valueToFormat( slice( record, start, text.length ) ); + const block = transformation.transform( content ); + onCreateUndoLevel(); onReplace( [ block ] ); return record; }, ( record ) => { - if ( multiline ) { + const BACKTICK = '`'; + const start = getSelectionStart( record ); + const text = getTextContent( record ); + const characterBefore = text.slice( start - 1, start ); + + // Quick check the text for the necessary character. + if ( characterBefore !== BACKTICK ) { return record; } - const text = getTextContent( record ); + const textBefore = text.slice( 0, start - 1 ); + const indexBefore = textBefore.lastIndexOf( BACKTICK ); - // Quick check the text for the necessary character. - if ( text.indexOf( '`' ) === -1 ) { + if ( indexBefore === -1 ) { return record; } - const match = text.match( /`([^`]+)`/ ); + const startIndex = indexBefore; + const endIndex = start - 2; - if ( ! match ) { + if ( startIndex === endIndex ) { return record; } - const start = match.index; - const end = start + match[ 1 ].length; + onChange( record ); - record = remove( record, start, start + 1 ); - record = remove( record, end, end + 1 ); - record = applyFormat( record, { type: 'code' }, start, end ); + record = remove( record, startIndex, startIndex + 1 ); + record = remove( record, endIndex, endIndex + 1 ); + record = applyFormat( record, { type: 'code' }, startIndex, endIndex ); return record; }, diff --git a/test/e2e/specs/__snapshots__/rich-text.test.js.snap b/test/e2e/specs/__snapshots__/rich-text.test.js.snap index fc96d2d6f1fab5..e1324add1205e7 100644 --- a/test/e2e/specs/__snapshots__/rich-text.test.js.snap +++ b/test/e2e/specs/__snapshots__/rich-text.test.js.snap @@ -23,3 +23,15 @@ exports[`RichText should handle change in tag name gracefully 1`] = ` <h3></h3> <!-- /wp:heading -->" `; + +exports[`RichText should transform backtick to code 1`] = ` +"<!-- wp:paragraph --> +<p>A <code>backtick</code></p> +<!-- /wp:paragraph -->" +`; + +exports[`RichText should transform backtick to code 2`] = ` +"<!-- wp:paragraph --> +<p>A \`backtick\`</p> +<!-- /wp:paragraph -->" +`; diff --git a/test/e2e/specs/blocks/__snapshots__/code.test.js.snap b/test/e2e/specs/blocks/__snapshots__/code.test.js.snap new file mode 100644 index 00000000000000..50ee83d373c65e --- /dev/null +++ b/test/e2e/specs/blocks/__snapshots__/code.test.js.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Code can be created by three backticks and enter 1`] = ` +"<!-- wp:code --> +<pre class=\\"wp-block-code\\"><code>&lt;?php</code></pre> +<!-- /wp:code -->" +`; diff --git a/test/e2e/specs/blocks/__snapshots__/heading.test.js.snap b/test/e2e/specs/blocks/__snapshots__/heading.test.js.snap new file mode 100644 index 00000000000000..6cffb6ad28a9fc --- /dev/null +++ b/test/e2e/specs/blocks/__snapshots__/heading.test.js.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Separator can be created by prefixing existing content with number signs and a space 1`] = ` +"<!-- wp:heading {\\"level\\":4} --> +<h4>4</h4> +<!-- /wp:heading -->" +`; + +exports[`Separator can be created by prefixing number sign and a space 1`] = ` +"<!-- wp:heading {\\"level\\":3} --> +<h3>3</h3> +<!-- /wp:heading -->" +`; diff --git a/test/e2e/specs/blocks/__snapshots__/list.test.js.snap b/test/e2e/specs/blocks/__snapshots__/list.test.js.snap index dbc0bee94158fd..a804ea84132d3d 100644 --- a/test/e2e/specs/blocks/__snapshots__/list.test.js.snap +++ b/test/e2e/specs/blocks/__snapshots__/list.test.js.snap @@ -74,6 +74,12 @@ exports[`List can be created by using an asterisk at the start of a paragraph bl <!-- /wp:list -->" `; +exports[`List can undo asterisk transform 1`] = ` +"<!-- wp:paragraph --> +<p>1.</p> +<!-- /wp:paragraph -->" +`; + exports[`List should create paragraph on split at end and merge back with content 1`] = ` "<!-- wp:list --> <ul><li>one</li></ul> diff --git a/test/e2e/specs/blocks/__snapshots__/separator.test.js.snap b/test/e2e/specs/blocks/__snapshots__/separator.test.js.snap new file mode 100644 index 00000000000000..41466bc4de2500 --- /dev/null +++ b/test/e2e/specs/blocks/__snapshots__/separator.test.js.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Separator can be created by three dashes and enter 1`] = ` +"<!-- wp:separator --> +<hr class=\\"wp-block-separator\\"/> +<!-- /wp:separator -->" +`; diff --git a/test/e2e/specs/blocks/code.test.js b/test/e2e/specs/blocks/code.test.js new file mode 100644 index 00000000000000..85cdbe1dd715ff --- /dev/null +++ b/test/e2e/specs/blocks/code.test.js @@ -0,0 +1,23 @@ +/** + * Internal dependencies + */ +import { + clickBlockAppender, + getEditedPostContent, + newPost, +} from '../../support/utils'; + +describe( 'Code', () => { + beforeEach( async () => { + await newPost(); + } ); + + it( 'can be created by three backticks and enter', async () => { + await clickBlockAppender(); + await page.keyboard.type( '```' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( '<?php' ); + + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); +} ); diff --git a/test/e2e/specs/blocks/heading.test.js b/test/e2e/specs/blocks/heading.test.js new file mode 100644 index 00000000000000..101dbf88317367 --- /dev/null +++ b/test/e2e/specs/blocks/heading.test.js @@ -0,0 +1,30 @@ +/** + * Internal dependencies + */ +import { + clickBlockAppender, + getEditedPostContent, + newPost, +} from '../../support/utils'; + +describe( 'Separator', () => { + beforeEach( async () => { + await newPost(); + } ); + + it( 'can be created by prefixing number sign and a space', async () => { + await clickBlockAppender(); + await page.keyboard.type( '### 3' ); + + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); + + it( 'can be created by prefixing existing content with number signs and a space', async () => { + await clickBlockAppender(); + await page.keyboard.type( '4' ); + await page.keyboard.press( 'ArrowLeft' ); + await page.keyboard.type( '#### ' ); + + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); +} ); diff --git a/test/e2e/specs/blocks/list.test.js b/test/e2e/specs/blocks/list.test.js index 80d8297acdc7ac..3d416b998d1371 100644 --- a/test/e2e/specs/blocks/list.test.js +++ b/test/e2e/specs/blocks/list.test.js @@ -46,6 +46,14 @@ describe( 'List', () => { expect( await getEditedPostContent() ).toMatchSnapshot(); } ); + it( 'can undo asterisk transform', async () => { + await clickBlockAppender(); + await page.keyboard.type( '1. ' ); + await pressWithModifier( 'primary', 'z' ); + + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); + it( 'can be created by typing "/list"', async () => { // Create a list with the slash block shortcut. await clickBlockAppender(); diff --git a/test/e2e/specs/blocks/separator.test.js b/test/e2e/specs/blocks/separator.test.js new file mode 100644 index 00000000000000..348d7a6ed45fc8 --- /dev/null +++ b/test/e2e/specs/blocks/separator.test.js @@ -0,0 +1,22 @@ +/** + * Internal dependencies + */ +import { + clickBlockAppender, + getEditedPostContent, + newPost, +} from '../../support/utils'; + +describe( 'Separator', () => { + beforeEach( async () => { + await newPost(); + } ); + + it( 'can be created by three dashes and enter', async () => { + await clickBlockAppender(); + await page.keyboard.type( '---' ); + await page.keyboard.press( 'Enter' ); + + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); +} ); diff --git a/test/e2e/specs/rich-text.test.js b/test/e2e/specs/rich-text.test.js index 4bf15fd9fcd582..dca248c8289314 100644 --- a/test/e2e/specs/rich-text.test.js +++ b/test/e2e/specs/rich-text.test.js @@ -56,4 +56,15 @@ describe( 'RichText', () => { expect( await getEditedPostContent() ).toMatchSnapshot(); } ); + + it( 'should transform backtick to code', async () => { + await clickBlockAppender(); + await page.keyboard.type( 'A `backtick`' ); + + expect( await getEditedPostContent() ).toMatchSnapshot(); + + await pressWithModifier( 'primary', 'z' ); + + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); } ); From 9e464a0c838d758a06cd23092f0c0f8731c81843 Mon Sep 17 00:00:00 2001 From: Marcus Kazmierczak <marcus@mkaz.com> Date: Mon, 19 Nov 2018 11:49:10 -0800 Subject: [PATCH 052/254] Add ability to specify a different default editor font per locale (#11826) * Add ability to specify a different default font Using the language translation, allows specifying a different Google font to load and what the CSS font-family should be. * Switch translation name to not be specific to Noto --- lib/client-assets.php | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/lib/client-assets.php b/lib/client-assets.php index 2b78d56cf2f09c..9a46ea80063345 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -356,19 +356,19 @@ function gutenberg_register_scripts_and_styles() { // Editor Styles. // This empty stylesheet is defined to ensure backwards compatibility. gutenberg_override_style( 'wp-blocks', false ); - $fonts_url = ''; /* - * Translators: If there are characters in your language that are not supported - * by Noto Serif, translate this to 'off'. Do not translate into your own language. + * Translators: Use this to specify the proper Google Font name and variants + * to load that is supported by your language. Do not translate. + * Set to 'off' to disable loading. */ - if ( 'off' !== _x( 'on', 'Noto Serif font: on or off', 'gutenberg' ) ) { + $font_family = _x( 'Noto Serif:400,400i,700,700i', 'Google Font Name and Variants', 'gutenberg' ); + if ( 'off' !== $font_family ) { $query_args = array( - 'family' => urlencode( 'Noto Serif:400,400i,700,700i' ), + 'family' => urlencode( $font_family ), ); - - $fonts_url = esc_url_raw( add_query_arg( $query_args, 'https://fonts.googleapis.com/css' ) ); + $fonts_url = esc_url_raw( add_query_arg( $query_args, 'https://fonts.googleapis.com/css' ) ); } gutenberg_override_style( @@ -1172,6 +1172,16 @@ function gutenberg_editor_scripts_and_styles( $hook ) { ), ), ); + + /* + * Set a locale specific default font. + * Translators: Use this to specify the CSS font family for the default font + */ + $locale_font_family = esc_html_x( 'Noto Serif', 'CSS Font Family for Editor Font', 'gutenberg' ); + $styles[] = array( + 'css' => "body { font-family: '$locale_font_family' }", + ); + if ( $editor_styles && current_theme_supports( 'editor-styles' ) ) { foreach ( $editor_styles as $style ) { if ( filter_var( $style, FILTER_VALIDATE_URL ) ) { From cddee64697706f6b797a9fba39d075eff95ee0b0 Mon Sep 17 00:00:00 2001 From: Marty Helmick <info@martyhelmick.com> Date: Mon, 19 Nov 2018 15:10:42 -0500 Subject: [PATCH 053/254] Fix/button css specificity (#12005) * Reduce specificity of button CSS * increase specificity of color utility CSS * Add a comment in the colors stylesheet. --- packages/block-library/src/button/style.scss | 73 +++++++------------- packages/block-library/src/style.scss | 47 +++++++------ 2 files changed, 49 insertions(+), 71 deletions(-) diff --git a/packages/block-library/src/button/style.scss b/packages/block-library/src/button/style.scss index 49ae444c831495..fa72a8bc7835d5 100644 --- a/packages/block-library/src/button/style.scss +++ b/packages/block-library/src/button/style.scss @@ -2,28 +2,9 @@ $blocks-button__height: 46px; $blocks-button__line-height: $big-font-size + 6px; .wp-block-button { + color: $white; margin-bottom: 1.5em; - & .wp-block-button__link { - border: none; - border-radius: $blocks-button__height / 2; - box-shadow: none; - cursor: pointer; - display: inline-block; - font-size: $big-font-size; - line-height: $blocks-button__line-height; - margin: 0; - padding: ($blocks-button__height - $blocks-button__line-height) / 2 24px; - text-align: center; - text-decoration: none; - white-space: normal; - word-break: break-all; - } - - &.is-style-squared .wp-block-button__link { - border-radius: 0; - } - &.aligncenter { text-align: center; } @@ -34,45 +15,39 @@ $blocks-button__line-height: $big-font-size + 6px; } } -.wp-block-button__link:not(.has-background) { +.wp-block-button__link { background-color: $dark-gray-700; + border: none; + border-radius: $blocks-button__height / 2; + box-shadow: none; + color: inherit; + cursor: pointer; + display: inline-block; + font-size: $big-font-size; + line-height: $blocks-button__line-height; + margin: 0; + padding: ($blocks-button__height - $blocks-button__line-height) / 2 24px; + text-align: center; + text-decoration: none; + white-space: normal; + word-break: break-all; &:hover, &:focus, &:active { - background-color: $dark-gray-700; + color: inherit; } } -.wp-block-button.is-style-outline { - .wp-block-button__link { - background: transparent; - border: 2px solid currentcolor; - - &:hover, - &:focus, - &:active { - border-color: currentcolor; - } - } - - .wp-block-button__link:not(.has-text-color) { - color: $dark-gray-700; - - &:hover, - &:focus, - &:active { - color: $dark-gray-700; - } - } +.is-style-squared .wp-block-button__link { + border-radius: 0; } -.wp-block-button__link:not(.has-text-color) { - color: $white; +.is-style-outline { + color: $dark-gray-700; - &:hover, - &:focus, - &:active { - color: $white; + .wp-block-button__link { + background: transparent; + border: 2px solid currentcolor; } } diff --git a/packages/block-library/src/style.scss b/packages/block-library/src/style.scss index de1d605d489c61..9d9b2379696b2f 100644 --- a/packages/block-library/src/style.scss +++ b/packages/block-library/src/style.scss @@ -22,91 +22,94 @@ @import "./verse/style.scss"; @import "./video/style.scss"; -.has-pale-pink-background-color { +// Class names are doubled to increase specificity to assure colors take effect +// over another base class color. + +.has-pale-pink-background-color.has-pale-pink-background-color { background-color: #f78da7; } -.has-vivid-red-background-color { +.has-vivid-red-background-color.has-vivid-red-background-color { background-color: #cf2e2e; } -.has-luminous-vivid-orange-background-color { +.has-luminous-vivid-orange-background-color.has-luminous-vivid-orange-background-color { background-color: #ff6900; } -.has-luminous-vivid-amber-background-color { +.has-luminous-vivid-amber-background-color.has-luminous-vivid-amber-background-color { background-color: #fcb900; } -.has-light-green-cyan-background-color { +.has-light-green-cyan-background-color.has-light-green-cyan-background-color { background-color: #7bdcb5; } -.has-vivid-green-cyan-background-color { +.has-vivid-green-cyan-background-color.has-vivid-green-cyan-background-color { background-color: #00d084; } -.has-pale-cyan-blue-background-color { +.has-pale-cyan-blue-background-color.has-pale-cyan-blue-background-color { background-color: #8ed1fc; } -.has-vivid-cyan-blue-background-color { +.has-vivid-cyan-blue-background-color.has-vivid-cyan-blue-background-color { background-color: #0693e3; } -.has-very-light-gray-background-color { +.has-very-light-gray-background-color.has-very-light-gray-background-color { background-color: #eee; } -.has-cyan-bluish-gray-background-color { +.has-cyan-bluish-gray-background-color.has-cyan-bluish-gray-background-color { background-color: #abb8c3; } -.has-very-dark-gray-background-color { +.has-very-dark-gray-background-color.has-very-dark-gray-background-color { background-color: #313131; } -.has-pale-pink-color { +.has-pale-pink-color.has-pale-pink-color { color: #f78da7; } -.has-vivid-red-color { +.has-vivid-red-color.has-vivid-red-color { color: #cf2e2e; } -.has-luminous-vivid-orange-color { +.has-luminous-vivid-orange-color.has-luminous-vivid-orange-color { color: #ff6900; } -.has-luminous-vivid-amber-color { +.has-luminous-vivid-amber-color.has-luminous-vivid-amber-color { color: #fcb900; } -.has-light-green-cyan-color { +.has-light-green-cyan-color.has-light-green-cyan-color { color: #7bdcb5; } -.has-vivid-green-cyan-color { +.has-vivid-green-cyan-color.has-vivid-green-cyan-color { color: #00d084; } -.has-pale-cyan-blue-color { +.has-pale-cyan-blue-color.has-pale-cyan-blue-color { color: #8ed1fc; } -.has-vivid-cyan-blue-color { +.has-vivid-cyan-blue-color.has-vivid-cyan-blue-color { color: #0693e3; } -.has-very-light-gray-color { +.has-very-light-gray-color.has-very-light-gray-color { color: #eee; } -.has-cyan-bluish-gray-color { +.has-cyan-bluish-gray-color.has-cyan-bluish-gray-color { color: #abb8c3; } -.has-very-dark-gray-color { +.has-very-dark-gray-color.has-very-dark-gray-color { color: #313131; } From 300a2de122c45ba974ccc0a918f3b2b1a7b925a9 Mon Sep 17 00:00:00 2001 From: Jorge <jorge.costa@developer.pt> Date: Mon, 19 Nov 2018 20:13:58 +0000 Subject: [PATCH 054/254] Fix: Align: Only add data-align for wide/full aligns if editor/theme supports them (#9481) Full and wide aligns block alignments were always shown in the editor provided they were previously set or set because of a default value even if the current theme does not support them. This PR does not make the editor change anything that has been saved. It just updates the editor to not show wide and full alignments on existing blocks (and default attributes) when they are not supported by the current theme so what the user sees in backend matches the front-end. Unless explicitly changed by the user the aligns continue to be wide/full and if later the user changes to a theme that supports this aligns the editor will show again wide/full without any change. The wide and full alignments were already not being shown in the Block alignment toolbar if the theme does not support them. --- packages/editor/src/hooks/align.js | 210 ++++++++++++++++-------- packages/editor/src/hooks/test/align.js | 117 ++++++++----- 2 files changed, 215 insertions(+), 112 deletions(-) diff --git a/packages/editor/src/hooks/align.js b/packages/editor/src/hooks/align.js index 7b80924c433e20..7b7062ba558f5f 100644 --- a/packages/editor/src/hooks/align.js +++ b/packages/editor/src/hooks/align.js @@ -2,20 +2,72 @@ * External dependencies */ import classnames from 'classnames'; -import { assign, get, has, includes } from 'lodash'; +import { assign, get, has, includes, without } from 'lodash'; /** * WordPress dependencies */ -import { createHigherOrderComponent } from '@wordpress/compose'; +import { compose, createHigherOrderComponent } from '@wordpress/compose'; import { addFilter } from '@wordpress/hooks'; -import { hasBlockSupport, getBlockSupport, getBlockType } from '@wordpress/blocks'; +import { getBlockSupport, getBlockType, hasBlockSupport } from '@wordpress/blocks'; +import { withSelect } from '@wordpress/data'; /** * Internal dependencies */ import { BlockControls, BlockAlignmentToolbar } from '../components'; +/** + * An array which includes all possible valid alignments, + * used to validate if an alignment is valid or not. + * + * @constant + * @type {string[]} +*/ +const ALL_ALIGNMENTS = [ 'left', 'center', 'right', 'wide', 'full' ]; + +/** + * An array which includes all wide alignments. + * In order for this alignments to be valid they need to be supported by the block, + * and by the theme. + * + * @constant + * @type {string[]} +*/ +const WIDE_ALIGNMENTS = [ 'wide', 'full' ]; + +/** + * Returns the valid alignments. + * Takes into consideration the aligns supported by a block, if the block supports wide controls or not and if theme supports wide controls or not. + * Exported just for testing purposes, not exported outside the module. + * + * @param {?boolean|string[]} blockAlign Aligns supported by the block. + * @param {?boolean} hasWideBlockSupport True if block supports wide alignments. And False otherwise. + * @param {?boolean} hasWideEnabled True if theme supports wide alignments. And False otherwise. + * + * @return {string[]} Valid alignments. + */ +export function getValidAlignments( blockAlign, hasWideBlockSupport = true, hasWideEnabled = true ) { + let validAlignments; + if ( Array.isArray( blockAlign ) ) { + validAlignments = blockAlign; + } else if ( blockAlign === true ) { + // `true` includes all alignments... + validAlignments = ALL_ALIGNMENTS; + } else { + validAlignments = []; + } + + if ( + ! hasWideEnabled || + ( blockAlign === true && ! hasWideBlockSupport ) + ) { + return without( validAlignments, ...WIDE_ALIGNMENTS ); + } + + return validAlignments; +} + /** * Filters registered block settings, extending attributes to include `align`. * @@ -39,33 +91,6 @@ export function addAttribute( settings ) { return settings; } -/** - * Returns an array of valid alignments for a block type depending on its - * defined supports. Returns an empty array if block does not support align. - * - * @param {string} blockName Block name to check - * @return {string[]} Valid alignments for block - */ -export function getBlockValidAlignments( blockName ) { - // Explicitly defined array set of valid alignments - const blockAlign = getBlockSupport( blockName, 'align' ); - if ( Array.isArray( blockAlign ) ) { - return blockAlign; - } - - const validAlignments = []; - if ( true === blockAlign ) { - // `true` includes all alignments... - validAlignments.push( 'left', 'center', 'right' ); - - if ( hasBlockSupport( blockName, 'alignWide', true ) ) { - validAlignments.push( 'wide', 'full' ); - } - } - - return validAlignments; -} - /** * Override the default edit UI to include new toolbar controls for block * alignment, if block defines support. @@ -73,46 +98,57 @@ export function getBlockValidAlignments( blockName ) { * @param {Function} BlockEdit Original component * @return {Function} Wrapped component */ -export const withToolbarControls = createHigherOrderComponent( ( BlockEdit ) => { - return ( props ) => { - const validAlignments = getBlockValidAlignments( props.name ); - - const updateAlignment = ( nextAlign ) => { - if ( ! nextAlign ) { - const blockType = getBlockType( props.name ); - const blockDefaultAlign = get( blockType, [ 'attributes', 'align', 'default' ] ); - if ( blockDefaultAlign ) { - nextAlign = ''; +export const withToolbarControls = createHigherOrderComponent( + ( BlockEdit ) => ( + ( props ) => { + const { name: blockName } = props; + // Compute valid alignments without taking into account, + // if the theme supports wide alignments or not. + // BlockAlignmentToolbar takes into account the theme support. + const validAlignments = getValidAlignments( + getBlockSupport( blockName, 'align' ), + hasBlockSupport( blockName, 'alignWide', true ), + ); + + const updateAlignment = ( nextAlign ) => { + if ( ! nextAlign ) { + const blockType = getBlockType( props.name ); + const blockDefaultAlign = get( blockType, [ 'attributes', 'align', 'default' ] ); + if ( blockDefaultAlign ) { + nextAlign = ''; + } } - } - props.setAttributes( { align: nextAlign } ); - }; - - return [ - validAlignments.length > 0 && props.isSelected && ( - <BlockControls key="align-controls"> - <BlockAlignmentToolbar - value={ props.attributes.align } - onChange={ updateAlignment } - controls={ validAlignments } - /> - </BlockControls> - ), - <BlockEdit key="edit" { ...props } />, - ]; - }; -}, 'withToolbarControls' ); - -/** - * Override the default block element to add alignment wrapper props. - * - * @param {Function} BlockListBlock Original component - * @return {Function} Wrapped component - */ -export const withDataAlign = createHigherOrderComponent( ( BlockListBlock ) => { - return ( props ) => { - const { align } = props.block.attributes; - const validAlignments = getBlockValidAlignments( props.block.name ); + props.setAttributes( { align: nextAlign } ); + }; + + return [ + validAlignments.length > 0 && props.isSelected && ( + <BlockControls key="align-controls"> + <BlockAlignmentToolbar + value={ props.attributes.align } + onChange={ updateAlignment } + controls={ validAlignments } + /> + </BlockControls> + ), + <BlockEdit key="edit" { ...props } />, + ]; + } + ), + 'withToolbarControls' +); + +// Exported just for testing purposes, not exported outside the module. +export const insideSelectWithDataAlign = ( BlockListBlock ) => ( + ( props ) => { + const { block, hasWideEnabled } = props; + const { name: blockName } = block; + const { align } = block.attributes; + const validAlignments = getValidAlignments( + getBlockSupport( blockName, 'align' ), + hasBlockSupport( blockName, 'alignWide', true ), + hasWideEnabled + ); let wrapperProps = props.wrapperProps; if ( includes( validAlignments, align ) ) { @@ -120,8 +156,28 @@ export const withDataAlign = createHigherOrderComponent( ( BlockListBlock ) => { } return <BlockListBlock { ...props } wrapperProps={ wrapperProps } />; - }; -}, 'withDataAlign' ); + } +); + +/** + * Override the default block element to add alignment wrapper props. + * + * @param {Function} BlockListBlock Original component + * @return {Function} Wrapped component + */ +export const withDataAlign = createHigherOrderComponent( + compose( [ + withSelect( + ( select ) => { + const { getEditorSettings } = select( 'core/editor' ); + return { + hasWideEnabled: !! getEditorSettings().alignWide, + }; + } + ), + insideSelectWithDataAlign, + ] ) +); /** * Override props assigned to save component to inject alignment class name if @@ -134,8 +190,16 @@ export const withDataAlign = createHigherOrderComponent( ( BlockListBlock ) => { */ export function addAssignedAlign( props, blockType, attributes ) { const { align } = attributes; - - if ( includes( getBlockValidAlignments( blockType ), align ) ) { + const blockAlign = getBlockSupport( blockType, 'align' ); + const hasWideBlockSupport = hasBlockSupport( blockType, 'alignWide', true ); + const isAlignValid = includes( + // Compute valid alignments without taking into account, + // if the theme supports wide alignments or not. + // This way changing themes does not impacts the block save. + getValidAlignments( blockAlign, hasWideBlockSupport ), + align + ); + if ( isAlignValid ) { props.className = classnames( `align${ align }`, props.className ); } diff --git a/packages/editor/src/hooks/test/align.js b/packages/editor/src/hooks/test/align.js index ccc678eee8d772..5d3f3dba87c287 100644 --- a/packages/editor/src/hooks/test/align.js +++ b/packages/editor/src/hooks/test/align.js @@ -18,9 +18,9 @@ import { * Internal dependencies */ import { - getBlockValidAlignments, + getValidAlignments, withToolbarControls, - withDataAlign, + insideSelectWithDataAlign, addAssignedAlign, } from '../align'; @@ -58,49 +58,61 @@ describe( 'align', () => { } ); } ); - describe( 'getBlockValidAlignments()', () => { + describe( 'getValidAlignments()', () => { it( 'should return an empty array if block does not define align support', () => { - registerBlockType( 'core/foo', blockSettings ); - const validAlignments = getBlockValidAlignments( 'core/foo' ); + expect( getValidAlignments() ).toEqual( [] ); + } ); - expect( validAlignments ).toEqual( [] ); + it( 'should return all custom aligns set', () => { + expect( + getValidAlignments( [ 'left', 'right' ] ) + ).toEqual( + [ 'left', 'right' ] + ); } ); - it( 'should return all custom align set', () => { - registerBlockType( 'core/foo', { - ...blockSettings, - supports: { - align: [ 'left', 'right' ], - }, - } ); - const validAlignments = getBlockValidAlignments( 'core/foo' ); + it( 'should return all aligns if block defines align support as true', () => { + expect( + getValidAlignments( true ) + ).toEqual( + [ 'left', 'center', 'right', 'wide', 'full' ] + ); + } ); - expect( validAlignments ).toEqual( [ 'left', 'right' ] ); + it( 'should return all aligns except wide if wide align explicitly false on the block', () => { + expect( + getValidAlignments( true, false, true ) + ).toEqual( [ 'left', 'center', 'right' ] ); + + expect( + getValidAlignments( true, false, false ) + ).toEqual( [ 'left', 'center', 'right' ] ); } ); - it( 'should return all aligns if block defines align support', () => { - registerBlockType( 'core/foo', { - ...blockSettings, - supports: { - align: true, - }, - } ); - const validAlignments = getBlockValidAlignments( 'core/foo' ); + it( 'should return all aligns except wide if wide align is not supported by the theme', () => { + expect( + getValidAlignments( true, true, false ) + ).toEqual( [ 'left', 'center', 'right' ] ); - expect( validAlignments ).toEqual( [ 'left', 'center', 'right', 'wide', 'full' ] ); + expect( + getValidAlignments( true, false, false ) + ).toEqual( [ 'left', 'center', 'right' ] ); } ); - it( 'should return all aligns except wide if wide align explicitly false', () => { - registerBlockType( 'core/foo', { - ...blockSettings, - supports: { - align: true, - alignWide: false, - }, - } ); - const validAlignments = getBlockValidAlignments( 'core/foo' ); + it( 'should not remove wide aligns if they are not supported by the block and were set using an array in supports align', () => { + expect( + getValidAlignments( [ 'left', 'right', 'wide', 'full' ], false, true ) + ).toEqual( [ 'left', 'right', 'wide', 'full' ] ); + } ); - expect( validAlignments ).toEqual( [ 'left', 'center', 'right' ] ); + it( 'should remove wide aligns if they are not supported by the theme and were set using an array in supports align', () => { + expect( + getValidAlignments( [ 'left', 'right', 'wide', 'full' ], true, false ) + ).toEqual( [ 'left', 'right' ] ); + + expect( + getValidAlignments( [ 'left', 'right', 'wide', 'full' ], false, false ) + ).toEqual( [ 'left', 'right' ] ); } ); } ); @@ -153,11 +165,11 @@ describe( 'align', () => { ...blockSettings, supports: { align: true, - alignWide: false, + alignWide: true, }, } ); - const EnhancedComponent = withDataAlign( ( { wrapperProps } ) => ( + const EnhancedComponent = insideSelectWithDataAlign( ( { wrapperProps } ) => ( <div { ...wrapperProps } /> ) ); @@ -166,16 +178,43 @@ describe( 'align', () => { block={ { name: 'core/foo', attributes: { - align: 'left', + align: 'wide', }, } } /> ); expect( wrapper.toTree().rendered.props.wrapperProps ).toEqual( { - 'data-align': 'left', + 'data-align': 'wide', } ); } ); + it( 'should not render wide/full wrapper props if wide controls are not enabled', () => { + registerBlockType( 'core/foo', { + ...blockSettings, + supports: { + align: true, + alignWide: true, + }, + } ); + + const EnhancedComponent = insideSelectWithDataAlign( ( { wrapperProps } ) => ( + <div { ...wrapperProps } /> + ) ); + + const wrapper = renderer.create( + <EnhancedComponent + block={ { + name: 'core/foo', + attributes: { + align: 'wide', + }, + } } + hasWideEnabled={ false } + /> + ); + expect( wrapper.toTree().rendered.props.wrapperProps ).toEqual( undefined ); + } ); + it( 'should not render invalid align', () => { registerBlockType( 'core/foo', { ...blockSettings, @@ -185,7 +224,7 @@ describe( 'align', () => { }, } ); - const EnhancedComponent = withDataAlign( ( { wrapperProps } ) => ( + const EnhancedComponent = insideSelectWithDataAlign( ( { wrapperProps } ) => ( <div { ...wrapperProps } /> ) ); From 1e5f88595cfc9304a16dbf2c4ef838376d9edcdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Wrede?= <soerenwrede@gmail.com> Date: Mon, 19 Nov 2018 21:21:10 +0100 Subject: [PATCH 055/254] Switch image and audio block to embed block if URL is embeddable (#11472) Allow transitioning from Audio and Image block to relevant embed when pasting a url in the insert URL field. --- packages/block-library/src/audio/edit.js | 13 +++++++++++++ packages/block-library/src/image/edit.js | 14 +++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/packages/block-library/src/audio/edit.js b/packages/block-library/src/audio/edit.js index ed5c7d5aee2d94..ee2966208d015b 100644 --- a/packages/block-library/src/audio/edit.js +++ b/packages/block-library/src/audio/edit.js @@ -21,6 +21,11 @@ import { } from '@wordpress/editor'; import { getBlobByURL, isBlobURL } from '@wordpress/blob'; +/** + * Internal dependencies + */ +import { createUpgradedEmbedBlock } from '../embed/util'; + const ALLOWED_MEDIA_TYPES = [ 'audio' ]; class AudioEdit extends Component { @@ -73,6 +78,14 @@ class AudioEdit extends Component { // Set the block's src from the edit component's state, and switch off // the editing UI. if ( newSrc !== src ) { + // Check if there's an embed block that handles this URL. + const embedBlock = createUpgradedEmbedBlock( + { attributes: { url: newSrc } } + ); + if ( undefined !== embedBlock ) { + this.props.onReplace( embedBlock ); + return; + } setAttributes( { src: newSrc, id: undefined } ); } diff --git a/packages/block-library/src/image/edit.js b/packages/block-library/src/image/edit.js index c708c1a485e401..7daffa06203b08 100644 --- a/packages/block-library/src/image/edit.js +++ b/packages/block-library/src/image/edit.js @@ -49,6 +49,7 @@ import { compose } from '@wordpress/compose'; /** * Internal dependencies */ +import { createUpgradedEmbedBlock } from '../embed/util'; import ImageSize from './image-size'; /** @@ -111,6 +112,7 @@ class ImageEdit extends Component { this.getFilename = this.getFilename.bind( this ); this.toggleIsEditing = this.toggleIsEditing.bind( this ); this.onUploadError = this.onUploadError.bind( this ); + this.onImageError = this.onImageError.bind( this ); this.state = { captionFocused: false, @@ -216,6 +218,16 @@ class ImageEdit extends Component { } ); } + onImageError( url ) { + // Check if there's an embed block that handles this URL. + const embedBlock = createUpgradedEmbedBlock( + { attributes: { url } } + ); + if ( undefined !== embedBlock ) { + this.props.onReplace( embedBlock ); + } + } + onSetCustomHref( value ) { this.props.setAttributes( { href: value } ); } @@ -570,7 +582,7 @@ class ImageEdit extends Component { // should direct focus to block. /* eslint-disable jsx-a11y/no-noninteractive-element-interactions */ <Fragment> - <img src={ url } alt={ defaultedAlt } onClick={ this.onImageClick } /> + <img src={ url } alt={ defaultedAlt } onClick={ this.onImageClick } onError={ () => this.onImageError( url ) } /> { isBlobURL( url ) && <Spinner /> } </Fragment> /* eslint-enable jsx-a11y/no-noninteractive-element-interactions */ From 9297fc93a2c7b47575007127ffe038d22101de81 Mon Sep 17 00:00:00 2001 From: Andrea Fercia <a.fercia@gmail.com> Date: Mon, 19 Nov 2018 22:27:17 +0100 Subject: [PATCH 056/254] Restore the block hover and focus styles in Unified Toolbar mode. (#11737) * Restore the block hover and focus styles in Unified Toolbar mode. * Remove hover outlines in Top Toolbar mode. --- packages/editor/src/components/block-list/block.js | 2 +- .../editor/src/components/block-list/breadcrumb.js | 14 +++----------- .../editor/src/components/block-list/style.scss | 5 ----- .../editor/src/components/post-title/style.scss | 4 +++- 4 files changed, 7 insertions(+), 18 deletions(-) diff --git a/packages/editor/src/components/block-list/block.js b/packages/editor/src/components/block-list/block.js index 1263bbe9e40bc5..e57a221531dcc0 100644 --- a/packages/editor/src/components/block-list/block.js +++ b/packages/editor/src/components/block-list/block.js @@ -424,7 +424,7 @@ export class BlockListBlock extends Component { // 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 shouldAppearSelected = ! isFocusMode && ! 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; diff --git a/packages/editor/src/components/block-list/breadcrumb.js b/packages/editor/src/components/block-list/breadcrumb.js index 2699fb22811614..ca14b1d346c947 100644 --- a/packages/editor/src/components/block-list/breadcrumb.js +++ b/packages/editor/src/components/block-list/breadcrumb.js @@ -1,8 +1,3 @@ -/** - * External dependencies - */ -import classnames from 'classnames'; - /** * WordPress dependencies */ @@ -53,12 +48,10 @@ export class BlockBreadcrumb extends Component { } render() { - const { clientId, rootClientId, isLight } = this.props; + const { clientId, rootClientId } = this.props; return ( - <div className={ classnames( 'editor-block-list__breadcrumb', { - 'is-light': isLight, - } ) }> + <div className={ 'editor-block-list__breadcrumb' }> <Toolbar> { rootClientId && ( <Fragment> @@ -75,12 +68,11 @@ export class BlockBreadcrumb extends Component { export default compose( [ withSelect( ( select, ownProps ) => { - const { getBlockRootClientId, getEditorSettings } = select( 'core/editor' ); + const { getBlockRootClientId } = select( 'core/editor' ); const { clientId } = ownProps; return { rootClientId: getBlockRootClientId( clientId ), - isLight: getEditorSettings().hasFixedToolbar, }; } ), ] )( BlockBreadcrumb ); diff --git a/packages/editor/src/components/block-list/style.scss b/packages/editor/src/components/block-list/style.scss index e6787f1128204f..e7e13f8a9e06aa 100644 --- a/packages/editor/src/components/block-list/style.scss +++ b/packages/editor/src/components/block-list/style.scss @@ -908,11 +908,6 @@ } } - &.is-light .components-toolbar { - background: rgba($white, 0.5); - color: $dark-gray-700; - } - // Position the breadcrumb closer on mobile. [data-align="left"] &, [data-align="right"] & { diff --git a/packages/editor/src/components/post-title/style.scss b/packages/editor/src/components/post-title/style.scss index 3f9f7e63266ed5..71fc698dea2024 100644 --- a/packages/editor/src/components/post-title/style.scss +++ b/packages/editor/src/components/post-title/style.scss @@ -47,7 +47,7 @@ } } - &:not(.is-focus-mode):not(.has-fixed-toolbar) { + &:not(.is-focus-mode) { &.is-selected .editor-post-title__input { // use opacity to work in various editor styles border-color: $dark-opacity-light-500; @@ -56,7 +56,9 @@ border-color: $light-opacity-light-500; } } + } + &:not(.is-focus-mode):not(.has-fixed-toolbar) { .editor-post-title__input:hover { border-color: theme(outlines); } From 53c975b06792d323e638a5d53874c9744332f553 Mon Sep 17 00:00:00 2001 From: Andrew Duthie <andrew@andrewduthie.com> Date: Tue, 20 Nov 2018 01:53:51 -0500 Subject: [PATCH 057/254] Block API: Preserve unknown, respect `null` in server attributes preparation (#12003) * Framework: Preserve unknown attributes in block render * Framework: Respect null as a valid attribute type * REST API: Update block renderer test to omit undefined defaults * REST API: Assign default for block renderer attributes Previously we would pass `NULL` to `render`. While `render` provides a default `array()` value, since an explicit value is provided, it would not take effect. With this change, the endpoint assures a default value to be provided as a parameter to get the `get_item` callback that is compatible with the function signature of `WP_BlockType::render`. Alternatively, it could be considered to have separate invocations of `render`, depending on whether the request parameter is set. --- lib/class-wp-block-type.php | 39 ++++++++++++------- ...lass-wp-rest-block-renderer-controller.php | 1 + phpunit/class-block-type-test.php | 21 +++++++++- ...ss-rest-block-renderer-controller-test.php | 4 +- 4 files changed, 48 insertions(+), 17 deletions(-) diff --git a/lib/class-wp-block-type.php b/lib/class-wp-block-type.php index 0c18bb6efa4682..c186eec88a0a3b 100644 --- a/lib/class-wp-block-type.php +++ b/lib/class-wp-block-type.php @@ -120,36 +120,47 @@ public function is_dynamic() { /** * Validates attributes against the current block schema, populating - * defaulted and missing values, and omitting unknown attributes. + * defaulted and missing values. * * @param array $attributes Original block attributes. * @return array Prepared block attributes. */ public function prepare_attributes_for_render( $attributes ) { + // If there are no attribute definitions for the block type, skip + // processing and return vebatim. if ( ! isset( $this->attributes ) ) { return $attributes; } - $prepared_attributes = array(); + foreach ( $attributes as $attribute_name => $value ) { + // If the attribute is not defined by the block type, it cannot be + // validated. + if ( ! isset( $this->attributes[ $attribute_name ] ) ) { + continue; + } - foreach ( $this->attributes as $attribute_name => $schema ) { - $value = null; + $schema = $this->attributes[ $attribute_name ]; - if ( isset( $attributes[ $attribute_name ] ) ) { - $is_valid = rest_validate_value_from_schema( $attributes[ $attribute_name ], $schema ); - if ( ! is_wp_error( $is_valid ) ) { - $value = rest_sanitize_value_from_schema( $attributes[ $attribute_name ], $schema ); - } + // Validate value by JSON schema. An invalid value should revert to + // its default, if one exists. This occurs by virtue of the missing + // attributes loop immediately following. If there is not a default + // assigned, the attribute value should remain unset. + $is_valid = rest_validate_value_from_schema( $value, $schema ); + if ( is_wp_error( $is_valid ) ) { + unset( $attributes[ $attribute_name ] ); } + } - if ( is_null( $value ) && isset( $schema['default'] ) ) { - $value = $schema['default']; + // Populate values of any missing attributes for which the block type + // defines a default. + $missing_schema_attributes = array_diff_key( $this->attributes, $attributes ); + foreach ( $missing_schema_attributes as $attribute_name => $schema ) { + if ( isset( $schema['default'] ) ) { + $attributes[ $attribute_name ] = $schema['default']; } - - $prepared_attributes[ $attribute_name ] = $value; } - return $prepared_attributes; + return $attributes; } /** diff --git a/lib/class-wp-rest-block-renderer-controller.php b/lib/class-wp-rest-block-renderer-controller.php index 3ecf25de449301..b9839f080376a2 100644 --- a/lib/class-wp-rest-block-renderer-controller.php +++ b/lib/class-wp-rest-block-renderer-controller.php @@ -59,6 +59,7 @@ public function register_routes() { 'type' => 'object', 'additionalProperties' => false, 'properties' => $block_type->get_attributes(), + 'default' => array(), ), 'post_id' => array( 'description' => __( 'ID of the post context.', 'gutenberg' ), diff --git a/phpunit/class-block-type-test.php b/phpunit/class-block-type-test.php index cada34b83cc09a..648893082b1041 100644 --- a/phpunit/class-block-type-test.php +++ b/phpunit/class-block-type-test.php @@ -139,7 +139,8 @@ function test_prepare_attributes() { 'wrongType' => 5, 'wrongTypeDefaulted' => 5, /* missingDefaulted */ - 'undefined' => 'omit', + 'undefined' => 'include', + 'intendedNull' => null, ); $block_type = new WP_Block_Type( @@ -160,6 +161,10 @@ function test_prepare_attributes() { 'type' => 'string', 'default' => 'define', ), + 'intendedNull' => array( + 'type' => array( 'string', 'null' ), + 'default' => 'wrong', + ), ), ) ); @@ -169,14 +174,26 @@ function test_prepare_attributes() { $this->assertEquals( array( 'correct' => 'include', - 'wrongType' => null, + /* wrongType */ 'wrongTypeDefaulted' => 'defaulted', 'missingDefaulted' => 'define', + 'undefined' => 'include', + 'intendedNull' => null, ), $prepared_attributes ); } + function test_prepare_attributes_none_defined() { + $attributes = array( 'exists' => 'keep' ); + + $block_type = new WP_Block_Type( 'core/dummy', array() ); + + $prepared_attributes = $block_type->prepare_attributes_for_render( $attributes ); + + $this->assertEquals( $attributes, $prepared_attributes ); + } + function test_has_block_with_mixed_content() { $mixed_post_content = 'before' . '<!-- wp:core/dummy --><!-- /wp:core/dummy -->' . diff --git a/phpunit/class-rest-block-renderer-controller-test.php b/phpunit/class-rest-block-renderer-controller-test.php index 66191e122ededb..3691c4d162af4f 100644 --- a/phpunit/class-rest-block-renderer-controller-test.php +++ b/phpunit/class-rest-block-renderer-controller-test.php @@ -272,7 +272,9 @@ public function test_get_item_default_attributes() { $block_type = WP_Block_Type_Registry::get_instance()->get_registered( self::$block_name ); $defaults = array(); foreach ( $block_type->attributes as $key => $attribute ) { - $defaults[ $key ] = isset( $attribute['default'] ) ? $attribute['default'] : null; + if ( isset( $attribute['default'] ) ) { + $defaults[ $key ] = $attribute['default']; + } } $request = new WP_REST_Request( 'GET', self::$rest_api_route . self::$block_name ); From f33498b0d6746e64a65a5dbccf31507842bf4e15 Mon Sep 17 00:00:00 2001 From: William Earnhardt <wearnhardt@gmail.com> Date: Tue, 20 Nov 2018 02:25:27 -0500 Subject: [PATCH 058/254] Update displayed permalink when slug is cleared (#11783) --- .../src/components/sidebar/post-link/index.js | 26 +++++++++++++++---- .../src/components/post-permalink/editor.js | 11 +++++--- .../src/components/post-permalink/index.js | 24 ++++++++++++----- packages/editor/src/utils/index.js | 1 + packages/editor/src/utils/test/url.js | 10 +++++++ packages/editor/src/utils/url.js | 25 ++++++++++++++++++ 6 files changed, 82 insertions(+), 15 deletions(-) create mode 100644 packages/editor/src/utils/test/url.js diff --git a/packages/edit-post/src/components/sidebar/post-link/index.js b/packages/edit-post/src/components/sidebar/post-link/index.js index 744d96a6ec37aa..9073402804dc9d 100644 --- a/packages/edit-post/src/components/sidebar/post-link/index.js +++ b/packages/edit-post/src/components/sidebar/post-link/index.js @@ -12,6 +12,7 @@ import { PanelBody, TextControl, ExternalLink } from '@wordpress/components'; import { withSelect, withDispatch } from '@wordpress/data'; import { compose, ifCondition, withState } from '@wordpress/compose'; import { addQueryArgs } from '@wordpress/url'; +import { cleanForSlug } from '@wordpress/editor'; /** * Module Constants @@ -27,15 +28,19 @@ function PostLink( { editPermalink, forceEmptyField, setState, + postTitle, + postSlug, + postID, } ) { - const { prefix, postName, suffix } = permalinkParts; + const { prefix, suffix } = permalinkParts; let prefixElement, postNameElement, suffixElement; + const currentSlug = postSlug || cleanForSlug( postTitle ) || postID; if ( isEditable ) { prefixElement = prefix && ( <span className="edit-post-post-link__link-prefix">{ prefix }</span> ); - postNameElement = postName && ( - <span className="edit-post-post-link__link-post-name">{ postName }</span> + postNameElement = currentSlug && ( + <span className="edit-post-post-link__link-post-name">{ currentSlug }</span> ); suffixElement = suffix && ( <span className="edit-post-post-link__link-suffix">{ suffix }</span> @@ -51,7 +56,7 @@ function PostLink( { { isEditable && ( <TextControl label={ __( 'URL' ) } - value={ forceEmptyField ? '' : postName } + value={ forceEmptyField ? '' : currentSlug } onChange={ ( newValue ) => { editPermalink( newValue ); // When we delete the field the permalink gets @@ -72,6 +77,14 @@ function PostLink( { } ); } } } + onBlur={ ( event ) => { + editPermalink( cleanForSlug( event.target.value ) ); + if ( forceEmptyField ) { + setState( { + forceEmptyField: false, + } ); + } + } } /> ) } <p className="edit-post-post-link__preview-label"> @@ -117,7 +130,7 @@ export default compose( [ getPostType, } = select( 'core' ); - const { link } = getCurrentPost(); + const { link, id } = getCurrentPost(); const postTypeName = getEditedPostAttribute( 'type' ); const postType = getPostType( postTypeName ); return { @@ -128,6 +141,9 @@ export default compose( [ isOpened: isEditorPanelOpened( PANEL_NAME ), permalinkParts: getPermalinkParts(), isViewable: get( postType, [ 'viewable' ], false ), + postTitle: getEditedPostAttribute( 'title' ), + postSlug: getEditedPostAttribute( 'slug' ), + postID: id, }; } ), ifCondition( ( { isNew, postLink, isViewable } ) => { diff --git a/packages/editor/src/components/post-permalink/editor.js b/packages/editor/src/components/post-permalink/editor.js index 3831d8a800579f..593af16f2c4686 100644 --- a/packages/editor/src/components/post-permalink/editor.js +++ b/packages/editor/src/components/post-permalink/editor.js @@ -7,19 +7,24 @@ import { __ } from '@wordpress/i18n'; import { Button } from '@wordpress/components'; import { compose } from '@wordpress/compose'; +/** + * Internal dependencies + */ +import { cleanForSlug } from '../../utils/url'; + class PostPermalinkEditor extends Component { - constructor( { permalinkParts } ) { + constructor( { permalinkParts, slug } ) { super( ...arguments ); this.state = { - editedPostName: permalinkParts.postName, + editedPostName: slug || permalinkParts.postName, }; this.onSavePermalink = this.onSavePermalink.bind( this ); } onSavePermalink( event ) { - const postName = this.state.editedPostName.replace( /\s+/g, '-' ); + const postName = cleanForSlug( this.state.editedPostName ); event.preventDefault(); diff --git a/packages/editor/src/components/post-permalink/index.js b/packages/editor/src/components/post-permalink/index.js index a7364322d4c7df..767f2507f77172 100644 --- a/packages/editor/src/components/post-permalink/index.js +++ b/packages/editor/src/components/post-permalink/index.js @@ -17,7 +17,7 @@ import { safeDecodeURI } from '@wordpress/url'; * Internal Dependencies */ import PostPermalinkEditor from './editor.js'; -import { getWPAdminURL } from '../../utils/url'; +import { getWPAdminURL, cleanForSlug } from '../../utils/url'; class PostPermalink extends Component { constructor() { @@ -57,14 +57,19 @@ class PostPermalink extends Component { } render() { - const { isNew, postLink, isEditable, samplePermalink, isPublished } = this.props; - const { isCopied, isEditingPermalink } = this.state; - const ariaLabel = isCopied ? __( 'Permalink copied' ) : __( 'Copy the permalink' ); + const { isNew, postLink, permalinkParts, postSlug, postTitle, postID, isEditable, isPublished } = this.props; if ( isNew || ! postLink ) { return null; } + const { isCopied, isEditingPermalink } = this.state; + const ariaLabel = isCopied ? __( 'Permalink copied' ) : __( 'Copy the permalink' ); + + const { prefix, suffix } = permalinkParts; + const slug = postSlug || cleanForSlug( postTitle ) || postID; + const samplePermalink = ( isEditable ) ? prefix + slug + suffix : prefix; + return ( <div className="editor-post-permalink"> <ClipboardButton @@ -92,6 +97,7 @@ class PostPermalink extends Component { { isEditingPermalink && <PostPermalinkEditor + slug={ slug } onSave={ () => this.setState( { isEditingPermalink: false } ) } /> } @@ -128,18 +134,22 @@ export default compose( [ isEditedPostNew, isPermalinkEditable, getCurrentPost, - getPermalink, + getPermalinkParts, + getEditedPostAttribute, isCurrentPostPublished, } = select( 'core/editor' ); - const { link } = getCurrentPost(); + const { id, link } = getCurrentPost(); return { isNew: isEditedPostNew(), postLink: link, + permalinkParts: getPermalinkParts(), + postSlug: getEditedPostAttribute( 'slug' ), isEditable: isPermalinkEditable(), - samplePermalink: getPermalink(), isPublished: isCurrentPostPublished(), + postTitle: getEditedPostAttribute( 'title' ), + postID: id, }; } ), withDispatch( ( dispatch ) => { diff --git a/packages/editor/src/utils/index.js b/packages/editor/src/utils/index.js index 3a49f66b86c902..0f246c16e4aec8 100644 --- a/packages/editor/src/utils/index.js +++ b/packages/editor/src/utils/index.js @@ -4,3 +4,4 @@ import mediaUpload from './media-upload'; export { mediaUpload }; +export { cleanForSlug } from './url.js'; diff --git a/packages/editor/src/utils/test/url.js b/packages/editor/src/utils/test/url.js new file mode 100644 index 00000000000000..9aca0fc8503e50 --- /dev/null +++ b/packages/editor/src/utils/test/url.js @@ -0,0 +1,10 @@ +/** + * Internal dependencies + */ +import { cleanForSlug } from '../url'; + +describe( 'cleanForSlug()', () => { + it( 'Should return string prepared for use as url slug', () => { + expect( cleanForSlug( ' /Déjà_vu. ' ) ).toBe( 'deja-vu' ); + } ); +} ); diff --git a/packages/editor/src/utils/url.js b/packages/editor/src/utils/url.js index 566775289f9777..371cdedc93ff45 100644 --- a/packages/editor/src/utils/url.js +++ b/packages/editor/src/utils/url.js @@ -1,3 +1,8 @@ +/** + * External dependencies + */ +import { deburr, toLower, trim } from 'lodash'; + /** * WordPress dependencies */ @@ -16,3 +21,23 @@ import { addQueryArgs } from '@wordpress/url'; export function getWPAdminURL( page, query ) { return addQueryArgs( page, query ); } + +/** + * Performs some basic cleanup of a string for use as a post slug + * + * This replicates some of what santize_title() does in WordPress core, but + * is only designed to approximate what the slug will be. + * + * Converts whitespace, periods, forward slashes and underscores to hyphens. + * Converts Latin-1 Supplement and Latin Extended-A letters to basic Latin + * letters. Removes combining diacritical marks. Converts remaining string + * to lowercase. It does not touch octets, HTML entities, or other encoded + * characters. + * + * @param {string} string Title or slug to be processed + * + * @return {string} Processed string + */ +export function cleanForSlug( string ) { + return toLower( deburr( trim( string.replace( /[\s\./_]+/g, '-' ), '-' ) ) ); +} From 2fc6cae4ea872fde9e0c50280becf2f5199aa2d7 Mon Sep 17 00:00:00 2001 From: Adam Silverstein <adam@10up.com> Date: Tue, 20 Nov 2018 03:55:39 -0700 Subject: [PATCH 059/254] Allow previewing changes to the post featured image (#12037) Match the Classic Editor's behaviour by appending _thumbnail_id to the post preview URL so that changes to the featured image are visible. --- .../editor/src/components/post-preview-button/index.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/editor/src/components/post-preview-button/index.js b/packages/editor/src/components/post-preview-button/index.js index ca86f8f28c7fd0..0cf5350a5672f5 100644 --- a/packages/editor/src/components/post-preview-button/index.js +++ b/packages/editor/src/components/post-preview-button/index.js @@ -12,6 +12,7 @@ import { __, _x } from '@wordpress/i18n'; import { withSelect, withDispatch } from '@wordpress/data'; import { DotTip } from '@wordpress/nux'; import { ifCondition, compose } from '@wordpress/compose'; +import { addQueryArgs } from '@wordpress/url'; function writeInterstitialMessage( targetDocument ) { let markup = renderToString( @@ -203,11 +204,18 @@ export default compose( [ const { getPostType, } = select( 'core' ); + + let previewLink = getAutosaveAttribute( 'preview_link' ); + const featuredImageId = getEditedPostAttribute( 'featured_media' ); + if ( previewLink && featuredImageId ) { + previewLink = addQueryArgs( previewLink, { _thumbnail_id: featuredImageId } ); + } + const postType = getPostType( getEditedPostAttribute( 'type' ) ); return { postId: getCurrentPostId(), currentPostLink: getCurrentPostAttribute( 'link' ), - previewLink: getAutosaveAttribute( 'preview_link' ), + previewLink, isSaveable: isEditedPostSaveable(), isAutosaveable: isEditedPostAutosaveable(), isViewable: get( postType, [ 'viewable' ], false ), From 58d9803a29d4b362aba98f1dfc723d38ecc3ca69 Mon Sep 17 00:00:00 2001 From: Joen Asmussen <joen@automattic.com> Date: Tue, 20 Nov 2018 12:35:31 +0100 Subject: [PATCH 060/254] Fix IE11 toolbar. (#12098) This PR is a better replacement for #12096. In IE11, in the toolbar markup, you can briefly see the appearance of a new HTML element called <jscomp_symbol_react.fragment16>. This is obviously not a real element, but it is rendered by IE11 as a real element, which means it breaks the flexing of the actual children of the toolbar. This PR, props to @youknowriad, adds a change to the assets so the element doesn't appear in the first place. --- lib/client-assets.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/client-assets.php b/lib/client-assets.php index 9a46ea80063345..dd2edaf8bc1e85 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -167,16 +167,16 @@ function gutenberg_register_scripts_and_styles() { register_tinymce_scripts(); - wp_script_add_data( + wp_add_inline_script( 'wp-polyfill', - 'data', gutenberg_get_script_polyfill( array( '\'fetch\' in window' => 'wp-polyfill-fetch', 'document.contains' => 'wp-polyfill-node-contains', 'window.FormData && window.FormData.prototype.keys' => 'wp-polyfill-formdata', 'Element.prototype.matches && Element.prototype.closest' => 'wp-polyfill-element-closest', - ) + ), + 'after' ) ); From 2945a62f51cacc9b354e790673593f866a9f2f1d Mon Sep 17 00:00:00 2001 From: Adam Silverstein <adam@10up.com> Date: Tue, 20 Nov 2018 04:40:59 -0700 Subject: [PATCH 061/254] Save meta on preview (#11409) * Set the preview url once metabox and autosaves are complete. * when previewing, requestMetaBoxUpdates * move preview autosave logic into edit-post effects * update docs & save logic * Fix save metaboxes condition * Refresh preview after metabox save * Fix consecutive previews * Fix unit tests * Fix autosaving status * Always return a boolean value from isAutosave selector. --- docs/data/data-core-editor.md | 18 +++++++++++++++++- .../edit-post/src/components/header/index.js | 5 ++++- packages/edit-post/src/store/effects.js | 14 ++++++++------ .../components/post-preview-button/index.js | 12 ++++-------- packages/editor/src/store/actions.js | 8 +++++--- packages/editor/src/store/effects/posts.js | 13 ++++++------- packages/editor/src/store/reducer.js | 8 +++++--- packages/editor/src/store/selectors.js | 13 ++++++++++++- packages/editor/src/store/test/reducer.js | 3 +++ 9 files changed, 64 insertions(+), 30 deletions(-) diff --git a/docs/data/data-core-editor.md b/docs/data/data-core-editor.md index d4c7b602001943..d0ccaabda71fb8 100644 --- a/docs/data/data-core-editor.md +++ b/docs/data/data-core-editor.md @@ -1071,6 +1071,18 @@ Returns true if the post is autosaving, or false otherwise. Whether the post is autosaving. +### isPreviewingPost + +Returns true if the post is being previewed, or false otherwise. + +*Parameters* + + * state: Global application state. + +*Returns* + +Whether the post is being previewed. + ### getSuggestedPostFormat Returns a suggested post format for the current post, inferred only if there @@ -1628,7 +1640,7 @@ Returns an action object to save the post. *Parameters* * options: Options for the save. - * options.autosave: Perform an autosave if true. + * options.isAutosave: Perform an autosave if true. ### mergeBlocks @@ -1643,6 +1655,10 @@ Returns an action object used in signalling that two blocks should be merged Returns an action object used in signalling that the post should autosave. +*Parameters* + + * options: Extra flags to identify the autosave. + ### redo Returns an action object used in signalling that undo history should diff --git a/packages/edit-post/src/components/header/index.js b/packages/edit-post/src/components/header/index.js index c63a7058eeb7cc..6fb3ec96eab749 100644 --- a/packages/edit-post/src/components/header/index.js +++ b/packages/edit-post/src/components/header/index.js @@ -51,7 +51,10 @@ function Header( { forceIsSaving={ isSaving } /> ) } - <PostPreviewButton /> + <PostPreviewButton + forceIsAutosaveable={ hasActiveMetaboxes } + forcePreviewLink={ isSaving ? null : undefined } + /> <PostPublishButtonOrToggle forceIsDirty={ hasActiveMetaboxes } forceIsSaving={ isSaving } diff --git a/packages/edit-post/src/store/effects.js b/packages/edit-post/src/store/effects.js index f0b6d39a107aa3..699b08f0636ed2 100644 --- a/packages/edit-post/src/store/effects.js +++ b/packages/edit-post/src/store/effects.js @@ -45,24 +45,26 @@ const effects = { let wasSavingPost = select( 'core/editor' ).isSavingPost(); let wasAutosavingPost = select( 'core/editor' ).isAutosavingPost(); + let wasPreviewingPost = select( 'core/editor' ).isPreviewingPost(); // Save metaboxes when performing a full save on the post. subscribe( () => { const isSavingPost = select( 'core/editor' ).isSavingPost(); const isAutosavingPost = select( 'core/editor' ).isAutosavingPost(); + const isPreviewingPost = select( 'core/editor' ).isPreviewingPost(); const hasActiveMetaBoxes = select( 'core/edit-post' ).hasMetaBoxes(); - // Save metaboxes on save completion when past save wasn't an autosave. + // Save metaboxes on save completion, except for autosaves that are not a post preview. const shouldTriggerMetaboxesSave = ( - hasActiveMetaBoxes && - wasSavingPost && - ! wasAutosavingPost && - ! isSavingPost && - ! isAutosavingPost + hasActiveMetaBoxes && ( + ( wasSavingPost && ! isSavingPost && ! wasAutosavingPost ) || + ( wasAutosavingPost && wasPreviewingPost && ! isPreviewingPost ) + ) ); // Save current state for next inspection. wasSavingPost = isSavingPost; wasAutosavingPost = isAutosavingPost; + wasPreviewingPost = isPreviewingPost; if ( shouldTriggerMetaboxesSave ) { store.dispatch( requestMetaBoxUpdates() ); diff --git a/packages/editor/src/components/post-preview-button/index.js b/packages/editor/src/components/post-preview-button/index.js index 0cf5350a5672f5..9dac5eedddef55 100644 --- a/packages/editor/src/components/post-preview-button/index.js +++ b/packages/editor/src/components/post-preview-button/index.js @@ -99,10 +99,6 @@ export class PostPreviewButton extends Component { // unintentional forceful redirects. if ( previewLink && ! prevProps.previewLink ) { this.setPreviewWindowLink( previewLink ); - - // Once popup redirect is evaluated, even if already closed, delete - // reference to avoid later assignment of location in post update. - delete this.previewWindow; } } @@ -152,7 +148,7 @@ export class PostPreviewButton extends Component { // Request an autosave. This happens asynchronously and causes the component // to update when finished. - this.props.autosave(); + this.props.autosave( { isPreview: true } ); // Display a 'Generating preview' message in the Preview tab while we wait for the // autosave to finish. @@ -192,7 +188,7 @@ export class PostPreviewButton extends Component { } export default compose( [ - withSelect( ( select ) => { + withSelect( ( select, { forcePreviewLink, forceIsAutosaveable } ) => { const { getCurrentPostId, getCurrentPostAttribute, @@ -215,9 +211,9 @@ export default compose( [ return { postId: getCurrentPostId(), currentPostLink: getCurrentPostAttribute( 'link' ), - previewLink, + previewLink: forcePreviewLink !== undefined ? forcePreviewLink : getAutosaveAttribute( 'preview_link' ), isSaveable: isEditedPostSaveable(), - isAutosaveable: isEditedPostAutosaveable(), + isAutosaveable: forceIsAutosaveable || isEditedPostAutosaveable(), isViewable: get( postType, [ 'viewable' ], false ), }; } ), diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index 864fcfdd518e5a..6302ce88947560 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -396,7 +396,7 @@ export function editPost( edits ) { * Returns an action object to save the post. * * @param {Object} options Options for the save. - * @param {boolean} options.autosave Perform an autosave if true. + * @param {boolean} options.isAutosave Perform an autosave if true. * * @return {Object} Action object. */ @@ -439,10 +439,12 @@ export function mergeBlocks( firstBlockClientId, secondBlockClientId ) { /** * Returns an action object used in signalling that the post should autosave. * + * @param {Object?} options Extra flags to identify the autosave. + * * @return {Object} Action object. */ -export function autosave() { - return savePost( { autosave: true } ); +export function autosave( options ) { + return savePost( { isAutosave: true, ...options } ); } /** diff --git a/packages/editor/src/store/effects/posts.js b/packages/editor/src/store/effects/posts.js index af722dd4512bd8..fd33234a53a5db 100644 --- a/packages/editor/src/store/effects/posts.js +++ b/packages/editor/src/store/effects/posts.js @@ -28,7 +28,6 @@ import { getEditedPostContent, getAutosave, getCurrentPostType, - isEditedPostAutosaveable, isEditedPostSaveable, isEditedPostNew, POST_UPDATE_TRANSACTION_ID, @@ -51,12 +50,11 @@ export const requestPostUpdate = async ( action, store ) => { const { dispatch, getState } = store; const state = getState(); const post = getCurrentPost( state ); - const isAutosave = !! action.options.autosave; + const isAutosave = !! action.options.isAutosave; // Prevent save if not saveable. - const isSaveable = isAutosave ? isEditedPostAutosaveable : isEditedPostSaveable; - - if ( ! isSaveable( state ) ) { + // We don't check for dirtiness here as this can be overriden in the UI. + if ( ! isEditedPostSaveable( state ) ) { return; } @@ -91,7 +89,7 @@ export const requestPostUpdate = async ( action, store ) => { dispatch( { type: 'REQUEST_POST_UPDATE_START', optimist: { type: BEGIN, id: POST_UPDATE_TRANSACTION_ID }, - isAutosave, + options: action.options, } ); // Optimistically apply updates under the assumption that the post @@ -150,7 +148,7 @@ export const requestPostUpdate = async ( action, store ) => { type: isRevision ? REVERT : COMMIT, id: POST_UPDATE_TRANSACTION_ID, }, - isAutosave, + options: action.options, postType, } ); } catch ( error ) { @@ -160,6 +158,7 @@ export const requestPostUpdate = async ( action, store ) => { post, edits, error, + options: action.options, } ); } }; diff --git a/packages/editor/src/store/reducer.js b/packages/editor/src/store/reducer.js index cc7cdeedb55f3c..e21d2c3d990b11 100644 --- a/packages/editor/src/store/reducer.js +++ b/packages/editor/src/store/reducer.js @@ -967,7 +967,7 @@ export function saving( state = {}, action ) { requesting: true, successful: false, error: null, - isAutosave: action.isAutosave, + options: action.options || {}, }; case 'REQUEST_POST_UPDATE_SUCCESS': @@ -975,6 +975,7 @@ export function saving( state = {}, action ) { requesting: false, successful: true, error: null, + options: action.options || {}, }; case 'REQUEST_POST_UPDATE_FAILURE': @@ -982,6 +983,7 @@ export function saving( state = {}, action ) { requesting: false, successful: false, error: action.error, + options: action.options || {}, }; } @@ -1204,9 +1206,9 @@ export function autosave( state = null, action ) { preview_link: post.preview_link, }; - case 'REQUEST_POST_UPDATE': + case 'REQUEST_POST_UPDATE_START': // Invalidate known preview link when autosave starts. - if ( state && action.options.autosave ) { + if ( state && action.options.isAutosave ) { return omit( state, 'preview_link' ); } break; diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index 3c8d2d121acd92..9d7855830d3b1d 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -1518,7 +1518,18 @@ export function didPostSaveRequestFail( state ) { * @return {boolean} Whether the post is autosaving. */ export function isAutosavingPost( state ) { - return isSavingPost( state ) && state.saving.isAutosave; + return isSavingPost( state ) && !! state.saving.options.isAutosave; +} + +/** + * Returns true if the post is being previewed, or false otherwise. + * + * @param {Object} state Global application state. + * + * @return {boolean} Whether the post is being previewed. + */ +export function isPreviewingPost( state ) { + return isSavingPost( state ) && !! state.saving.options.isPreview; } /** diff --git a/packages/editor/src/store/test/reducer.js b/packages/editor/src/store/test/reducer.js index 99ac0c6e369967..788e361a6a929e 100644 --- a/packages/editor/src/store/test/reducer.js +++ b/packages/editor/src/store/test/reducer.js @@ -1943,6 +1943,7 @@ describe( 'state', () => { requesting: true, successful: false, error: null, + options: {}, } ); } ); @@ -1954,6 +1955,7 @@ describe( 'state', () => { requesting: false, successful: true, error: null, + options: {}, } ); } ); @@ -1972,6 +1974,7 @@ describe( 'state', () => { code: 'pretend_error', message: 'update failed', }, + options: {}, } ); } ); } ); From b462725979aff3393681cfcae0550f59c4569c52 Mon Sep 17 00:00:00 2001 From: Robert Anderson <robert@noisysocks.com> Date: Tue, 20 Nov 2018 23:23:35 +1100 Subject: [PATCH 062/254] Reusable blocks: Use view context, always include title.raw and content.raw (#12084) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Reusable blocks: Use view context Existing reusable blocks on a post would not render if the user was an author or a contributor. This happened because requests to fetch a single block or post are blocked when ?context=edit is passed and the current user is not an editor. The fix is to stop passing `?context=edit` to block requests, but this causes another problem which is that Gutenberg needs access to both `title.raw` and `content.raw` in the API response. We therefore both stop passing `?context=edit` to block requests *and* modify the blocks API so that `title.raw` and `content.raw` are always provided. Lastly, we modify the blocks API to always omit `content.rendered`, as it doesn't make sense for a reusable block to have rendered content on its own, since rendering a block requires it to be inside a post or a page. * Delete fake user after each test case, not after all test cases * Typo: /** → /* Co-Authored-By: noisysocks <robert@noisysocks.com> * Create all fake data before any test runs * Omit `title.rendered` from the blocks REST API The rendered title isn't used, so we may as well remove it so that `content` and `title` are aligned. * Fix fetching when using reusable blocks as a contributor * Fix reusable block effect tests * Fix reusable block E2E tests --- lib/class-wp-rest-blocks-controller.php | 53 +++++++++++++++++ packages/block-library/src/block/index.js | 2 +- .../src/store/effects/reusable-blocks.js | 11 ++-- .../src/store/effects/test/reusable-blocks.js | 14 ++--- phpunit/class-rest-blocks-controller-test.php | 59 +++++++++++++------ test/e2e/specs/reusable-blocks.test.js | 9 ++- 6 files changed, 115 insertions(+), 33 deletions(-) diff --git a/lib/class-wp-rest-blocks-controller.php b/lib/class-wp-rest-blocks-controller.php index 9689820c7494be..47882fafbff042 100644 --- a/lib/class-wp-rest-blocks-controller.php +++ b/lib/class-wp-rest-blocks-controller.php @@ -33,4 +33,57 @@ public function check_read_permission( $post ) { return parent::check_read_permission( $post ); } + + /** + * Filters a response based on the context defined in the schema. + * + * @since 4.4.0 + * + * @param array $data Response data to fiter. + * @param string $context Context defined in the schema. + * @return array Filtered response. + */ + public function filter_response_by_context( $data, $context ) { + $data = parent::filter_response_by_context( $data, $context ); + + /* + * Remove `title.rendered` and `content.rendered` from the response. It + * doesn't make sense for a reusable block to have rendered content on its + * own, since rendering a block requires it to be inside a post or a page. + */ + unset( $data['title']['rendered'] ); + unset( $data['content']['rendered'] ); + + return $data; + } + + /** + * Retrieves the block's schema, conforming to JSON Schema. + * + * @since 4.4.0 + * + * @return array Item schema data. + */ + public function get_item_schema() { + $schema = parent::get_item_schema(); + + /* + * Allow all contexts to access `title.raw` and `content.raw`. Clients always + * need the raw markup of a reusable block to do anything useful, e.g. parse + * it or display it in an editor. + */ + $schema['properties']['title']['properties']['raw']['context'] = array( 'view', 'edit' ); + $schema['properties']['content']['properties']['raw']['context'] = array( 'view', 'edit' ); + + /* + * Remove `title.rendered` and `content.rendered` from the schema. It doesn’t + * make sense for a reusable block to have rendered content on its own, since + * rendering a block requires it to be inside a post or a page. + */ + unset( $schema['properties']['title']['properties']['rendered'] ); + unset( $schema['properties']['content']['properties']['rendered'] ); + + return $schema; + } + } diff --git a/packages/block-library/src/block/index.js b/packages/block-library/src/block/index.js index 799522b0bdbc50..2a3b451f8f2a98 100644 --- a/packages/block-library/src/block/index.js +++ b/packages/block-library/src/block/index.js @@ -15,7 +15,7 @@ export const settings = { category: 'reusable', - description: __( 'Create content, and save it to reuse across your site. Update the block, and the changes apply everywhere it’s used.' ), + description: __( 'Create content, and save it for you and other contributors to reuse across your site. Update the block, and the changes apply everywhere it’s used.' ), attributes: { ref: { diff --git a/packages/editor/src/store/effects/reusable-blocks.js b/packages/editor/src/store/effects/reusable-blocks.js index 0c569728b6a39d..07ed88ab69d520 100644 --- a/packages/editor/src/store/effects/reusable-blocks.js +++ b/packages/editor/src/store/effects/reusable-blocks.js @@ -24,7 +24,6 @@ import { dispatch as dataDispatch } from '@wordpress/data'; /** * Internal dependencies */ -import { resolveSelector } from './utils'; import { __experimentalReceiveReusableBlocks as receiveReusableBlocksAction, removeBlocks, @@ -57,16 +56,16 @@ export const fetchReusableBlocks = async ( action, store ) => { // TODO: these are potentially undefined, this fix is in place // until there is a filter to not use reusable blocks if undefined - const postType = await resolveSelector( 'core', 'getPostType', 'wp_block' ); + const postType = await apiFetch( { path: '/wp/v2/types/wp_block' } ); if ( ! postType ) { return; } let result; if ( id ) { - result = apiFetch( { path: `/wp/v2/${ postType.rest_base }/${ id }?context=edit` } ); + result = apiFetch( { path: `/wp/v2/${ postType.rest_base }/${ id }` } ); } else { - result = apiFetch( { path: `/wp/v2/${ postType.rest_base }?per_page=-1&context=edit` } ); + result = apiFetch( { path: `/wp/v2/${ postType.rest_base }?per_page=-1` } ); } try { @@ -109,7 +108,7 @@ export const fetchReusableBlocks = async ( action, store ) => { export const saveReusableBlocks = async ( action, store ) => { // TODO: these are potentially undefined, this fix is in place // until there is a filter to not use reusable blocks if undefined - const postType = await resolveSelector( 'core', 'getPostType', 'wp_block' ); + const postType = await apiFetch( { path: '/wp/v2/types/wp_block' } ); if ( ! postType ) { return; } @@ -153,7 +152,7 @@ export const saveReusableBlocks = async ( action, store ) => { export const deleteReusableBlocks = async ( action, store ) => { // TODO: these are potentially undefined, this fix is in place // until there is a filter to not use reusable blocks if undefined - const postType = await resolveSelector( 'core', 'getPostType', 'wp_block' ); + const postType = await apiFetch( { path: '/wp/v2/types/wp_block' } ); if ( ! postType ) { return; } diff --git a/packages/editor/src/store/effects/test/reusable-blocks.js b/packages/editor/src/store/effects/test/reusable-blocks.js index 2cc3b83e4f36f7..1e13dba9f97142 100644 --- a/packages/editor/src/store/effects/test/reusable-blocks.js +++ b/packages/editor/src/store/effects/test/reusable-blocks.js @@ -83,7 +83,7 @@ describe( 'reusable blocks effects', () => { } ); apiFetch.mockImplementation( ( options ) => { - if ( options.path === '/wp/v2/types/wp_block?context=edit' ) { + if ( options.path === '/wp/v2/types/wp_block' ) { return postTypePromise; } @@ -130,7 +130,7 @@ describe( 'reusable blocks effects', () => { } ); apiFetch.mockImplementation( ( options ) => { - if ( options.path === '/wp/v2/types/wp_block?context=edit' ) { + if ( options.path === '/wp/v2/types/wp_block' ) { return postTypePromise; } @@ -172,7 +172,7 @@ describe( 'reusable blocks effects', () => { } ); apiFetch.mockImplementation( ( options ) => { - if ( options.path === '/wp/v2/types/wp_block?context=edit' ) { + if ( options.path === '/wp/v2/types/wp_block' ) { return postTypePromise; } @@ -202,7 +202,7 @@ describe( 'reusable blocks effects', () => { } ); apiFetch.mockImplementation( ( options ) => { - if ( options.path === '/wp/v2/types/wp_block?context=edit' ) { + if ( options.path === '/wp/v2/types/wp_block' ) { return postTypePromise; } @@ -236,7 +236,7 @@ describe( 'reusable blocks effects', () => { } ); apiFetch.mockImplementation( ( options ) => { - if ( options.path === '/wp/v2/types/wp_block?context=edit' ) { + if ( options.path === '/wp/v2/types/wp_block' ) { return postTypePromise; } @@ -284,7 +284,7 @@ describe( 'reusable blocks effects', () => { } ); apiFetch.mockImplementation( ( options ) => { - if ( options.path === '/wp/v2/types/wp_block?context=edit' ) { + if ( options.path === '/wp/v2/types/wp_block' ) { return postTypePromise; } @@ -330,7 +330,7 @@ describe( 'reusable blocks effects', () => { } ); apiFetch.mockImplementation( ( options ) => { - if ( options.path === '/wp/v2/types/wp_block?context=edit' ) { + if ( options.path === '/wp/v2/types/wp_block' ) { return postTypePromise; } diff --git a/phpunit/class-rest-blocks-controller-test.php b/phpunit/class-rest-blocks-controller-test.php index a5c58c6f49805a..5a9e71af7e33e9 100644 --- a/phpunit/class-rest-blocks-controller-test.php +++ b/phpunit/class-rest-blocks-controller-test.php @@ -18,11 +18,11 @@ class REST_Blocks_Controller_Test extends WP_UnitTestCase { protected static $post_id; /** - * Our fake user's ID. + * Our fake user IDs, keyed by their role. * - * @var int + * @var array */ - protected static $user_id; + protected static $user_ids; /** * Create fake data before our tests run. @@ -35,14 +35,14 @@ public static function wpSetUpBeforeClass( $factory ) { 'post_type' => 'wp_block', 'post_status' => 'publish', 'post_title' => 'My cool block', - 'post_content' => '<!-- wp:core/paragraph --><p>Hello!</p><!-- /wp:core/paragraph -->', + 'post_content' => '<!-- wp:paragraph --><p>Hello!</p><!-- /wp:paragraph -->', ) ); - self::$user_id = $factory->user->create( - array( - 'role' => 'editor', - ) + self::$user_ids = array( + 'editor' => $factory->user->create( array( 'role' => 'editor' ) ), + 'author' => $factory->user->create( array( 'role' => 'author' ) ), + 'contributor' => $factory->user->create( array( 'role' => 'contributor' ) ), ); } @@ -52,7 +52,9 @@ public static function wpSetUpBeforeClass( $factory ) { public static function wpTearDownAfterClass() { wp_delete_post( self::$post_id ); - self::delete_user( self::$user_id ); + foreach ( self::$user_ids as $user_id ) { + self::delete_user( $user_id ); + } } /** @@ -89,7 +91,7 @@ public function data_capabilities() { */ public function test_capabilities( $action, $role, $expected_status ) { if ( $role ) { - $user_id = $this->factory->user->create( array( 'role' => $role ) ); + $user_id = self::$user_ids[ $role ]; wp_set_current_user( $user_id ); } else { wp_set_current_user( 0 ); @@ -101,7 +103,7 @@ public function test_capabilities( $action, $role, $expected_status ) { $request->set_body_params( array( 'title' => 'Test', - 'content' => '<!-- wp:core/paragraph --><p>Test</p><!-- /wp:core/paragraph -->', + 'content' => '<!-- wp:paragraph --><p>Test</p><!-- /wp:paragraph -->', ) ); @@ -124,7 +126,7 @@ public function test_capabilities( $action, $role, $expected_status ) { 'post_type' => 'wp_block', 'post_status' => 'publish', 'post_title' => 'My cool block', - 'post_content' => '<!-- wp:core/paragraph --><p>Hello!</p><!-- /wp:core/paragraph -->', + 'post_content' => '<!-- wp:paragraph --><p>Hello!</p><!-- /wp:paragraph -->', 'post_author' => $user_id, ) ); @@ -133,7 +135,7 @@ public function test_capabilities( $action, $role, $expected_status ) { $request->set_body_params( array( 'title' => 'Test', - 'content' => '<!-- wp:core/paragraph --><p>Test</p><!-- /wp:core/paragraph -->', + 'content' => '<!-- wp:paragraph --><p>Test</p><!-- /wp:paragraph -->', ) ); @@ -154,7 +156,7 @@ public function test_capabilities( $action, $role, $expected_status ) { $request->set_body_params( array( 'title' => 'Test', - 'content' => '<!-- wp:core/paragraph --><p>Test</p><!-- /wp:core/paragraph -->', + 'content' => '<!-- wp:paragraph --><p>Test</p><!-- /wp:paragraph -->', ) ); @@ -171,9 +173,32 @@ public function test_capabilities( $action, $role, $expected_status ) { default: $this->fail( "'$action' is not a valid action." ); } + } - if ( isset( $user_id ) ) { - self::delete_user( $user_id ); - } + /** + * Check that the raw title and content of a block can be accessed when there + * is no set schema, and that the rendered content of a block is not included + * in the response. + */ + public function test_content() { + wp_set_current_user( self::$user_ids['author'] ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/blocks/' . self::$post_id ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( + array( + 'raw' => 'My cool block', + ), + $data['title'] + ); + $this->assertEquals( + array( + 'raw' => '<!-- wp:paragraph --><p>Hello!</p><!-- /wp:paragraph -->', + 'protected' => false, + ), + $data['content'] + ); } } diff --git a/test/e2e/specs/reusable-blocks.test.js b/test/e2e/specs/reusable-blocks.test.js index 64ce2e938fcdad..3dc3d32afa6c16 100644 --- a/test/e2e/specs/reusable-blocks.test.js +++ b/test/e2e/specs/reusable-blocks.test.js @@ -188,8 +188,13 @@ describe( 'Reusable Blocks', () => { // Delete the block and accept the confirmation dialog await page.click( 'button[aria-label="More options"]' ); - const convertButton = await page.waitForXPath( '//button[text()="Remove from Reusable Blocks"]' ); - await Promise.all( [ waitForAndAcceptDialog(), convertButton.click() ] ); + const deleteButton = await page.waitForXPath( '//button[text()="Remove from Reusable Blocks"]' ); + await Promise.all( [ waitForAndAcceptDialog(), deleteButton.click() ] ); + + // Wait for deletion to finish + await page.waitForXPath( + '//*[contains(@class, "components-notice") and contains(@class, "is-success")]/*[text()="Block deleted."]' + ); // Check that we have an empty post again expect( await getEditedPostContent() ).toBe( '' ); From 015e1440842d1a6bd3c151290adaebc63be0b73a Mon Sep 17 00:00:00 2001 From: Joen Asmussen <joen@automattic.com> Date: Tue, 20 Nov 2018 13:32:30 +0100 Subject: [PATCH 063/254] Fix small gap in style variation button (#12090) This PR fixes #12066. I believe the issue is caused by the border on the button being an inset shadow, which stacks below the background colors of the content inside the preview. By making it a border, this becomes a non issue. --- packages/editor/src/components/block-styles/style.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor/src/components/block-styles/style.scss b/packages/editor/src/components/block-styles/style.scss index 3a6b7806b9c242..53a609012f74e4 100644 --- a/packages/editor/src/components/block-styles/style.scss +++ b/packages/editor/src/components/block-styles/style.scss @@ -29,7 +29,7 @@ .editor-block-styles__item-preview { outline: $border-width solid transparent; // Shown in Windows High Contrast mode. - box-shadow: inset 0 0 0 1px rgba($dark-gray-900, 0.2); + border: 1px solid rgba($dark-gray-900, 0.2); overflow: hidden; padding: 0; text-align: initial; From 563ef41c663f163a725c2ef7e961b11ea6981f6e Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Tue, 20 Nov 2018 14:27:46 +0100 Subject: [PATCH 064/254] Perform a complete draft save on preview (#12097) --- docs/data/data-core-editor.md | 12 ++++++++++ .../components/post-preview-button/index.js | 20 ++++++++-------- packages/editor/src/store/reducer.js | 24 ++++++++++++++++--- packages/editor/src/store/selectors.js | 18 ++++++++++++++ packages/editor/src/store/test/reducer.js | 2 -- test/e2e/specs/preview.test.js | 12 +--------- 6 files changed, 62 insertions(+), 26 deletions(-) diff --git a/docs/data/data-core-editor.md b/docs/data/data-core-editor.md index d0ccaabda71fb8..25ea6598990d83 100644 --- a/docs/data/data-core-editor.md +++ b/docs/data/data-core-editor.md @@ -1083,6 +1083,18 @@ Returns true if the post is being previewed, or false otherwise. Whether the post is being previewed. +### getEditedPostPreviewLink + +Returns the post preview link + +*Parameters* + + * state: Global application state. + +*Returns* + +Preview Link. + ### getSuggestedPostFormat Returns a suggested post format for the current post, inferred only if there diff --git a/packages/editor/src/components/post-preview-button/index.js b/packages/editor/src/components/post-preview-button/index.js index 9dac5eedddef55..5c31e9e271de8f 100644 --- a/packages/editor/src/components/post-preview-button/index.js +++ b/packages/editor/src/components/post-preview-button/index.js @@ -12,7 +12,6 @@ import { __, _x } from '@wordpress/i18n'; import { withSelect, withDispatch } from '@wordpress/data'; import { DotTip } from '@wordpress/nux'; import { ifCondition, compose } from '@wordpress/compose'; -import { addQueryArgs } from '@wordpress/url'; function writeInterstitialMessage( targetDocument ) { let markup = renderToString( @@ -148,7 +147,11 @@ export class PostPreviewButton extends Component { // Request an autosave. This happens asynchronously and causes the component // to update when finished. - this.props.autosave( { isPreview: true } ); + if ( this.props.isDraft ) { + this.props.savePost( { isPreview: true } ); + } else { + this.props.autosave( { isPreview: true } ); + } // Display a 'Generating preview' message in the Preview tab while we wait for the // autosave to finish. @@ -192,33 +195,30 @@ export default compose( [ const { getCurrentPostId, getCurrentPostAttribute, - getAutosaveAttribute, getEditedPostAttribute, isEditedPostSaveable, isEditedPostAutosaveable, + getEditedPostPreviewLink, } = select( 'core/editor' ); const { getPostType, } = select( 'core' ); - let previewLink = getAutosaveAttribute( 'preview_link' ); - const featuredImageId = getEditedPostAttribute( 'featured_media' ); - if ( previewLink && featuredImageId ) { - previewLink = addQueryArgs( previewLink, { _thumbnail_id: featuredImageId } ); - } - + const previewLink = getEditedPostPreviewLink(); const postType = getPostType( getEditedPostAttribute( 'type' ) ); return { postId: getCurrentPostId(), currentPostLink: getCurrentPostAttribute( 'link' ), - previewLink: forcePreviewLink !== undefined ? forcePreviewLink : getAutosaveAttribute( 'preview_link' ), + previewLink: forcePreviewLink !== undefined ? forcePreviewLink : previewLink, isSaveable: isEditedPostSaveable(), isAutosaveable: forceIsAutosaveable || isEditedPostAutosaveable(), isViewable: get( postType, [ 'viewable' ], false ), + isDraft: [ 'draft', 'auto-draft' ].indexOf( getEditedPostAttribute( 'status' ) ) !== -1, }; } ), withDispatch( ( dispatch ) => ( { autosave: dispatch( 'core/editor' ).autosave, + savePost: dispatch( 'core/editor' ).savePost, } ) ), ifCondition( ( { isViewable } ) => isViewable ), ] )( PostPreviewButton ); diff --git a/packages/editor/src/store/reducer.js b/packages/editor/src/store/reducer.js index e21d2c3d990b11..d241bb008f45a1 100644 --- a/packages/editor/src/store/reducer.js +++ b/packages/editor/src/store/reducer.js @@ -22,6 +22,7 @@ import { */ import { isReusableBlock } from '@wordpress/blocks'; import { combineReducers } from '@wordpress/data'; +import { addQueryArgs } from '@wordpress/url'; /** * Internal dependencies @@ -1203,13 +1204,29 @@ export function autosave( state = null, action ) { title, excerpt, content, - preview_link: post.preview_link, }; + } + + return state; +} + +/** + * Reducer returning the poost preview link + * + * @param {string?} state The preview link + * @param {Object} action Dispatched action. + * + * @return {string?} Updated state. + */ +export function previewLink( state = null, action ) { + switch ( action.type ) { + case 'REQUEST_POST_UPDATE_SUCCESS': + return action.post.preview_link || addQueryArgs( action.post.link, { preview: true } ); case 'REQUEST_POST_UPDATE_START': // Invalidate known preview link when autosave starts. - if ( state && action.options.isAutosave ) { - return omit( state, 'preview_link' ); + if ( state && action.options.isPreview ) { + return null; } break; } @@ -1233,6 +1250,7 @@ export default optimist( combineReducers( { reusableBlocks, template, autosave, + previewLink, settings, postSavingLock, } ) ); diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index 9d7855830d3b1d..5cede64dd95ade 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -33,6 +33,7 @@ import { } from '@wordpress/blocks'; import { isInTheFuture, getDate } from '@wordpress/date'; import { removep } from '@wordpress/autop'; +import { addQueryArgs } from '@wordpress/url'; /** * Dependencies @@ -1532,6 +1533,23 @@ export function isPreviewingPost( state ) { return isSavingPost( state ) && !! state.saving.options.isPreview; } +/** + * Returns the post preview link + * + * @param {Object} state Global application state. + * + * @return {string?} Preview Link. + */ +export function getEditedPostPreviewLink( state ) { + const featuredImageId = getEditedPostAttribute( state, 'featured_media' ); + const previewLink = state.previewLink; + if ( previewLink && featuredImageId ) { + return addQueryArgs( previewLink, { _thumbnail_id: featuredImageId } ); + } + + return previewLink; +} + /** * Returns a suggested post format for the current post, inferred only if there * is a single block within the post and it is of a type known to match a diff --git a/packages/editor/src/store/test/reducer.js b/packages/editor/src/store/test/reducer.js index 788e361a6a929e..2ddb631b5e2139 100644 --- a/packages/editor/src/store/test/reducer.js +++ b/packages/editor/src/store/test/reducer.js @@ -2422,7 +2422,6 @@ describe( 'state', () => { raw: 'The Excerpt', }, status: 'draft', - preview_link: 'https://wordpress.org/?p=1&preview=true', }, } ); @@ -2430,7 +2429,6 @@ describe( 'state', () => { title: 'The Title', content: 'The Content', excerpt: 'The Excerpt', - preview_link: 'https://wordpress.org/?p=1&preview=true', } ); } ); } ); diff --git a/test/e2e/specs/preview.test.js b/test/e2e/specs/preview.test.js index f18eaa251e10f5..225db55a901317 100644 --- a/test/e2e/specs/preview.test.js +++ b/test/e2e/specs/preview.test.js @@ -71,7 +71,7 @@ describe( 'Preview', () => { return window.location.search.match( /[\?&]post=(\d+)/ ); } ) ).jsonValue(); - let expectedPreviewURL = getUrl( '', `?p=${ postId }&preview=true` ); + const expectedPreviewURL = getUrl( '', `?p=${ postId }&preview=true` ); expect( previewPage.url() ).toBe( expectedPreviewURL ); // Title in preview should match input. @@ -97,16 +97,6 @@ describe( 'Preview', () => { // 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-publish-panel' ) ), - editorPage.click( '.editor-post-publish-panel__header button' ), - ] ); - expectedPreviewURL = await editorPage.$eval( '.components-notice.is-success a', ( node ) => node.href ); - - await editorPage.bringToFront(); - await waitForPreviewNavigation( previewPage ); - expect( previewPage.url() ).toBe( expectedPreviewURL ); // Return to editor to change title. await editorPage.bringToFront(); From d5cf1d7517b9c655b72a3b75be887ebd127636bd Mon Sep 17 00:00:00 2001 From: Andrew Ozz <azaozz@users.noreply.github.com> Date: Tue, 20 Nov 2018 15:48:49 +0200 Subject: [PATCH 065/254] Restore the help modal in the classic block (#11856) It has some duplicates with the main help modal, but many things are specific for the classic block. Also, can be "tweaked" specificlly for the classic block in the `wordpress` MCE plugin (in core). --- packages/block-library/src/classic/edit.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/block-library/src/classic/edit.js b/packages/block-library/src/classic/edit.js index 97307e91195533..db1467b6301399 100644 --- a/packages/block-library/src/classic/edit.js +++ b/packages/block-library/src/classic/edit.js @@ -79,13 +79,6 @@ export default class ClassicEdit extends Component { this.editor = editor; - // Disable TinyMCE's keyboard shortcut help. - editor.on( 'BeforeExecCommand', ( event ) => { - if ( event.command === 'WP_Help' ) { - event.preventDefault(); - } - } ); - if ( content ) { editor.on( 'loadContent', () => editor.setContent( content ) ); } From 095c65c83d9b5103d8f169e408819976cdfdd26b Mon Sep 17 00:00:00 2001 From: John Watkins <johnwatkins0@gmail.com> Date: Tue, 20 Nov 2018 09:09:49 -0600 Subject: [PATCH 066/254] Remove permalink-based features from nonpublic CPTs (#12046) * Remove permalink features from nonpublic CPTs * Use lodash get to get post type 'viewable' setting --- .../src/components/post-publish-panel/index.js | 11 ++++++++--- packages/editor/src/store/effects/posts.js | 4 ++-- packages/editor/src/store/test/effects.js | 17 +++++++++++++++++ 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/packages/editor/src/components/post-publish-panel/index.js b/packages/editor/src/components/post-publish-panel/index.js index 0b38b0dd78400d..0aef7ae9a3393e 100644 --- a/packages/editor/src/components/post-publish-panel/index.js +++ b/packages/editor/src/components/post-publish-panel/index.js @@ -40,8 +40,8 @@ export class PostPublishPanel extends Component { } onSubmit() { - const { onClose, hasPublishAction } = this.props; - if ( ! hasPublishAction ) { + const { onClose, hasPublishAction, isPostTypeViewable } = this.props; + if ( ! hasPublishAction || ! isPostTypeViewable ) { onClose(); } } @@ -61,7 +61,7 @@ export class PostPublishPanel extends Component { PrePublishExtension, ...additionalProps } = this.props; - const propsForPanel = omit( additionalProps, [ 'hasPublishAction', 'isDirty' ] ); + const propsForPanel = omit( additionalProps, [ 'hasPublishAction', 'isDirty', 'isPostTypeViewable' ] ); const isPublishedOrScheduled = isPublished || ( isScheduled && isBeingScheduled ); const isPrePublish = ! isPublishedOrScheduled && ! isSaving; const isPostPublish = isPublishedOrScheduled && ! isSaving; @@ -112,8 +112,10 @@ export class PostPublishPanel extends Component { export default compose( [ withSelect( ( select ) => { + const { getPostType } = select( 'core' ); const { getCurrentPost, + getEditedPostAttribute, isCurrentPostPublished, isCurrentPostScheduled, isEditedPostBeingScheduled, @@ -121,8 +123,11 @@ export default compose( [ isSavingPost, } = select( 'core/editor' ); const { isPublishSidebarEnabled } = select( 'core/editor' ); + const postType = getPostType( getEditedPostAttribute( 'type' ) ); + return { hasPublishAction: get( getCurrentPost(), [ '_links', 'wp:action-publish' ], false ), + isPostTypeViewable: get( postType, [ 'viewable' ], false ), isBeingScheduled: isEditedPostBeingScheduled(), isDirty: isEditedPostDirty(), isPublished: isCurrentPostPublished(), diff --git a/packages/editor/src/store/effects/posts.js b/packages/editor/src/store/effects/posts.js index fd33234a53a5db..a1a24a1f6d3be6 100644 --- a/packages/editor/src/store/effects/posts.js +++ b/packages/editor/src/store/effects/posts.js @@ -2,7 +2,7 @@ * External dependencies */ import { BEGIN, COMMIT, REVERT } from 'redux-optimist'; -import { pick, includes } from 'lodash'; +import { get, pick, includes } from 'lodash'; /** * WordPress dependencies @@ -182,7 +182,7 @@ export const requestPostUpdateSuccess = ( action ) => { const willPublish = includes( publishStatus, post.status ); let noticeMessage; - let shouldShowLink = true; + let shouldShowLink = get( postType, [ 'viewable' ], false ); if ( ! isPublished && ! willPublish ) { // If saving a non-published post, don't show notice. diff --git a/packages/editor/src/store/test/effects.js b/packages/editor/src/store/test/effects.js index ce54c418d5fa11..48985b9cacfbc5 100644 --- a/packages/editor/src/store/test/effects.js +++ b/packages/editor/src/store/test/effects.js @@ -245,6 +245,7 @@ describe( 'effects', () => { item_reverted_to_draft: 'Post reverted to draft.', item_updated: 'Post updated.', }, + viewable: true, } ); it( 'should dispatch notices when publishing or scheduling a post', () => { @@ -265,6 +266,22 @@ describe( 'effects', () => { ); } ); + it( 'should dispatch notices when publishing or scheduling an unviewable post', () => { + const previousPost = getDraftPost(); + const post = getPublishedPost(); + const postType = { ...getPostType(), viewable: false }; + + handler( { post, previousPost, postType } ); + + expect( dataDispatch( 'core/notices' ).createSuccessNotice ).toHaveBeenCalledWith( + 'Post published.', + { + id: SAVE_POST_NOTICE_ID, + actions: [], + } + ); + } ); + it( 'should dispatch notices when reverting a published post to a draft', () => { const previousPost = getPublishedPost(); const post = getDraftPost(); From f91e66ca2bcd9e1e7b97aa39c01f2888fc751712 Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Tue, 20 Nov 2018 16:10:36 +0100 Subject: [PATCH 067/254] Fix popovers position in RTL languages (#12117) --- packages/components/src/popover/style.scss | 10 ++-------- packages/components/src/popover/utils.js | 7 +++++++ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/components/src/popover/style.scss b/packages/components/src/popover/style.scss index c47cffe2e616f3..6b9ed471028d5d 100644 --- a/packages/components/src/popover/style.scss +++ b/packages/components/src/popover/style.scss @@ -1,5 +1,6 @@ $arrow-size: 8px; +/*!rtl:begin:ignore*/ .components-popover { position: fixed; z-index: z-index(".components-popover"); @@ -75,7 +76,6 @@ $arrow-size: 8px; } &.is-middle.is-left { - /*!rtl:begin:ignore*/ margin-left: -$arrow-size; &::before { @@ -93,11 +93,9 @@ $arrow-size: 8px; border-right: none; border-top-color: transparent; } - /*!rtl:end:ignore*/ } &.is-middle.is-right { - /*!rtl:begin:ignore*/ margin-left: $arrow-size; &::before { @@ -115,7 +113,6 @@ $arrow-size: 8px; border-right-style: solid; border-top-color: transparent; } - /*!rtl:end:ignore*/ } } @@ -165,23 +162,19 @@ $arrow-size: 8px; .components-popover:not(.is-mobile).is-right & { position: absolute; - /*!rtl:ignore*/ left: 100%; } .components-popover:not(.is-mobile):not(.is-middle).is-right & { - /*!rtl:ignore*/ margin-left: -24px; } .components-popover:not(.is-mobile).is-left & { position: absolute; - /*!rtl:ignore*/ right: 100%; } .components-popover:not(.is-mobile):not(.is-middle).is-left & { - /*!rtl:ignore*/ margin-right: -24px; } } @@ -211,3 +204,4 @@ $arrow-size: 8px; .components-popover__close.components-icon-button { z-index: z-index(".components-popover__close"); } +/*!rtl:end:ignore*/ diff --git a/packages/components/src/popover/utils.js b/packages/components/src/popover/utils.js index e7b98c5f5a4df9..292f7ab3276775 100644 --- a/packages/components/src/popover/utils.js +++ b/packages/components/src/popover/utils.js @@ -4,6 +4,7 @@ */ const HEIGHT_OFFSET = 10; // used by the arrow and a bit of empty space const isMobileViewport = () => window.innerWidth < 782; +const isRTL = () => document.documentElement.dir === 'rtl'; /** * Utility used to compute the popover position over the xAxis @@ -18,6 +19,12 @@ const isMobileViewport = () => window.innerWidth < 782; */ export function computePopoverXAxisPosition( anchorRect, contentSize, xAxis, chosenYAxis ) { const { width } = contentSize; + // Correct xAxis for RTL support + if ( xAxis === 'left' && isRTL() ) { + xAxis = 'right'; + } else if ( xAxis === 'right' && isRTL() ) { + xAxis = 'left'; + } // x axis alignment choices const anchorMidPoint = Math.round( anchorRect.left + ( anchorRect.width / 2 ) ); From 1e855248e9dde4a03e637ed9eee6656ed3472851 Mon Sep 17 00:00:00 2001 From: Daniel Bachhuber <daniel@bachhuber.co> Date: Tue, 20 Nov 2018 07:50:21 -0800 Subject: [PATCH 068/254] Update `package-lock.json` from latest `npm install` (#12107) --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5dca87bdac5845..b98a21aac33c39 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11402,7 +11402,7 @@ }, "yargs": { "version": "11.1.0", - "resolved": "http://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz", "integrity": "sha512-NwW69J42EsCSanF8kyn5upxvjp5ds+t3+udGBeTbFnERA+lF541DDpMawzo4z6W/QrzNM18D+BPMiOBibnFV5A==", "dev": true, "requires": { @@ -12713,7 +12713,7 @@ }, "yargs": { "version": "11.1.0", - "resolved": "http://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz", "integrity": "sha512-NwW69J42EsCSanF8kyn5upxvjp5ds+t3+udGBeTbFnERA+lF541DDpMawzo4z6W/QrzNM18D+BPMiOBibnFV5A==", "dev": true, "requires": { From d387ac55cbdf5dc6bbe2daafe897a7a96bfb0eb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20Van=C2=A0Dorpe?= <iseulde@automattic.com> Date: Tue, 20 Nov 2018 16:54:03 +0100 Subject: [PATCH 069/254] Convert to blocks: handle invalid lists and HTML without block equivalent * convert unhandled to html * restore correcting invalid list when converting blocks * Add e2e tests --- packages/blocks/src/api/raw-handling/index.js | 15 +++++++++------ .../blocks-raw-handling.spec.js.snap | 15 ++++++++++++++- .../integration/fixtures/wordpress-convert.html | 17 +++++++++++++++++ 3 files changed, 40 insertions(+), 7 deletions(-) diff --git a/packages/blocks/src/api/raw-handling/index.js b/packages/blocks/src/api/raw-handling/index.js index db51f13e0fc6cf..2de9bff8e20595 100644 --- a/packages/blocks/src/api/raw-handling/index.js +++ b/packages/blocks/src/api/raw-handling/index.js @@ -84,13 +84,14 @@ function htmlToBlocks( { html, rawTransforms } ) { 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 createBlock( + // Should not be hardcoded. + 'core/html', + getBlockAttributes( + 'core/html', + node.outerHTML + ) ); - - return; } const { transform, blockName } = rawTransform; @@ -269,6 +270,8 @@ export function rawHandler( { HTML = '' } ) { // from raw HTML. These filters move around some content or add // additional tags, they do not remove any content. const filters = [ + // Needed to adjust invalid lists. + listReducer, // Needed to create more and nextpage blocks. specialCommentConverter, // Needed to create media blocks. diff --git a/test/integration/__snapshots__/blocks-raw-handling.spec.js.snap b/test/integration/__snapshots__/blocks-raw-handling.spec.js.snap index f92c383e637686..1fbb36f44d9abe 100644 --- a/test/integration/__snapshots__/blocks-raw-handling.spec.js.snap +++ b/test/integration/__snapshots__/blocks-raw-handling.spec.js.snap @@ -31,5 +31,18 @@ exports[`Blocks raw handling rawHandler should convert HTML post to blocks with <!-- wp:gallery {\\"ids\\":[1],\\"columns\\":3,\\"linkTo\\":\\"attachment\\"} --> <ul class=\\"wp-block-gallery columns-3 is-cropped\\"><li class=\\"blocks-gallery-item\\"><figure><img data-id=\\"1\\" class=\\"wp-image-1\\"/></figure></li></ul> -<!-- /wp:gallery -->" +<!-- /wp:gallery --> + +<!-- wp:html --> +<dl> + <dt>Term</dt> + <dd> + Description. + </dd> +</dl> +<!-- /wp:html --> + +<!-- wp:list {\\"ordered\\":true} --> +<ol><li>Item</li></ol> +<!-- /wp:list -->" `; diff --git a/test/integration/fixtures/wordpress-convert.html b/test/integration/fixtures/wordpress-convert.html index 1cb01568c37059..0a8a20c6e19b12 100644 --- a/test/integration/fixtures/wordpress-convert.html +++ b/test/integration/fixtures/wordpress-convert.html @@ -6,3 +6,20 @@ <h3>More tag</h3> <p><!--more--></p> <h3>Shortcode</h3> <p>[gallery ids="1"]</p> + +<!-- HTML tags without block equivalent --> + +<dl> + <dt>Term</dt> + <dd> + Description. + </dd> +</dl> + +<!-- Invalid list --> + +<ol> + <ol> + <li>Item</li> + </ol> +</ol> From a61be201a6a2c55c101b46c15cde40bc7fea0015 Mon Sep 17 00:00:00 2001 From: George Hotelling <george@hotelling.net> Date: Tue, 20 Nov 2018 11:05:50 -0500 Subject: [PATCH 070/254] Localize DateTimePicker Component (#11938) * Add localizations to global moment * Add day-of-week support * Add date formats to moment * Code Sniffer --- lib/client-assets.php | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/lib/client-assets.php b/lib/client-assets.php index dd2edaf8bc1e85..620fe7c3498524 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -250,6 +250,33 @@ function gutenberg_register_scripts_and_styles() { ), 'after' ); + wp_add_inline_script( + 'moment', + sprintf( + "moment.locale( '%s', %s );", + get_user_locale(), + wp_json_encode( + array( + 'months' => array_values( $wp_locale->month ), + 'monthsShort' => array_values( $wp_locale->month_abbrev ), + 'weekdays' => array_values( $wp_locale->weekday ), + 'weekdaysShort' => array_values( $wp_locale->weekday_abbrev ), + 'week' => array( + 'dow' => (int) get_option( 'start_of_week', 0 ), + ), + 'longDateFormat' => array( + 'LT' => get_option( 'time_format', __( 'g:i a', 'default' ) ), + 'LTS' => null, + 'L' => null, + 'LL' => get_option( 'date_format', __( 'F j, Y', 'default' ) ), + 'LLL' => __( 'F j, Y g:i a', 'default' ), + 'LLLL' => null, + ), + ) + ) + ), + 'after' + ); // Loading the old editor and its config to ensure the classic block works as expected. wp_add_inline_script( 'editor', From 18d0fbc3e6c8c635e2dcb6fa84a954f0579c5b06 Mon Sep 17 00:00:00 2001 From: Andrew Ozz <azaozz@users.noreply.github.com> Date: Tue, 20 Nov 2018 19:00:10 +0200 Subject: [PATCH 071/254] Fix the TinyMCE init array (#10968) * Fix the TinyMCE init array - Added "manual" convert from php to js, same as in class-wp-editor.php. - Reformated the code so it's more readable. * Fix missing space to fix failing tests... * Fix phpcs --- lib/client-assets.php | 196 ++++++++++++++++++++++-------------------- 1 file changed, 102 insertions(+), 94 deletions(-) diff --git a/lib/client-assets.php b/lib/client-assets.php index 620fe7c3498524..fd1553c3cfd87b 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -283,102 +283,110 @@ function gutenberg_register_scripts_and_styles() { 'window.wp.oldEditor = window.wp.editor;', 'after' ); - $tinymce_settings = apply_filters( - 'tiny_mce_before_init', - array( - 'plugins' => implode( - ',', - array_unique( - apply_filters( - 'tiny_mce_plugins', - array( - 'charmap', - 'colorpicker', - 'hr', - 'lists', - 'media', - 'paste', - 'tabfocus', - 'textcolor', - 'fullscreen', - 'wordpress', - 'wpautoresize', - 'wpeditimage', - 'wpemoji', - 'wpgallery', - 'wplink', - 'wpdialogs', - 'wptextpattern', - 'wpview', - ) - ) - ) - ), - 'toolbar1' => implode( - ',', - apply_filters( - 'mce_buttons', - array( - 'formatselect', - 'bold', - 'italic', - 'bullist', - 'numlist', - 'blockquote', - 'alignleft', - 'aligncenter', - 'alignright', - 'link', - 'unlink', - 'wp_more', - 'spellchecker', - 'wp_add_media', - 'kitchensink', - ), - 'editor' - ) - ), - 'toolbar2' => implode( - ',', - apply_filters( - 'mce_buttons_2', - array( - 'strikethrough', - 'hr', - 'forecolor', - 'pastetext', - 'removeformat', - 'charmap', - 'outdent', - 'indent', - 'undo', - 'redo', - 'wp_help', - ), - 'editor' - ) - ), - 'toolbar3' => implode( ',', apply_filters( 'mce_buttons_3', array(), 'editor' ) ), - 'toolbar4' => implode( ',', apply_filters( 'mce_buttons_4', array(), 'editor' ) ), - 'external_plugins' => apply_filters( 'mce_external_plugins', array() ), - ), - 'editor' + + $tinymce_plugins = array( + 'charmap', + 'colorpicker', + 'hr', + 'lists', + 'media', + 'paste', + 'tabfocus', + 'textcolor', + 'fullscreen', + 'wordpress', + 'wpautoresize', + 'wpeditimage', + 'wpemoji', + 'wpgallery', + 'wplink', + 'wpdialogs', + 'wptextpattern', + 'wpview', ); - if ( isset( $tinymce_settings['style_formats'] ) && is_string( $tinymce_settings['style_formats'] ) ) { - // Decode the options as we used to recommende json_encoding the TinyMCE settings. - $tinymce_settings['style_formats'] = json_decode( $tinymce_settings['style_formats'] ); - } - wp_localize_script( - 'wp-block-library', - 'wpEditorL10n', - array( - 'tinymce' => array( - 'baseURL' => includes_url( 'js/tinymce' ), - 'suffix' => SCRIPT_DEBUG ? '' : '.min', - 'settings' => $tinymce_settings, - ), - ) + $tinymce_plugins = apply_filters( 'tiny_mce_plugins', $tinymce_plugins, 'classic-block' ); + $tinymce_plugins = array_unique( $tinymce_plugins ); + + $toolbar1 = array( + 'formatselect', + 'bold', + 'italic', + 'bullist', + 'numlist', + 'blockquote', + 'alignleft', + 'aligncenter', + 'alignright', + 'link', + 'unlink', + 'wp_more', + 'spellchecker', + 'wp_add_media', + 'kitchensink', + ); + $toolbar1 = apply_filters( 'mce_buttons', $toolbar1, 'classic-block' ); + + $toolbar2 = array( + 'strikethrough', + 'hr', + 'forecolor', + 'pastetext', + 'removeformat', + 'charmap', + 'outdent', + 'indent', + 'undo', + 'redo', + 'wp_help', + ); + $toolbar2 = apply_filters( 'mce_buttons_2', $toolbar2, 'classic-block' ); + + $toolbar3 = apply_filters( 'mce_buttons_3', array(), 'classic-block' ); + $toolbar4 = apply_filters( 'mce_buttons_4', array(), 'classic-block' ); + + $external_plugins = apply_filters( 'mce_external_plugins', array(), 'classic-block' ); + + $tinymce_settings = array( + 'plugins' => implode( ',', $tinymce_plugins ), + 'toolbar1' => implode( ',', $toolbar1 ), + 'toolbar2' => implode( ',', $toolbar2 ), + 'toolbar3' => implode( ',', $toolbar3 ), + 'toolbar4' => implode( ',', $toolbar4 ), + 'external_plugins' => wp_json_encode( $external_plugins ), + 'classic_block_editor' => true, ); + $tinymce_settings = apply_filters( 'tiny_mce_before_init', $tinymce_settings, 'classic-block' ); + + // Do "by hand" translation from PHP array to js object. + // Prevents breakage in some custom settings. + $init_obj = ''; + foreach ( $tinymce_settings as $key => $value ) { + if ( is_bool( $value ) ) { + $val = $value ? 'true' : 'false'; + $init_obj .= $key . ':' . $val . ','; + continue; + } elseif ( ! empty( $value ) && is_string( $value ) && ( + ( '{' == $value{0} && '}' == $value{strlen( $value ) - 1} ) || + ( '[' == $value{0} && ']' == $value{strlen( $value ) - 1} ) || + preg_match( '/^\(?function ?\(/', $value ) ) ) { + + $init_obj .= $key . ':' . $value . ','; + continue; + } + $init_obj .= $key . ':"' . $value . '",'; + } + + $init_obj = '{' . trim( $init_obj, ' ,' ) . '}'; + + $script = 'window.wpEditorL10n = { + tinymce: { + baseURL: ' . wp_json_encode( includes_url( 'js/tinymce' ) ) . ', + suffix: ' . ( SCRIPT_DEBUG ? '""' : '".min"' ) . ', + settings: ' . $init_obj . ', + } + }'; + + wp_add_inline_script( 'wp-block-library', $script, 'before' ); // Editor Styles. // This empty stylesheet is defined to ensure backwards compatibility. From 3f74cd18e8f1e2944758994877361c66ea78766c Mon Sep 17 00:00:00 2001 From: Joen Asmussen <joen@automattic.com> Date: Tue, 20 Nov 2018 18:02:34 +0100 Subject: [PATCH 072/254] Fix issue with disabled togglecontrol double border (#12091) * Fix issue with disabled togglecontrol double border Fixes #12049. This PR fixes an issue where a togglecontrol, wrapped in a Disabled component, would show a double border. The issue was that it inherited a border from the parent checkbox style (because the toggle is technically a reskinned checkbox). * Address feedback. - Add a better disabled state that works for both checked and unchecked states. - Move code to the right component. - Fix some additional bleed issues. --- .../components/src/form-toggle/style.scss | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/packages/components/src/form-toggle/style.scss b/packages/components/src/form-toggle/style.scss index fe66e3455adf0b..0def01e03beaa6 100644 --- a/packages/components/src/form-toggle/style.scss +++ b/packages/components/src/form-toggle/style.scss @@ -63,7 +63,7 @@ $toggle-border-width: 2px; } } - // checked state + // Checked state. &.is-checked .components-form-toggle__track { background-color: theme(toggle); border: $toggle-border-width solid theme(toggle); @@ -86,9 +86,15 @@ $toggle-border-width: 2px; border: $toggle-border-width solid theme(toggle); } } + + // Disabled state: + .components-disabled & { + opacity: 0.3; + } } -.components-form-toggle__input[type="checkbox"] { +// This needs specificity to override inherited checkbox styles. +.components-form-toggle input.components-form-toggle__input[type="checkbox"] { position: absolute; top: 0; left: 0; @@ -98,6 +104,17 @@ $toggle-border-width: 2px; margin: 0; padding: 0; z-index: z-index(".components-form-toggle__input"); + + // This overrides a border style that is inherited from parent checkbox styles. + border: none; + &:checked { + background: none; + } + + // Don't show custom checkbox checkmark. + &::before { + content: ""; + } } // Ensure on indicator works in normal and Windows high contrast mode both. From ff299e6257ac0d7c769a1c1564093e578d113fd2 Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Tue, 20 Nov 2018 18:13:18 +0100 Subject: [PATCH 073/254] Replace gutenberg domain with default for Core blocks (#12108) --- packages/block-library/src/archives/index.php | 14 +++++++------- packages/block-library/src/categories/index.php | 2 +- .../block-library/src/latest-comments/index.php | 6 +++--- packages/block-library/src/latest-posts/index.php | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/block-library/src/archives/index.php b/packages/block-library/src/archives/index.php index 97c8849ffc8188..85186ce6123a3f 100644 --- a/packages/block-library/src/archives/index.php +++ b/packages/block-library/src/archives/index.php @@ -32,7 +32,7 @@ function render_block_core_archives( $attributes ) { $class .= ' wp-block-archives-dropdown'; $dropdown_id = esc_attr( uniqid( 'wp-block-archives-' ) ); - $title = __( 'Archives', 'gutenberg' ); + $title = __( 'Archives', 'default' ); /** This filter is documented in wp-includes/widgets/class-wp-widget-archives.php */ $dropdown_args = apply_filters( @@ -50,19 +50,19 @@ function render_block_core_archives( $attributes ) { switch ( $dropdown_args['type'] ) { case 'yearly': - $label = __( 'Select Year', 'gutenberg' ); + $label = __( 'Select Year', 'default' ); break; case 'monthly': - $label = __( 'Select Month', 'gutenberg' ); + $label = __( 'Select Month', 'default' ); break; case 'daily': - $label = __( 'Select Day', 'gutenberg' ); + $label = __( 'Select Day', 'default' ); break; case 'weekly': - $label = __( 'Select Week', 'gutenberg' ); + $label = __( 'Select Week', 'default' ); break; default: - $label = __( 'Select Post', 'gutenberg' ); + $label = __( 'Select Post', 'default' ); break; } @@ -101,7 +101,7 @@ function render_block_core_archives( $attributes ) { $block_content = sprintf( '<div class="%1$s">%2$s</div>', $classnames, - __( 'No archives to show.', 'gutenberg' ) + __( 'No archives to show.', 'default' ) ); } else { diff --git a/packages/block-library/src/categories/index.php b/packages/block-library/src/categories/index.php index 83bf1c9db26397..478d4579207eac 100644 --- a/packages/block-library/src/categories/index.php +++ b/packages/block-library/src/categories/index.php @@ -27,7 +27,7 @@ function render_block_core_categories( $attributes ) { if ( ! empty( $attributes['displayAsDropdown'] ) ) { $id = 'wp-block-categories-' . $block_id; $args['id'] = $id; - $args['show_option_none'] = __( 'Select Category', 'gutenberg' ); + $args['show_option_none'] = __( 'Select Category', 'default' ); $wrapper_markup = '<div class="%1$s">%2$s</div>'; $items_markup = wp_dropdown_categories( $args ); $type = 'dropdown'; diff --git a/packages/block-library/src/latest-comments/index.php b/packages/block-library/src/latest-comments/index.php index 199a426a93ef64..927157eb43481d 100644 --- a/packages/block-library/src/latest-comments/index.php +++ b/packages/block-library/src/latest-comments/index.php @@ -29,7 +29,7 @@ function gutenberg_draft_or_post_title( $post = 0 ) { $title = get_the_title( $post ); if ( empty( $title ) ) { - $title = __( '(no title)', 'gutenberg' ); + $title = __( '(no title)', 'default' ); } return esc_html( $title ); } @@ -98,7 +98,7 @@ function gutenberg_render_block_core_latest_comments( $attributes = array() ) { $list_items_markup .= sprintf( /* translators: 1: author name (inside <a> or <span> tag, based on if they have a URL), 2: post title related to this comment */ - __( '%1$s on %2$s', 'gutenberg' ), + __( '%1$s on %2$s', 'default' ), $author_markup, $post_title ); @@ -143,7 +143,7 @@ function gutenberg_render_block_core_latest_comments( $attributes = array() ) { ) : sprintf( '<div class="%1$s">%2$s</div>', $classnames, - __( 'No comments to show.', 'gutenberg' ) + __( 'No comments to show.', 'default' ) ); return $block_content; diff --git a/packages/block-library/src/latest-posts/index.php b/packages/block-library/src/latest-posts/index.php index b395d8a2bacfcc..850192a5492c3e 100644 --- a/packages/block-library/src/latest-posts/index.php +++ b/packages/block-library/src/latest-posts/index.php @@ -30,7 +30,7 @@ function render_block_core_latest_posts( $attributes ) { $title = get_the_title( $post_id ); if ( ! $title ) { - $title = __( '(Untitled)', 'gutenberg' ); + $title = __( '(Untitled)', 'default' ); } $list_items_markup .= sprintf( '<li><a href="%1$s">%2$s</a>', From c97cec8618a8654e12904563ff2ded0f58f34c1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20Van=C2=A0Dorpe?= <iseulde@automattic.com> Date: Tue, 20 Nov 2018 18:13:41 +0100 Subject: [PATCH 074/254] preserve quote content (#12122) * preserve quote content * Add comment --- packages/block-library/src/quote/index.js | 7 ++++++- .../__snapshots__/blocks-raw-handling.spec.js.snap | 10 +++++++++- test/integration/fixtures/wordpress-convert.html | 13 +++++++++++++ 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/packages/block-library/src/quote/index.js b/packages/block-library/src/quote/index.js index 78e53875b771db..d3f020a220c3db 100644 --- a/packages/block-library/src/quote/index.js +++ b/packages/block-library/src/quote/index.js @@ -100,7 +100,12 @@ export const settings = { }, { type: 'raw', - selector: 'blockquote', + isMatch: ( node ) => ( + node.nodeName === 'BLOCKQUOTE' && + // The quote block can only handle multiline paragraph + // content. + Array.from( node.childNodes ).every( ( child ) => child.nodeName === 'P' ) + ), schema: { blockquote: { children: { diff --git a/test/integration/__snapshots__/blocks-raw-handling.spec.js.snap b/test/integration/__snapshots__/blocks-raw-handling.spec.js.snap index 1fbb36f44d9abe..320b201a836e3c 100644 --- a/test/integration/__snapshots__/blocks-raw-handling.spec.js.snap +++ b/test/integration/__snapshots__/blocks-raw-handling.spec.js.snap @@ -44,5 +44,13 @@ exports[`Blocks raw handling rawHandler should convert HTML post to blocks with <!-- wp:list {\\"ordered\\":true} --> <ol><li>Item</li></ol> -<!-- /wp:list -->" +<!-- /wp:list --> + +<!-- wp:quote --> +<blockquote class=\\"wp-block-quote\\"><p>Text.</p></blockquote> +<!-- /wp:quote --> + +<!-- wp:html --> +<blockquote><h1>Heading</h1><p>Text.</p></blockquote> +<!-- /wp:html -->" `; diff --git a/test/integration/fixtures/wordpress-convert.html b/test/integration/fixtures/wordpress-convert.html index 0a8a20c6e19b12..0fe1de21dc6621 100644 --- a/test/integration/fixtures/wordpress-convert.html +++ b/test/integration/fixtures/wordpress-convert.html @@ -23,3 +23,16 @@ <h3>Shortcode</h3> <li>Item</li> </ol> </ol> + +<!-- Quote with paragraphs --> + +<blockquote> + <p>Text.</p> +</blockquote> + +<!-- Quote with more than paragraphs --> + +<blockquote> + <h1>Heading</h1> + <p>Text.</p> +</blockquote> From 943790c1d09f7d652c58cca8dbd3585465c907e6 Mon Sep 17 00:00:00 2001 From: Matias Ventura <mv@matiasventura.com> Date: Tue, 20 Nov 2018 14:18:17 -0300 Subject: [PATCH 075/254] Remove "permalink settings" link from permalink panel. (#12121) --- .../edit-post/src/components/sidebar/post-link/index.js | 8 -------- .../edit-post/src/components/sidebar/post-link/style.scss | 5 ----- 2 files changed, 13 deletions(-) diff --git a/packages/edit-post/src/components/sidebar/post-link/index.js b/packages/edit-post/src/components/sidebar/post-link/index.js index 9073402804dc9d..64b694a21ab8b5 100644 --- a/packages/edit-post/src/components/sidebar/post-link/index.js +++ b/packages/edit-post/src/components/sidebar/post-link/index.js @@ -11,7 +11,6 @@ import { __ } from '@wordpress/i18n'; import { PanelBody, TextControl, ExternalLink } from '@wordpress/components'; import { withSelect, withDispatch } from '@wordpress/data'; import { compose, ifCondition, withState } from '@wordpress/compose'; -import { addQueryArgs } from '@wordpress/url'; import { cleanForSlug } from '@wordpress/editor'; /** @@ -102,13 +101,6 @@ function PostLink( { postLink } </ExternalLink> - <ExternalLink - className="edit-post-post-link__permalink-settings" - href={ addQueryArgs( 'options-permalink.php' ) } - target="_blank" - > - { __( 'Permalink Settings' ) } - </ExternalLink> </PanelBody> ); } diff --git a/packages/edit-post/src/components/sidebar/post-link/style.scss b/packages/edit-post/src/components/sidebar/post-link/style.scss index d32fba6567c2c9..a20084e63ab241 100644 --- a/packages/edit-post/src/components/sidebar/post-link/style.scss +++ b/packages/edit-post/src/components/sidebar/post-link/style.scss @@ -9,8 +9,3 @@ .edit-post-post-link__link { word-wrap: break-word; } - -.edit-post-post-link__permalink-settings { - margin-top: 1em; - display: block; -} From 5a8c012de43780f7187eecf7ac796e6f57a04c4f Mon Sep 17 00:00:00 2001 From: Andrew Duthie <andrew@andrewduthie.com> Date: Tue, 20 Nov 2018 14:12:09 -0500 Subject: [PATCH 076/254] Framework: Regenerate fixtures (#12124) --- .../fixtures/core__audio.parsed.json | 8 ++++++-- .../fixtures/core__block.parsed.json | 4 +++- .../fixtures/core__button__center.parsed.json | 8 ++++++-- .../fixtures/core__categories.parsed.json | 4 +++- .../fixtures/core__code.parsed.json | 8 ++++++-- .../fixtures/core__column.parsed.json | 20 +++++++++++++++---- .../core__image__custom-link-class.json | 4 ++-- ...core__image__custom-link-class.parsed.json | 20 +++++++++---------- .../core__image__custom-link-rel.json | 4 ++-- .../core__image__custom-link-rel.parsed.json | 20 +++++++++---------- .../fixtures/core__missing.parsed.json | 8 ++++++-- .../fixtures/core__more.parsed.json | 8 ++++++-- ...core__more__custom-text-teaser.parsed.json | 8 ++++++-- .../fixtures/core__nextpage.parsed.json | 8 ++++++-- .../core__paragraph__align-right.parsed.json | 8 ++++++-- .../core__paragraph__deprecated.parsed.json | 8 ++++++-- .../fixtures/core__preformatted.parsed.json | 8 ++++++-- .../fixtures/core__pullquote.parsed.json | 8 ++++++-- ...re__pullquote__multi-paragraph.parsed.json | 8 ++++++-- .../fixtures/core__quote__style-1.parsed.json | 8 ++++++-- .../fixtures/core__quote__style-2.parsed.json | 8 ++++++-- .../fixtures/core__separator.parsed.json | 8 ++++++-- .../fixtures/core__shortcode.parsed.json | 8 ++++++-- .../fixtures/core__spacer.parsed.json | 8 ++++++-- .../fixtures/core__subhead.parsed.json | 8 ++++++-- .../fixtures/core__table.parsed.json | 8 ++++++-- .../fixtures/core__text-columns.parsed.json | 8 ++++++-- ...e__text__converts-to-paragraph.parsed.json | 8 ++++++-- .../fixtures/core__verse.parsed.json | 8 ++++++-- .../fixtures/core__video.parsed.json | 8 ++++++-- 30 files changed, 184 insertions(+), 76 deletions(-) diff --git a/test/integration/full-content/fixtures/core__audio.parsed.json b/test/integration/full-content/fixtures/core__audio.parsed.json index 6b0acbd0c4a1fb..8024c293a5498f 100644 --- a/test/integration/full-content/fixtures/core__audio.parsed.json +++ b/test/integration/full-content/fixtures/core__audio.parsed.json @@ -6,13 +6,17 @@ }, "innerBlocks": [], "innerHTML": "\n<figure class=\"wp-block-audio alignright\">\n <audio controls=\"\" src=\"https://media.simplecast.com/episodes/audio/80564/draft-podcast-51-livePublish2.mp3\"></audio>\n</figure>\n", - "innerContent": [ "\n<figure class=\"wp-block-audio alignright\">\n <audio controls=\"\" src=\"https://media.simplecast.com/episodes/audio/80564/draft-podcast-51-livePublish2.mp3\"></audio>\n</figure>\n" ] + "innerContent": [ + "\n<figure class=\"wp-block-audio alignright\">\n <audio controls=\"\" src=\"https://media.simplecast.com/episodes/audio/80564/draft-podcast-51-livePublish2.mp3\"></audio>\n</figure>\n" + ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], "innerHTML": "\n", - "innerContent": [ "\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 33c4d86f6c6c7c..4dda73b389eeb1 100644 --- a/test/integration/full-content/fixtures/core__block.parsed.json +++ b/test/integration/full-content/fixtures/core__block.parsed.json @@ -13,6 +13,8 @@ "attrs": {}, "innerBlocks": [], "innerHTML": "\n", - "innerContent": [ "\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 352767e1c1a8f3..45ed5130cbf15e 100644 --- a/test/integration/full-content/fixtures/core__button__center.parsed.json +++ b/test/integration/full-content/fixtures/core__button__center.parsed.json @@ -6,13 +6,17 @@ }, "innerBlocks": [], "innerHTML": "\n<div class=\"wp-block-button aligncenter\"><a class=\"wp-block-button__link\" href=\"https://github.com/WordPress/gutenberg\">Help build Gutenberg</a></div>\n", - "innerContent": [ "\n<div class=\"wp-block-button aligncenter\"><a class=\"wp-block-button__link\" href=\"https://github.com/WordPress/gutenberg\">Help build Gutenberg</a></div>\n" ] + "innerContent": [ + "\n<div class=\"wp-block-button aligncenter\"><a class=\"wp-block-button__link\" href=\"https://github.com/WordPress/gutenberg\">Help build Gutenberg</a></div>\n" + ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], "innerHTML": "\n", - "innerContent": [ "\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 60d03d7bc50625..102791b85219e0 100644 --- a/test/integration/full-content/fixtures/core__categories.parsed.json +++ b/test/integration/full-content/fixtures/core__categories.parsed.json @@ -15,6 +15,8 @@ "attrs": {}, "innerBlocks": [], "innerHTML": "\n", - "innerContent": [ "\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 d9bf0a215e82b8..4aa3fbe8c40d47 100644 --- a/test/integration/full-content/fixtures/core__code.parsed.json +++ b/test/integration/full-content/fixtures/core__code.parsed.json @@ -4,13 +4,17 @@ "attrs": {}, "innerBlocks": [], "innerHTML": "\n<pre class=\"wp-block-code\"><code>export default function MyButton() {\n\treturn &lt;Button&gt;Click Me!&lt;/Button&gt;;\n}</code></pre>\n", - "innerContent": [ "\n<pre class=\"wp-block-code\"><code>export default function MyButton() {\n\treturn &lt;Button&gt;Click Me!&lt;/Button&gt;;\n}</code></pre>\n" ] + "innerContent": [ + "\n<pre class=\"wp-block-code\"><code>export default function MyButton() {\n\treturn &lt;Button&gt;Click Me!&lt;/Button&gt;;\n}</code></pre>\n" + ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], "innerHTML": "\n", - "innerContent": [ "\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 10f1e1a07cf05b..91b566bf7e5c42 100644 --- a/test/integration/full-content/fixtures/core__column.parsed.json +++ b/test/integration/full-content/fixtures/core__column.parsed.json @@ -8,24 +8,36 @@ "attrs": {}, "innerBlocks": [], "innerHTML": "\n\t<p>Column One, Paragraph One</p>\n\t", - "innerContent": [ "\n\t<p>Column One, Paragraph One</p>\n\t" ] + "innerContent": [ + "\n\t<p>Column One, Paragraph One</p>\n\t" + ] }, { "blockName": "core/paragraph", "attrs": {}, "innerBlocks": [], "innerHTML": "\n\t<p>Column One, Paragraph Two</p>\n\t", - "innerContent": [ "\n\t<p>Column One, Paragraph Two</p>\n\t" ] + "innerContent": [ + "\n\t<p>Column One, Paragraph Two</p>\n\t" + ] } ], "innerHTML": "\n<div class=\"wp-block-column\">\n\t\n\t\n</div>\n", - "innerContent": [ "\n<div class=\"wp-block-column\">\n\t", null, "\n\t", null, "\n</div>\n" ] + "innerContent": [ + "\n<div class=\"wp-block-column\">\n\t", + null, + "\n\t", + null, + "\n</div>\n" + ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], "innerHTML": "\n", - "innerContent": [ "\n" ] + "innerContent": [ + "\n" + ] } ] diff --git a/test/integration/full-content/fixtures/core__image__custom-link-class.json b/test/integration/full-content/fixtures/core__image__custom-link-class.json index 7009c3f2dede0c..d47fe4162c9c62 100644 --- a/test/integration/full-content/fixtures/core__image__custom-link-class.json +++ b/test/integration/full-content/fixtures/core__image__custom-link-class.json @@ -8,8 +8,8 @@ "alt": "", "caption": "", "href": "https://wordpress.org/", - "linkDestination": "custom", - "linkClass": "custom-link" + "linkClass": "custom-link", + "linkDestination": "custom" }, "innerBlocks": [], "originalContent": "<figure class=\"wp-block-image\"><a class=\"custom-link\" href=\"https://wordpress.org/\"><img src=\"https://cldup.com/uuUqE_dXzy.jpg\" alt=\"\" /></a></figure>" diff --git a/test/integration/full-content/fixtures/core__image__custom-link-class.parsed.json b/test/integration/full-content/fixtures/core__image__custom-link-class.parsed.json index 73638304d13812..c69b53dcc08aa9 100644 --- a/test/integration/full-content/fixtures/core__image__custom-link-class.parsed.json +++ b/test/integration/full-content/fixtures/core__image__custom-link-class.parsed.json @@ -6,17 +6,17 @@ }, "innerBlocks": [], "innerHTML": "\n<figure class=\"wp-block-image\"><a class=\"custom-link\" href=\"https://wordpress.org/\"><img src=\"https://cldup.com/uuUqE_dXzy.jpg\" alt=\"\" /></a></figure>\n", - "innerContent": [ - "\n<figure class=\"wp-block-image\"><a class=\"custom-link\" href=\"https://wordpress.org/\"><img src=\"https://cldup.com/uuUqE_dXzy.jpg\" alt=\"\" /></a></figure>\n" - ] - }, + "innerContent": [ + "\n<figure class=\"wp-block-image\"><a class=\"custom-link\" href=\"https://wordpress.org/\"><img src=\"https://cldup.com/uuUqE_dXzy.jpg\" alt=\"\" /></a></figure>\n" + ] + }, { - "attrs": {}, - "blockName": null, - "innerBlocks": [], - "innerHTML": "\n", - "innerContent": [ + "blockName": null, + "attrs": {}, + "innerBlocks": [], + "innerHTML": "\n", + "innerContent": [ "\n" - ] + ] } ] diff --git a/test/integration/full-content/fixtures/core__image__custom-link-rel.json b/test/integration/full-content/fixtures/core__image__custom-link-rel.json index 3826afc9861b8e..6000da69608e62 100644 --- a/test/integration/full-content/fixtures/core__image__custom-link-rel.json +++ b/test/integration/full-content/fixtures/core__image__custom-link-rel.json @@ -8,8 +8,8 @@ "alt": "", "caption": "", "href": "https://wordpress.org/", - "linkDestination": "custom", - "rel": "external" + "rel": "external", + "linkDestination": "custom" }, "innerBlocks": [], "originalContent": "<figure class=\"wp-block-image\"><a href=\"https://wordpress.org/\" rel=\"external\"><img src=\"https://cldup.com/uuUqE_dXzy.jpg\" alt=\"\" /></a></figure>" diff --git a/test/integration/full-content/fixtures/core__image__custom-link-rel.parsed.json b/test/integration/full-content/fixtures/core__image__custom-link-rel.parsed.json index a1c7d028f72c9d..91649db09a595f 100644 --- a/test/integration/full-content/fixtures/core__image__custom-link-rel.parsed.json +++ b/test/integration/full-content/fixtures/core__image__custom-link-rel.parsed.json @@ -5,18 +5,18 @@ "linkDestination": "custom" }, "innerBlocks": [], - "innerHTML": "\n<figure class=\"wp-block-image\"><a href=\"https://wordpress.org/\" rel=\"external\"><img src=\"https://cldup.com/uuUqE_dXzy.jpg\" alt=\"\" /></a></figure>\n", - "innerContent": [ - "\n<figure class=\"wp-block-image\"><a href=\"https://wordpress.org/\" rel=\"external\"><img src=\"https://cldup.com/uuUqE_dXzy.jpg\" alt=\"\" /></a></figure>\n" - ] - }, + "innerHTML": "\n<figure class=\"wp-block-image\"><a href=\"https://wordpress.org/\" rel=\"external\"><img src=\"https://cldup.com/uuUqE_dXzy.jpg\" alt=\"\" /></a></figure>\n", + "innerContent": [ + "\n<figure class=\"wp-block-image\"><a href=\"https://wordpress.org/\" rel=\"external\"><img src=\"https://cldup.com/uuUqE_dXzy.jpg\" alt=\"\" /></a></figure>\n" + ] + }, { - "attrs": {}, - "blockName": null, - "innerBlocks": [], + "blockName": null, + "attrs": {}, + "innerBlocks": [], "innerHTML": "\n", - "innerContent": [ + "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 85ab1542c72a3b..59d7fb99eb59d4 100644 --- a/test/integration/full-content/fixtures/core__missing.parsed.json +++ b/test/integration/full-content/fixtures/core__missing.parsed.json @@ -7,13 +7,17 @@ }, "innerBlocks": [], "innerHTML": "\n<p>Testing missing block with some</p>\n<div class=\"wp-some-class\">\n\tHTML <span style=\"color: red;\">content</span>\n</div>\n", - "innerContent": [ "\n<p>Testing missing block with some</p>\n<div class=\"wp-some-class\">\n\tHTML <span style=\"color: red;\">content</span>\n</div>\n" ] + "innerContent": [ + "\n<p>Testing missing block with some</p>\n<div class=\"wp-some-class\">\n\tHTML <span style=\"color: red;\">content</span>\n</div>\n" + ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], "innerHTML": "\n", - "innerContent": [ "\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 b806b0c0af8dba..624431c0413032 100644 --- a/test/integration/full-content/fixtures/core__more.parsed.json +++ b/test/integration/full-content/fixtures/core__more.parsed.json @@ -4,13 +4,17 @@ "attrs": {}, "innerBlocks": [], "innerHTML": "\n<!--more-->\n", - "innerContent": [ "\n<!--more-->\n" ] + "innerContent": [ + "\n<!--more-->\n" + ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], "innerHTML": "\n", - "innerContent": [ "\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 e3096b9d7cdbb2..c58996ee1ef1c3 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 @@ -7,13 +7,17 @@ }, "innerBlocks": [], "innerHTML": "\n<!--more Continue Reading-->\n<!--noteaser-->\n", - "innerContent": [ "\n<!--more Continue Reading-->\n<!--noteaser-->\n" ] + "innerContent": [ + "\n<!--more Continue Reading-->\n<!--noteaser-->\n" + ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], "innerHTML": "\n", - "innerContent": [ "\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 ff3f2703bf69ae..600c2fa3c7a43a 100644 --- a/test/integration/full-content/fixtures/core__nextpage.parsed.json +++ b/test/integration/full-content/fixtures/core__nextpage.parsed.json @@ -4,13 +4,17 @@ "attrs": {}, "innerBlocks": [], "innerHTML": "\n<!--nextpage-->\n", - "innerContent": [ "\n<!--nextpage-->\n" ] + "innerContent": [ + "\n<!--nextpage-->\n" + ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], "innerHTML": "\n", - "innerContent": [ "\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 e0c2ab7be237bc..a8f850f47f72b5 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 @@ -6,13 +6,17 @@ }, "innerBlocks": [], "innerHTML": "\n<p style=\"text-align:right;\">... like this one, which is separate from the above and right aligned.</p>\n", - "innerContent": [ "\n<p style=\"text-align:right;\">... like this one, which is separate from the above and right aligned.</p>\n" ] + "innerContent": [ + "\n<p style=\"text-align:right;\">... like this one, which is separate from the above and right aligned.</p>\n" + ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], "innerHTML": "\n", - "innerContent": [ "\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 e6b914e24e1110..523743bab9e465 100644 --- a/test/integration/full-content/fixtures/core__paragraph__deprecated.parsed.json +++ b/test/integration/full-content/fixtures/core__paragraph__deprecated.parsed.json @@ -4,13 +4,17 @@ "attrs": {}, "innerBlocks": [], "innerHTML": "\nUnwrapped is <em>still</em> valid.\n", - "innerContent": [ "\nUnwrapped is <em>still</em> valid.\n" ] + "innerContent": [ + "\nUnwrapped is <em>still</em> valid.\n" + ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], "innerHTML": "\n", - "innerContent": [ "\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 c78497076e90d2..0eb6c9a5b30bc6 100644 --- a/test/integration/full-content/fixtures/core__preformatted.parsed.json +++ b/test/integration/full-content/fixtures/core__preformatted.parsed.json @@ -4,13 +4,17 @@ "attrs": {}, "innerBlocks": [], "innerHTML": "\n<pre class=\"wp-block-preformatted\">Some <em>preformatted</em> text...<br>And more!</pre>\n", - "innerContent": [ "\n<pre class=\"wp-block-preformatted\">Some <em>preformatted</em> text...<br>And more!</pre>\n" ] + "innerContent": [ + "\n<pre class=\"wp-block-preformatted\">Some <em>preformatted</em> text...<br>And more!</pre>\n" + ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], "innerHTML": "\n", - "innerContent": [ "\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 033b311fa5a190..1126f70a2ed1d6 100644 --- a/test/integration/full-content/fixtures/core__pullquote.parsed.json +++ b/test/integration/full-content/fixtures/core__pullquote.parsed.json @@ -4,13 +4,17 @@ "attrs": {}, "innerBlocks": [], "innerHTML": "\n<figure class=\"wp-block-pullquote\">\n <blockquote>\n <p>Testing pullquote block...</p><cite>...with a caption</cite>\n </blockquote>\n</figure>\n", - "innerContent": [ "\n<figure class=\"wp-block-pullquote\">\n <blockquote>\n <p>Testing pullquote block...</p><cite>...with a caption</cite>\n </blockquote>\n</figure>\n" ] + "innerContent": [ + "\n<figure class=\"wp-block-pullquote\">\n <blockquote>\n <p>Testing pullquote block...</p><cite>...with a caption</cite>\n </blockquote>\n</figure>\n" + ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], "innerHTML": "\n", - "innerContent": [ "\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 fe8abfce70a364..c025cf8cae24e7 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 @@ -4,13 +4,17 @@ "attrs": {}, "innerBlocks": [], "innerHTML": "\n<figure class=\"wp-block-pullquote\">\n <blockquote>\n <p>Paragraph <strong>one</strong></p>\n <p>Paragraph two</p>\n <cite>by whomever</cite>\n\t</blockquote>\n</figure>\n", - "innerContent": [ "\n<figure class=\"wp-block-pullquote\">\n <blockquote>\n <p>Paragraph <strong>one</strong></p>\n <p>Paragraph two</p>\n <cite>by whomever</cite>\n\t</blockquote>\n</figure>\n" ] + "innerContent": [ + "\n<figure class=\"wp-block-pullquote\">\n <blockquote>\n <p>Paragraph <strong>one</strong></p>\n <p>Paragraph two</p>\n <cite>by whomever</cite>\n\t</blockquote>\n</figure>\n" + ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], "innerHTML": "\n", - "innerContent": [ "\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 6a873438f17316..c86fe92fb11d8f 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 @@ -4,13 +4,17 @@ "attrs": {}, "innerBlocks": [], "innerHTML": "\n<blockquote class=\"wp-block-quote\"><p>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.</p><cite>Matt Mullenweg, 2017</cite></blockquote>\n", - "innerContent": [ "\n<blockquote class=\"wp-block-quote\"><p>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.</p><cite>Matt Mullenweg, 2017</cite></blockquote>\n" ] + "innerContent": [ + "\n<blockquote class=\"wp-block-quote\"><p>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.</p><cite>Matt Mullenweg, 2017</cite></blockquote>\n" + ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], "innerHTML": "\n", - "innerContent": [ "\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 6470afbc17a2e1..5543ac33cfd6c1 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 @@ -6,13 +6,17 @@ }, "innerBlocks": [], "innerHTML": "\n<blockquote class=\"wp-block-quote is-style-large\"><p>There is no greater agony than bearing an untold story inside you.</p><cite>Maya Angelou</cite></blockquote>\n", - "innerContent": [ "\n<blockquote class=\"wp-block-quote is-style-large\"><p>There is no greater agony than bearing an untold story inside you.</p><cite>Maya Angelou</cite></blockquote>\n" ] + "innerContent": [ + "\n<blockquote class=\"wp-block-quote is-style-large\"><p>There is no greater agony than bearing an untold story inside you.</p><cite>Maya Angelou</cite></blockquote>\n" + ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], "innerHTML": "\n", - "innerContent": [ "\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 48a8e742c35b05..cb5714d4df31b3 100644 --- a/test/integration/full-content/fixtures/core__separator.parsed.json +++ b/test/integration/full-content/fixtures/core__separator.parsed.json @@ -4,13 +4,17 @@ "attrs": {}, "innerBlocks": [], "innerHTML": "\n<hr class=\"wp-block-separator\" />\n", - "innerContent": [ "\n<hr class=\"wp-block-separator\" />\n" ] + "innerContent": [ + "\n<hr class=\"wp-block-separator\" />\n" + ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], "innerHTML": "\n", - "innerContent": [ "\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 b875770f15a452..85ae209c2a1d37 100644 --- a/test/integration/full-content/fixtures/core__shortcode.parsed.json +++ b/test/integration/full-content/fixtures/core__shortcode.parsed.json @@ -4,13 +4,17 @@ "attrs": {}, "innerBlocks": [], "innerHTML": "\n[gallery ids=\"238,338\"]\n", - "innerContent": [ "\n[gallery ids=\"238,338\"]\n" ] + "innerContent": [ + "\n[gallery ids=\"238,338\"]\n" + ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], "innerHTML": "\n", - "innerContent": [ "\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 c3c0938df5b9da..68591dd47de553 100644 --- a/test/integration/full-content/fixtures/core__spacer.parsed.json +++ b/test/integration/full-content/fixtures/core__spacer.parsed.json @@ -4,13 +4,17 @@ "attrs": {}, "innerBlocks": [], "innerHTML": "\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"></div>\n", - "innerContent": [ "\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"></div>\n" ] + "innerContent": [ + "\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"></div>\n" + ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], "innerHTML": "\n", - "innerContent": [ "\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 d88b9ee4c90b69..c0dbef54853747 100644 --- a/test/integration/full-content/fixtures/core__subhead.parsed.json +++ b/test/integration/full-content/fixtures/core__subhead.parsed.json @@ -4,13 +4,17 @@ "attrs": {}, "innerBlocks": [], "innerHTML": "\n<p class=\"wp-block-subhead\">This is a <em>subhead</em>.</p>\n", - "innerContent": [ "\n<p class=\"wp-block-subhead\">This is a <em>subhead</em>.</p>\n" ] + "innerContent": [ + "\n<p class=\"wp-block-subhead\">This is a <em>subhead</em>.</p>\n" + ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], "innerHTML": "\n", - "innerContent": [ "\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 7a2d91003f4373..5462dae3908a8c 100644 --- a/test/integration/full-content/fixtures/core__table.parsed.json +++ b/test/integration/full-content/fixtures/core__table.parsed.json @@ -4,13 +4,17 @@ "attrs": {}, "innerBlocks": [], "innerHTML": "\n<table class=\"wp-block-table\"><thead><tr><th>Version</th><th>Musician</th><th>Date</th></tr></thead><tbody><tr><td><a href=\"https://wordpress.org/news/2003/05/wordpress-now-available/\">.70</a></td><td>No musician chosen.</td><td>May 27, 2003</td></tr><tr><td><a href=\"https://wordpress.org/news/2004/01/wordpress-10/\">1.0</a></td><td>Miles Davis</td><td>January 3, 2004</td></tr><tr><td>Lots of versions skipped, see <a href=\"https://codex.wordpress.org/WordPress_Versions\">the full list</a></td><td>&hellip;</td><td>&hellip;</td></tr><tr><td><a href=\"https://wordpress.org/news/2015/12/clifford/\">4.4</a></td><td>Clifford Brown</td><td>December 8, 2015</td></tr><tr><td><a href=\"https://wordpress.org/news/2016/04/coleman/\">4.5</a></td><td>Coleman Hawkins</td><td>April 12, 2016</td></tr><tr><td><a href=\"https://wordpress.org/news/2016/08/pepper/\">4.6</a></td><td>Pepper Adams</td><td>August 16, 2016</td></tr><tr><td><a href=\"https://wordpress.org/news/2016/12/vaughan/\">4.7</a></td><td>Sarah Vaughan</td><td>December 6, 2016</td></tr></tbody></table>\n", - "innerContent": [ "\n<table class=\"wp-block-table\"><thead><tr><th>Version</th><th>Musician</th><th>Date</th></tr></thead><tbody><tr><td><a href=\"https://wordpress.org/news/2003/05/wordpress-now-available/\">.70</a></td><td>No musician chosen.</td><td>May 27, 2003</td></tr><tr><td><a href=\"https://wordpress.org/news/2004/01/wordpress-10/\">1.0</a></td><td>Miles Davis</td><td>January 3, 2004</td></tr><tr><td>Lots of versions skipped, see <a href=\"https://codex.wordpress.org/WordPress_Versions\">the full list</a></td><td>&hellip;</td><td>&hellip;</td></tr><tr><td><a href=\"https://wordpress.org/news/2015/12/clifford/\">4.4</a></td><td>Clifford Brown</td><td>December 8, 2015</td></tr><tr><td><a href=\"https://wordpress.org/news/2016/04/coleman/\">4.5</a></td><td>Coleman Hawkins</td><td>April 12, 2016</td></tr><tr><td><a href=\"https://wordpress.org/news/2016/08/pepper/\">4.6</a></td><td>Pepper Adams</td><td>August 16, 2016</td></tr><tr><td><a href=\"https://wordpress.org/news/2016/12/vaughan/\">4.7</a></td><td>Sarah Vaughan</td><td>December 6, 2016</td></tr></tbody></table>\n" ] + "innerContent": [ + "\n<table class=\"wp-block-table\"><thead><tr><th>Version</th><th>Musician</th><th>Date</th></tr></thead><tbody><tr><td><a href=\"https://wordpress.org/news/2003/05/wordpress-now-available/\">.70</a></td><td>No musician chosen.</td><td>May 27, 2003</td></tr><tr><td><a href=\"https://wordpress.org/news/2004/01/wordpress-10/\">1.0</a></td><td>Miles Davis</td><td>January 3, 2004</td></tr><tr><td>Lots of versions skipped, see <a href=\"https://codex.wordpress.org/WordPress_Versions\">the full list</a></td><td>&hellip;</td><td>&hellip;</td></tr><tr><td><a href=\"https://wordpress.org/news/2015/12/clifford/\">4.4</a></td><td>Clifford Brown</td><td>December 8, 2015</td></tr><tr><td><a href=\"https://wordpress.org/news/2016/04/coleman/\">4.5</a></td><td>Coleman Hawkins</td><td>April 12, 2016</td></tr><tr><td><a href=\"https://wordpress.org/news/2016/08/pepper/\">4.6</a></td><td>Pepper Adams</td><td>August 16, 2016</td></tr><tr><td><a href=\"https://wordpress.org/news/2016/12/vaughan/\">4.7</a></td><td>Sarah Vaughan</td><td>December 6, 2016</td></tr></tbody></table>\n" + ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], "innerHTML": "\n", - "innerContent": [ "\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 1a7db4e09ed3a3..b00a69b7e3f546 100644 --- a/test/integration/full-content/fixtures/core__text-columns.parsed.json +++ b/test/integration/full-content/fixtures/core__text-columns.parsed.json @@ -6,13 +6,17 @@ }, "innerBlocks": [], "innerHTML": "\n<div class=\"wp-block-text-columns aligncenter columns-2\">\n <div class=\"wp-block-column\">\n <p>One</p>\n </div>\n <div class=\"wp-block-column\">\n <p>Two</p>\n </div>\n</div>\n", - "innerContent": [ "\n<div class=\"wp-block-text-columns aligncenter columns-2\">\n <div class=\"wp-block-column\">\n <p>One</p>\n </div>\n <div class=\"wp-block-column\">\n <p>Two</p>\n </div>\n</div>\n" ] + "innerContent": [ + "\n<div class=\"wp-block-text-columns aligncenter columns-2\">\n <div class=\"wp-block-column\">\n <p>One</p>\n </div>\n <div class=\"wp-block-column\">\n <p>Two</p>\n </div>\n</div>\n" + ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], "innerHTML": "\n", - "innerContent": [ "\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 75a5ca1140907a..1e472910342844 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 @@ -4,13 +4,17 @@ "attrs": {}, "innerBlocks": [], "innerHTML": "\n<p>This is an old-style text block. Changed to <code>paragraph</code> in #2135.</p>\n", - "innerContent": [ "\n<p>This is an old-style text block. Changed to <code>paragraph</code> in #2135.</p>\n" ] + "innerContent": [ + "\n<p>This is an old-style text block. Changed to <code>paragraph</code> in #2135.</p>\n" + ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], "innerHTML": "\n", - "innerContent": [ "\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 4cccc9383a50cb..2fbff3b1b326ed 100644 --- a/test/integration/full-content/fixtures/core__verse.parsed.json +++ b/test/integration/full-content/fixtures/core__verse.parsed.json @@ -4,13 +4,17 @@ "attrs": {}, "innerBlocks": [], "innerHTML": "\n<pre class=\"wp-block-verse\">A <em>verse</em>…<br>And more!</pre>\n", - "innerContent": [ "\n<pre class=\"wp-block-verse\">A <em>verse</em>…<br>And more!</pre>\n" ] + "innerContent": [ + "\n<pre class=\"wp-block-verse\">A <em>verse</em>…<br>And more!</pre>\n" + ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], "innerHTML": "\n", - "innerContent": [ "\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 e9be9d8a2ea2c0..5fd5505e395f76 100644 --- a/test/integration/full-content/fixtures/core__video.parsed.json +++ b/test/integration/full-content/fixtures/core__video.parsed.json @@ -4,13 +4,17 @@ "attrs": {}, "innerBlocks": [], "innerHTML": "\n<figure class=\"wp-block-video\"><video controls src=\"https://awesome-fake.video/file.mp4\"></video></figure>\n", - "innerContent": [ "\n<figure class=\"wp-block-video\"><video controls src=\"https://awesome-fake.video/file.mp4\"></video></figure>\n" ] + "innerContent": [ + "\n<figure class=\"wp-block-video\"><video controls src=\"https://awesome-fake.video/file.mp4\"></video></figure>\n" + ] }, { "blockName": null, "attrs": {}, "innerBlocks": [], "innerHTML": "\n", - "innerContent": [ "\n" ] + "innerContent": [ + "\n" + ] } ] From c77cf7440b1291bdfbb1b6a7572050a0f4e792b5 Mon Sep 17 00:00:00 2001 From: Chris Van Patten <hello@chrisvanpatten.com> Date: Tue, 20 Nov 2018 16:05:48 -0500 Subject: [PATCH 077/254] Reorganise documentation (#11817) * Initial reworking; props @0aveRyan * Fixes for the manifest/generators * A few missed items and small formatting tweaks * Simplify/flatten the directory structure * Cheat my way around a merge conflict pt1 * fight the git powers * JSON version of TOC instead of YML * Ensure all files have good titles * Add a generator script and re-generate the root-manifest * Update docs/designers-developers/designers/block-design.md Co-Authored-By: chrisvanpatten <hello@chrisvanpatten.com> * Update docs/designers-developers/key-concepts.md Co-Authored-By: chrisvanpatten <hello@chrisvanpatten.com> * Get a handful of internal link references fixed * Another batch of internal links * Update docs/tool/manifest.js Co-Authored-By: chrisvanpatten <hello@chrisvanpatten.com> * Additional broken internal link fixes * A few more links * Revert use of lt/gt symbols in link * Fix more broken internal links * Remove docs generator * Broken manifest.json --- .../coding-guidelines.md | 0 .../{reference => contributors}/copy-guide.md | 0 docs/{ => contributors}/design.md | 0 docs/{ => contributors}/grammar.md | 0 docs/{reference => contributors}/history.md | 0 docs/{ => contributors}/outreach.md | 0 docs/{ => contributors}/outreach/articles.md | 0 docs/{ => contributors}/outreach/meetups.md | 0 docs/{ => contributors}/outreach/resources.md | 0 docs/{ => contributors}/outreach/talks.md | 0 docs/{ => contributors}/principles.md | 0 .../principles/the-block.md | 0 docs/contributors/readme.md | 0 docs/{ => contributors}/reference.md | 8 +- .../release-screenshot.png | Bin docs/{reference => contributors}/release.md | 0 .../repository-management.md | 0 docs/{reference => contributors}/scripts.md | 0 .../testing-overview.md | 0 docs/data/README.md | 10 - docs/designers-developers/designers/README.md | 3 + .../assets}/advanced-settings-do.png | Bin .../designers/assets}/block-controls-do.png | Bin .../designers/assets}/block-controls-dont.png | Bin .../assets}/block-descriptions-do.png | Bin .../assets}/block-descriptions-dont.png | Bin .../designers/assets}/blocks-do.png | Bin .../designers/assets}/blocks-dont.png | Bin .../designers/assets}/placeholder-do.png | Bin .../designers/assets}/placeholder-dont.png | Bin .../designers}/block-design.md | 22 +- .../designers}/design-patterns.md | 0 .../designers}/design-resources.md | 0 .../designers-developers/developers/README.md | 45 ++ .../backwards-compatibility/README.md | 1 + .../backwards-compatibility/deprecations.md} | 2 + .../backwards-compatibility}/meta-box.md | 0 .../developers/block-api/README.md | 11 + .../block-api/block-annotations.md} | 0 .../developers/block-api/block-attributes.md} | 0 .../block-api/block-deprecation.md} | 6 +- .../developers}/block-api/block-edit-save.md | 6 +- .../block-api/block-registration.md} | 12 +- .../developers/block-api/block-templates.md} | 0 .../developers/data/README.md | 10 + .../developers}/data/data-core-annotations.md | 0 .../developers}/data/data-core-blocks.md | 0 .../developers}/data/data-core-edit-post.md | 0 .../developers}/data/data-core-editor.md | 0 .../developers}/data/data-core-notices.md | 0 .../developers}/data/data-core-nux.md | 0 .../developers}/data/data-core-viewport.md | 0 .../developers}/data/data-core.md | 0 .../developers/filters/README.md | 7 + .../filters/autocomplete-filters.md} | 5 +- .../developers/filters/block-filters.md} | 9 +- .../developers/filters/editor-filters.md | 18 + .../developers/filters/parser-filters.md} | 2 +- .../developers/internationalization.md | 33 ++ .../developers}/packages.md | 4 +- .../developers/themes/README.md | 5 + .../developers/themes}/theme-support.md | 25 +- .../applying-styles-with-stylesheets.md | 0 .../block-controls-toolbars-and-inspector.md | 0 .../creating-dynamic-blocks.md | 0 .../generate-blocks-with-wp-cli.md | 0 .../tutorials/block-tutorial}/inspector.png | Bin .../tutorials/block-tutorial/intro.md} | 0 ...roducing-attributes-and-editable-fields.md | 2 +- .../writing-your-first-block-type.md | 0 .../faq.md | 16 +- .../glossary.md | 32 +- .../key-concepts.md} | 94 ++-- docs/designers-developers/readme.md | 11 + docs/extensibility.md | 82 ---- docs/extensibility/extending-editor.md | 21 - docs/manifest.json | 308 +++++-------- docs/readme.md | 22 +- docs/root-manifest.json | 428 +++++++----------- docs/toc.json | 38 ++ docs/tool/config.js | 2 +- docs/tool/generator.js | 2 +- docs/tool/manifest.js | 6 +- docs/users/readme.md | 0 84 files changed, 634 insertions(+), 674 deletions(-) rename docs/{reference => contributors}/coding-guidelines.md (100%) rename docs/{reference => contributors}/copy-guide.md (100%) rename docs/{ => contributors}/design.md (100%) rename docs/{ => contributors}/grammar.md (100%) rename docs/{reference => contributors}/history.md (100%) rename docs/{ => contributors}/outreach.md (100%) rename docs/{ => contributors}/outreach/articles.md (100%) rename docs/{ => contributors}/outreach/meetups.md (100%) rename docs/{ => contributors}/outreach/resources.md (100%) rename docs/{ => contributors}/outreach/talks.md (100%) rename docs/{ => contributors}/principles.md (100%) rename docs/{ => contributors}/principles/the-block.md (100%) create mode 100644 docs/contributors/readme.md rename docs/{ => contributors}/reference.md (67%) rename docs/{reference => contributors}/release-screenshot.png (100%) rename docs/{reference => contributors}/release.md (100%) rename docs/{reference => contributors}/repository-management.md (100%) rename docs/{reference => contributors}/scripts.md (100%) rename docs/{reference => contributors}/testing-overview.md (100%) delete mode 100644 docs/data/README.md create mode 100644 docs/designers-developers/designers/README.md rename docs/{design => designers-developers/designers/assets}/advanced-settings-do.png (100%) rename docs/{design => designers-developers/designers/assets}/block-controls-do.png (100%) rename docs/{design => designers-developers/designers/assets}/block-controls-dont.png (100%) rename docs/{design => designers-developers/designers/assets}/block-descriptions-do.png (100%) rename docs/{design => designers-developers/designers/assets}/block-descriptions-dont.png (100%) rename docs/{design => designers-developers/designers/assets}/blocks-do.png (100%) rename docs/{design => designers-developers/designers/assets}/blocks-dont.png (100%) rename docs/{design => designers-developers/designers/assets}/placeholder-do.png (100%) rename docs/{design => designers-developers/designers/assets}/placeholder-dont.png (100%) rename docs/{design => designers-developers/designers}/block-design.md (91%) rename docs/{design => designers-developers/designers}/design-patterns.md (100%) rename docs/{design => designers-developers/designers}/design-resources.md (100%) create mode 100644 docs/designers-developers/developers/README.md create mode 100644 docs/designers-developers/developers/backwards-compatibility/README.md rename docs/{reference/deprecated.md => designers-developers/developers/backwards-compatibility/deprecations.md} (99%) rename docs/{extensibility => designers-developers/developers/backwards-compatibility}/meta-box.md (100%) create mode 100644 docs/designers-developers/developers/block-api/README.md rename docs/{extensibility/annotations.md => designers-developers/developers/block-api/block-annotations.md} (100%) rename docs/{block-api/attributes.md => designers-developers/developers/block-api/block-attributes.md} (100%) rename docs/{block-api/deprecated-blocks.md => designers-developers/developers/block-api/block-deprecation.md} (92%) rename docs/{ => designers-developers/developers}/block-api/block-edit-save.md (87%) rename docs/{block-api.md => designers-developers/developers/block-api/block-registration.md} (96%) rename docs/{templates.md => designers-developers/developers/block-api/block-templates.md} (100%) create mode 100644 docs/designers-developers/developers/data/README.md rename docs/{ => designers-developers/developers}/data/data-core-annotations.md (100%) rename docs/{ => designers-developers/developers}/data/data-core-blocks.md (100%) rename docs/{ => designers-developers/developers}/data/data-core-edit-post.md (100%) rename docs/{ => designers-developers/developers}/data/data-core-editor.md (100%) rename docs/{ => designers-developers/developers}/data/data-core-notices.md (100%) rename docs/{ => designers-developers/developers}/data/data-core-nux.md (100%) rename docs/{ => designers-developers/developers}/data/data-core-viewport.md (100%) rename docs/{ => designers-developers/developers}/data/data-core.md (100%) create mode 100644 docs/designers-developers/developers/filters/README.md rename docs/{extensibility/autocomplete.md => designers-developers/developers/filters/autocomplete-filters.md} (93%) rename docs/{extensibility/extending-blocks.md => designers-developers/developers/filters/block-filters.md} (88%) create mode 100644 docs/designers-developers/developers/filters/editor-filters.md rename docs/{extensibility/parser.md => designers-developers/developers/filters/parser-filters.md} (98%) create mode 100644 docs/designers-developers/developers/internationalization.md rename docs/{ => designers-developers/developers}/packages.md (73%) create mode 100644 docs/designers-developers/developers/themes/README.md rename docs/{extensibility => designers-developers/developers/themes}/theme-support.md (88%) rename docs/{blocks => designers-developers/developers/tutorials/block-tutorial}/applying-styles-with-stylesheets.md (100%) rename docs/{blocks => designers-developers/developers/tutorials/block-tutorial}/block-controls-toolbars-and-inspector.md (100%) rename docs/{blocks => designers-developers/developers/tutorials/block-tutorial}/creating-dynamic-blocks.md (100%) rename docs/{blocks => designers-developers/developers/tutorials/block-tutorial}/generate-blocks-with-wp-cli.md (100%) rename docs/{blocks => designers-developers/developers/tutorials/block-tutorial}/inspector.png (100%) rename docs/{blocks.md => designers-developers/developers/tutorials/block-tutorial/intro.md} (100%) rename docs/{blocks => designers-developers/developers/tutorials/block-tutorial}/introducing-attributes-and-editable-fields.md (96%) rename docs/{blocks => designers-developers/developers/tutorials/block-tutorial}/writing-your-first-block-type.md (100%) rename docs/{reference => designers-developers}/faq.md (97%) rename docs/{reference => designers-developers}/glossary.md (65%) rename docs/{language.md => designers-developers/key-concepts.md} (75%) create mode 100644 docs/designers-developers/readme.md delete mode 100644 docs/extensibility.md delete mode 100644 docs/extensibility/extending-editor.md create mode 100644 docs/toc.json create mode 100644 docs/users/readme.md diff --git a/docs/reference/coding-guidelines.md b/docs/contributors/coding-guidelines.md similarity index 100% rename from docs/reference/coding-guidelines.md rename to docs/contributors/coding-guidelines.md diff --git a/docs/reference/copy-guide.md b/docs/contributors/copy-guide.md similarity index 100% rename from docs/reference/copy-guide.md rename to docs/contributors/copy-guide.md diff --git a/docs/design.md b/docs/contributors/design.md similarity index 100% rename from docs/design.md rename to docs/contributors/design.md diff --git a/docs/grammar.md b/docs/contributors/grammar.md similarity index 100% rename from docs/grammar.md rename to docs/contributors/grammar.md diff --git a/docs/reference/history.md b/docs/contributors/history.md similarity index 100% rename from docs/reference/history.md rename to docs/contributors/history.md diff --git a/docs/outreach.md b/docs/contributors/outreach.md similarity index 100% rename from docs/outreach.md rename to docs/contributors/outreach.md diff --git a/docs/outreach/articles.md b/docs/contributors/outreach/articles.md similarity index 100% rename from docs/outreach/articles.md rename to docs/contributors/outreach/articles.md diff --git a/docs/outreach/meetups.md b/docs/contributors/outreach/meetups.md similarity index 100% rename from docs/outreach/meetups.md rename to docs/contributors/outreach/meetups.md diff --git a/docs/outreach/resources.md b/docs/contributors/outreach/resources.md similarity index 100% rename from docs/outreach/resources.md rename to docs/contributors/outreach/resources.md diff --git a/docs/outreach/talks.md b/docs/contributors/outreach/talks.md similarity index 100% rename from docs/outreach/talks.md rename to docs/contributors/outreach/talks.md diff --git a/docs/principles.md b/docs/contributors/principles.md similarity index 100% rename from docs/principles.md rename to docs/contributors/principles.md diff --git a/docs/principles/the-block.md b/docs/contributors/principles/the-block.md similarity index 100% rename from docs/principles/the-block.md rename to docs/contributors/principles/the-block.md diff --git a/docs/contributors/readme.md b/docs/contributors/readme.md new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/docs/reference.md b/docs/contributors/reference.md similarity index 67% rename from docs/reference.md rename to docs/contributors/reference.md index 78ba03435973b2..2a4714d5e75a23 100644 --- a/docs/reference.md +++ b/docs/contributors/reference.md @@ -1,9 +1,9 @@ # Reference -- [Glossary](../docs/reference/glossary.md) -- [Coding Guidelines](../docs/reference/coding-guidelines.md) -- [Testing Overview](../docs/reference/testing-overview.md) -- [Frequently Asked Questions](../docs/reference/faq.md) +- [Glossary](../../docs/designers-developers/glossary.md) +- [Coding Guidelines](../../docs/contributors/coding-guidelines.md) +- [Testing Overview](../../docs/contributors/testing-overview.md) +- [Frequently Asked Questions](../../docs/designers-developers/faq.md) ## Logo <img width="200" src="https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/final-g-wapuu-black.svg?sanitize=true" alt="Gutenberg Logo" /> diff --git a/docs/reference/release-screenshot.png b/docs/contributors/release-screenshot.png similarity index 100% rename from docs/reference/release-screenshot.png rename to docs/contributors/release-screenshot.png diff --git a/docs/reference/release.md b/docs/contributors/release.md similarity index 100% rename from docs/reference/release.md rename to docs/contributors/release.md diff --git a/docs/reference/repository-management.md b/docs/contributors/repository-management.md similarity index 100% rename from docs/reference/repository-management.md rename to docs/contributors/repository-management.md diff --git a/docs/reference/scripts.md b/docs/contributors/scripts.md similarity index 100% rename from docs/reference/scripts.md rename to docs/contributors/scripts.md diff --git a/docs/reference/testing-overview.md b/docs/contributors/testing-overview.md similarity index 100% rename from docs/reference/testing-overview.md rename to docs/contributors/testing-overview.md diff --git a/docs/data/README.md b/docs/data/README.md deleted file mode 100644 index ac44230651976e..00000000000000 --- a/docs/data/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# 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) - - [**core/notices**: Notices Data](../../docs/data/data-core-notices.md) - - [**core/nux**: The NUX (New User Experience) Data](../../docs/data/data-core-nux.md) - - [**core/viewport**: The Viewport Data](../../docs/data/data-core-viewport.md) \ No newline at end of file diff --git a/docs/designers-developers/designers/README.md b/docs/designers-developers/designers/README.md new file mode 100644 index 00000000000000..362cf794885f20 --- /dev/null +++ b/docs/designers-developers/designers/README.md @@ -0,0 +1,3 @@ +# Designer Documentation + +For those designing blocks and other Block Editor integrations, this documentation will provide resources for creating beautiful and intuitive layouts. diff --git a/docs/design/advanced-settings-do.png b/docs/designers-developers/designers/assets/advanced-settings-do.png similarity index 100% rename from docs/design/advanced-settings-do.png rename to docs/designers-developers/designers/assets/advanced-settings-do.png diff --git a/docs/design/block-controls-do.png b/docs/designers-developers/designers/assets/block-controls-do.png similarity index 100% rename from docs/design/block-controls-do.png rename to docs/designers-developers/designers/assets/block-controls-do.png diff --git a/docs/design/block-controls-dont.png b/docs/designers-developers/designers/assets/block-controls-dont.png similarity index 100% rename from docs/design/block-controls-dont.png rename to docs/designers-developers/designers/assets/block-controls-dont.png diff --git a/docs/design/block-descriptions-do.png b/docs/designers-developers/designers/assets/block-descriptions-do.png similarity index 100% rename from docs/design/block-descriptions-do.png rename to docs/designers-developers/designers/assets/block-descriptions-do.png diff --git a/docs/design/block-descriptions-dont.png b/docs/designers-developers/designers/assets/block-descriptions-dont.png similarity index 100% rename from docs/design/block-descriptions-dont.png rename to docs/designers-developers/designers/assets/block-descriptions-dont.png diff --git a/docs/design/blocks-do.png b/docs/designers-developers/designers/assets/blocks-do.png similarity index 100% rename from docs/design/blocks-do.png rename to docs/designers-developers/designers/assets/blocks-do.png diff --git a/docs/design/blocks-dont.png b/docs/designers-developers/designers/assets/blocks-dont.png similarity index 100% rename from docs/design/blocks-dont.png rename to docs/designers-developers/designers/assets/blocks-dont.png diff --git a/docs/design/placeholder-do.png b/docs/designers-developers/designers/assets/placeholder-do.png similarity index 100% rename from docs/design/placeholder-do.png rename to docs/designers-developers/designers/assets/placeholder-do.png diff --git a/docs/design/placeholder-dont.png b/docs/designers-developers/designers/assets/placeholder-dont.png similarity index 100% rename from docs/design/placeholder-dont.png rename to docs/designers-developers/designers/assets/placeholder-dont.png diff --git a/docs/design/block-design.md b/docs/designers-developers/designers/block-design.md similarity index 91% rename from docs/design/block-design.md rename to docs/designers-developers/designers/block-design.md index f73b61a72498e8..ae6abaf4c8a37f 100644 --- a/docs/design/block-design.md +++ b/docs/designers-developers/designers/block-design.md @@ -27,13 +27,11 @@ A block should have a straightforward, short name so users can easily find it in Blocks should have an identifying icon, ideally using a single color. Try to avoid using the same icon used by an existing block. The core block icons are based on [Material Design Icons](https://material.io/tools/icons/). Look to that icon set, or to [Dashicons](https://developer.wordpress.org/resource/dashicons/) for style inspiration. -![A screenshot of the Block Library with concise block names](https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/design/blocks-do.png) - +![A screenshot of the Block Library with concise block names](https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/designers/assets/blocks-do.png) **Do:** Use concise block names. -![A screenshot of the Block Library with long, multi-line block names](https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/design/blocks-dont.png) - +![A screenshot of the Block Library with long, multi-line block names](https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/designers/assets/blocks-dont.png) **Don't:** Avoid long, multi-line block names. @@ -41,13 +39,11 @@ Avoid long, multi-line block names. Every block should include a description in the “Block” tab of the Settings sidebar. This description should explain your block's function clearly. Keep it to a single sentence. -![A screenshot of a short block description](https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/design/block-descriptions-do.png) - +![A screenshot of a short block description](https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/designers/assets/block-descriptions-do.png) **Do:** Use a short, simple, block description. -![A screenshot of a long block description that includes branding](https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/design/block-descriptions-dont.png) - +![A screenshot of a long block description that includes branding](https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/designers/assets/block-descriptions-dont.png) **Don't:** Avoid long descriptions and branding. @@ -55,11 +51,11 @@ Avoid long descriptions and branding. If your block requires a user to configure some options before you can display it, you should provide an instructive placeholder state. -![A screenshot of the Gallery block's placeholder](https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/design/placeholder-do.png) +![A screenshot of the Gallery block's placeholder](https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/designers/assets/placeholder-do.png) **Do:** Provide an instructive placeholder state. -![An example Gallery block placeholder but with intense, distracting colors and no instructions](https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/design/placeholder-dont.png) +![An example Gallery block placeholder but with intense, distracting colors and no instructions](https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/designers/assets/placeholder-dont.png) **Don't:** Avoid branding and relying on the title alone to convey instructions. @@ -69,11 +65,11 @@ When unselected, your block should preview its content as closely to the front-e When selected, your block may surface additional options like input fields or buttons to configure the block directly, especially when they are necessary for basic operation. -![A Google Maps block with inline, always-accessible controls required for the block to function](https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/design/block-controls-do.png) +![A Google Maps block with inline, always-accessible controls required for the block to function](https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/designers/assets/block-controls-do.png) **Do:** For controls that are essential the the operation of the block, provide them directly in inside the block edit view. -![A Google Maps block with essential controls moved to the sidebar where they can be contextually hidden](https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/design/block-controls-dont.png) +![A Google Maps block with essential controls moved to the sidebar where they can be contextually hidden](https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/designers/assets/block-controls-dont.png) **Don't:** Do not put controls that are essential to the block in the sidebar, or the block will appear non-functional to mobile users, or desktop users who have dismissed the sidebar. @@ -81,7 +77,7 @@ Do not put controls that are essential to the block in the sidebar, or the block The “Block” tab of the Settings Sidebar can contain additional block options and configuration. Keep in mind that a user can dismiss the sidebar and never use it. You should not put critical options in the Sidebar. -![A screenshot of the paragraph block's advanced settings in the sidebar](https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/design/advanced-settings-do.png) +![A screenshot of the paragraph block's advanced settings in the sidebar](https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/designers/assets/advanced-settings-do.png) **Do:** Because the Drop Cap feature is not necessary for the basic operation of the block, you can put it ub the Block tab as optional configuration. diff --git a/docs/design/design-patterns.md b/docs/designers-developers/designers/design-patterns.md similarity index 100% rename from docs/design/design-patterns.md rename to docs/designers-developers/designers/design-patterns.md diff --git a/docs/design/design-resources.md b/docs/designers-developers/designers/design-resources.md similarity index 100% rename from docs/design/design-resources.md rename to docs/designers-developers/designers/design-resources.md diff --git a/docs/designers-developers/developers/README.md b/docs/designers-developers/developers/README.md new file mode 100644 index 00000000000000..e88dd2cd6b2ea1 --- /dev/null +++ b/docs/designers-developers/developers/README.md @@ -0,0 +1,45 @@ +# Developer Documentation + +Gutenberg is highly flexible, like most of WordPress. You can build custom blocks, modify the editor's appearance, add special plugins, and much more. + +## Creating Blocks + +Gutenberg is about blocks, and the main extensibility API of Gutenberg is the Block API. It allows you to create your own static blocks, dynamic blocks rendered on the server and also blocks capable of saving data to Post Meta for more structured content. + +If you want to learn more about block creation, the [Blocks Tutorial](../../../docs/designers-developers/developers/tutorials/block-tutorial/intro.md) is the best place to start. + +## Extending Blocks + +It is also possible to modify the behavior of existing blocks or even remove them completely using filters. + +Learn more in the [Block Filters](../../../docs/designers-developers/developers/reference/hooks/block-filters.md) section. + +## Extending the Editor UI + +Extending the editor UI can be accomplished with the `registerPlugin` API, allowing you to define all your plugin's UI elements in one place. + +Refer to the [Plugins](https://github.com/WordPress/gutenberg/blob/master/packages/plugins/README.md) and [Edit Post](https://github.com/WordPress/gutenberg/blob/master/packages/edit-post/README.md) section for more information. + +You can also filter certain aspects of the editor; this is documented on the [Editor Filters](../../../docs/designers-developers/developers/reference/hooks/editor-filters.md) page. + +## Meta Boxes + +**Porting PHP meta boxes to blocks and Gutenberg plugins is highly encouraged!** + +Discover how [Meta Box](../../../docs/designers-developers/developers/backwards-compatibility/meta-box.md) support works in Gutenberg. + +## Theme Support + +By default, blocks provide their styles to enable basic support for blocks in themes without any change. Themes can add/override these styles, or rely on defaults. + +There are some advanced block features which require opt-in support in the theme. See [theme support](../../../docs/designers-developers/developers/themes/theme-support.md). + +## Autocomplete + +Autocompleters within blocks may be extended and overridden. Learn more about the [autocomplete](../../../docs/designers-developers/developers/filters/autocomplete-filters.md) filters. + +## Block Parsing and Serialization + +Posts in the editor move through a couple of different stages between being stored in `post_content` and appearing in the editor. Since the blocks themselves are data structures that live in memory it takes a parsing and serialization step to transform out from and into the stored format in the database. + +Customizing the parser is an advanced topic that you can learn more about in the [Extending the Parser](../../../docs/designers-developers/developers/filters/parser-filters.md) section. diff --git a/docs/designers-developers/developers/backwards-compatibility/README.md b/docs/designers-developers/developers/backwards-compatibility/README.md new file mode 100644 index 00000000000000..bd453cba0e56a4 --- /dev/null +++ b/docs/designers-developers/developers/backwards-compatibility/README.md @@ -0,0 +1 @@ +# Backwards Compatibility diff --git a/docs/reference/deprecated.md b/docs/designers-developers/developers/backwards-compatibility/deprecations.md similarity index 99% rename from docs/reference/deprecated.md rename to docs/designers-developers/developers/backwards-compatibility/deprecations.md index 13efe9f8940bdf..804f55c0a2c19a 100644 --- a/docs/reference/deprecated.md +++ b/docs/designers-developers/developers/backwards-compatibility/deprecations.md @@ -1,3 +1,5 @@ +# Deprecations + 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 diff --git a/docs/extensibility/meta-box.md b/docs/designers-developers/developers/backwards-compatibility/meta-box.md similarity index 100% rename from docs/extensibility/meta-box.md rename to docs/designers-developers/developers/backwards-compatibility/meta-box.md diff --git a/docs/designers-developers/developers/block-api/README.md b/docs/designers-developers/developers/block-api/README.md new file mode 100644 index 00000000000000..1301449ecdf2c1 --- /dev/null +++ b/docs/designers-developers/developers/block-api/README.md @@ -0,0 +1,11 @@ +# Block API Reference + +Blocks are the fundamental element of the Gutenberg editor. They are the primary way in which plugins and themes can register their own functionality and extend the capabilities of the editor. + +## Registering a block + +All blocks must be registered before they can be used in the editor. You can learn about block registration, and the available options, in the [block registration](block-api/block-registration.md) documentation. + +## Block `edit` and `save` + +The `edit` and `save` functions define the editor interface with which a user would interact, and the markup to be serialized back when a post is saved. They are the heart of how a block operates, so they are [covered separately](block-api/block-edit-save.md). diff --git a/docs/extensibility/annotations.md b/docs/designers-developers/developers/block-api/block-annotations.md similarity index 100% rename from docs/extensibility/annotations.md rename to docs/designers-developers/developers/block-api/block-annotations.md diff --git a/docs/block-api/attributes.md b/docs/designers-developers/developers/block-api/block-attributes.md similarity index 100% rename from docs/block-api/attributes.md rename to docs/designers-developers/developers/block-api/block-attributes.md diff --git a/docs/block-api/deprecated-blocks.md b/docs/designers-developers/developers/block-api/block-deprecation.md similarity index 92% rename from docs/block-api/deprecated-blocks.md rename to docs/designers-developers/developers/block-api/block-deprecation.md index 9f1e368aed6826..3fd500c1645417 100644 --- a/docs/block-api/deprecated-blocks.md +++ b/docs/designers-developers/developers/block-api/block-deprecation.md @@ -9,9 +9,9 @@ A block can have several deprecated versions. A deprecation will be tried if a p Deprecations are defined on a block type as its `deprecated` property, an array of deprecation objects where each object takes the form: -- `attributes` (Object): The [attributes definition](../docs/block-api/attributes.md) of the deprecated form of the block. -- `support` (Object): The [supports definition](../docs/block-api.md) of the deprecated form of the block. -- `save` (Function): The [save implementation](../docs/block-api/block-edit-save.md) of the deprecated form of the block. +- `attributes` (Object): The [attributes definition](../../../../docs/designers-developers/developers/block-api/block-attributes.md) of the deprecated form of the block. +- `support` (Object): The [supports definition](../../../../docs/designers-developers/developers/block-api/block-registration.md) of the deprecated form of the block. +- `save` (Function): The [save implementation](../../../../docs/designers-developers/developers/block-api/block-edit-save.md) of the deprecated form of the block. - `migrate` (Function, Optional): A function which, given the attributes and inner blocks of the parsed block, is expected to return either the attributes compatible with the deprecated block, or a tuple array of `[ attributes, innerBlocks ]`. - `isEligible` (Function, Optional): A function which, given the attributes and inner blocks of the parsed block, returns true if the deprecation can handle the block migration. This is particularly useful in cases where a block is technically valid even once deprecated, and requires updates to its attributes or inner blocks. diff --git a/docs/block-api/block-edit-save.md b/docs/designers-developers/developers/block-api/block-edit-save.md similarity index 87% rename from docs/block-api/block-edit-save.md rename to docs/designers-developers/developers/block-api/block-edit-save.md index 4d4ee948e3ba4e..c6f4d603cc3085 100644 --- a/docs/block-api/block-edit-save.md +++ b/docs/designers-developers/developers/block-api/block-edit-save.md @@ -103,7 +103,7 @@ For most blocks, the return value of `save` should be an [instance of WordPress _Note:_ While it is possible to return a string value from `save`, it _will be escaped_. If the string includes HTML markup, the markup will be shown on the front of the site verbatim, not as the equivalent HTML node content. If you must return raw HTML from `save`, use `wp.element.RawHTML`. As the name implies, this is prone to [cross-site scripting](https://en.wikipedia.org/wiki/Cross-site_scripting) and therefore is discouraged in favor of a WordPress Element hierarchy whenever possible. -For [dynamic blocks](../../docs/blocks/creating-dynamic-blocks.md), the return value of `save` could either represent a cached copy of the block's content to be shown only in case the plugin implementing the block is ever disabled. Alternatively, return a `null` (empty) value to save no markup in post content for the dynamic block, instead deferring this to always be calculated when the block is shown on the front of the site. +For [dynamic blocks](../../../../docs/designers-developers/developers/tutorials/block-tutorial/creating-dynamic-blocks.md), the return value of `save` could either represent a cached copy of the block's content to be shown only in case the plugin implementing the block is ever disabled. Alternatively, return a `null` (empty) value to save no markup in post content for the dynamic block, instead deferring this to always be calculated when the block is shown on the front of the site. ### attributes @@ -153,10 +153,10 @@ The two most common sources of block invalidations are: Before starting to debug, be sure to familiarize yourself with the validation step described above documenting the process for detecting whether a block is invalid. A block is invalid if its regenerated markup does not match what is saved in post content, so often this can be caused by the attributes of a block being parsed incorrectly from the saved content. -If you're using [attribute sources](../../docs/block-api/attributes.md), be sure that attributes sourced from markup are saved exactly as you expect, and in the correct type (usually a `'string'` or `'number'`). +If you're using [attribute sources](../../../../docs/designers-developers/developers/block-api/block-attributes.md), be sure that attributes sourced from markup are saved exactly as you expect, and in the correct type (usually a `'string'` or `'number'`). When a block is detected as invalid, a warning will be logged into your browser's developer tools console. The warning will include specific details about the exact point at which a difference in markup occurred. Be sure to look closely at any differences in the expected and actual markups to see where problems are occurring. **I've changed my block's `save` behavior and old content now includes invalid blocks. How can I fix this?** -Refer to the guide on [Deprecated Blocks](../../docs/block-api/deprecated-blocks.md) to learn more about how to accommodate legacy content in intentional markup changes. +Refer to the guide on [Deprecated Blocks](../../../../docs/designers-developers/developers/block-api/block-deprecations.md) to learn more about how to accommodate legacy content in intentional markup changes. diff --git a/docs/block-api.md b/docs/designers-developers/developers/block-api/block-registration.md similarity index 96% rename from docs/block-api.md rename to docs/designers-developers/developers/block-api/block-registration.md index 155bd417d21a27..9738ac482353b7 100644 --- a/docs/block-api.md +++ b/docs/designers-developers/developers/block-api/block-registration.md @@ -1,8 +1,6 @@ -# Block API +# Block Registration -Blocks are the fundamental element of the Gutenberg editor. They are the primary way in which plugins and themes can register their own functionality and extend the capabilities of the editor. This document covers the main properties of block registration. - -## Register Block Type +## `register_block_type` * **Type:** `Function` @@ -466,6 +464,8 @@ parent: [ 'core/columns' ], #### supports (optional) +*Some [block supports](#supports-optional) — for example, `anchor` or `className` — apply their attributes by adding additional props on the element returned by `save`. This will work automatically for default HTML tag elements (`div`, etc). However, if the return value of your `save` is a custom component element, you will need to ensure that your custom component handles these props in order for the attributes to be persisted.* + * **Type:** `Object` Optional block extended support features. The following options are supported: @@ -548,8 +548,4 @@ By default all blocks can be converted to a reusable block. If supports reusable // 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). - -*Some [block supports](#supports-optional) — for example, `anchor` or `className` — apply their attributes by adding additional props on the element returned by `save`. This will work automatically for default HTML tag elements (`div`, etc). However, if the return value of your `save` is a custom component element, you will need to ensure that your custom component handles these props in order for the attributes to be persisted.* diff --git a/docs/templates.md b/docs/designers-developers/developers/block-api/block-templates.md similarity index 100% rename from docs/templates.md rename to docs/designers-developers/developers/block-api/block-templates.md diff --git a/docs/designers-developers/developers/data/README.md b/docs/designers-developers/developers/data/README.md new file mode 100644 index 00000000000000..5233753c1445e5 --- /dev/null +++ b/docs/designers-developers/developers/data/README.md @@ -0,0 +1,10 @@ +# Data Module Reference + + - [**core**: WordPress Core Data](../../docs/designers-developers/developers/data/data-core.md) + - [**core/annotations**: Annotations](../../docs/designers-developers/developers/data/data-core-annotations.md) + - [**core/blocks**: Block Types Data](../../docs/designers-developers/developers/data/data-core-blocks.md) + - [**core/editor**: The Editor’s Data](../../docs/designers-developers/developers/data/data-core-editor.md) + - [**core/edit-post**: The Editor’s UI Data](../../docs/designers-developers/developers/data/data-core-edit-post.md) + - [**core/notices**: Notices Data](../../docs/designers-developers/developers/data/data-core-notices.md) + - [**core/nux**: The NUX (New User Experience) Data](../../docs/designers-developers/developers/data/data-core-nux.md) + - [**core/viewport**: The Viewport Data](../../docs/designers-developers/developers/data/data-core-viewport.md) \ No newline at end of file diff --git a/docs/data/data-core-annotations.md b/docs/designers-developers/developers/data/data-core-annotations.md similarity index 100% rename from docs/data/data-core-annotations.md rename to docs/designers-developers/developers/data/data-core-annotations.md diff --git a/docs/data/data-core-blocks.md b/docs/designers-developers/developers/data/data-core-blocks.md similarity index 100% rename from docs/data/data-core-blocks.md rename to docs/designers-developers/developers/data/data-core-blocks.md diff --git a/docs/data/data-core-edit-post.md b/docs/designers-developers/developers/data/data-core-edit-post.md similarity index 100% rename from docs/data/data-core-edit-post.md rename to docs/designers-developers/developers/data/data-core-edit-post.md diff --git a/docs/data/data-core-editor.md b/docs/designers-developers/developers/data/data-core-editor.md similarity index 100% rename from docs/data/data-core-editor.md rename to docs/designers-developers/developers/data/data-core-editor.md diff --git a/docs/data/data-core-notices.md b/docs/designers-developers/developers/data/data-core-notices.md similarity index 100% rename from docs/data/data-core-notices.md rename to docs/designers-developers/developers/data/data-core-notices.md diff --git a/docs/data/data-core-nux.md b/docs/designers-developers/developers/data/data-core-nux.md similarity index 100% rename from docs/data/data-core-nux.md rename to docs/designers-developers/developers/data/data-core-nux.md diff --git a/docs/data/data-core-viewport.md b/docs/designers-developers/developers/data/data-core-viewport.md similarity index 100% rename from docs/data/data-core-viewport.md rename to docs/designers-developers/developers/data/data-core-viewport.md diff --git a/docs/data/data-core.md b/docs/designers-developers/developers/data/data-core.md similarity index 100% rename from docs/data/data-core.md rename to docs/designers-developers/developers/data/data-core.md diff --git a/docs/designers-developers/developers/filters/README.md b/docs/designers-developers/developers/filters/README.md new file mode 100644 index 00000000000000..9715e15885935c --- /dev/null +++ b/docs/designers-developers/developers/filters/README.md @@ -0,0 +1,7 @@ +# Filter Reference + +[Hooks](https://developer.wordpress.org/plugins/hooks/) are a way for one piece of code to interact/modify another piece of code. They provide one way for plugins and themes interact with Gutenberg, but they’re also used extensively by WordPress Core itself. + +There are two types of hooks: [Actions](https://developer.wordpress.org/plugins/hooks/actions/) and [Filters](https://developer.wordpress.org/plugins/hooks/filters/). In addition to PHP actions and filters, Gutenberg also provides a mechanism for registering and executing hooks in JavaScript. This functionality is also available on npm as the [@wordpress/hooks](https://www.npmjs.com/package/@wordpress/hooks) package, for general purpose use. + +You can also learn more about both APIs: [PHP](https://codex.wordpress.org/Plugin_API/) and [JavaScript](https://github.com/WordPress/packages/tree/master/packages/hooks). diff --git a/docs/extensibility/autocomplete.md b/docs/designers-developers/developers/filters/autocomplete-filters.md similarity index 93% rename from docs/extensibility/autocomplete.md rename to docs/designers-developers/developers/filters/autocomplete-filters.md index ee1f2bc945852e..2b9d60476de80a 100644 --- a/docs/extensibility/autocomplete.md +++ b/docs/designers-developers/developers/filters/autocomplete-filters.md @@ -1,7 +1,6 @@ -Autocomplete -============ +# Autocomplete -Gutenberg provides a `editor.Autocomplete.completers` filter for extending and overriding the list of autocompleters used by blocks. +Gutenberg provides an `editor.Autocomplete.completers` filter for extending and overriding the list of autocompleters used by blocks. The `Autocomplete` component found in `@wordpress/editor` applies this filter. The `@wordpress/components` package provides the foundational `Autocomplete` component that does not apply such a filter, but blocks should generally use the component provided by `@wordpress/editor`. diff --git a/docs/extensibility/extending-blocks.md b/docs/designers-developers/developers/filters/block-filters.md similarity index 88% rename from docs/extensibility/extending-blocks.md rename to docs/designers-developers/developers/filters/block-filters.md index d00282e6b4665a..378241356578a0 100644 --- a/docs/extensibility/extending-blocks.md +++ b/docs/designers-developers/developers/filters/block-filters.md @@ -1,8 +1,4 @@ -# Extending Blocks (Experimental) - -[Hooks](https://developer.wordpress.org/plugins/hooks/) are a way for one piece of code to interact/modify another piece of code. They make up the foundation for how plugins and themes interact with Gutenberg, but they’re also used extensively by WordPress Core itself. There are two types of hooks: [Actions](https://developer.wordpress.org/plugins/hooks/actions/) and [Filters](https://developer.wordpress.org/plugins/hooks/filters/). They were initially implemented in PHP, but for the purpose of Gutenberg they were ported to JavaScript and published to npm as [@wordpress/hooks](https://www.npmjs.com/package/@wordpress/hooks) package for general purpose use. You can also learn more about both APIs: [PHP](https://codex.wordpress.org/Plugin_API/) and [JavaScript](https://github.com/WordPress/packages/tree/master/packages/hooks). - -## Modifying Blocks +# Block Filters To modify the behavior of existing blocks, Gutenberg exposes several APIs: @@ -93,7 +89,7 @@ wp.hooks.addFilter( ); ``` -_Note:_ This filter must always be run on every page load, and not in your browser's developer tools console. Otherwise, a [block validation](../../docs/block-api/block-edit-save.md#validation) error will occur the next time the post is edited. This is due to the fact that block validation occurs by verifying that the saved output matches what is stored in the post's content during editor initialization. So, if this filter does not exist when the editor loads, the block will be marked as invalid. +_Note:_ This filter must always be run on every page load, and not in your browser's developer tools console. Otherwise, a [block validation](../../../../docs/designers-developers/developers/block-api/block-edit-save.md#validation) error will occur the next time the post is edited. This is due to the fact that block validation occurs by verifying that the saved output matches what is stored in the post's content during editor initialization. So, if this filter does not exist when the editor loads, the block will be marked as invalid. #### `blocks.getBlockDefaultClassName` @@ -342,4 +338,3 @@ add_filter( 'block_categories', 'my_plugin_block_categories', 10, 2 ); ``` You can also display an icon with your block category by setting an `icon` attribute. The value can be the slug of a [WordPress Dashicon](https://developer.wordpress.org/resource/dashicons/), or a custom `svg` element. - diff --git a/docs/designers-developers/developers/filters/editor-filters.md b/docs/designers-developers/developers/filters/editor-filters.md new file mode 100644 index 00000000000000..1289b8241936ba --- /dev/null +++ b/docs/designers-developers/developers/filters/editor-filters.md @@ -0,0 +1,18 @@ +# Editor Filters (Experimental) + +To modify the behavior of the editor experience, Gutenberg exposes the following Filters: + +### `editor.PostFeaturedImage.imageSize` + +Used to modify the image size displayed in the Post Featured Image component. It defaults to `'post-thumbnail'`, and will fail back to the `full` image size when the specified image size doesn't exist in the media object. It's modeled after the `admin_post_thumbnail_size` filter in the Classic Editor. + +_Example:_ + +```js +var withImageSize = function( size, mediaId, postId ) { + return 'large'; +}; + +wp.hooks.addFilter( 'editor.PostFeaturedImage.imageSize', 'my-plugin/with-image-size', withImageSize ); +``` + diff --git a/docs/extensibility/parser.md b/docs/designers-developers/developers/filters/parser-filters.md similarity index 98% rename from docs/extensibility/parser.md rename to docs/designers-developers/developers/filters/parser-filters.md index 7c1e5bc1be7c21..3acfb2489182db 100644 --- a/docs/extensibility/parser.md +++ b/docs/designers-developers/developers/filters/parser-filters.md @@ -1,4 +1,4 @@ -# Extending the Parser +# Parser Filters When the editor is interacting with blocks, these are stored in memory as data structures comprising a few basic properties and attributes. Upon saving a working post we serialize these data structures into a specific HTML structure and save the resultant string into the `post_content` property of the post in the WordPress database. When we load that post back into the editor we have to make the reverse transformation to build those data structures from the serialized format in HTML. diff --git a/docs/designers-developers/developers/internationalization.md b/docs/designers-developers/developers/internationalization.md new file mode 100644 index 00000000000000..602b7494880855 --- /dev/null +++ b/docs/designers-developers/developers/internationalization.md @@ -0,0 +1,33 @@ +# Internationalization + +## PHP + +WordPress has long offered a number of ways to create translatable strings in PHP. + +### Common methods + +- `__( $string_to_translate, $text_domain )` - translatable string wrapper, denoting translation namespace +- `_e( $string_to_translate, $text_domain )` - transltable string wrapper, with echo to print. +- `esc_html__( $string_to_translate, $text_domain )` - escapes and returns translation +- `esc_html_e( $string_to_translate, $text_domain )` - escapes, translates, and prints +- `_n( $singular, $plural, $number, $text_domain )` - Translatable singular/plural string, using %d to inject changing number digit. + +### More Resources + +- i18n for Developers - Covers numbers in translatable strings, best practices. +- WP-CLI can be used to generate translation files. + +## JavaScript + +Historically, `wp_localize_script()` has been used to put server-side PHP data into a properly-escaped native JavaScript object. + +The new editor introduces a new approach to translating strings for the editor through a new package called `@wordpress/i18n` and a build tool for Babel called `@wordpress/babel-plugin-makepot` to create the necessary translation file (requires use of babel to compile code to extract the i18n methods). + +The new script package is registered with WordPress as `wp-i18n` and should be declared as a dependency during `wp_register_script()` and imported as a global off the Window object as `wp.i18n`. + +### Common methods in wp.i18n (may look similar) + +- `setLocaleData( data: Object, domain: string )` - Create new Jed instance providing translation data for a domain (probably writing this to the DOM in escaped in PHP function). +- `__( stringToTranslate, textDomain )` - translatable string wrapper, denoting translation namespace +- `_n( singular, plural, number, textDomain )` - Translatable singular/plural string, using %d to inject changing number digit. +- `_x( singular, plural, number, textDomain )` - gettext equivalent for translation diff --git a/docs/packages.md b/docs/designers-developers/developers/packages.md similarity index 73% rename from docs/packages.md rename to docs/designers-developers/developers/packages.md index ac767e389d18c9..cde5bf98553aeb 100644 --- a/docs/packages.md +++ b/docs/designers-developers/developers/packages.md @@ -1,4 +1,4 @@ -# packages +# Packages Gutenberg exposes a list of JavaScript packages and tools for WordPress development. @@ -7,5 +7,3 @@ Gutenberg exposes a list of JavaScript packages and tools for WordPress developm JavaScript packages are available as a registered script in WordPress and can be accessed using the `wp` global variable. All the packages are also available on [npm](https://www.npmjs.com/org/wordpress) if you want to bundle them in your code. - -<br/><br/><p align="center"><img src="https://s.w.org/style/images/codeispoetry.png?1" alt="Code is Poetry." /></p> \ No newline at end of file diff --git a/docs/designers-developers/developers/themes/README.md b/docs/designers-developers/developers/themes/README.md new file mode 100644 index 00000000000000..e2bc67c0599eaa --- /dev/null +++ b/docs/designers-developers/developers/themes/README.md @@ -0,0 +1,5 @@ +# Theming for Gutenberg + +The new editor provides a number of options for theme designers and developers, including theme-defined color settings, font size control, and much more. + +In this section, you'll learn about the ways that themes can customise the editor. diff --git a/docs/extensibility/theme-support.md b/docs/designers-developers/developers/themes/theme-support.md similarity index 88% rename from docs/extensibility/theme-support.md rename to docs/designers-developers/developers/themes/theme-support.md index db0b129608eca2..9c9a5cc63539c5 100644 --- a/docs/extensibility/theme-support.md +++ b/docs/designers-developers/developers/themes/theme-support.md @@ -1,5 +1,15 @@ # Theme Support +The new Blocks include baseline support in all themes, enhancements to opt-in to and the ability to extend and customize. + +There are a few new concepts to consider when building themes: + +- **Editor Color Palette** - A default set of colors is provided, but themes and register their own and optionally lock users into picking from the defined palette. +- **Editor Text Size Palette** - A default set of sizes is provided, but themes and register their own and optionally lock users into picking from preselected sizes. +- **Responsive Embeds** - Themes must opt-in to responsive embeds. +- **Frontend & Editor Styles** - To get the most out of blocks, theme authors will want to make sure Core styles look good and opt-in, or write their own styles to best fit their theme. +- **Dark Mode** - If a Theme is a Dark Theme with a dark background containing light text, the theme author can opt-in to the Dark Mode. + By default, blocks provide their styles to enable basic support for blocks in themes without any change. Themes can add/override these styles, or they can provide no styles at all, and rely fully on what the blocks provide. Some advanced block features require opt-in support in the theme itself as it's difficult for the block to provide these styles, they may require some architecting of the theme itself, in order to work well. @@ -57,7 +67,7 @@ Here's the markup for an `Image` with a caption: ```html <figure class="wp-block-image"> - <img src="..." alt="" width="200px"> + <img src="..." alt="" width="200px" /> <figcaption>Short image caption.</figcaption> </figure> ``` @@ -67,7 +77,7 @@ Here's the markup for a left-floated image: ```html <div class="wp-block-image"> <figure class="alignleft"> - <img src="..." alt="" width="200px"> + <img src="..." alt="" width="200px" /> <figcaption>Short image caption.</figcaption> </figure> </div> @@ -118,13 +128,12 @@ Themes are responsible for creating the classes that apply the colors in differe } ``` -The class name is built appending 'has-', followed by the class name *using* kebab case and ending with the context name. +The class name is built appending 'has-', followed by the class name _using_ kebab case and ending with the context name. ### Block Font Sizes: Blocks may allow the user to configure the font sizes they use, e.g., the paragraph block. Gutenberg provides a default set of font sizes, but a theme can overwrite it and provide its own: - ```php add_theme_support( 'editor-font-sizes', array( array( @@ -157,13 +166,13 @@ add_theme_support( 'editor-font-sizes', array( The font sizes are rendered on the font size picker in the order themes provide them. Themes are responsible for creating the classes that apply the correct font size styles. -The class name is built appending 'has-', followed by the font size name *using* kebab case and ending with `-font-size`. +The class name is built appending 'has-', followed by the font size name _using_ kebab case and ending with `-font-size`. As an example for the regular font size, a theme may provide the following class. ```css .has-regular-font-size { - font-size: 16px; + font-size: 16px; } ``` @@ -262,9 +271,7 @@ add_theme_support( 'wp-block-styles' ); The embed blocks automatically apply styles to embedded content to reflect the aspect ratio of content that is embedded in an iFrame. A block styled with the aspect ratio responsive styles would look like: ```html -<figure class="wp-embed-aspect-16-9 wp-has-aspect-ratio"> - ... -</figure> +<figure class="wp-embed-aspect-16-9 wp-has-aspect-ratio">...</figure> ``` To make the content resize and keep its aspect ratio, the `<body>` element needs the `wp-embed-responsive` class. This is not set by default, and requires the theme to opt in to the `responsive-embeds` feature: diff --git a/docs/blocks/applying-styles-with-stylesheets.md b/docs/designers-developers/developers/tutorials/block-tutorial/applying-styles-with-stylesheets.md similarity index 100% rename from docs/blocks/applying-styles-with-stylesheets.md rename to docs/designers-developers/developers/tutorials/block-tutorial/applying-styles-with-stylesheets.md diff --git a/docs/blocks/block-controls-toolbars-and-inspector.md b/docs/designers-developers/developers/tutorials/block-tutorial/block-controls-toolbars-and-inspector.md similarity index 100% rename from docs/blocks/block-controls-toolbars-and-inspector.md rename to docs/designers-developers/developers/tutorials/block-tutorial/block-controls-toolbars-and-inspector.md diff --git a/docs/blocks/creating-dynamic-blocks.md b/docs/designers-developers/developers/tutorials/block-tutorial/creating-dynamic-blocks.md similarity index 100% rename from docs/blocks/creating-dynamic-blocks.md rename to docs/designers-developers/developers/tutorials/block-tutorial/creating-dynamic-blocks.md diff --git a/docs/blocks/generate-blocks-with-wp-cli.md b/docs/designers-developers/developers/tutorials/block-tutorial/generate-blocks-with-wp-cli.md similarity index 100% rename from docs/blocks/generate-blocks-with-wp-cli.md rename to docs/designers-developers/developers/tutorials/block-tutorial/generate-blocks-with-wp-cli.md diff --git a/docs/blocks/inspector.png b/docs/designers-developers/developers/tutorials/block-tutorial/inspector.png similarity index 100% rename from docs/blocks/inspector.png rename to docs/designers-developers/developers/tutorials/block-tutorial/inspector.png diff --git a/docs/blocks.md b/docs/designers-developers/developers/tutorials/block-tutorial/intro.md similarity index 100% rename from docs/blocks.md rename to docs/designers-developers/developers/tutorials/block-tutorial/intro.md diff --git a/docs/blocks/introducing-attributes-and-editable-fields.md b/docs/designers-developers/developers/tutorials/block-tutorial/introducing-attributes-and-editable-fields.md similarity index 96% rename from docs/blocks/introducing-attributes-and-editable-fields.md rename to docs/designers-developers/developers/tutorials/block-tutorial/introducing-attributes-and-editable-fields.md index 7f96bd78986798..3a993ce1c2b71a 100644 --- a/docs/blocks/introducing-attributes-and-editable-fields.md +++ b/docs/designers-developers/developers/tutorials/block-tutorial/introducing-attributes-and-editable-fields.md @@ -110,7 +110,7 @@ registerBlockType( 'gutenberg-boilerplate-esnext/hello-world-step-03', { ``` {% end %} -When registering a new block type, the `attributes` property describes the shape of the attributes object you'd like to receive in the `edit` and `save` functions. Each value is a [source function](../../docs/block-api/attributes.md) to find the desired value from the markup of the block. +When registering a new block type, the `attributes` property describes the shape of the attributes object you'd like to receive in the `edit` and `save` functions. Each value is a [source function](../../../../../docs/designers-developers/developers/block-api/block-attributes.md) to find the desired value from the markup of the block. In the code snippet above, when loading the editor, we will extract the `content` value as the HTML of the paragraph element in the saved post's markup. diff --git a/docs/blocks/writing-your-first-block-type.md b/docs/designers-developers/developers/tutorials/block-tutorial/writing-your-first-block-type.md similarity index 100% rename from docs/blocks/writing-your-first-block-type.md rename to docs/designers-developers/developers/tutorials/block-tutorial/writing-your-first-block-type.md diff --git a/docs/reference/faq.md b/docs/designers-developers/faq.md similarity index 97% rename from docs/reference/faq.md rename to docs/designers-developers/faq.md index 740c641f7914e5..547395acb1eb23 100644 --- a/docs/reference/faq.md +++ b/docs/designers-developers/faq.md @@ -251,7 +251,7 @@ Our [list of supported browsers can be found in the Make WordPress handbook](htt ## How do I make my own block? -The API for creating blocks is a crucial aspect of the project. We are working on improved documentation and tutorials. Check out the [Creating Block Types](../../docs/blocks.md) section to get started. +The API for creating blocks is a crucial aspect of the project. We are working on improved documentation and tutorials. Check out the [Creating Block Types](../../docs/designers-developers/developers/tutorials/block-tutorial/intro.md) section to get started. ## Does Gutenberg involve editing posts/pages in the front-end? @@ -297,7 +297,7 @@ Blocks will be able to provide base structural CSS styles, and themes can add st Other features, like the new _wide_ and _full-wide_ alignment options, will simply be CSS classes applied to blocks that offer this alignment. We are looking at how a theme can opt in to this feature, for example using `add_theme_support`. -*See:* [Theme Support](../../docs/extensibility/theme-support.md) +*See:* [Theme Support](../../docs/designers-developers/developers/themes/theme-support.md) ## How will editor styles work? @@ -310,7 +310,7 @@ function gutenbergtheme_editor_styles() { add_action( 'enqueue_block_editor_assets', 'gutenbergtheme_editor_styles' ); ``` -*Details:* [Editor Styles](../../docs/extensibility/theme-support.md#editor-styles) +*See:* [Editor Styles](../../docs/designers-developers/developers/themes/theme-support.md#editor-styles) ## Should I be concerned that Gutenberg will make my plugin obsolete? @@ -333,11 +333,13 @@ Custom TinyMCE buttons will still work in the “Classic” block, which is a bl (Gutenberg comes with a new universal inserter tool, which gives you access to every block available, searchable, sorted by recency and categories. This inserter tool levels the playing field for every plugin that adds content to the editor, and provides a single interface to learn how to use.) ## How will shortcodes work in Gutenberg? + Shortcodes will continue to work as they do now. However we see the block as an evolution of the `[shortcode]`. Instead of having to type out code, you can use the universal inserter tray to pick a block and get a richer interface for both configuring the block and previewing it. We would recommend people eventually upgrade their shortcodes to be blocks. ## Should I move shortcodes to content blocks? + We think so. Blocks are designed to be visually representative of the final look, and they will likely become the expected way in which users will discover and insert content in WordPress. ## Will Gutenberg be made properly accessible? @@ -346,19 +348,13 @@ Accessibility is not an afterthought. Not every aspect of Gutenberg is accessibl If you would like to contribute to the accessibility of Gutenberg, we can always use more people to test and contribute. -## Are there any design resources for Gutenberg? - -Yes, primarily in [design principles](../../docs/reference/design-principles.md) - -We are still adding more documentation. - ## How is data stored? I've seen HTML comments, what is their purpose? Our approach—as outlined in [the technical overview introduction](https://make.wordpress.org/core/2017/01/17/editor-technical-overview/)—is to augment the existing data format in a way that doesn’t break the decade-and-a-half-fabric of content WordPress provides. In other terms, this optimizes for a format that prioritizes human readability (the HTML document of the web) and easy-to-render-anywhere over a machine convenient file (JSON in post-meta) that benefits the editing context primarily. This also [gives us the flexibility](https://github.com/WordPress/gutenberg/issues/1516) to store those blocks that are inherently separate from the content stream (reusable pieces like widgets or small post type elements) elsewhere, and just keep token references for their placement. -We suggest you look at the [language of Gutenberg](../../docs/language.md) to learn more about how this aspect of the project works. +We suggest you look at the [Gutenberg key concepts](../../docs/designers-developers/key-concepts.md) to learn more about how this aspect of the project works. ## How can I parse the post content back out into blocks in PHP or JS? In JS: diff --git a/docs/reference/glossary.md b/docs/designers-developers/glossary.md similarity index 65% rename from docs/reference/glossary.md rename to docs/designers-developers/glossary.md index f7818d9a785c23..afa574179db276 100644 --- a/docs/reference/glossary.md +++ b/docs/designers-developers/glossary.md @@ -1,15 +1,21 @@ # Glossary -- __Attribute sources__: An object describing the attributes shape of a block. The keys can be named as most appropriate to describe the state of a block type. The value for each key is a function which describes the strategy by which the attribute value should be extracted from the content of a saved post's content. When processed, a new object is created, taking the form of the keys defined in the attribute sources, where each value is the result of the attribute source function. -- __Attributes__: The object representation of the current state of a block in post content. When loading a saved post, this is determined by the attribute sources for the block type. These values can change over time during an editing session when the user modifies a block, and are used when determining how to serialize the block. -- __Block__: The abstract term used to describe units of markup that, composed together, form the content or layout of a webpage. The idea combines concepts of what in WordPress today we achieve with shortcodes, custom HTML, and embed discovery into a single consistent API and user experience. -- __Block name__: A unique identifier for a block type, consisting of a plugin-specific namespace and a short label describing the block's intent. e.g. `core/image` -- __Block type__: In contrast with the blocks composing a particular post, a block type describes the blueprint by which any block of that type should behave. So while there may be many images within a post, each behaves consistent with a unified image block type definition. -- __Dynamic block__: A type of block where the content of which may change and cannot be determined at the time of saving a post, instead calculated any time the post is shown on the front of a site. These blocks may save fallback content or no content at all in their JavaScript implementation, instead deferring to a PHP block implementation for runtime rendering. -- __RichText__: A common component enabling rich content editing including bold, italics, hyperlinks, etc. It is not too much unlike the single editor region of the legacy post editor, and is in fact powered by the same TinyMCE library. -- __Inspector__: A block settings region shown in place of the post settings when a block is selected. Fields may be shown here to allow the user to customize the selected block. -- __Post settings__: A sidebar region containing metadata fields for the post, including scheduling, visibility, terms, and featured image. -- __Serialization__: The process of converting a block's attributes object into HTML markup, typically occurring when saving the post. -- __Static block__: A type of block where the content of which is known at the time of saving a post. A static block will be saved with HTML markup directly in post content. -- __TinyMCE__: [TinyMCE](https://www.tinymce.com/) is a web-based JavaScript WYSIWYG (What You See Is What You Get) editor. -- __Toolbar__: A set of button controls. In the context of a block, usually referring to the toolbar of block controls shown above the selected block. +- **Attribute sources**: An object describing the attributes shape of a block. The keys can be named as most appropriate to describe the state of a block type. The value for each key is a function which describes the strategy by which the attribute value should be extracted from the content of a saved post's content. When processed, a new object is created, taking the form of the keys defined in the attribute sources, where each value is the result of the attribute source function. +- **Attributes**: The object representation of the current state of a block in post content. When loading a saved post, this is determined by the attribute sources for the block type. These values can change over time during an editing session when the user modifies a block, and are used when determining how to serialize the block. +- **Block**: The abstract term used to describe units of markup that, composed together, form the content or layout of a webpage. The idea combines concepts of what in WordPress today we achieve with shortcodes, custom HTML, and embed discovery into a single consistent API and user experience. +- **Block Categories**: These are not a WordPress taxonomy, but instead used internally to sort blocks in the Block Inserter. +- **Block Inserter**: Primary interface for selecting from the available blocks, triggered by plus icon buttons on Blocks or in the top-left of the editor interface. +- **Block name**: A unique identifier for a block type, consisting of a plugin-specific namespace and a short label describing the block's intent. e.g. `core/image` +- **Block type**: In contrast with the blocks composing a particular post, a block type describes the blueprint by which any block of that type should behave. So while there may be many images within a post, each behaves consistent with a unified image block type definition. +- **Classic block**: +- **Dynamic block**: A type of block where the content of which may change and cannot be determined at the time of saving a post, instead calculated any time the post is shown on the front of a site. These blocks may save fallback content or no content at all in their JavaScript implementation, instead deferring to a PHP block implementation for runtime rendering. +- **RichText**: A common component enabling rich content editing including bold, italics, hyperlinks, etc. It is not too much unlike the single editor region of the legacy post editor, and is in fact powered by the same TinyMCE library. +- **Inspector**: A block settings region shown in place of the post settings when a block is selected. Fields may be shown here to allow the user to customize the selected block. +- **Post settings**: A sidebar region containing metadata fields for the post, including scheduling, visibility, terms, and featured image. +- **Reusable block**: +- **Sidebar**: +- **Serialization**: The process of converting a block's attributes object into HTML markup, typically occurring when saving the post. +- **Static block**: A type of block where the content of which is known at the time of saving a post. A static block will be saved with HTML markup directly in post content. +- **TinyMCE**: [TinyMCE](https://www.tinymce.com/) is a web-based JavaScript WYSIWYG (What You See Is What You Get) editor. +- **Toolbar**: A set of button controls. In the context of a block, usually referring to the toolbar of block controls shown above the selected block. +- **Template**: diff --git a/docs/language.md b/docs/designers-developers/key-concepts.md similarity index 75% rename from docs/language.md rename to docs/designers-developers/key-concepts.md index 8a71e707619fbc..ce270c0d2ee816 100644 --- a/docs/language.md +++ b/docs/designers-developers/key-concepts.md @@ -1,4 +1,52 @@ -# The Language of Gutenberg +# Key Concepts + +## Blocks + +Blocks are an abstract unit for organizing and composing content, strung together to create content for a webpage. + +Blocks are hiearchical, in that a block can be a child or parent to another block. One example is a two-column Columns block can be the parent block to multiple child blocks in each column. + +If it helps, you can think of blocks as a more graceful shortcode, with rich formatting tools for users to compose content. To this point, there is a new Block Grammar. Distilled, the block grammar is an HTML comment, either a self-closing tag or with a begining tag and ending tag. In the main tag, depending on the block type and user customizations, there can be a JSON object. This raw form of the block is referred to as serialized. + +```html +<!-- wp:paragraph {"key": "value"} --> +<p>Welcome to the world of blocks.</p> +<!-- /wp:paragraph --> +``` + +Blocks can be static or dynamic. Static blocks contain rendered content and an object of Attributes used to re-render based on changes. Dynamic blocks require server-side data and rendering while the post content is being generated (rendering). + +Each block contains Attributes or configuration settings, which can be sourced from raw HTML in the content, via meta or other customizible origins. + +The Paragraph is the default Block. Instead of a new line upon typing return on a keyboard, try to think of it as an empty paragraph block (type / to trigger an autocompleting Slash Inserter -- /image will pull up Images as well as Instagram embeds). + +Users insert new blocks by clicking the plus button for the Block Inserter, typing / for the Slash Inserter or typing return for a blank Paragraph block. + +Blocks can be duplicated within content using the menu from the block's toolbar or via keyboard shortcut. + +Blocks can be made repeatable, shared across posts and post types and used multiple times in the same post. Changes in one place reflect everywhere that reusable block is used. + +Blocks can be limited or locked-in-place by Templates and custom code. + +#### More on Blocks + +- **Block API** +- **Block Styles** +- **Tutorial: Building A Custom Block** + +## Block Categories + +In the Block Inserter, the accordion-sorted, popup modal that shows a site's available blocks to users, each accordion title is a Block Category, which are either the defaults or customized by developers through Plugins or code. + +## Reusable Blocks + +Reusable Blocks are a way to share a block (or multiple blocks) as a reusable, repeatable piece of content. + +Any edits to a reusable block are made to every usage of a repeatable block. + +Reusable Blocks are stored as a hidden post type, and are dynamic blocks that "ref" or reference the post_id and return the post_content for that wp_block. + +## Templates At the core of Gutenberg lies the concept of the block. From a technical point of view, blocks both raise the level of abstraction from a single document to a collection of meaningful elements, and they replace ambiguity—inherent in HTML—with explicit structure. A post in Gutenberg is then a _collection of blocks_. @@ -8,7 +56,7 @@ This is true for content blocks. They are the way in which the user creates thei Content in WordPress is stored as HTML-like text in `post_content`. HTML is a robust document markup format and has been used to describe content as simple as unformatted paragraphs of text and as complex as entire application interfaces. Understanding HTML is not trivial; a significant number of existing documents and tools deal with technically invalid or ambiguous code. This code, even when valid, can be incredibly tricky and complicated to parse – and to understand. -The main point is to let the machines work at what they are good at, and optimize for the user and the document. The analogy with the printing press can be taken further in that what matters is the printed page, not the arrangement of metal type that originated it. As a matter of fact, the arrangement of type is a pretty inconvenient storage mechanism. The page is both the result _and_ the proper way to store the data. The metal type is just an instrument for publication and editing, but more ephemeral in nature. Exactly as our use of an object tree (e.g. JSON) in the editor. We have the ability to rebuild this structure from the printed page, as if we printed notations in the margins that allows a machine to know which [sorts](https://en.wikipedia.org/wiki/Sort_(typesetting)) (metal type) to assemble to recreate the page. +The main point is to let the machines work at what they are good at, and optimize for the user and the document. The analogy with the printing press can be taken further in that what matters is the printed page, not the arrangement of metal type that originated it. As a matter of fact, the arrangement of type is a pretty inconvenient storage mechanism. The page is both the result _and_ the proper way to store the data. The metal type is just an instrument for publication and editing, but more ephemeral in nature. Exactly as our use of an object tree (e.g. JSON) in the editor. We have the ability to rebuild this structure from the printed page, as if we printed notations in the margins that allows a machine to know which [sorts](<https://en.wikipedia.org/wiki/Sort_(typesetting)>) (metal type) to assemble to recreate the page. ## Blocks are higher-level than HTML @@ -20,7 +68,7 @@ Additionally, how do we even know this came from our editor? Maybe someone snuck ## The post dichotomy -A Gutenberg post is the proper block-aware representation of a post, a collection of semantically consistent descriptions of what each block is and what its essential data is. This representation only ever exists in memory. It is the [chase](https://en.wikipedia.org/wiki/Chase_(printing)) in the typesetter's workshop, ever-shifting as sorts are attached and repositioned. +A Gutenberg post is the proper block-aware representation of a post, a collection of semantically consistent descriptions of what each block is and what its essential data is. This representation only ever exists in memory. It is the [chase](<https://en.wikipedia.org/wiki/Chase_(printing)>) in the typesetter's workshop, ever-shifting as sorts are attached and repositioned. A Gutenberg post is not the artifact it produces, namely the `post_content`. The latter is the printed page, optimized for the reader, but retaining its invisible markings for later editing. @@ -34,25 +82,21 @@ The tree of objects describes the list of blocks that compose a post. ```js [ - { - type: 'core/cover-image', - attributes: { - url: 'my-hero.jpg', - align: 'full', - hasParallax: false, - hasBackgroundDim: true - }, - children: [ - "Gutenberg posts aren't HTML" - ] - }, - { - type: 'core/paragraph', - children: [ - "Lately I've been hearing plen…" - ] - } -] + { + type: "core/cover-image", + attributes: { + url: "my-hero.jpg", + align: "full", + hasParallax: false, + hasBackgroundDim: true + }, + children: ["Gutenberg posts aren't HTML"] + }, + { + type: "core/paragraph", + children: ["Lately I've been hearing plen…"] + } +]; ``` ## Serialization and the Purpose of HTML Comments @@ -79,7 +123,7 @@ After running this through the parser we're left with a simple object we can man This has dramatic implications for how simple and performant we can make our parser. These explicit boundaries also protect damage in a single block from bleeding into other blocks or tarnishing the entire document. It also allows the system to identify unrecognized blocks before rendering them. -_N.B.:_ The defining aspect of blocks are their semantics and the isolation mechanism they provide; in other words, their identity. On the other hand, where their data is stored is a more liberal aspect. Blocks support more than just static local data (via JSON literals inside the HTML comment or within the block's HTML), and more mechanisms (_e.g._, global blocks or otherwise resorting to storage in complementary `WP_Post` objects) are expected. See [attributes](../docs/block-api/attributes.md) for details. +_N.B.:_ The defining aspect of blocks are their semantics and the isolation mechanism they provide; in other words, their identity. On the other hand, where their data is stored is a more liberal aspect. Blocks support more than just static local data (via JSON literals inside the HTML comment or within the block's HTML), and more mechanisms (_e.g._, global blocks or otherwise resorting to storage in complementary `WP_Post` objects) are expected. See [attributes](../../docs/designers-developers/developers/block-api/block-attributes.md) for details. ## The Anatomy of a Serialized Block @@ -87,9 +131,7 @@ When blocks are saved to the content, after the editing session, its attributes ```html <!-- wp:image --> -<figure class="wp-block-image"> - <img src="source.jpg" alt="" /> -</figure> +<figure class="wp-block-image"><img src="source.jpg" alt="" /></figure> <!-- /wp:image --> ``` diff --git a/docs/designers-developers/readme.md b/docs/designers-developers/readme.md new file mode 100644 index 00000000000000..5e54798a3c7873 --- /dev/null +++ b/docs/designers-developers/readme.md @@ -0,0 +1,11 @@ +# Designer & Developer Handbook + +Gutenberg is a transformation of the WordPress editor for working with content. + +![Gutenberg Demo](https://cldup.com/kZXGDcGPMU.gif) + +Using a system of Blocks to compose and format content, the new block-based editor is designed to create rich, flexible layouts for websites and digital products. Content is created in the unit of blocks instead of freeform text with inserted media, embeds and Shortcodes (there's a Shortcode block though). + +Blocks treat Paragraphs, Headings, Media, Embeds all as components that strung together make up the content stored in the WordPress database, replacing the traditional concept of freeform text with embeded media and shortcodes. The new editor is designed with progressive enhancement, meaning it is back-compatible with all legacy content, offers a process to try to convert and split a Classic block into block equivalents using client-side parsing and finally the blocks offer enhanced editing and format controls. + +The Editor offers rich new value to users with visual, drag-and-drop creation tools and powerful developer enhancements with modern vendor packages, reusable components, rich APIs and hooks to modify and extend the editor through Custom Blocks, Custom Block Styles and Plugins. diff --git a/docs/extensibility.md b/docs/extensibility.md deleted file mode 100644 index ab6f8c8db6361b..00000000000000 --- a/docs/extensibility.md +++ /dev/null @@ -1,82 +0,0 @@ -# Extensibility - -Extensibility is key for WordPress and, like the rest of WordPress components, Gutenberg is highly extensible. - - -## Creating Blocks - -Gutenberg is about blocks, and the main extensibility API of Gutenberg is the Block API. It allows you to create your own static blocks, dynamic blocks rendered on the server and also blocks capable of saving data to Post Meta for more structured content. - -Here is a small example of a static custom block type (you can try it in your browser's console): - -{% codetabs %} -{% ES5 %} -```js -var el = wp.element.createElement; - -wp.blocks.registerBlockType( 'mytheme/red-block', { - title: 'Red Block', - icon: 'universal-access-alt', - category: 'layout', - edit: function() { - return el( 'div', { style: { backgroundColor: '#900', color: '#fff', padding: '20px' } }, 'I am a red block.' ); - }, - save: function() { - return el( 'div', { style: { backgroundColor: '#900', color: '#fff', padding: '20px' } }, 'I am a red block.' ); - } -} ); -``` -{% ESNext %} -```js -const { registerBlockType } = wp.blocks; -const blockStyle = { backgroundColor: '#900', color: '#fff', padding: '20px' }; - -registerBlockType( 'mytheme/red-block', { - title: 'Red Block', - icon: 'universal-access-alt', - category: 'layout', - edit: function() { - return <div style={ blockStyle }>I am a red block</div> - }, - save: function() { - return <div style={ blockStyle }>I am a red block</div> - } -} ); -``` -{% end %} - -If you want to learn more about block creation, the [Blocks Tutorial](../docs/blocks.md) is the best place to start. - -## Extending Blocks - -It is also possible to modify the behavior of existing blocks or even remove them completely using filters. - -Learn more in the [Extending Blocks](../docs/extensibility/extending-blocks.md) section. - -## Extending the Editor UI - -Extending the editor UI can be accomplished with the `registerPlugin` API, allowing you to define all your plugin's UI elements in one place. - -Refer to the [Plugins](https://github.com/WordPress/gutenberg/blob/master/packages/plugins/README.md) and [Edit Post](https://github.com/WordPress/gutenberg/blob/master/packages/edit-post/README.md) section for more information. - -## Meta Boxes - -**Porting PHP meta boxes to blocks is highly encouraged!** - -Discover how [Meta Box](../docs/extensibility/meta-box.md) support works in Gutenberg. - -## Theme Support - -By default, blocks provide their styles to enable basic support for blocks in themes without any change. Themes can add/override these styles, or rely on defaults. - -There are some advanced block features which require opt-in support in the theme. See [theme support](../docs/extensibility/theme-support.md). - -## Autocomplete - -Autocompleters within blocks may be extended and overridden. See [autocomplete](../docs/extensibility/autocomplete.md). - -## Block Parsing and Serialization - -Posts in the editor move through a couple of different stages between being stored in `post_content` and appearing in the editor. Since the blocks themselves are data structures that live in memory it takes a parsing and serialization step to transform out from and into the stored format in the database. - -Customizing the parser is an advanced topic that you can learn more about in the [Extending the Parser](../docs/extensibility/parser.md) section. diff --git a/docs/extensibility/extending-editor.md b/docs/extensibility/extending-editor.md deleted file mode 100644 index ddb874682ab168..00000000000000 --- a/docs/extensibility/extending-editor.md +++ /dev/null @@ -1,21 +0,0 @@ -# Extending Editor (Experimental) - -[Hooks](https://developer.wordpress.org/plugins/hooks/) are a way for one piece of code to interact/modify another piece of code. They make up the foundation for how plugins and themes interact with Gutenberg, but they’re also used extensively by WordPress Core itself. There are two types of hooks: [Actions](https://developer.wordpress.org/plugins/hooks/actions/) and [Filters](https://developer.wordpress.org/plugins/hooks/filters/). They were initially implemented in PHP, but for the purpose of Gutenberg they were ported to JavaScript and published to npm as [@wordpress/hooks](https://www.npmjs.com/package/@wordpress/hooks) package for general purpose use. You can also learn more about both APIs: [PHP](https://codex.wordpress.org/Plugin_API/) and [JavaScript](https://github.com/WordPress/packages/tree/master/packages/hooks). - -## Modifying Editor - -To modify the behavior of the editor experience, Gutenberg exposes the following Filters: - -### `editor.PostFeaturedImage.imageSize` - -Used to modify the image size displayed in the Post Featured Image component. It defaults to `'post-thumbnail'`, and will fail back to the `full` image size when the specified image size doesn't exist in the media object. It's modeled after the `admin_post_thumbnail_size` filter in the Classic Editor. - -_Example:_ - -```js -var withImageSize = function( size, mediaId, postId ) { - return 'large'; -}; - -wp.hooks.addFilter( 'editor.PostFeaturedImage.imageSize', 'my-plugin/with-image-size', withImageSize ); -``` diff --git a/docs/manifest.json b/docs/manifest.json index 21c5a5c12d6592..c461c5d3b9a361 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -1,260 +1,182 @@ [ { - "title": "Introduction", - "slug": "handbook", + "title": "Gutenberg Handbook", + "slug": "readme", "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/readme.md", "parent": null }, { - "title": "The Language of Gutenberg", - "slug": "language", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/language.md", + "title": "Designer & Developer Handbook", + "slug": "designers-developers", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/README.md", "parent": null }, { - "title": "The Gutenberg block grammar", - "slug": "grammar", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/grammar.md", - "parent": "language" + "title": "Key Concepts", + "slug": "key-concepts", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/key-concepts.md", + "parent": "designers-developers" }, { - "title": "Block API", + "title": "Developer Documentation", + "slug": "developers", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/README.md", + "parent": "designers-developers" + }, + { + "title": "Block API Reference", "slug": "block-api", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/block-api.md", - "parent": null + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/block-api/README.md", + "parent": "designers-developers/developers" }, { - "title": "Attributes", - "slug": "attributes", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/block-api/attributes.md", - "parent": "block-api" + "title": "Block Registration ", + "slug": "block-registration", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/block-api/block-registration.md", + "parent": "designers-developers/developers/block-api" }, { "title": "Edit and Save", "slug": "block-edit-save", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/block-api/block-edit-save.md", - "parent": "block-api" + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/block-api/block-edit-save.md", + "parent": "designers-developers/developers/block-api" }, { - "title": "RichText API", - "slug": "rich-text-api", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/packages/editor/src/components/rich-text/README.md", - "parent": "block-api" + "title": "Attributes", + "slug": "block-attributes", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/block-api/block-attributes.md", + "parent": "designers-developers/developers/block-api" }, { "title": "Deprecated Blocks", - "slug": "deprecated-blocks", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/block-api/deprecated-blocks.md", - "parent": "block-api" + "slug": "block-deprecation", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/block-api/block-deprecation.md", + "parent": "designers-developers/developers/block-api" }, { "title": "Templates", - "slug": "templates", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/templates.md", - "parent": null + "slug": "block-templates", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/block-api/block-templates.md", + "parent": "designers-developers/developers/block-api" }, { - "title": "Extensibility", - "slug": "extensibility", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/extensibility.md", - "parent": null + "title": "Annotations", + "slug": "block-annotations", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/block-api/block-annotations.md", + "parent": "designers-developers/developers/block-api" }, { - "title": "Extending Blocks", - "slug": "extending-blocks", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/extensibility/extending-blocks.md", - "parent": "extensibility" + "title": "Filter Reference", + "slug": "filters", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/filters/README.md", + "parent": "designers-developers/developers" }, { - "title": "Extending Editor", - "slug": "extending-editor", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/extensibility/extending-editor.md", - "parent": "extensibility" + "title": "Block Filters", + "slug": "block-filters", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/filters/block-filters.md", + "parent": "designers-developers/developers/filters" }, { - "title": "Meta Boxes", - "slug": "meta-box", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/extensibility/meta-box.md", - "parent": "extensibility" + "title": "Editor Filters (Experimental)", + "slug": "editor-filters", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/filters/editor-filters.md", + "parent": "designers-developers/developers/filters" }, { - "title": "Theme Support", - "slug": "theme-support", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/extensibility/theme-support.md", - "parent": "extensibility" + "title": "Parser Filters", + "slug": "parser-filters", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/filters/parser-filters.md", + "parent": "designers-developers/developers/filters" }, { "title": "Autocomplete", - "slug": "autocomplete", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/extensibility/autocomplete.md", - "parent": "extensibility" + "slug": "autocomplete-filters", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/filters/autocomplete-filters.md", + "parent": "designers-developers/developers/filters" }, { - "title": "Annotations", - "slug": "annotations", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/extensibility/annotations.md", - "parent": "extensibility" - }, - { - "title": "Design", - "slug": "design", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/design.md", - "parent": null - }, - { - "title": "Patterns", - "slug": "design-patterns", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/design/design-patterns.md", - "parent": "design" + "title": "Data Module Reference", + "slug": "data", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/data/README.md", + "parent": "designers-developers/developers" }, { - "title": "Block Design", - "slug": "block-design", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/design/block-design.md", - "parent": "design" + "title": "Packages", + "slug": "packages", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/packages.md", + "parent": "designers-developers/developers" }, { - "title": "Resources", - "slug": "design-resources", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/design/design-resources.md", - "parent": "design" + "title": "Theming for Gutenberg", + "slug": "themes", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/themes/README.md", + "parent": "designers-developers/developers" }, { - "title": "Creating Block Types", - "slug": "blocks", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/blocks.md", - "parent": null + "title": "Theme Support", + "slug": "theme-support", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/themes/theme-support.md", + "parent": "designers-developers/developers/themes" }, { - "title": "Writing Your First Block Type", - "slug": "writing-your-first-block-type", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/blocks/writing-your-first-block-type.md", - "parent": "blocks" + "title": "Backwards Compatibility", + "slug": "backwards-compatibility", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/backwards-compatibility/README.md", + "parent": "designers-developers/developers" }, { - "title": "Applying Styles With Stylesheets", - "slug": "applying-styles-with-stylesheets", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/blocks/applying-styles-with-stylesheets.md", - "parent": "blocks" + "title": "Deprecations", + "slug": "deprecations", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/backwards-compatibility/deprecations.md", + "parent": "designers-developers/developers/backwards-compatibility" }, { - "title": "Introducing Attributes and Editable Fields", - "slug": "introducing-attributes-and-editable-fields", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/blocks/introducing-attributes-and-editable-fields.md", - "parent": "blocks" + "title": "Meta Boxes", + "slug": "meta-box", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/backwards-compatibility/meta-box.md", + "parent": "designers-developers/developers/backwards-compatibility" }, { - "title": "Block Controls: Toolbars and Inspector", - "slug": "block-controls-toolbars-and-inspector", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/blocks/block-controls-toolbars-and-inspector.md", - "parent": "blocks" + "title": "Designer Documentation", + "slug": "designers", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/designers/README.md", + "parent": "designers-developers" }, { - "title": "Creating dynamic blocks", - "slug": "creating-dynamic-blocks", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/blocks/creating-dynamic-blocks.md", - "parent": "blocks" + "title": "Block Design", + "slug": "block-design", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/designers/block-design.md", + "parent": "designers-developers/designers" }, { - "title": "Generate Blocks with WP-CLI", - "slug": "generate-blocks-with-wp-cli", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/blocks/generate-blocks-with-wp-cli.md", - "parent": "blocks" + "title": "Patterns", + "slug": "design-patterns", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/designers/design-patterns.md", + "parent": "designers-developers/designers" }, { - "title": "Reference", - "slug": "reference", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/reference.md", - "parent": null + "title": "Resources", + "slug": "design-resources", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/designers/design-resources.md", + "parent": "designers-developers/designers" }, { "title": "Glossary", "slug": "glossary", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/reference/glossary.md", - "parent": "reference" - }, - { - "title": "History", - "slug": "history", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/reference/history.md", - "parent": "reference" - }, - { - "title": "Coding Guidelines", - "slug": "coding-guidelines", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/reference/coding-guidelines.md", - "parent": "reference" - }, - { - "title": "Testing Overview", - "slug": "testing-overview", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/reference/testing-overview.md", - "parent": "reference" + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/glossary.md", + "parent": "designers-developers" }, { "title": "Frequently Asked Questions", "slug": "faq", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/reference/faq.md", - "parent": "reference" - }, - { - "title": "Release Process", - "slug": "release", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/reference/release.md", - "parent": "reference" - }, - { - "title": "Scripts", - "slug": "scripts", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/reference/scripts.md", - "parent": "reference" - }, - { - "title": "Deprecated Features", - "slug": "deprecated", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/reference/deprecated.md", - "parent": "reference" - }, - { - "title": "Repository Management", - "slug": "repository-management", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/reference/repository-management.md", - "parent": "reference" - }, - { - "title": "Outreach", - "slug": "outreach", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/outreach.md", - "parent": null - }, - { - "title": "Articles", - "slug": "articles", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/outreach/docs/articles.md", - "parent": "outreach" - }, - { - "title": "Meetups", - "slug": "meetups", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/outreach/meetups.md", - "parent": "outreach" - }, - { - "title": "Talks", - "slug": "talks", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/outreach/talks.md", - "parent": "outreach" - }, - { - "title": "Resources", - "slug": "resources", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/outreach/resources.md", - "parent": "outreach" + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/faq.md", + "parent": "designers-developers" }, { "title": "Packages", "slug": "packages", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/packages.md", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/packages.md", "parent": null }, { @@ -938,55 +860,55 @@ { "title": "Data Package Reference", "slug": "data", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/data/README.md", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/data/README.md", "parent": null }, { "title": "WordPress Core Data", "slug": "data-core", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/data/data-core.md", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/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", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/data/data-core-annotations.md", "parent": "data" }, { "title": "Block Types Data", "slug": "data-core-blocks", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/data/data-core-blocks.md", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/data/data-core-blocks.md", "parent": "data" }, { "title": "The Editor’s Data", "slug": "data-core-editor", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/data/data-core-editor.md", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/data/data-core-editor.md", "parent": "data" }, { "title": "The Editor’s UI Data", "slug": "data-core-edit-post", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/data/data-core-edit-post.md", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/data/data-core-edit-post.md", "parent": "data" }, { "title": "Notices Data", "slug": "data-core-notices", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/data/data-core-notices.md", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/data/data-core-notices.md", "parent": "data" }, { "title": "The NUX (New User Experience) Data", "slug": "data-core-nux", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/data/data-core-nux.md", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/data/data-core-nux.md", "parent": "data" }, { "title": "The Viewport Data", "slug": "data-core-viewport", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/data/data-core-viewport.md", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/data/data-core-viewport.md", "parent": "data" } ] \ No newline at end of file diff --git a/docs/readme.md b/docs/readme.md index 4d1c74172ec236..93b58f0b17cda7 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -1,11 +1,21 @@ -# Introduction +# Gutenberg Handbook -Gutenberg began as a transformation of the WordPress editor — a new interface for adding, editing, and manipulating content. It seeks to make it easy for anyone to create rich, flexible content layouts with a block-based UI. All types of page components are represented as modular blocks, which means they can be accessed from a unified block menu, dropped anywhere on a page, and directly edited to create the custom presentation the user wants. +The Gutenberg project provides three sources of documentation: -It is a fundamental modernization and transformation of how the WordPress experience works, creating new opportunities for both users and developers. Gutenberg introduces new frameworks, interaction patterns, functionality, and user experiences for WordPress. And similar to a new macOS version, we will talk about “Gutenberg”, and all the new possibilities it enables, until eventually the idea of Gutenberg as a separate entity will fade and it will simply be WordPress. +## Designer & Developer Handbook -![Gutenberg Demo](https://cldup.com/kZXGDcGPMU.gif) +Learn how to build blocks and extend the editor, best practices for designing block interfaces, and how to create themes that make the most of the new features Gutenberg provides. -Gutenberg brings many changes to WordPress, but the biggest impact comes from the way it can enable a much clearer product architecture — one which enables modularity, consistency, and interoperability — and the positive impact that can have on the end user experience of WordPress. This handbook will describe the scope of those architectural and user experience (UX) changes, including the central “block as the interface” principle — the most crucial conceptual change to understand about Gutenberg. +[Visit the Designer & Developer Handbook](../docs/designers-developers/readme.md) -Here you can also find design guidelines, API documentation, and tutorials about getting started with Gutenberg development. +## User Handbook + +Discover the new features Gutenberg offers, learn how your site will be affected by the new editor and how to keep using the old interface, and tips for creating beautiful posts and pages. + +[Visit the User Handbook](../docs/users/readme.md) + +## Contributor Handbook + +Help make Gutenberg better by contributing ideas, code, testing, and more. + +[Visit the Contributor Handbook](../docs/contributors/readme.md) diff --git a/docs/root-manifest.json b/docs/root-manifest.json index 1202ab14eacb4a..784008dd17bb50 100644 --- a/docs/root-manifest.json +++ b/docs/root-manifest.json @@ -1,254 +1,176 @@ [ - { - "title": "Introduction", - "slug": "handbook", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/readme.md", - "parent": null - }, - { - "title": "The Language of Gutenberg", - "slug": "language", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/language.md", - "parent": null - }, - { - "title": "The Gutenberg block grammar", - "slug": "grammar", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/grammar.md", - "parent": "language" - }, - { - "title": "Block API", - "slug": "block-api", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/block-api.md", - "parent": null - }, - { - "title": "Attributes", - "slug": "attributes", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/block-api\/attributes.md", - "parent": "block-api" - }, - { - "title": "Edit and Save", - "slug": "block-edit-save", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/block-api\/block-edit-save.md", - "parent": "block-api" - }, - { - "title": "RichText API", - "slug": "rich-text-api", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/packages\/editor\/src\/components\/rich-text\/README.md", - "parent": "block-api" - }, - { - "title": "Deprecated Blocks", - "slug": "deprecated-blocks", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/block-api\/deprecated-blocks.md", - "parent": "block-api" - }, - { - "title": "Templates", - "slug": "templates", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/templates.md", - "parent": null - }, - { - "title": "Extensibility", - "slug": "extensibility", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/extensibility.md", - "parent": null - }, - { - "title": "Extending Blocks", - "slug": "extending-blocks", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/extensibility/extending-blocks.md", - "parent": "extensibility" - }, - { - "title": "Extending Editor", - "slug": "extending-editor", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/extensibility/extending-editor.md", - "parent": "extensibility" - }, - { - "title": "Meta Boxes", - "slug": "meta-box", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/extensibility/meta-box.md", - "parent": "extensibility" - }, - { - "title": "Theme Support", - "slug": "theme-support", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/extensibility/theme-support.md", - "parent": "extensibility" - }, - { - "title": "Autocomplete", - "slug": "autocomplete", - "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", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/design.md", - "parent": null - }, - { - "title": "Patterns", - "slug": "design-patterns", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/design\/design-patterns.md", - "parent": "design" - }, - { - "title": "Block Design", - "slug": "block-design", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/design\/block-design.md", - "parent": "design" - }, - { - "title": "Resources", - "slug": "design-resources", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/design\/design-resources.md", - "parent": "design" - }, - { - "title": "Creating Block Types", - "slug": "blocks", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/blocks.md", - "parent": null - }, - { - "title": "Writing Your First Block Type", - "slug": "writing-your-first-block-type", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/blocks\/writing-your-first-block-type.md", - "parent": "blocks" - }, - { - "title": "Applying Styles With Stylesheets", - "slug": "applying-styles-with-stylesheets", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/blocks\/applying-styles-with-stylesheets.md", - "parent": "blocks" - }, - { - "title": "Introducing Attributes and Editable Fields", - "slug": "introducing-attributes-and-editable-fields", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/blocks\/introducing-attributes-and-editable-fields.md", - "parent": "blocks" - }, - { - "title": "Block Controls: Toolbars and Inspector", - "slug": "block-controls-toolbars-and-inspector", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/blocks\/block-controls-toolbars-and-inspector.md", - "parent": "blocks" - }, - { - "title": "Creating dynamic blocks", - "slug": "creating-dynamic-blocks", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/blocks\/creating-dynamic-blocks.md", - "parent": "blocks" - }, - { - "title": "Generate Blocks with WP-CLI", - "slug": "generate-blocks-with-wp-cli", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/blocks\/generate-blocks-with-wp-cli.md", - "parent": "blocks" - }, - { - "title": "Reference", - "slug": "reference", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/reference.md", - "parent": null - }, - { - "title": "Glossary", - "slug": "glossary", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/reference\/glossary.md", - "parent": "reference" - }, - { - "title": "History", - "slug": "history", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/reference\/history.md", - "parent": "reference" - }, - { - "title": "Coding Guidelines", - "slug": "coding-guidelines", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/reference\/coding-guidelines.md", - "parent": "reference" - }, - { - "title": "Testing Overview", - "slug": "testing-overview", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/reference\/testing-overview.md", - "parent": "reference" - }, - { - "title": "Frequently Asked Questions", - "slug": "faq", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/reference\/faq.md", - "parent": "reference" - }, - { - "title": "Release Process", - "slug": "release", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/reference\/release.md", - "parent": "reference" - }, - { - "title": "Scripts", - "slug": "scripts", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/reference\/scripts.md", - "parent": "reference" - }, - { - "title": "Deprecated Features", - "slug": "deprecated", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/reference\/deprecated.md", - "parent": "reference" - }, - { - "title": "Repository Management", - "slug": "repository-management", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/reference\/repository-management.md", - "parent": "reference" - }, - { - "title": "Outreach", - "slug": "outreach", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/outreach.md", - "parent": null - }, - { - "title": "Articles", - "slug": "articles", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/outreach\/docs\/articles.md", - "parent": "outreach" - }, - { - "title": "Meetups", - "slug": "meetups", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/outreach\/meetups.md", - "parent": "outreach" - }, - { - "title": "Talks", - "slug": "talks", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/outreach\/talks.md", - "parent": "outreach" - }, - { - "title": "Resources", - "slug": "resources", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/outreach\/resources.md", - "parent": "outreach" - } -] + { + "title": "Gutenberg Handbook", + "slug": "readme", + "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/readme.md", + "parent": null + }, + { + "title": "Designer & Developer Handbook", + "slug": "designers-developers", + "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/README.md", + "parent": null + }, + { + "title": "Key Concepts", + "slug": "key-concepts", + "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/key-concepts.md", + "parent": "designers-developers" + }, + { + "title": "Developer Documentation", + "slug": "developers", + "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/README.md", + "parent": "designers-developers" + }, + { + "title": "Block API Reference", + "slug": "block-api", + "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/block-api\/README.md", + "parent": "designers-developers\/developers" + }, + { + "title": "Block Registration ", + "slug": "block-registration", + "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/block-api\/block-registration.md", + "parent": "designers-developers\/developers\/block-api" + }, + { + "title": "Edit and Save", + "slug": "block-edit-save", + "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/block-api\/block-edit-save.md", + "parent": "designers-developers\/developers\/block-api" + }, + { + "title": "Attributes", + "slug": "block-attributes", + "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/block-api\/block-attributes.md", + "parent": "designers-developers\/developers\/block-api" + }, + { + "title": "Deprecated Blocks", + "slug": "block-deprecation", + "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/block-api\/block-deprecation.md", + "parent": "designers-developers\/developers\/block-api" + }, + { + "title": "Templates", + "slug": "block-templates", + "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/block-api\/block-templates.md", + "parent": "designers-developers\/developers\/block-api" + }, + { + "title": "Annotations", + "slug": "block-annotations", + "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/block-api\/block-annotations.md", + "parent": "designers-developers\/developers\/block-api" + }, + { + "title": "Filter Reference", + "slug": "filters", + "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/filters\/README.md", + "parent": "designers-developers\/developers" + }, + { + "title": "Block Filters", + "slug": "block-filters", + "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/filters\/block-filters.md", + "parent": "designers-developers\/developers\/filters" + }, + { + "title": "Editor Filters (Experimental)", + "slug": "editor-filters", + "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/filters\/editor-filters.md", + "parent": "designers-developers\/developers\/filters" + }, + { + "title": "Parser Filters", + "slug": "parser-filters", + "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/filters\/parser-filters.md", + "parent": "designers-developers\/developers\/filters" + }, + { + "title": "Autocomplete", + "slug": "autocomplete-filters", + "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/filters\/autocomplete-filters.md", + "parent": "designers-developers\/developers\/filters" + }, + { + "title": "Data Module Reference", + "slug": "data", + "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/data\/README.md", + "parent": "designers-developers\/developers" + }, + { + "title": "Packages", + "slug": "packages", + "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/packages.md", + "parent": "designers-developers\/developers" + }, + { + "title": "Theming for Gutenberg", + "slug": "themes", + "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/themes\/README.md", + "parent": "designers-developers\/developers" + }, + { + "title": "Theme Support", + "slug": "theme-support", + "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/themes\/theme-support.md", + "parent": "designers-developers\/developers\/themes" + }, + { + "title": "Backwards Compatibility", + "slug": "backwards-compatibility", + "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/backwards-compatibility\/README.md", + "parent": "designers-developers\/developers" + }, + { + "title": "Deprecations", + "slug": "deprecations", + "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/backwards-compatibility\/deprecations.md", + "parent": "designers-developers\/developers\/backwards-compatibility" + }, + { + "title": "Meta Boxes", + "slug": "meta-box", + "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/backwards-compatibility\/meta-box.md", + "parent": "designers-developers\/developers\/backwards-compatibility" + }, + { + "title": "Designer Documentation", + "slug": "designers", + "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/designers\/README.md", + "parent": "designers-developers" + }, + { + "title": "Block Design", + "slug": "block-design", + "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/designers\/block-design.md", + "parent": "designers-developers\/designers" + }, + { + "title": "Patterns", + "slug": "design-patterns", + "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/designers\/design-patterns.md", + "parent": "designers-developers\/designers" + }, + { + "title": "Resources", + "slug": "design-resources", + "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/designers\/design-resources.md", + "parent": "designers-developers\/designers" + }, + { + "title": "Glossary", + "slug": "glossary", + "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/glossary.md", + "parent": "designers-developers" + }, + { + "title": "Frequently Asked Questions", + "slug": "faq", + "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/faq.md", + "parent": "designers-developers" + } +] \ No newline at end of file diff --git a/docs/toc.json b/docs/toc.json new file mode 100644 index 00000000000000..d519837b89a8a8 --- /dev/null +++ b/docs/toc.json @@ -0,0 +1,38 @@ +[ + {"docs/readme.md": []}, + {"docs/designers-developers/README.md": [ + {"docs/designers-developers/key-concepts.md": []}, + {"docs/designers-developers/developers/README.md": [ + {"docs/designers-developers/developers/block-api/README.md": [ + {"docs/designers-developers/developers/block-api/block-registration.md": []}, + {"docs/designers-developers/developers/block-api/block-edit-save.md": []}, + {"docs/designers-developers/developers/block-api/block-attributes.md": []}, + {"docs/designers-developers/developers/block-api/block-deprecation.md": []}, + {"docs/designers-developers/developers/block-api/block-templates.md": []}, + {"docs/designers-developers/developers/block-api/block-annotations.md": []} + ]}, + {"docs/designers-developers/developers/filters/README.md": [ + {"docs/designers-developers/developers/filters/block-filters.md": []}, + {"docs/designers-developers/developers/filters/editor-filters.md": []}, + {"docs/designers-developers/developers/filters/parser-filters.md": []}, + {"docs/designers-developers/developers/filters/autocomplete-filters.md": []} + ]}, + {"docs/designers-developers/developers/data/README.md": "{{data}}"}, + {"docs/designers-developers/developers/packages.md": "{{packages}}"}, + {"docs/designers-developers/developers/themes/README.md": [ + {"docs/designers-developers/developers/themes/theme-support.md": []} + ]}, + {"docs/designers-developers/developers/backwards-compatibility/README.md": [ + {"docs/designers-developers/developers/backwards-compatibility/deprecations.md": []}, + {"docs/designers-developers/developers/backwards-compatibility/meta-box.md": []} + ]} + ]}, + {"docs/designers-developers/designers/README.md": [ + {"docs/designers-developers/designers/block-design.md": []}, + {"docs/designers-developers/designers/design-patterns.md": []}, + {"docs/designers-developers/designers/design-resources.md": []} + ]}, + {"docs/designers-developers/glossary.md": []}, + {"docs/designers-developers/faq.md": []} + ]} +] diff --git a/docs/tool/config.js b/docs/tool/config.js index 6758ac592a72f7..1a3e69450c9a8e 100644 --- a/docs/tool/config.js +++ b/docs/tool/config.js @@ -51,7 +51,7 @@ module.exports = { actions: [ path.resolve( root, 'packages/viewport/src/store/actions.js' ) ], }, }, - dataDocsOutput: path.resolve( __dirname, '../data' ), + dataDocsOutput: path.resolve( __dirname, '../designers-developers/developers/data' ), packageFileNames: glob( 'packages/*/package.json' ) .map( ( fileName ) => fileName.split( '/' )[ 1 ] ), diff --git a/docs/tool/generator.js b/docs/tool/generator.js index 401ea43f33fa0f..1f7a0744dbf3e3 100644 --- a/docs/tool/generator.js +++ b/docs/tool/generator.js @@ -17,7 +17,7 @@ function generateTableOfContent( parsedNamespaces ) { '# Data Module Reference', '', Object.values( parsedNamespaces ).map( ( parsedNamespace ) => { - return ` - [**${ parsedNamespace.name }**: ${ parsedNamespace.title }](../../docs/data/data-${ kebabCase( parsedNamespace.name ) }.md)`; + return ` - [**${ parsedNamespace.name }**: ${ parsedNamespace.title }](../../docs/designers-developers/developers/data/data-${ kebabCase( parsedNamespace.name ) }.md)`; } ).join( '\n' ), ].join( '\n' ); } diff --git a/docs/tool/manifest.js b/docs/tool/manifest.js index 5b5a4bd5ea4021..22d98734278161 100644 --- a/docs/tool/manifest.js +++ b/docs/tool/manifest.js @@ -17,7 +17,7 @@ function getPackageManifest( packageFolderNames ) { { title: 'Packages', slug: 'packages', - markdown_source: `${ baseRepoUrl }/docs/packages.md`, + markdown_source: `${ baseRepoUrl }/docs/designers-developers/developers/packages.md`, parent: null, }, ].concat( @@ -72,7 +72,7 @@ function getDataManifest( parsedNamespaces ) { return [ { title: 'Data Package Reference', slug: 'data', - markdown_source: `${ baseRepoUrl }/docs/data/README.md`, + markdown_source: `${ baseRepoUrl }/docs/designers-developers/developers/data/README.md`, parent: null, } ].concat( Object.values( parsedNamespaces ).map( ( parsedNamespace ) => { @@ -80,7 +80,7 @@ function getDataManifest( parsedNamespaces ) { return { title: parsedNamespace.title, slug, - markdown_source: `${ baseRepoUrl }/docs/data/${ slug }.md`, + markdown_source: `${ baseRepoUrl }/docs/designers-developers/developers/data/${ slug }.md`, parent: 'data', }; } ) diff --git a/docs/users/readme.md b/docs/users/readme.md new file mode 100644 index 00000000000000..e69de29bb2d1d6 From 6a8da2da64e8b94b0db303e4e83593310f098703 Mon Sep 17 00:00:00 2001 From: Matias Ventura <mv@matiasventura.com> Date: Tue, 20 Nov 2018 18:55:33 -0300 Subject: [PATCH 078/254] Update plugin to version 4.5.0. (#12078) --- 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 4c99e466cc6b6a..d2a073b74968dc 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.4.0 + * Version: 4.5.0 * Author: Gutenberg Team * * @package gutenberg diff --git a/package-lock.json b/package-lock.json index b98a21aac33c39..e5d3df1c2322bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "4.4.0", + "version": "4.5.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 19548299b7e193..65928bc9ee5e96 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "4.4.0", + "version": "4.5.0", "private": true, "description": "A new WordPress editor experience", "repository": "git+https://github.com/WordPress/gutenberg.git", From a1a350839cc7f6e6fb07d03e0b54dd7e9a9bc6cf Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Tue, 20 Nov 2018 23:07:34 +0100 Subject: [PATCH 079/254] chore(release): publish - @wordpress/annotations@1.0.2 - @wordpress/api-fetch@2.2.5 - @wordpress/block-library@2.2.5 - @wordpress/blocks@6.0.1 - @wordpress/components@7.0.0 - @wordpress/core-data@2.0.14 - @wordpress/data@4.0.1 - @wordpress/dom@2.0.7 - @wordpress/edit-post@3.1.0 - @wordpress/editor@9.0.0 - @wordpress/format-library@1.2.3 - @wordpress/jest-console@2.0.7 - @wordpress/jest-preset-default@3.0.3 - @wordpress/keycodes@2.0.4 - @wordpress/list-reusable-blocks@1.1.13 - @wordpress/notices@1.1.0 - @wordpress/nux@3.0.1 - @wordpress/rich-text@3.0.1 - @wordpress/scripts@2.4.4 - @wordpress/token-list@1.1.0 - @wordpress/url@2.3.1 - @wordpress/viewport@2.0.12 --- packages/annotations/package.json | 2 +- packages/api-fetch/package.json | 2 +- packages/block-library/package.json | 2 +- packages/blocks/package.json | 2 +- packages/components/package.json | 2 +- packages/core-data/package.json | 2 +- packages/data/package.json | 2 +- packages/dom/package.json | 2 +- packages/edit-post/package.json | 2 +- packages/editor/package.json | 2 +- packages/format-library/package.json | 2 +- packages/jest-console/package.json | 2 +- packages/jest-preset-default/package.json | 2 +- packages/keycodes/package.json | 2 +- packages/list-reusable-blocks/package.json | 2 +- packages/notices/package.json | 2 +- packages/nux/package.json | 2 +- packages/rich-text/package.json | 2 +- packages/scripts/package.json | 2 +- packages/token-list/package.json | 2 +- packages/url/package.json | 2 +- packages/viewport/package.json | 2 +- 22 files changed, 22 insertions(+), 22 deletions(-) diff --git a/packages/annotations/package.json b/packages/annotations/package.json index 89eb55c53d0a67..ec6f312c18f1de 100644 --- a/packages/annotations/package.json +++ b/packages/annotations/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/annotations", - "version": "1.0.1", + "version": "1.0.2", "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 fc9d2ac3295902..3cb6fb3c22979d 100644 --- a/packages/api-fetch/package.json +++ b/packages/api-fetch/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/api-fetch", - "version": "2.2.4", + "version": "2.2.5", "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 b2d15a2e64cef5..064177c097fcee 100644 --- a/packages/block-library/package.json +++ b/packages/block-library/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-library", - "version": "2.2.4", + "version": "2.2.5", "description": "Block library for the WordPress editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/blocks/package.json b/packages/blocks/package.json index 176def63903a7c..d03529c48731a5 100644 --- a/packages/blocks/package.json +++ b/packages/blocks/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/blocks", - "version": "6.0.0", + "version": "6.0.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 24ad3e68fbd39d..4e0cfe2929f10e 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/components", - "version": "6.0.2", + "version": "7.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 0ae20c85125e77..ad9917d785bad3 100644 --- a/packages/core-data/package.json +++ b/packages/core-data/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/core-data", - "version": "2.0.13", + "version": "2.0.14", "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 c0bd206f435136..493c9af3bd7be5 100644 --- a/packages/data/package.json +++ b/packages/data/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/data", - "version": "4.0.0", + "version": "4.0.1", "description": "Data 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 896a7bbf21a3eb..4da707b746bfe7 100644 --- a/packages/dom/package.json +++ b/packages/dom/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/dom", - "version": "2.0.6", + "version": "2.0.7", "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 05132ff4e3974a..a6e719bc603b99 100644 --- a/packages/edit-post/package.json +++ b/packages/edit-post/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/edit-post", - "version": "3.0.2", + "version": "3.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 71b536aa1ba38b..aa24d3bd941aec 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/editor", - "version": "8.0.0", + "version": "9.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 ada7e7d088d2b3..0047fff85984ed 100644 --- a/packages/format-library/package.json +++ b/packages/format-library/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/format-library", - "version": "1.2.2", + "version": "1.2.3", "description": "Format library for the WordPress editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/jest-console/package.json b/packages/jest-console/package.json index a962af91ec62ff..f2b63d889ac04e 100644 --- a/packages/jest-console/package.json +++ b/packages/jest-console/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/jest-console", - "version": "2.0.6", + "version": "2.0.7", "description": "Custom Jest matchers for the Console object.", "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 29402dfb79ec8c..29eb38c9cb8239 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.2", + "version": "3.0.3", "description": "Default Jest preset for WordPress development.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/keycodes/package.json b/packages/keycodes/package.json index 331f0f40a03233..399cdf6bc96918 100644 --- a/packages/keycodes/package.json +++ b/packages/keycodes/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/keycodes", - "version": "2.0.3", + "version": "2.0.4", "description": "Keycodes utilities for WordPress. Used to check for keyboard events across browsers/operating systems.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/list-reusable-blocks/package.json b/packages/list-reusable-blocks/package.json index 41a0a9031e5a6a..6407589f85a68b 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.12", + "version": "1.1.13", "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 5a600a1b5c5552..03ca94a9c9bb6c 100644 --- a/packages/notices/package.json +++ b/packages/notices/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/notices", - "version": "1.0.5", + "version": "1.1.0", "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 d4e338cb4662dd..6f6a4ac3d3a06e 100644 --- a/packages/nux/package.json +++ b/packages/nux/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/nux", - "version": "3.0.0", + "version": "3.0.1", "description": "NUX (New User eXperience) module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/rich-text/package.json b/packages/rich-text/package.json index e14fcf4f8f5145..de834f6c7451e2 100644 --- a/packages/rich-text/package.json +++ b/packages/rich-text/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/rich-text", - "version": "3.0.0", + "version": "3.0.1", "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 9656fa50392b1e..fbbbff832e7360 100644 --- a/packages/scripts/package.json +++ b/packages/scripts/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/scripts", - "version": "2.4.3", + "version": "2.4.4", "description": "Collection of JS scripts for WordPress development.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/token-list/package.json b/packages/token-list/package.json index 10d61bc732a78a..96543d049246f5 100644 --- a/packages/token-list/package.json +++ b/packages/token-list/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/token-list", - "version": "1.0.2", + "version": "1.1.0", "description": "Constructable, plain JavaScript DOMTokenList implementation, supporting non-browser runtimes.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/url/package.json b/packages/url/package.json index ddf20dd9cde912..197f2726a64136 100644 --- a/packages/url/package.json +++ b/packages/url/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/url", - "version": "2.3.0", + "version": "2.3.1", "description": "WordPress URL utilities.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/viewport/package.json b/packages/viewport/package.json index cca7198e6588b7..11da1c485c9d70 100644 --- a/packages/viewport/package.json +++ b/packages/viewport/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/viewport", - "version": "2.0.11", + "version": "2.0.12", "description": "Viewport module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", From 6a8ee534f19588dc7d08c4b783dc941b66e68680 Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Tue, 20 Nov 2018 23:42:18 +0100 Subject: [PATCH 080/254] Update published packages changelogs (#12137) --- packages/annotations/CHANGELOG.md | 2 ++ packages/api-fetch/CHANGELOG.md | 4 +++- packages/block-library/CHANGELOG.md | 2 ++ packages/blocks/CHANGELOG.md | 10 ++++++---- packages/components/CHANGELOG.md | 2 +- packages/core-data/CHANGELOG.md | 4 +++- packages/data/CHANGELOG.md | 6 ++++-- packages/dom/CHANGELOG.md | 4 +++- packages/edit-post/CHANGELOG.md | 2 +- packages/editor/CHANGELOG.md | 2 +- packages/format-library/CHANGELOG.md | 3 ++- packages/jest-console/CHANGELOG.md | 4 +++- packages/jest-preset-default/CHANGELOG.md | 8 +++++--- packages/keycodes/CHANGELOG.md | 4 +++- packages/list-reusable-blocks/CHANGELOG.md | 2 ++ packages/notices/CHANGELOG.md | 2 +- packages/nux/CHANGELOG.md | 2 ++ packages/rich-text/CHANGELOG.md | 2 ++ packages/scripts/CHANGELOG.md | 2 ++ packages/token-list/CHANGELOG.md | 2 +- packages/url/CHANGELOG.md | 5 +++-- packages/viewport/CHANGELOG.md | 4 +++- 22 files changed, 55 insertions(+), 23 deletions(-) diff --git a/packages/annotations/CHANGELOG.md b/packages/annotations/CHANGELOG.md index f8bd3d08f2c7bc..16735d5488527c 100644 --- a/packages/annotations/CHANGELOG.md +++ b/packages/annotations/CHANGELOG.md @@ -1,3 +1,5 @@ +## 1.0.2 (2018-11-20) + ## 1.0.1 (2018-11-15) ## 1.0.0 (2018-11-12) diff --git a/packages/api-fetch/CHANGELOG.md b/packages/api-fetch/CHANGELOG.md index 733d979bc8ce89..912d833f3b909e 100644 --- a/packages/api-fetch/CHANGELOG.md +++ b/packages/api-fetch/CHANGELOG.md @@ -1,3 +1,5 @@ +## 2.2.5 (2018-11-20) + ## 2.2.4 (2018-11-15) ## 2.2.3 (2018-11-12) @@ -22,4 +24,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. diff --git a/packages/block-library/CHANGELOG.md b/packages/block-library/CHANGELOG.md index 9dc4bfe833a91b..742b16f21091b5 100644 --- a/packages/block-library/CHANGELOG.md +++ b/packages/block-library/CHANGELOG.md @@ -1,3 +1,5 @@ +## 2.2.5 (2018-11-20) + ## 2.2.4 (2018-11-15) ## 2.2.3 (2018-11-12) diff --git a/packages/blocks/CHANGELOG.md b/packages/blocks/CHANGELOG.md index 05a69fef416970..dfc293e7cd1267 100644 --- a/packages/blocks/CHANGELOG.md +++ b/packages/blocks/CHANGELOG.md @@ -1,3 +1,5 @@ +## 6.0.1 (2018-11-20) + ## 6.0.0 (2018-11-15) ### Breaking Changes @@ -19,9 +21,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. +- 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) @@ -64,7 +66,7 @@ ### Breaking Changes - The `isSharedBlock` function is removed. Use `isReusableBlock` instead. -- 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 81726381c57f13..7829968a373b2b 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -1,4 +1,4 @@ -## 7.0.0 (Unreleased) +## 7.0.0 (2018-11-20) ### Breaking Change diff --git a/packages/core-data/CHANGELOG.md b/packages/core-data/CHANGELOG.md index aebc238e5fe4aa..98ed42a5473f08 100644 --- a/packages/core-data/CHANGELOG.md +++ b/packages/core-data/CHANGELOG.md @@ -1,3 +1,5 @@ +## 2.0.14 (2018-11-20) + ## 2.0.13 (2018-11-15) ## 2.0.12 (2018-11-12) @@ -26,4 +28,4 @@ - `select("core").getCategories` has been deprecated. Please use `select("core").getEntityRecords` instead. - `wp.data.select("core").isRequestingCategories` has been deprecated. Please use `wp.data.select("core/data").isResolving` instead. - `select("core").isRequestingTerms` has been deprecated. Please use `select("core").isResolving` instead. -- 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. diff --git a/packages/data/CHANGELOG.md b/packages/data/CHANGELOG.md index 24d44618c11ffd..18139501460cc3 100644 --- a/packages/data/CHANGELOG.md +++ b/packages/data/CHANGELOG.md @@ -1,3 +1,5 @@ +## 4.0.1 (2018-11-20) + ## 4.0.0 (2018-11-15) ### Breaking Changes @@ -68,5 +70,5 @@ - The `withRehdyration` function is removed. Use the persistence plugin instead. - The `loadAndPersist` function is removed. Use the persistence plugin instead. -- `restrictPersistence`, `setPersistenceStorage` and `setupPersistence` functions have been removed. Please use the data persistence plugin instead. -- 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. +- `restrictPersistence`, `setPersistenceStorage` and `setupPersistence` functions have been removed. Please use the data persistence plugin instead. +- 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. diff --git a/packages/dom/CHANGELOG.md b/packages/dom/CHANGELOG.md index a14324346a986b..9e07c5fa5ef3ce 100644 --- a/packages/dom/CHANGELOG.md +++ b/packages/dom/CHANGELOG.md @@ -1,3 +1,5 @@ +## 2.0.7 (2018-11-20) + ## 2.0.6 (2018-11-09) ## 2.0.5 (2018-11-09) @@ -10,4 +12,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. diff --git a/packages/edit-post/CHANGELOG.md b/packages/edit-post/CHANGELOG.md index 4eeda8d66d7502..0b741baabd375c 100644 --- a/packages/edit-post/CHANGELOG.md +++ b/packages/edit-post/CHANGELOG.md @@ -1,4 +1,4 @@ -## 3.1.0 (Unreleased) +## 3.1.0 (2018-11-20) ### New Feature diff --git a/packages/editor/CHANGELOG.md b/packages/editor/CHANGELOG.md index 65eb76bccf8f7b..45eb7c15d2418e 100644 --- a/packages/editor/CHANGELOG.md +++ b/packages/editor/CHANGELOG.md @@ -1,4 +1,4 @@ -## 9.0.0 (Unreleased) +## 9.0.0 (2018-11-20) ### Breaking Changes diff --git a/packages/format-library/CHANGELOG.md b/packages/format-library/CHANGELOG.md index eb34600a9949e1..58bd7408389532 100644 --- a/packages/format-library/CHANGELOG.md +++ b/packages/format-library/CHANGELOG.md @@ -1,6 +1,7 @@ -## 1.2.3 (Unreleased) +## 1.2.3 (2018-11-20) ### Bug fixes + - Link URL validation now works correctly when a URL includes a fragment. Previously any URL containing a fragment failed validation. - Link URL validation catches an incorrect number of forward slashes following a url using the http protocol. diff --git a/packages/jest-console/CHANGELOG.md b/packages/jest-console/CHANGELOG.md index ac2322f83c5641..040d1ce478d1fe 100644 --- a/packages/jest-console/CHANGELOG.md +++ b/packages/jest-console/CHANGELOG.md @@ -1,3 +1,5 @@ +## 2.0.7 (2018-11-20) + ## 2.0.5 (2018-09-30) ### Bug Fixes @@ -19,7 +21,7 @@ ### Polish -- Fix: Standardized `package.json` format ([#119](https://github.com/WordPress/packages/pull/119)) +- Fix: Standardized `package.json` format ([#119](https://github.com/WordPress/packages/pull/119)) ## 1.0.6 (2018-02-28) diff --git a/packages/jest-preset-default/CHANGELOG.md b/packages/jest-preset-default/CHANGELOG.md index 3d88696419728b..d8e7f824556493 100644 --- a/packages/jest-preset-default/CHANGELOG.md +++ b/packages/jest-preset-default/CHANGELOG.md @@ -1,3 +1,5 @@ +## 3.0.3 (2018-11-20) + ## 3.0.2 (2018-11-09) ## 3.0.1 (2018-11-09) @@ -22,11 +24,11 @@ ### Polish -- Fix: Standardized `package.json` format ([#119](https://github.com/WordPress/packages/pull/119)) +- Fix: Standardized `package.json` format ([#119](https://github.com/WordPress/packages/pull/119)) ## 1.0.5 (2018-03-22) ### Polish -- Docs: Wrap filename in backticks ([#89](https://github.com/WordPress/packages/pull/89)) -- Add `jest-preset` keyword to `jest-preset-default` package ([#92](https://github.com/WordPress/packages/pull/92)) +- Docs: Wrap filename in backticks ([#89](https://github.com/WordPress/packages/pull/89)) +- Add `jest-preset` keyword to `jest-preset-default` package ([#92](https://github.com/WordPress/packages/pull/92)) diff --git a/packages/keycodes/CHANGELOG.md b/packages/keycodes/CHANGELOG.md index 9c0e0dcaa72dce..ab84c2c4dc075a 100644 --- a/packages/keycodes/CHANGELOG.md +++ b/packages/keycodes/CHANGELOG.md @@ -1,7 +1,9 @@ +## 2.0.4 (2018-11-20) + ## 2.0.3 (2018-10-30) ## 2.0.0 (2018-09-05) ### 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. diff --git a/packages/list-reusable-blocks/CHANGELOG.md b/packages/list-reusable-blocks/CHANGELOG.md index 0712d0cf932867..26020674f2a31e 100644 --- a/packages/list-reusable-blocks/CHANGELOG.md +++ b/packages/list-reusable-blocks/CHANGELOG.md @@ -1,3 +1,5 @@ +## 1.1.13 (2018-11-20) + ## 1.1.12 (2018-11-15) ## 1.1.11 (2018-11-12) diff --git a/packages/notices/CHANGELOG.md b/packages/notices/CHANGELOG.md index 6a38ce167d73af..3699727ec708e7 100644 --- a/packages/notices/CHANGELOG.md +++ b/packages/notices/CHANGELOG.md @@ -1,4 +1,4 @@ -## 1.1.0 (Unreleased) +## 1.1.0 (2018-11-20) ### New Feature diff --git a/packages/nux/CHANGELOG.md b/packages/nux/CHANGELOG.md index 1db9b6a1e2ae8f..5adb00aacdd307 100644 --- a/packages/nux/CHANGELOG.md +++ b/packages/nux/CHANGELOG.md @@ -1,3 +1,5 @@ +## 3.0.1 (2018-11-20) + ## 3.0.0 (2018-11-15) ### Breaking Changes diff --git a/packages/rich-text/CHANGELOG.md b/packages/rich-text/CHANGELOG.md index d9c2664864efa9..4a2ad2d43f85fd 100644 --- a/packages/rich-text/CHANGELOG.md +++ b/packages/rich-text/CHANGELOG.md @@ -1,3 +1,5 @@ +## 3.0.1 (2018-11-20) + ## 3.0.0 (2018-11-15) ### Breaking Changes diff --git a/packages/scripts/CHANGELOG.md b/packages/scripts/CHANGELOG.md index 89b2da214d8aba..c07fe9a2e6d566 100644 --- a/packages/scripts/CHANGELOG.md +++ b/packages/scripts/CHANGELOG.md @@ -1,3 +1,5 @@ +## 2.4.4 (2018-11-20) + ## 2.4.3 (2018-11-09) ## 2.4.2 (2018-11-09) diff --git a/packages/token-list/CHANGELOG.md b/packages/token-list/CHANGELOG.md index 8bf612a4669aa9..4f09e8ebd8395f 100644 --- a/packages/token-list/CHANGELOG.md +++ b/packages/token-list/CHANGELOG.md @@ -1,4 +1,4 @@ -## 1.1.0 (Unreleased) +## 1.1.0 (2018-11-20) ### Enhancements diff --git a/packages/url/CHANGELOG.md b/packages/url/CHANGELOG.md index 0ec863c87def5a..984fb0428eb281 100644 --- a/packages/url/CHANGELOG.md +++ b/packages/url/CHANGELOG.md @@ -1,6 +1,7 @@ -## 2.3.1 (Unreleased) +## 2.3.1 (2018-11-20) ### Bug fixes + - The `isValidProtocol` function now correctly considers the protocol of the URL as only incoporating characters up to and including the colon (':'). - `getFragment` is now greedier and matches fragments from the first occurence of the '#' symbol instead of the last. @@ -43,4 +44,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. diff --git a/packages/viewport/CHANGELOG.md b/packages/viewport/CHANGELOG.md index 092b542e113d49..0dd9a1369466c0 100644 --- a/packages/viewport/CHANGELOG.md +++ b/packages/viewport/CHANGELOG.md @@ -1,3 +1,5 @@ +## 2.0.12 (2018-11-20) + ## 2.0.11 (2018-11-15) ## 2.0.10 (2018-11-09) @@ -16,4 +18,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. From eccb423b37de112e5fc78078b5a0435d1fe0f713 Mon Sep 17 00:00:00 2001 From: Tugdual de Kerviler <dekervit@gmail.com> Date: Wed, 21 Nov 2018 00:07:57 +0100 Subject: [PATCH 081/254] Add react-native module property to html-entities package.json (#12131) --- packages/html-entities/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/html-entities/package.json b/packages/html-entities/package.json index 1b74e5ebb8f89c..89754753841825 100644 --- a/packages/html-entities/package.json +++ b/packages/html-entities/package.json @@ -19,6 +19,7 @@ }, "main": "build/index.js", "module": "build-module/index.js", + "react-native": "src/index", "dependencies": { "@babel/runtime": "^7.0.0" }, From 114c443fc6e6289c5acf8c1851037d5dbb1bf8c6 Mon Sep 17 00:00:00 2001 From: Robert Anderson <robert@noisysocks.com> Date: Wed, 21 Nov 2018 15:11:13 +1100 Subject: [PATCH 082/254] Fix undefined index warnings in Latest Comments & Latest Posts (#12149) In 53c975b, `prepare_attributes_for_render` was changed so that the `$attributes` array that is passed to `render_callback` contains the same keys as what is passed to the block on the front-end. This caused some 'Undefined index' notices in the Latest Comments and Latest Posts blocks. We need to be using `isset()` when using attributes that do not have a default value defined. --- .../src/latest-comments/index.php | 2 +- .../block-library/src/latest-posts/index.php | 19 +++++++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/block-library/src/latest-comments/index.php b/packages/block-library/src/latest-comments/index.php index 927157eb43481d..b06cccd6c0a789 100644 --- a/packages/block-library/src/latest-comments/index.php +++ b/packages/block-library/src/latest-comments/index.php @@ -119,7 +119,7 @@ function gutenberg_render_block_core_latest_comments( $attributes = array() ) { } $class = 'wp-block-latest-comments'; - if ( $attributes['align'] ) { + if ( isset( $attributes['align'] ) ) { $class .= " align{$attributes['align']}"; } if ( $attributes['displayAvatar'] ) { diff --git a/packages/block-library/src/latest-posts/index.php b/packages/block-library/src/latest-posts/index.php index 850192a5492c3e..5a582957c6b7f8 100644 --- a/packages/block-library/src/latest-posts/index.php +++ b/packages/block-library/src/latest-posts/index.php @@ -13,16 +13,19 @@ * @return string Returns the post content with latest posts added. */ function render_block_core_latest_posts( $attributes ) { - $recent_posts = wp_get_recent_posts( - array( - 'numberposts' => $attributes['postsToShow'], - 'post_status' => 'publish', - 'order' => $attributes['order'], - 'orderby' => $attributes['orderBy'], - 'category' => $attributes['categories'], - ) + $args = array( + 'numberposts' => $attributes['postsToShow'], + 'post_status' => 'publish', + 'order' => $attributes['order'], + 'orderby' => $attributes['orderBy'], ); + if ( isset( $attributes['categories'] ) ) { + $args['categories'] = $attributes['categories']; + } + + $recent_posts = wp_get_recent_posts( $args ); + $list_items_markup = ''; foreach ( $recent_posts as $post ) { From ec1a4cbe126d860f0feb0dda8c0c8f8c1d38419b Mon Sep 17 00:00:00 2001 From: Kevin Killingsworth <kevin@redfenix.com> Date: Wed, 21 Nov 2018 01:21:38 -0600 Subject: [PATCH 083/254] Docs: Fix dead links in CONTRIBUTING.md (#12144) This fixes some now-dead links that fell victim to moving and renaming things. --- CONTRIBUTING.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 37c7e602bb7085..4ccb59aa2df443 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -104,11 +104,11 @@ For example, `add/gallery-block` means you're working on adding a new gallery bl You can pick among all the <a href="https://github.com/WordPress/gutenberg/issues">tickets</a>, or some of the ones labelled <a href="https://github.com/WordPress/gutenberg/labels/Good%20First%20Issue">Good First Issue</a>. -The workflow is documented in greater detail in the [repository management](./docs/reference/repository-management.md) document. +The workflow is documented in greater detail in the [repository management](./docs/contributors/repository-management.md) document. ## Testing -Gutenberg contains both PHP and JavaScript code, and encourages testing and code style linting for both. It also incorporates end-to-end testing using [Google Puppeteer](https://developers.google.com/web/tools/puppeteer/). You can find out more details in [Testing Overview document](./docs/reference/testing-overview.md). +Gutenberg contains both PHP and JavaScript code, and encourages testing and code style linting for both. It also incorporates end-to-end testing using [Google Puppeteer](https://developers.google.com/web/tools/puppeteer/). You can find out more details in [Testing Overview document](./docs/contributors/testing-overview.md). ## Managing packages @@ -238,7 +238,7 @@ Choose the correct version based on `CHANGELOG.md` files, confirm your choices a ## How Designers Can Contribute -If you'd like to contribute to the design or front-end, feel free to contribute to tickets labelled <a href="https://github.com/WordPress/gutenberg/issues?q=is%3Aissue+is%3Aopen+label%3ADesign">Design</a>. We could use your thoughtful replies, mockups, animatics, sketches, doodles. Proposed changes are best done as minimal and specific iterations on the work that precedes it so we can compare. If you use <a href="https://www.sketchapp.com/">Sketch</a>, you can grab <a href="https://cloudup.com/cMPXM8Va2cy">the source file for the mockups</a> (updated April 6th). +If you'd like to contribute to the design or front-end, feel free to contribute to tickets labelled [Needs Design](https://github.com/WordPress/gutenberg/issues?q=is%3Aissue+is%3Aopen+label%3A%22Needs+Design%22) or [Needs Design Feedback](https://github.com/WordPress/gutenberg/issues?q=is%3Aissue+is%3Aopen+label%3A"Needs+Design+Feedback%22). We could use your thoughtful replies, mockups, animatics, sketches, doodles. Proposed changes are best done as minimal and specific iterations on the work that precedes it so we can compare. If you use <a href="https://www.sketchapp.com/">Sketch</a>, you can grab <a href="https://cloudup.com/cMPXM8Va2cy">the source file for the mockups</a> (updated April 6th). ## Contribute to the Documentation From 278b32204ddf63a3c7022ce42145137131a44e4b Mon Sep 17 00:00:00 2001 From: Robert Anderson <robert@noisysocks.com> Date: Wed, 21 Nov 2018 19:16:16 +1100 Subject: [PATCH 084/254] Revert "Honor the Disable Visual Editor setting (#12000)" (#12151) * Revert "Honor the Disable Visual Editor setting (#12000)" This reverts commit 095d18fe5f1b55ab243cdba947740542e5b0fe47. * Add support for RichEditing in Gutenberg * Typo --- gutenberg.php | 7 --- lib/client-assets.php | 8 ++++ .../components/header/header-toolbar/index.js | 6 +-- .../components/header/mode-switcher/index.js | 4 +- .../components/keyboard-shortcuts/index.js | 6 ++- .../edit-post/src/components/layout/index.js | 6 ++- .../src/components/text-editor/index.js | 46 +++++++++++-------- packages/editor/src/store/defaults.js | 20 ++++---- 8 files changed, 62 insertions(+), 41 deletions(-) diff --git a/gutenberg.php b/gutenberg.php index d2a073b74968dc..89c0d0b151e67f 100644 --- a/gutenberg.php +++ b/gutenberg.php @@ -200,13 +200,6 @@ function gutenberg_pre_init() { add_filter( 'replace_editor', 'gutenberg_init', 10, 2 ); } -/** - * Enable Gutenberg based on user_can_richedit setting. - * Set gutenberg_can_edit_post based on user setting for disable visual editor. - */ -add_filter( 'gutenberg_can_edit_post_type', 'user_can_richedit', 5 ); -add_filter( 'gutenberg_can_edit_post', 'user_can_richedit', 5 ); - /** * Initialize Gutenberg. * diff --git a/lib/client-assets.php b/lib/client-assets.php index fd1553c3cfd87b..6d1522b57d3702 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -1055,6 +1055,13 @@ function gutenberg_editor_scripts_and_styles( $hook ) { 'after' ); + // Ignore Classic Editor's `rich_editing` user option, aka "Disable visual + // editor". Forcing this to be true guarantees that TinyMCE and its plugins + // are available in Gutenberg. Fixes + // https://github.com/WordPress/gutenberg/issues/5667. + $user_can_richedit = user_can_richedit(); + add_filter( 'user_can_richedit', '__return_true' ); + wp_enqueue_script( 'wp-edit-post' ); wp_enqueue_script( 'wp-format-library' ); wp_enqueue_style( 'wp-format-library' ); @@ -1294,6 +1301,7 @@ function gutenberg_editor_scripts_and_styles( $hook ) { 'allowedMimeTypes' => get_allowed_mime_types(), 'styles' => $styles, 'imageSizes' => gutenberg_get_available_image_sizes(), + 'richEditingEnabled' => $user_can_richedit, // Ideally, we'd remove this and rely on a REST API endpoint. 'postLock' => $lock_details, diff --git a/packages/edit-post/src/components/header/header-toolbar/index.js b/packages/edit-post/src/components/header/header-toolbar/index.js index e5e92af1f2a645..ef56ca64b3ad77 100644 --- a/packages/edit-post/src/components/header/header-toolbar/index.js +++ b/packages/edit-post/src/components/header/header-toolbar/index.js @@ -21,7 +21,7 @@ import { */ import FullscreenModeClose from '../fullscreen-mode-close'; -function HeaderToolbar( { hasFixedToolbar, isLargeViewport, mode } ) { +function HeaderToolbar( { hasFixedToolbar, isLargeViewport, showInserter } ) { const toolbarAriaLabel = hasFixedToolbar ? /* translators: accessibility text for the editor toolbar when Top Toolbar is on */ __( 'Document and block tools' ) : @@ -35,7 +35,7 @@ function HeaderToolbar( { hasFixedToolbar, isLargeViewport, mode } ) { > <FullscreenModeClose /> <div> - <Inserter disabled={ mode !== 'visual' } position="bottom right" /> + <Inserter disabled={ ! showInserter } position="bottom right" /> <DotTip tipId="core/editor.inserter"> { __( 'Welcome to the wonderful world of blocks! Click the “+” (“Add block”) button to add a new block. There are blocks available for all kinds of content: you can insert text, headings, images, lists, and lots more!' ) } </DotTip> @@ -56,7 +56,7 @@ function HeaderToolbar( { hasFixedToolbar, isLargeViewport, mode } ) { export default compose( [ withSelect( ( select ) => ( { hasFixedToolbar: select( 'core/edit-post' ).isFeatureActive( 'fixedToolbar' ), - mode: select( 'core/edit-post' ).getEditorMode(), + showInserter: select( 'core/edit-post' ).getEditorMode() === 'visual' && select( 'core/editor' ).getEditorSettings().richEditingEnabled, } ) ), withViewportMatch( { isLargeViewport: 'medium' } ), ] )( HeaderToolbar ); diff --git a/packages/edit-post/src/components/header/mode-switcher/index.js b/packages/edit-post/src/components/header/mode-switcher/index.js index 849284d8e73a75..52ca0b78cfc945 100644 --- a/packages/edit-post/src/components/header/mode-switcher/index.js +++ b/packages/edit-post/src/components/header/mode-switcher/index.js @@ -3,7 +3,7 @@ */ import { __ } from '@wordpress/i18n'; import { MenuItemsChoice, MenuGroup } from '@wordpress/components'; -import { compose } from '@wordpress/compose'; +import { compose, ifCondition } from '@wordpress/compose'; import { withSelect, withDispatch } from '@wordpress/data'; /** @@ -49,8 +49,10 @@ function ModeSwitcher( { onSwitch, mode } ) { export default compose( [ withSelect( ( select ) => ( { + isRichEditingEnabled: select( 'core/editor' ).getEditorSettings().richEditingEnabled, mode: select( 'core/edit-post' ).getEditorMode(), } ) ), + ifCondition( ( { isRichEditingEnabled } ) => isRichEditingEnabled ), withDispatch( ( dispatch, ownProps ) => ( { onSwitch( mode ) { dispatch( 'core/edit-post' ).switchEditorMode( mode ); diff --git a/packages/edit-post/src/components/keyboard-shortcuts/index.js b/packages/edit-post/src/components/keyboard-shortcuts/index.js index 625f29e99bccd2..30bd8d02a4d11a 100644 --- a/packages/edit-post/src/components/keyboard-shortcuts/index.js +++ b/packages/edit-post/src/components/keyboard-shortcuts/index.js @@ -20,7 +20,10 @@ class EditorModeKeyboardShortcuts extends Component { } toggleMode() { - const { mode, switchMode } = this.props; + const { mode, switchMode, isRichEditingEnabled } = this.props; + if ( ! isRichEditingEnabled ) { + return; + } switchMode( mode === 'visual' ? 'text' : 'visual' ); } @@ -52,6 +55,7 @@ class EditorModeKeyboardShortcuts extends Component { export default compose( [ withSelect( ( select ) => ( { + isRichEditingEnabled: select( 'core/editor' ).getEditorSettings().richEditingEnabled, mode: select( 'core/edit-post' ).getEditorMode(), isEditorSidebarOpen: select( 'core/edit-post' ).isEditorSidebarOpened(), hasBlockSelection: !! select( 'core/editor' ).getBlockSelectionStart(), diff --git a/packages/edit-post/src/components/layout/index.js b/packages/edit-post/src/components/layout/index.js index ed5773e5182f5b..5a4018bc9dd1b6 100644 --- a/packages/edit-post/src/components/layout/index.js +++ b/packages/edit-post/src/components/layout/index.js @@ -50,6 +50,7 @@ function Layout( { hasActiveMetaboxes, isSaving, isMobileViewport, + isRichEditingEnabled, } ) { const sidebarIsOpened = editorSidebarOpened || pluginSidebarOpened || publishSidebarOpened; @@ -84,8 +85,8 @@ function Layout( { <EditorModeKeyboardShortcuts /> <KeyboardShortcutHelpModal /> <OptionsModal /> - { mode === 'text' && <TextEditor /> } - { mode === 'visual' && <VisualEditor /> } + { ( mode === 'text' || ! isRichEditingEnabled ) && <TextEditor /> } + { isRichEditingEnabled && mode === 'visual' && <VisualEditor /> } <div className="edit-post-layout__metaboxes"> <MetaBoxes location="normal" /> </div> @@ -137,6 +138,7 @@ export default compose( hasFixedToolbar: select( 'core/edit-post' ).isFeatureActive( 'fixedToolbar' ), hasActiveMetaboxes: select( 'core/edit-post' ).hasMetaBoxes(), isSaving: select( 'core/edit-post' ).isSavingMetaBoxes(), + isRichEditingEnabled: select( 'core/editor' ).getEditorSettings().richEditingEnabled, } ) ), withDispatch( ( dispatch ) => { const { closePublishSidebar, togglePublishSidebar } = dispatch( 'core/edit-post' ); diff --git a/packages/edit-post/src/components/text-editor/index.js b/packages/edit-post/src/components/text-editor/index.js index 2f3e27e25fc455..019a19b2e518c8 100644 --- a/packages/edit-post/src/components/text-editor/index.js +++ b/packages/edit-post/src/components/text-editor/index.js @@ -3,23 +3,26 @@ */ import { PostTextEditor, PostTitle } from '@wordpress/editor'; import { IconButton } from '@wordpress/components'; -import { withDispatch } from '@wordpress/data'; +import { withDispatch, withSelect } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; import { displayShortcut } from '@wordpress/keycodes'; +import { compose } from '@wordpress/compose'; -function TextEditor( { onExit } ) { +function TextEditor( { onExit, isRichEditingEnabled } ) { return ( <div className="edit-post-text-editor"> - <div className="edit-post-text-editor__toolbar"> - <h2>{ __( 'Editing Code' ) }</h2> - <IconButton - onClick={ onExit } - icon="no-alt" - shortcut={ displayShortcut.secondary( 'm' ) } - > - { __( 'Exit Code Editor' ) } - </IconButton> - </div> + { isRichEditingEnabled && ( + <div className="edit-post-text-editor__toolbar"> + <h2>{ __( 'Editing Code' ) }</h2> + <IconButton + onClick={ onExit } + icon="no-alt" + shortcut={ displayShortcut.secondary( 'm' ) } + > + { __( 'Exit Code Editor' ) } + </IconButton> + </div> + ) } <div className="edit-post-text-editor__body"> <PostTitle /> <PostTextEditor /> @@ -28,10 +31,15 @@ function TextEditor( { onExit } ) { ); } -export default withDispatch( ( dispatch ) => { - return { - onExit() { - dispatch( 'core/edit-post' ).switchEditorMode( 'visual' ); - }, - }; -} )( TextEditor ); +export default compose( + withSelect( ( select ) => ( { + isRichEditingEnabled: select( 'core/editor' ).getEditorSettings().richEditingEnabled, + } ) ), + withDispatch( ( dispatch ) => { + return { + onExit() { + dispatch( 'core/edit-post' ).switchEditorMode( 'visual' ); + }, + }; + } ) +)( TextEditor ); diff --git a/packages/editor/src/store/defaults.js b/packages/editor/src/store/defaults.js index 78a05a7a315bf3..dc4111ef641d40 100644 --- a/packages/editor/src/store/defaults.js +++ b/packages/editor/src/store/defaults.js @@ -11,14 +11,15 @@ export const PREFERENCES_DEFAULTS = { /** * The default editor settings * - * 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 + * 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 + * richEditingEnabled boolean Whether rich editing is enabled or not */ export const EDITOR_SETTINGS_DEFAULTS = { alignWide: false, @@ -126,6 +127,9 @@ export const EDITOR_SETTINGS_DEFAULTS = { // List of allowed mime types and file extensions. allowedMimeTypes: null, + + // Whether richs editing is enabled or not. + richEditingEnabled: true, }; /** From e05eb6d0d1219c7c0bdd977eeb5c04f3a3ffd373 Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Wed, 21 Nov 2018 09:19:09 +0100 Subject: [PATCH 085/254] Avoid showing draft revert message on autosaves (#12155) * Avoid showing draft revert message on autosaves * Fix unit tests --- packages/editor/src/store/effects/posts.js | 4 ++-- packages/editor/src/store/test/effects.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/editor/src/store/effects/posts.js b/packages/editor/src/store/effects/posts.js index a1a24a1f6d3be6..ed5f596b167d42 100644 --- a/packages/editor/src/store/effects/posts.js +++ b/packages/editor/src/store/effects/posts.js @@ -170,10 +170,10 @@ export const requestPostUpdate = async ( action, store ) => { * @param {Object} store Redux Store. */ export const requestPostUpdateSuccess = ( action ) => { - const { previousPost, post, isAutosave, postType } = action; + const { previousPost, post, postType } = action; // Autosaves are neither shown a notice nor redirected. - if ( isAutosave ) { + if ( get( action.options, [ 'isAutosave' ] ) ) { return; } diff --git a/packages/editor/src/store/test/effects.js b/packages/editor/src/store/test/effects.js index 48985b9cacfbc5..a6ce470d86e541 100644 --- a/packages/editor/src/store/test/effects.js +++ b/packages/editor/src/store/test/effects.js @@ -320,7 +320,7 @@ describe( 'effects', () => { const previousPost = getPublishedPost(); const post = { ...getPublishedPost(), id: defaultPost.id + 1 }; - handler( { post, previousPost, isAutosave: true } ); + handler( { post, previousPost, options: { isAutosave: true } } ); expect( dataDispatch( 'core/notices' ).createSuccessNotice ).not.toHaveBeenCalled(); } ); From fd1b074c7767bfc82d960a0a7ad845c22a20d638 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20Van=C2=A0Dorpe?= <iseulde@automattic.com> Date: Wed, 21 Nov 2018 09:48:16 +0100 Subject: [PATCH 086/254] Raw Handling: fix consecutive lists with one item (#12156) --- packages/blocks/src/api/raw-handling/list-reducer.js | 6 +++++- packages/blocks/src/api/raw-handling/test/list-reducer.js | 6 ++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/blocks/src/api/raw-handling/list-reducer.js b/packages/blocks/src/api/raw-handling/list-reducer.js index 0025cc6cb5cb60..a5f079dcb24880 100644 --- a/packages/blocks/src/api/raw-handling/list-reducer.js +++ b/packages/blocks/src/api/raw-handling/list-reducer.js @@ -29,7 +29,11 @@ export default function( node ) { prevElement.nodeName === node.nodeName && list.children.length === 1 ) { - prevElement.appendChild( list.firstChild ); + // Move all child nodes, including any text nodes, if any. + while ( list.firstChild ) { + prevElement.appendChild( list.firstChild ); + } + list.parentNode.removeChild( list ); } diff --git a/packages/blocks/src/api/raw-handling/test/list-reducer.js b/packages/blocks/src/api/raw-handling/test/list-reducer.js index cb55a63181a385..9d1b053627295d 100644 --- a/packages/blocks/src/api/raw-handling/test/list-reducer.js +++ b/packages/blocks/src/api/raw-handling/test/list-reducer.js @@ -11,6 +11,12 @@ describe( 'listReducer', () => { expect( deepFilterHTML( input, [ listReducer ] ) ).toEqual( output ); } ); + it( 'should merge lists with whitespace', () => { + const input = '<ul>\n<li>one</li>\n</ul>\n<ul>\n<li>two</li>\n</ul>'; + const output = '<ul>\n<li>one</li>\n\n<li>two</li>\n</ul>\n'; + expect( deepFilterHTML( input, [ listReducer ] ) ).toEqual( output ); + } ); + it( 'should not merge lists if it has more than one item', () => { const input = '<ul><li>one</li></ul><ul><li>two</li><li>three</li></ul>'; expect( deepFilterHTML( input, [ listReducer ] ) ).toEqual( input ); From e651069da2302499d1254c3c02442993547a9410 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20Van=C2=A0Dorpe?= <iseulde@automattic.com> Date: Wed, 21 Nov 2018 10:05:48 +0100 Subject: [PATCH 087/254] RichText: List: Sync DOM after editor command (#12089) * Fix list dom sync * Add e2e test --- .../editor/src/components/rich-text/index.js | 1 + .../src/components/rich-text/list-edit.js | 34 +++++++++++++++---- .../blocks/__snapshots__/list.test.js.snap | 6 ++++ test/e2e/specs/blocks/list.test.js | 9 +++++ 4 files changed, 43 insertions(+), 7 deletions(-) diff --git a/packages/editor/src/components/rich-text/index.js b/packages/editor/src/components/rich-text/index.js index 43df39403e3bbd..2033ce14cb8f7d 100644 --- a/packages/editor/src/components/rich-text/index.js +++ b/packages/editor/src/components/rich-text/index.js @@ -861,6 +861,7 @@ export class RichText extends Component { editor={ this.editor } onTagNameChange={ onTagNameChange } tagName={ Tagname } + onSyncDOM={ () => this.onChange( this.createRecord() ) } /> ) } { isSelected && ! inlineToolbar && ( diff --git a/packages/editor/src/components/rich-text/list-edit.js b/packages/editor/src/components/rich-text/list-edit.js index 9d7e40f4b414cd..1e92d3d792eb49 100644 --- a/packages/editor/src/components/rich-text/list-edit.js +++ b/packages/editor/src/components/rich-text/list-edit.js @@ -35,27 +35,39 @@ function isActiveListType( editor, tagName, rootTagName ) { return list.nodeName.toLowerCase() === tagName; } -export const ListEdit = ( { editor, onTagNameChange, tagName } ) => ( +export const ListEdit = ( { editor, onTagNameChange, tagName, onSyncDOM } ) => ( <Fragment> <RichTextShortcut type="primary" character="[" - onUse={ () => editor.execCommand( 'Outdent' ) } + onUse={ () => { + editor.execCommand( 'Outdent' ); + onSyncDOM(); + } } /> <RichTextShortcut type="primary" character="]" - onUse={ () => editor.execCommand( 'Indent' ) } + onUse={ () => { + editor.execCommand( 'Indent' ); + onSyncDOM(); + } } /> <RichTextShortcut type="primary" character="m" - onUse={ () => editor.execCommand( 'Indent' ) } + onUse={ () => { + editor.execCommand( 'Indent' ); + onSyncDOM(); + } } /> <RichTextShortcut type="primaryShift" character="m" - onUse={ () => editor.execCommand( 'Outdent' ) } + onUse={ () => { + editor.execCommand( 'Outdent' ); + onSyncDOM(); + } } /> <BlockFormatControls> <Toolbar @@ -69,6 +81,7 @@ export const ListEdit = ( { editor, onTagNameChange, tagName } ) => ( onTagNameChange( 'ul' ); } else { editor.execCommand( 'InsertUnorderedList' ); + onSyncDOM(); } }, }, @@ -81,18 +94,25 @@ export const ListEdit = ( { editor, onTagNameChange, tagName } ) => ( onTagNameChange( 'ol' ); } else { editor.execCommand( 'InsertOrderedList' ); + onSyncDOM(); } }, }, { icon: 'editor-outdent', title: __( 'Outdent list item' ), - onClick: () => editor.execCommand( 'Outdent' ), + onClick() { + editor.execCommand( 'Outdent' ); + onSyncDOM(); + }, }, { icon: 'editor-indent', title: __( 'Indent list item' ), - onClick: () => editor.execCommand( 'Indent' ), + onClick() { + editor.execCommand( 'Indent' ); + onSyncDOM(); + }, }, ] } /> diff --git a/test/e2e/specs/blocks/__snapshots__/list.test.js.snap b/test/e2e/specs/blocks/__snapshots__/list.test.js.snap index a804ea84132d3d..19a5186156809e 100644 --- a/test/e2e/specs/blocks/__snapshots__/list.test.js.snap +++ b/test/e2e/specs/blocks/__snapshots__/list.test.js.snap @@ -80,6 +80,12 @@ exports[`List can undo asterisk transform 1`] = ` <!-- /wp:paragraph -->" `; +exports[`List should be immeadiately saved on indentation 1`] = ` +"<!-- wp:list --> +<ul><li>one<ul><li></li></ul></li></ul> +<!-- /wp:list -->" +`; + exports[`List should create paragraph on split at end and merge back with content 1`] = ` "<!-- wp:list --> <ul><li>one</li></ul> diff --git a/test/e2e/specs/blocks/list.test.js b/test/e2e/specs/blocks/list.test.js index 3d416b998d1371..f4bd5b29d90943 100644 --- a/test/e2e/specs/blocks/list.test.js +++ b/test/e2e/specs/blocks/list.test.js @@ -194,4 +194,13 @@ describe( 'List', () => { expect( await getEditedPostContent() ).toMatchSnapshot(); } ); + + it( 'should be immeadiately saved on indentation', async () => { + await insertBlock( 'List' ); + await page.keyboard.type( 'one' ); + await page.keyboard.press( 'Enter' ); + await pressWithModifier( 'primary', 'm' ); + + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); } ); From 01be7ac89b97b76c490d57a15c16466657240770 Mon Sep 17 00:00:00 2001 From: Anton Timmermans <email@atimmer.com> Date: Wed, 21 Nov 2018 11:02:20 +0100 Subject: [PATCH 088/254] Fix RichText rerendering when it shouldn't (#12161) * Fix RichText rerendering when it shouldn't The prepareEditableTree and onChangeEditableValue function stacks would have a new reference on every render. This prevents that by memoized the stack based on the previous stack and the newly added function. * Rename emptyArray to EMPTY_ARRAY * Add memize as a dependency to annotations package --- package-lock.json | 1 + packages/annotations/package.json | 1 + packages/annotations/src/format/annotation.js | 55 ++++++++++++++----- packages/annotations/src/store/selectors.js | 17 +++++- .../rich-text/src/register-format-type.js | 35 +++++++++--- 5 files changed, 84 insertions(+), 25 deletions(-) diff --git a/package-lock.json b/package-lock.json index e5d3df1c2322bf..90395bff8c7c94 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2283,6 +2283,7 @@ "@wordpress/i18n": "file:packages/i18n", "@wordpress/rich-text": "file:packages/rich-text", "lodash": "^4.17.10", + "memize": "^1.0.5", "rememo": "^3.0.0", "uuid": "^3.3.2" } diff --git a/packages/annotations/package.json b/packages/annotations/package.json index ec6f312c18f1de..6a177913bbc094 100644 --- a/packages/annotations/package.json +++ b/packages/annotations/package.json @@ -26,6 +26,7 @@ "@wordpress/i18n": "file:../i18n", "@wordpress/rich-text": "file:../rich-text", "lodash": "^4.17.10", + "memize": "^1.0.5", "rememo": "^3.0.0", "uuid": "^3.3.2" }, diff --git a/packages/annotations/src/format/annotation.js b/packages/annotations/src/format/annotation.js index a2f6f2973c7268..e72786bad0e3bc 100644 --- a/packages/annotations/src/format/annotation.js +++ b/packages/annotations/src/format/annotation.js @@ -1,3 +1,8 @@ +/** + * External dependencies + */ +import memize from 'memize'; + /** * WordPress dependencies */ @@ -115,6 +120,40 @@ function updateAnnotationsWithPositions( annotations, positions, { removeAnnotat } ); } +/** + * Create prepareEditableTree memoized based on the annotation props. + * + * @param {Object} The props with annotations in them. + * + * @return {Function} The prepareEditableTree. + */ +const createPrepareEditableTree = memize( ( props ) => { + const { annotations } = props; + + return ( formats, text ) => { + if ( annotations.length === 0 ) { + return formats; + } + + let record = { formats, text }; + record = applyAnnotations( record, annotations ); + return record.formats; + }; +} ); + +/** + * Returns the annotations as a props object. Memoized to prevent re-renders. + * + * @param {Array} The annotations to put in the object. + * + * @return {Object} The annotations props object. + */ +const getAnnotationObject = memize( ( annotations ) => { + return { + annotations, + }; +} ); + export const annotation = { name: FORMAT_NAME, title: __( 'Annotation' ), @@ -128,21 +167,9 @@ export const annotation = { return null; }, __experimentalGetPropsForEditableTreePreparation( select, { richTextIdentifier, blockClientId } ) { - return { - annotations: select( STORE_KEY ).__experimentalGetAnnotationsForRichText( blockClientId, richTextIdentifier ), - }; - }, - __experimentalCreatePrepareEditableTree( { annotations } ) { - return ( formats, text ) => { - if ( annotations.length === 0 ) { - return formats; - } - - let record = { formats, text }; - record = applyAnnotations( record, annotations ); - return record.formats; - }; + return getAnnotationObject( select( STORE_KEY ).__experimentalGetAnnotationsForRichText( blockClientId, richTextIdentifier ) ); }, + __experimentalCreatePrepareEditableTree: createPrepareEditableTree, __experimentalGetPropsForEditableTreeChangeHandler( dispatch ) { return { removeAnnotation: dispatch( STORE_KEY ).__experimentalRemoveAnnotation, diff --git a/packages/annotations/src/store/selectors.js b/packages/annotations/src/store/selectors.js index ca6fcb64796d5f..a39a315c92f907 100644 --- a/packages/annotations/src/store/selectors.js +++ b/packages/annotations/src/store/selectors.js @@ -4,6 +4,17 @@ import createSelector from 'rememo'; import { get, flatMap } from 'lodash'; +/** + * Shared reference to an empty array for cases where it is important to avoid + * returning a new array reference on every invocation, as in a connected or + * other pure component which performs `shouldComponentUpdate` check on props. + * This should be used as a last resort, since the normalized data should be + * maintained by the reducer result in state. + * + * @type {Array} + */ +const EMPTY_ARRAY = []; + /** * Returns the annotations for a specific client ID. * @@ -19,12 +30,12 @@ export const __experimentalGetAnnotationsForBlock = createSelector( } ); }, ( state, blockClientId ) => [ - get( state, blockClientId, [] ), + get( state, blockClientId, EMPTY_ARRAY ), ] ); export const __experimentalGetAllAnnotationsForBlock = function( state, blockClientId ) { - return get( state, blockClientId, [] ); + return get( state, blockClientId, EMPTY_ARRAY ); }; /** @@ -54,7 +65,7 @@ export const __experimentalGetAnnotationsForRichText = createSelector( } ); }, ( state, blockClientId ) => [ - get( state, blockClientId, [] ), + get( state, blockClientId, EMPTY_ARRAY ), ] ); diff --git a/packages/rich-text/src/register-format-type.js b/packages/rich-text/src/register-format-type.js index 8b61fc3a4308b1..262812a22b248f 100644 --- a/packages/rich-text/src/register-format-type.js +++ b/packages/rich-text/src/register-format-type.js @@ -2,6 +2,7 @@ * External dependencies */ import { mapKeys } from 'lodash'; +import memize from 'memize'; /** * WordPress dependencies @@ -10,6 +11,17 @@ import { select, dispatch, withSelect, withDispatch } from '@wordpress/data'; import { addFilter } from '@wordpress/hooks'; import { compose } from '@wordpress/compose'; +/** + * Shared reference to an empty array for cases where it is important to avoid + * returning a new array reference on every invocation, as in a connected or + * other pure component which performs `shouldComponentUpdate` check on props. + * This should be used as a last resort, since the normalized data should be + * maintained by the reducer result in state. + * + * @type {Array} + */ +const EMPTY_ARRAY = []; + /** * Registers a new format provided a unique name and an object defining its * behavior. @@ -119,6 +131,13 @@ export function registerFormatType( name, settings ) { dispatch( 'core/rich-text' ).addFormatTypes( settings ); + const getFunctionStackMemoized = memize( ( previousStack = EMPTY_ARRAY, newFunction ) => { + return [ + ...previousStack, + newFunction, + ]; + } ); + if ( settings.__experimentalGetPropsForEditableTreePreparation ) { @@ -133,13 +152,13 @@ export function registerFormatType( name, settings ) { const additionalProps = {}; if ( settings.__experimentalCreatePrepareEditableTree ) { - additionalProps.prepareEditableTree = [ - ...( props.prepareEditableTree || [] ), + additionalProps.prepareEditableTree = getFunctionStackMemoized( + props.prepareEditableTree, settings.__experimentalCreatePrepareEditableTree( props[ `format_${ name }` ], { richTextIdentifier: props.identifier, blockClientId: props.clientId, - } ), - ]; + } ) + ); } if ( settings.__experimentalCreateOnChangeEditableValue ) { @@ -155,16 +174,16 @@ export function registerFormatType( name, settings ) { return accumulator; }, {} ); - additionalProps.onChangeEditableValue = [ - ...( props.onChangeEditableValue || [] ), + additionalProps.onChangeEditableValue = getFunctionStackMemoized( + props.onChangeEditableValue, settings.__experimentalCreateOnChangeEditableValue( { ...props[ `format_${ name }` ], ...dispatchProps, }, { richTextIdentifier: props.identifier, blockClientId: props.clientId, - } ), - ]; + } ) + ); } return <OriginalComponent From f16642df555c1ddf62bb8f187c0c46f2a77f19dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20=28Greg=29=20Zi=C3=B3=C5=82kowski?= <grzegorz@gziolo.pl> Date: Wed, 21 Nov 2018 11:15:17 +0100 Subject: [PATCH 089/254] Fix keycodes package missing i18n dependency (#12164) --- lib/packages-dependencies.php | 1 + package-lock.json | 5 +++-- packages/keycodes/package.json | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/packages-dependencies.php b/lib/packages-dependencies.php index ee99db32c2005b..0e5bd808399b37 100644 --- a/lib/packages-dependencies.php +++ b/lib/packages-dependencies.php @@ -184,6 +184,7 @@ 'wp-is-shallow-equal' => array(), 'wp-keycodes' => array( 'lodash', + 'wp-i18n', ), 'wp-list-reusable-blocks' => array( 'lodash', diff --git a/package-lock.json b/package-lock.json index 90395bff8c7c94..80c04029c22c52 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2660,6 +2660,7 @@ "version": "file:packages/keycodes", "requires": { "@babel/runtime": "^7.0.0", + "@wordpress/i18n": "file:packages/i18n", "lodash": "^4.17.10" } }, @@ -11403,7 +11404,7 @@ }, "yargs": { "version": "11.1.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz", + "resolved": "http://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz", "integrity": "sha512-NwW69J42EsCSanF8kyn5upxvjp5ds+t3+udGBeTbFnERA+lF541DDpMawzo4z6W/QrzNM18D+BPMiOBibnFV5A==", "dev": true, "requires": { @@ -12714,7 +12715,7 @@ }, "yargs": { "version": "11.1.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz", + "resolved": "http://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz", "integrity": "sha512-NwW69J42EsCSanF8kyn5upxvjp5ds+t3+udGBeTbFnERA+lF541DDpMawzo4z6W/QrzNM18D+BPMiOBibnFV5A==", "dev": true, "requires": { diff --git a/packages/keycodes/package.json b/packages/keycodes/package.json index 399cdf6bc96918..d3f9ce99a7e165 100644 --- a/packages/keycodes/package.json +++ b/packages/keycodes/package.json @@ -21,6 +21,7 @@ "react-native": "src/index", "dependencies": { "@babel/runtime": "^7.0.0", + "@wordpress/i18n": "file:../i18n", "lodash": "^4.17.10" }, "publishConfig": { From 66d901e641423d7259894d3d2664f9f8872e3d85 Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Wed, 21 Nov 2018 11:15:53 +0100 Subject: [PATCH 090/254] Bump plugin version to 4.5.1 (#12158) --- 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 89c0d0b151e67f..327a6dba8e76fc 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.5.0 + * Version: 4.5.1 * Author: Gutenberg Team * * @package gutenberg diff --git a/package-lock.json b/package-lock.json index 80c04029c22c52..c2e08b8f0d9031 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "4.5.0", + "version": "4.5.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 65928bc9ee5e96..41b226272d1bad 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "4.5.0", + "version": "4.5.1", "private": true, "description": "A new WordPress editor experience", "repository": "git+https://github.com/WordPress/gutenberg.git", From 301b902d9c8ca6b1ed6a4deed3f3818d4000c67b Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski <grzegorz@gziolo.pl> Date: Wed, 21 Nov 2018 11:27:18 +0100 Subject: [PATCH 091/254] chore(release): publish - @wordpress/annotations@1.0.3 - @wordpress/block-library@2.2.6 - @wordpress/blocks@6.0.2 - @wordpress/components@7.0.1 - @wordpress/edit-post@3.1.1 - @wordpress/editor@9.0.1 - @wordpress/format-library@1.2.4 - @wordpress/html-entities@2.0.3 - @wordpress/keycodes@2.0.5 - @wordpress/list-reusable-blocks@1.1.14 - @wordpress/nux@3.0.2 - @wordpress/rich-text@3.0.2 --- packages/annotations/package.json | 2 +- packages/block-library/package.json | 2 +- packages/blocks/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/html-entities/package.json | 2 +- packages/keycodes/package.json | 2 +- packages/list-reusable-blocks/package.json | 2 +- packages/nux/package.json | 2 +- packages/rich-text/package.json | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/annotations/package.json b/packages/annotations/package.json index 6a177913bbc094..5a73fb5e31ae49 100644 --- a/packages/annotations/package.json +++ b/packages/annotations/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/annotations", - "version": "1.0.2", + "version": "1.0.3", "description": "Annotate content in the Gutenberg editor.", "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 064177c097fcee..33633f77deb614 100644 --- a/packages/block-library/package.json +++ b/packages/block-library/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-library", - "version": "2.2.5", + "version": "2.2.6", "description": "Block library for the WordPress editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/blocks/package.json b/packages/blocks/package.json index d03529c48731a5..0235f2c45f3e78 100644 --- a/packages/blocks/package.json +++ b/packages/blocks/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/blocks", - "version": "6.0.1", + "version": "6.0.2", "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 4e0cfe2929f10e..d0da7943e3255f 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/components", - "version": "7.0.0", + "version": "7.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 a6e719bc603b99..502b6541493a82 100644 --- a/packages/edit-post/package.json +++ b/packages/edit-post/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/edit-post", - "version": "3.1.0", + "version": "3.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 aa24d3bd941aec..7d46585ca6aad6 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/editor", - "version": "9.0.0", + "version": "9.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 0047fff85984ed..6c607684652bd6 100644 --- a/packages/format-library/package.json +++ b/packages/format-library/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/format-library", - "version": "1.2.3", + "version": "1.2.4", "description": "Format library for the WordPress editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/html-entities/package.json b/packages/html-entities/package.json index 89754753841825..bd46481b93cf04 100644 --- a/packages/html-entities/package.json +++ b/packages/html-entities/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/html-entities", - "version": "2.0.2", + "version": "2.0.3", "description": "HTML entity utilities for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/keycodes/package.json b/packages/keycodes/package.json index d3f9ce99a7e165..69970f60f38906 100644 --- a/packages/keycodes/package.json +++ b/packages/keycodes/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/keycodes", - "version": "2.0.4", + "version": "2.0.5", "description": "Keycodes utilities for WordPress. Used to check for keyboard events across browsers/operating systems.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/list-reusable-blocks/package.json b/packages/list-reusable-blocks/package.json index 6407589f85a68b..2e015ac6737d6c 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.13", + "version": "1.1.14", "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 6f6a4ac3d3a06e..820b6afa670267 100644 --- a/packages/nux/package.json +++ b/packages/nux/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/nux", - "version": "3.0.1", + "version": "3.0.2", "description": "NUX (New User eXperience) module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/rich-text/package.json b/packages/rich-text/package.json index de834f6c7451e2..aeb67dbfa3506a 100644 --- a/packages/rich-text/package.json +++ b/packages/rich-text/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/rich-text", - "version": "3.0.1", + "version": "3.0.2", "description": "Rich text value and manipulation API.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", From 51c3137007660158e48abc3a7167b415d48c3be4 Mon Sep 17 00:00:00 2001 From: Joen Asmussen <joen@automattic.com> Date: Wed, 21 Nov 2018 11:58:23 +0100 Subject: [PATCH 092/254] Fix issue with dragondrop in columns (#12159) The selector for enabling pointer events was a little too deep, which meant that dragondrop broke. This fixes it, but the columns block retains the simplicity of not allowing you to click/select columns still. --- packages/block-library/src/columns/editor.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-library/src/columns/editor.scss b/packages/block-library/src/columns/editor.scss index db8b558c4a85a7..50b21f23523313 100644 --- a/packages/block-library/src/columns/editor.scss +++ b/packages/block-library/src/columns/editor.scss @@ -138,6 +138,6 @@ } // This selector re-enables clicking on any child of a column block. -:not(.components-disabled) > .wp-block-columns > .editor-inner-blocks > .editor-block-list__layout > [data-type="core/column"] > .editor-block-list__block-edit .editor-block-list__layout > * { +:not(.components-disabled) > .wp-block-columns > .editor-inner-blocks > .editor-block-list__layout > [data-type="core/column"] > .editor-block-list__block-edit > * { pointer-events: all; } From 4ddb5aa2353aecf3b170ba5444b9605ce40ab352 Mon Sep 17 00:00:00 2001 From: Jorge <jorge.costa@developer.pt> Date: Wed, 21 Nov 2018 11:00:39 +0000 Subject: [PATCH 093/254] Fix: remove formats from document outline headings. (#12130) After #8204 was merged, we started using the heading content as the outline item. So if the heading contained custom formats, they were shown in the document outline. Showing the formats in the outline may break the design. This commit makes sure we show the plain text in the hading without any formats. --- .../src/components/document-outline/index.js | 12 ++++++---- .../test/__snapshots__/index.js.snap | 24 ++++--------------- 2 files changed, 11 insertions(+), 25 deletions(-) diff --git a/packages/editor/src/components/document-outline/index.js b/packages/editor/src/components/document-outline/index.js index ad2ca5c2b61816..258bb792e7e3fd 100644 --- a/packages/editor/src/components/document-outline/index.js +++ b/packages/editor/src/components/document-outline/index.js @@ -9,12 +9,15 @@ import { countBy, flatMap, get } from 'lodash'; import { __ } from '@wordpress/i18n'; import { compose } from '@wordpress/compose'; import { withSelect, withDispatch } from '@wordpress/data'; +import { + create, + getTextContent, +} from '@wordpress/rich-text'; /** * Internal dependencies */ import DocumentOutlineItem from './item'; -import RichText from './../rich-text'; /** * Module constants @@ -120,10 +123,9 @@ export const DocumentOutline = ( { blocks = [], title, onSelect, isTitleSupporte > { item.isEmpty ? emptyHeadingContent : - <RichText.Content - tagName="span" - value={ item.attributes.content } - /> + getTextContent( + create( { html: item.attributes.content } ) + ) } { isIncorrectLevel && incorrectLevelContent } { item.level === 1 && hasMultipleH1 && multipleH1Headings } diff --git a/packages/editor/src/components/document-outline/test/__snapshots__/index.js.snap b/packages/editor/src/components/document-outline/test/__snapshots__/index.js.snap index a038d12930951e..89d0ebb04d6582 100644 --- a/packages/editor/src/components/document-outline/test/__snapshots__/index.js.snap +++ b/packages/editor/src/components/document-outline/test/__snapshots__/index.js.snap @@ -12,11 +12,7 @@ exports[`DocumentOutline header blocks present should match snapshot 1`] = ` onClick={[Function]} path={Array []} > - <Component - format="string" - tagName="span" - value="Heading parent" - /> + Heading parent </TableOfContentsItem> <TableOfContentsItem isValid={true} @@ -25,11 +21,7 @@ exports[`DocumentOutline header blocks present should match snapshot 1`] = ` onClick={[Function]} path={Array []} > - <Component - format="string" - tagName="span" - value="Heading child" - /> + Heading child </TableOfContentsItem> </ul> </div> @@ -47,11 +39,7 @@ exports[`DocumentOutline header blocks present should render warnings for multip onClick={[Function]} path={Array []} > - <Component - format="string" - tagName="span" - value="Heading 1" - /> + Heading 1 <br key="incorrect-break-multiple-h1" /> @@ -68,11 +56,7 @@ exports[`DocumentOutline header blocks present should render warnings for multip onClick={[Function]} path={Array []} > - <Component - format="string" - tagName="span" - value="Heading 1" - /> + Heading 1 <br key="incorrect-break-multiple-h1" /> From 0000e348c7561f3ab53014c4906362a2fd3738bb Mon Sep 17 00:00:00 2001 From: William Earnhardt <wearnhardt@gmail.com> Date: Wed, 21 Nov 2018 06:03:13 -0500 Subject: [PATCH 094/254] Remove .md only commit check in travis config (#12069) --- .travis.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5dc3f1ee26230a..bd4b9a57fc707e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,11 +19,6 @@ cache: - $HOME/.npm before_install: - - | - git diff --name-only $TRAVIS_COMMIT_RANGE | grep -qvE '(\.md$)' || { - echo "Only docs were updated, stopping build process." - exit - } - nvm install && nvm use - npm install npm -g From 5d275d1c07358edf065b3a43684c669f0ed74a12 Mon Sep 17 00:00:00 2001 From: Raquel Smith <raquelmsmith@users.noreply.github.com> Date: Wed, 21 Nov 2018 04:36:55 -0800 Subject: [PATCH 095/254] Add e2e tests for the format API (#11948) * Add e2e tests for the format API * Fixes some whitespace issues * Update format-api.test.js * Add missing dependencies to the script registration * Add snapshot to test * Check for custom button, remove extra lines * Change modifier keys used From #11855 --- .../__snapshots__/format-api.test.js.snap | 7 ++++ test/e2e/specs/format-api.test.js | 41 +++++++++++++++++++ test/e2e/test-plugins/format-api.php | 23 +++++++++++ test/e2e/test-plugins/format-api/index.js | 33 +++++++++++++++ 4 files changed, 104 insertions(+) create mode 100644 test/e2e/specs/__snapshots__/format-api.test.js.snap create mode 100644 test/e2e/specs/format-api.test.js create mode 100644 test/e2e/test-plugins/format-api.php create mode 100644 test/e2e/test-plugins/format-api/index.js diff --git a/test/e2e/specs/__snapshots__/format-api.test.js.snap b/test/e2e/specs/__snapshots__/format-api.test.js.snap new file mode 100644 index 00000000000000..8dbbc915521be4 --- /dev/null +++ b/test/e2e/specs/__snapshots__/format-api.test.js.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Using Format API Clicking the control wraps the selected text properly with HTML code 1`] = ` +"<!-- wp:paragraph --> +<p><a href=\\"#test\\" class=\\"my-plugin-link\\">First paragraph</a></p> +<!-- /wp:paragraph -->" +`; diff --git a/test/e2e/specs/format-api.test.js b/test/e2e/specs/format-api.test.js new file mode 100644 index 00000000000000..6a1361a854e133 --- /dev/null +++ b/test/e2e/specs/format-api.test.js @@ -0,0 +1,41 @@ +/** + * Internal dependencies + */ +import { + clickBlockAppender, + getEditedPostContent, + newPost, + pressWithModifier, +} from '../support/utils'; +import { activatePlugin, deactivatePlugin } from '../support/plugins'; + +describe( 'Using Format API', () => { + beforeAll( async () => { + await activatePlugin( 'gutenberg-test-format-api' ); + } ); + + afterAll( async () => { + await deactivatePlugin( 'gutenberg-test-format-api' ); + } ); + + beforeEach( async () => { + await newPost(); + } ); + + it( 'Format toolbar is present in a paragraph block', async () => { + await clickBlockAppender(); + await page.keyboard.type( 'First paragraph' ); + await page.mouse.move( 200, 300, { steps: 10 } ); + expect( await page.$( '[aria-label="Custom Link"]' ) ).not.toBeNull(); + } ); + + it( 'Clicking the control wraps the selected text properly with HTML code', async () => { + await clickBlockAppender(); + await page.keyboard.type( 'First paragraph' ); + await pressWithModifier( 'shiftAlt', 'ArrowLeft' ); + await pressWithModifier( 'primary', 'A' ); + await page.mouse.move( 200, 300, { steps: 10 } ); + await page.click( '[aria-label="Custom Link"]' ); + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); +} ); diff --git a/test/e2e/test-plugins/format-api.php b/test/e2e/test-plugins/format-api.php new file mode 100644 index 00000000000000..dc33462affbc89 --- /dev/null +++ b/test/e2e/test-plugins/format-api.php @@ -0,0 +1,23 @@ +<?php +/** + * Plugin Name: Gutenberg Test Format API + * Plugin URI: https://github.com/WordPress/gutenberg + * Author: Gutenberg Team + * + * @package gutenberg-test-format-api + */ + +/** + * Enqueue plugin JavaScript for the editor + */ +function gutenberg_test_format_api_scripts() { + wp_enqueue_script( + 'gutenberg-test-format-api', + plugins_url( 'format-api/index.js', __FILE__ ), + array( 'wp-editor', 'wp-element', 'wp-rich-text' ), + filemtime( plugin_dir_path( __FILE__ ) . 'format-api/index.js' ), + true + ); +} + +add_action( 'enqueue_block_editor_assets', 'gutenberg_test_format_api_scripts' ); diff --git a/test/e2e/test-plugins/format-api/index.js b/test/e2e/test-plugins/format-api/index.js new file mode 100644 index 00000000000000..4d41f7c26c4b3a --- /dev/null +++ b/test/e2e/test-plugins/format-api/index.js @@ -0,0 +1,33 @@ +( function() { + wp.richText.registerFormatType( + 'my-plugin/link', { + title: 'Custom Link', + tagName: 'a', + attributes: { + url: 'href', + }, + className: 'my-plugin-link', + edit: function( props ) { + return wp.element.createElement( + wp.editor.RichTextToolbarButton, { + icon: 'admin-links', + title: 'Custom Link', + onClick: function() { + props.onChange( + wp.richText.toggleFormat( + props.value, { + type: 'my-plugin/link', + attributes: { + url: '#test', + } + } + ) + ); + }, + isActive: props.isActive, + } + ); + } + } + ); +} )(); From 17deea5f7555e020f1c249a925038430e23379fb Mon Sep 17 00:00:00 2001 From: Joen Asmussen <joen@automattic.com> Date: Wed, 21 Nov 2018 14:31:52 +0100 Subject: [PATCH 096/254] Improve scoping of nested paragraph right-padding rule (#12106) * Improve scoping of nested paragraph right-padding rule This addresses feedback in https://github.com/WordPress/gutenberg/pull/11757#issuecomment-440269586. The initial issue was that in the default appender, and in empty paragraphs, the plus to add blocks overlaps the placeholder text. The right-padding fixed that. But it bled into other blocks with placeholders, such as the Button block. This PR scopes the rich text rule to only apply to empty paragraphs. The appender was already scoped. * Move paragraph styles to the right place. * Move to editor.scss --- packages/block-library/src/editor.scss | 1 + packages/block-library/src/paragraph/editor.scss | 6 ++++++ packages/editor/src/components/rich-text/style.scss | 6 ------ 3 files changed, 7 insertions(+), 6 deletions(-) create mode 100644 packages/block-library/src/paragraph/editor.scss diff --git a/packages/block-library/src/editor.scss b/packages/block-library/src/editor.scss index 31c6eccaf829c1..eb2dea826189bd 100644 --- a/packages/block-library/src/editor.scss +++ b/packages/block-library/src/editor.scss @@ -18,6 +18,7 @@ @import "./list/editor.scss"; @import "./more/editor.scss"; @import "./nextpage/editor.scss"; +@import "./paragraph/editor.scss"; @import "./preformatted/editor.scss"; @import "./pullquote/editor.scss"; @import "./quote/editor.scss"; diff --git a/packages/block-library/src/paragraph/editor.scss b/packages/block-library/src/paragraph/editor.scss new file mode 100644 index 00000000000000..98ea5e4e2b85a4 --- /dev/null +++ b/packages/block-library/src/paragraph/editor.scss @@ -0,0 +1,6 @@ +// Specific to the empty paragraph placeholder: +// when shown on mobile and in nested contexts, the plus to add blocks shows up on the right. +// This padding makes sure it doesn't overlap text. +.editor-rich-text__tinymce[data-is-placeholder-visible="true"] + .editor-rich-text__tinymce.wp-block-paragraph { + padding-right: $icon-button-size; +} diff --git a/packages/editor/src/components/rich-text/style.scss b/packages/editor/src/components/rich-text/style.scss index b4ebf2b4b8c69f..731809331440cb 100644 --- a/packages/editor/src/components/rich-text/style.scss +++ b/packages/editor/src/components/rich-text/style.scss @@ -100,12 +100,6 @@ // Ensure that if placeholder wraps (mobile/nested contexts) the clickable area is full-height. height: 100%; - - // On mobile and in nested contexts, the plus to add blocks shows up on the right. - // This padding makes sure it doesn't overlap text. - & + .editor-rich-text__tinymce { - padding-right: $icon-button-size; - } } // Placeholder text. From 8bae9259cd621b2417e2e7b95d78825b3c2a5655 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20Van=C2=A0Dorpe?= <iseulde@automattic.com> Date: Wed, 21 Nov 2018 14:33:03 +0100 Subject: [PATCH 097/254] RichText: remove onSplit from docs (#12174) --- packages/editor/src/components/rich-text/README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/editor/src/components/rich-text/README.md b/packages/editor/src/components/rich-text/README.md index b5449fbe4ec9e0..19ddd35bd26664 100644 --- a/packages/editor/src/components/rich-text/README.md +++ b/packages/editor/src/components/rich-text/README.md @@ -25,10 +25,6 @@ Render a rich [`contenteditable` input](https://developer.mozilla.org/en-US/docs *Optional.* By default, a line break will be inserted on <kbd>Enter</kbd>. If the editable field can contain multiple paragraphs, this property can be set to create new paragraphs on <kbd>Enter</kbd>. -### `onSplit( before: Array|String, after: Array|String, ...blocks: Object ): Function` - -*Optional.* Called when the content can be split with `before` and `after`. There might be blocks present, which should be inserted in between. - ### `onReplace( blocks: Array ): Function` *Optional.* Called when the `RichText` instance is empty and it can be replaced with the given blocks. From 18316878470559a985c580ac4c210a9ca2a73654 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20Van=C2=A0Dorpe?= <iseulde@automattic.com> Date: Wed, 21 Nov 2018 14:37:45 +0100 Subject: [PATCH 098/254] Fix TinyMCE list plugin registration (#12170) --- docs/contributors/scripts.md | 3 +-- lib/client-assets.php | 8 ++------ lib/packages-dependencies.php | 2 +- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/docs/contributors/scripts.md b/docs/contributors/scripts.md index 0bd0cf98927921..dd7e1d295d528c 100644 --- a/docs/contributors/scripts.md +++ b/docs/contributors/scripts.md @@ -50,13 +50,12 @@ The editor also uses some popular third-party packages and scripts. Plugin devel | [React](https://reactjs.org) | react | React is a JavaScript library for building user interfaces | | [React Dom](https://reactjs.org/docs/react-dom.html) | react-dom | Serves as the entry point to the DOM and server renderers for React, intended to be paired with React | | [Moment](https://momentjs.com/) | moment| Parse, validate, manipulate, and display dates and times in JavaScript | -| [TinyMCE Lists](https://www.tiny.cloud/docs/plugins/lists/) | tinymce-latest-lists| The `lists` plugin allows you to add numbered and bulleted lists to TinyMCE | | [Lodash](https://lodash.com) | lodash| Lodash is a JavaScript library which provides utility functions for common programming tasks | ## Polyfill Scripts The editor also provides polyfills for certain features that may not be available in all modern browsers. -It is recommened to use the main `wp-polyfill` script handle which takes care of loading all the below mentioned polyfills. +It is recommened to use the main `wp-polyfill` script handle which takes care of loading all the below mentioned polyfills. | Script Name | Handle | Description | |-------------|--------|-------------| diff --git a/lib/client-assets.php b/lib/client-assets.php index 6d1522b57d3702..5dfb4c18c0a777 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -87,6 +87,8 @@ function register_tinymce_scripts() { gutenberg_override_script( 'wp-tinymce-root', includes_url( 'js/tinymce/' ) . "tinymce{$mce_suffix}.js", array(), $tinymce_version ); gutenberg_override_script( 'wp-tinymce', includes_url( 'js/tinymce/' ) . "plugins/compat3x/plugin{$suffix}.js", array( 'wp-tinymce-root' ), $tinymce_version ); } + + gutenberg_override_script( 'wp-tinymce-lists', includes_url( 'js/tinymce/' ) . "plugins/lists/plugin{$suffix}.js", array( 'wp-tinymce' ), $tinymce_version ); } } @@ -620,12 +622,6 @@ function gutenberg_register_vendor_scripts() { 'https://unpkg.com/moment@2.22.1/' . $moment_script, array() ); - $tinymce_version = '4.7.11'; - gutenberg_register_vendor_script( - 'tinymce-latest-lists', - 'https://unpkg.com/tinymce@' . $tinymce_version . '/plugins/lists/plugin' . $suffix . '.js', - array( 'wp-tinymce' ) - ); gutenberg_register_vendor_script( 'lodash', 'https://unpkg.com/lodash@4.17.5/lodash' . $suffix . '.js' diff --git a/lib/packages-dependencies.php b/lib/packages-dependencies.php index 0e5bd808399b37..f4f1695c966a31 100644 --- a/lib/packages-dependencies.php +++ b/lib/packages-dependencies.php @@ -134,7 +134,7 @@ 'wp-editor' => array( 'jquery', 'lodash', - 'tinymce-latest-lists', + 'wp-tinymce-lists', 'wp-a11y', 'wp-api-fetch', 'wp-blob', From 53e4c40bda48a9ca3df930cd4a437001272a257e Mon Sep 17 00:00:00 2001 From: Oliver Juhas <webmandesign@users.noreply.github.com> Date: Wed, 21 Nov 2018 14:51:04 +0100 Subject: [PATCH 099/254] Fixing IE11 flexbox alignment when min-width is set (#9196) * Fixing IE11 flexbox alignment when min-width is set For more info on the fix please see https://github.com/philipwalton/flexbugs/issues/231 * Updating the IE11 alignment fix Updating the fix to use https://github.com/philipwalton/flexbugs/issues/231#issuecomment-362790042 approach for cases with larger cover image text. * Documenting the code * chore: Tweak code style * Add display: block, remove Github ticket mention. * Ignore the IE11 cover image fix in browsers that support flex. * Add a better description of the IE fix. * Fix typo. --- packages/block-library/src/cover/style.scss | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/block-library/src/cover/style.scss b/packages/block-library/src/cover/style.scss index 806ed02fb2dc9b..a9223bfb30c2fe 100644 --- a/packages/block-library/src/cover/style.scss +++ b/packages/block-library/src/cover/style.scss @@ -96,6 +96,20 @@ width: 100%; } + // Using flexbox without an assigned height property breaks vertical center alignment in IE11. + // Appending an empty ::after element tricks IE11 into giving the cover image an implicit height, which sidesteps this issue. + &::after { + display: block; + content: ""; + font-size: 0; + min-height: inherit; + + // IE doesn't support flex so omit that. + @supports (position: sticky) { + content: none; + } + } + // Aligned cover blocks should not use our global alignment rules &.aligncenter, &.alignleft, From 4a44b55b9e66d7f3287850b520eaa461d01b0af9 Mon Sep 17 00:00:00 2001 From: Tim Wright <timwright12@gmail.com> Date: Wed, 21 Nov 2018 13:12:53 -0500 Subject: [PATCH 100/254] Tooltips: Consistency and contrast: Issue #11180 (#11307) * merge from upstream * adjusted editor header tooltip label positions for better contrast * removed gutenberg mobile * udpated snapshot --- packages/components/src/icon-button/index.js | 7 ++----- .../edit-post/src/components/header/more-menu/index.js | 1 + .../header/more-menu/test/__snapshots__/index.js.snap | 2 ++ packages/editor/src/components/inserter/index.js | 1 + packages/editor/src/components/table-of-contents/index.js | 1 + 5 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/components/src/icon-button/index.js b/packages/components/src/icon-button/index.js index b90ee85d2b6080..bec3d38d548646 100644 --- a/packages/components/src/icon-button/index.js +++ b/packages/components/src/icon-button/index.js @@ -20,7 +20,7 @@ import Dashicon from '../dashicon'; // is common to apply a ref to the button element (only supported in class) class IconButton extends Component { render() { - const { icon, children, label, className, tooltip, shortcut, ...additionalProps } = this.props; + const { icon, children, label, className, tooltip, shortcut, labelPosition, ...additionalProps } = this.props; const classes = classnames( 'components-icon-button', className ); const tooltipText = tooltip || label; @@ -49,10 +49,7 @@ class IconButton extends Component { if ( showTooltip ) { element = ( - <Tooltip - text={ tooltipText } - shortcut={ shortcut } - > + <Tooltip text={ tooltipText } shortcut={ shortcut } position={ labelPosition }> { element } </Tooltip> ); diff --git a/packages/edit-post/src/components/header/more-menu/index.js b/packages/edit-post/src/components/header/more-menu/index.js index a65aa8c9b2fbc8..223c2c0f272612 100644 --- a/packages/edit-post/src/components/header/more-menu/index.js +++ b/packages/edit-post/src/components/header/more-menu/index.js @@ -26,6 +26,7 @@ const MoreMenu = () => ( <IconButton icon="ellipsis" label={ isOpen ? ariaOpen : ariaClosed } + labelPosition="bottom" onClick={ onToggle } aria-expanded={ isOpen } /> diff --git a/packages/edit-post/src/components/header/more-menu/test/__snapshots__/index.js.snap b/packages/edit-post/src/components/header/more-menu/test/__snapshots__/index.js.snap index 244a185376638c..4049a747b3fe0a 100644 --- a/packages/edit-post/src/components/header/more-menu/test/__snapshots__/index.js.snap +++ b/packages/edit-post/src/components/header/more-menu/test/__snapshots__/index.js.snap @@ -16,9 +16,11 @@ exports[`MoreMenu should match snapshot 1`] = ` aria-expanded={false} icon="ellipsis" label="Show more tools & options" + labelPosition="bottom" onClick={[Function]} > <Tooltip + position="bottom" text="Show more tools & options" > <Button diff --git a/packages/editor/src/components/inserter/index.js b/packages/editor/src/components/inserter/index.js index 1133f6e827faed..0012fbc8ddb1d1 100644 --- a/packages/editor/src/components/inserter/index.js +++ b/packages/editor/src/components/inserter/index.js @@ -16,6 +16,7 @@ const defaultRenderToggle = ( { onToggle, disabled, isOpen } ) => ( <IconButton icon="insert" label={ __( 'Add block' ) } + labelPosition="bottom" onClick={ onToggle } className="editor-inserter__toggle" aria-haspopup="true" diff --git a/packages/editor/src/components/table-of-contents/index.js b/packages/editor/src/components/table-of-contents/index.js index 7930b33276b258..59e2a2c736679e 100644 --- a/packages/editor/src/components/table-of-contents/index.js +++ b/packages/editor/src/components/table-of-contents/index.js @@ -22,6 +22,7 @@ function TableOfContents( { hasBlocks } ) { icon="info-outline" aria-expanded={ isOpen } label={ __( 'Content structure' ) } + labelPosition="bottom" disabled={ ! hasBlocks } /> ) } From c9fad3bc11eb58c5d3047a6ba1d6b16d15d1acf5 Mon Sep 17 00:00:00 2001 From: Jorge <jorge.costa@developer.pt> Date: Wed, 21 Nov 2018 18:29:13 +0000 Subject: [PATCH 101/254] Fix: Disable HTML edit from Media & Text block (#12191) Blocks that rely on InnerBlocks have some problems if the user uses the HTML edit function of the block the editor may crash. To solve that on the columns block HTML edit was disabled, this commit applies the same solution to the Media & Text block. --- packages/block-library/src/media-text/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/block-library/src/media-text/index.js b/packages/block-library/src/media-text/index.js index 281db991ae268a..6cb54f5d3c8309 100644 --- a/packages/block-library/src/media-text/index.js +++ b/packages/block-library/src/media-text/index.js @@ -83,6 +83,7 @@ export const settings = { supports: { align: [ 'wide', 'full' ], + html: false, }, transforms: { From 36acf7dc6fc17bf1b9602d79ec2d7cbacc1fee0e Mon Sep 17 00:00:00 2001 From: Danilo Ercoli <ercoli@gmail.com> Date: Wed, 21 Nov 2018 19:32:48 +0100 Subject: [PATCH 102/254] [RNMobile] Wire onReplace on Para block (#12125) * Wire onReplace and start using it when splitting blocks * Remove onReplace that is still not used in rich-text/index.native.js. It will be used on cut&paste feature. * Remove testing code --- packages/block-library/src/paragraph/edit.native.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/block-library/src/paragraph/edit.native.js b/packages/block-library/src/paragraph/edit.native.js index 91cc885170144d..4e07a0b4560958 100644 --- a/packages/block-library/src/paragraph/edit.native.js +++ b/packages/block-library/src/paragraph/edit.native.js @@ -42,6 +42,7 @@ class ParagraphEdit extends Component { attributes, insertBlocksAfter, setAttributes, + onReplace, } = this.props; if ( after !== null ) { @@ -57,8 +58,7 @@ class ParagraphEdit extends Component { const { content } = attributes; if ( before === null ) { - // TODO : If before content is omitted, treat as intent to delete block. - // onReplace( [] ); + onReplace( [] ); } else if ( content !== before ) { // Only update content if it has in-fact changed. In case that user // has created a new paragraph at end of an existing one, the value From 43bb1eddb0c3f67eb8d56fff0c853787f4c78e16 Mon Sep 17 00:00:00 2001 From: Jon Desrosiers <desrosj@users.noreply.github.com> Date: Wed, 21 Nov 2018 13:53:40 -0500 Subject: [PATCH 103/254] Fix cover block placeholder background color (#12187) Prevent the `components` package styles from overriding the cover block background color. Fixes #12179. Related: [Trac-45279](https://core.trac.wordpress.org/ticket/45279). For details on how to reproduce the issue, see #12179. ## Checklist: - [x] My code is tested. - [x] My code follows the WordPress code style. <!-- Check code: `npm run lint`, Guidelines: https://make.wordpress.org/core/handbook/best-practices/coding-standards/javascript/ --> - ~[ ] My code follows the accessibility standards.~ <!-- Guidelines: https://make.wordpress.org/core/handbook/best-practices/coding-standards/accessibility-coding-standards/ --> - ~[ ] My code has proper inline documentation.~ <!-- Guidelines: https://make.wordpress.org/core/handbook/best-practices/inline-documentation-standards/javascript/ --> --- packages/block-library/src/cover/style.scss | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/block-library/src/cover/style.scss b/packages/block-library/src/cover/style.scss index a9223bfb30c2fe..6cbc5f1e3366c6 100644 --- a/packages/block-library/src/cover/style.scss +++ b/packages/block-library/src/cover/style.scss @@ -84,7 +84,13 @@ } &.components-placeholder { - height: inherit; + // use opacity to work in various editor styles + background: $dark-opacity-light-200; + min-height: 200px; + + .is-dark-theme & { + background: $light-opacity-light-200; + } } // Apply max-width to floated items that have no intrinsic width From 89f8897879886d6434aec469db3ab9d5840f085b Mon Sep 17 00:00:00 2001 From: Jorge <jorge.costa@developer.pt> Date: Wed, 21 Nov 2018 18:57:40 +0000 Subject: [PATCH 104/254] Fix: Unregistering a block type causes blocks that convert to it to break the editor (#12192) --- packages/editor/src/components/block-switcher/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor/src/components/block-switcher/index.js b/packages/editor/src/components/block-switcher/index.js index 390e6a7f321371..adc4bf4efc142e 100644 --- a/packages/editor/src/components/block-switcher/index.js +++ b/packages/editor/src/components/block-switcher/index.js @@ -47,7 +47,7 @@ export class BlockSwitcher extends Component { const possibleBlockTransformations = orderBy( filter( getPossibleBlockTransformations( blocks ), - ( block ) => !! itemsByName[ block.name ] + ( block ) => block && !! itemsByName[ block.name ] ), ( block ) => itemsByName[ block.name ].frecency, 'desc' From b7bfcd1d5ba501742517cd738862a4a42ae9650b Mon Sep 17 00:00:00 2001 From: Robert Anderson <robert@noisysocks.com> Date: Thu, 22 Nov 2018 15:24:32 +1100 Subject: [PATCH 105/254] Fix exception when editing non-public CPTs (#12201) * Fix exception when editing non-public CPTs - Changes the posts API endpoint to only set `permalink_template` and `generated_slug` when the post is public and queryable. This matches the behaviour in Core. - Changes the `getPermalink()` and `getPermalinkParts()` selectors to return null when there is no `permalink_template`. This fixes an exception on non-public CPTs caused by attempting to parse an undefined `permalink_template`. * Generate docs --- .../developers/data/data-core-editor.md | 8 ++++--- lib/rest-api.php | 5 +++++ packages/editor/src/store/selectors.js | 19 ++++++++++++---- packages/editor/src/store/test/selectors.js | 22 +++++++++++++++++++ 4 files changed, 47 insertions(+), 7 deletions(-) diff --git a/docs/designers-developers/developers/data/data-core-editor.md b/docs/designers-developers/developers/data/data-core-editor.md index 25ea6598990d83..f48dc8539dd2ef 100644 --- a/docs/designers-developers/developers/data/data-core-editor.md +++ b/docs/designers-developers/developers/data/data-core-editor.md @@ -1292,11 +1292,12 @@ Returns the permalink for the post. *Returns* -The permalink. +The permalink, or null if the post is not viewable. ### getPermalinkParts -Returns the permalink for a post, split into it's three parts: the prefix, the postName, and the suffix. +Returns the permalink for a post, split into it's three parts: the prefix, +the postName, and the suffix. *Parameters* @@ -1304,7 +1305,8 @@ Returns the permalink for a post, split into it's three parts: the prefix, the p *Returns* -The prefix, postName, and suffix for the permalink. +An object containing the prefix, postName, and suffix for + the permalink, or null if the post is not viewable. ### inSomeHistory diff --git a/lib/rest-api.php b/lib/rest-api.php index 35f4af5c52e77e..2baec1befdd19d 100644 --- a/lib/rest-api.php +++ b/lib/rest-api.php @@ -206,6 +206,11 @@ function gutenberg_add_permalink_template_to_posts( $response, $post, $request ) return $response; } + $post_type_obj = get_post_type_object( $post->post_type ); + if ( ! is_post_type_viewable( $post_type_obj ) || ! $post_type_obj->public ) { + return $response; + } + if ( ! function_exists( 'get_sample_permalink' ) ) { require_once ABSPATH . '/wp-admin/includes/post.php'; } diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index 5cede64dd95ade..9cc31b46373cb8 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -2119,10 +2119,15 @@ export function isPermalinkEditable( state ) { * * @param {Object} state Editor state. * - * @return {string} The permalink. + * @return {?string} The permalink, or null if the post is not viewable. */ export function getPermalink( state ) { - const { prefix, postName, suffix } = getPermalinkParts( state ); + const permalinkParts = getPermalinkParts( state ); + if ( ! permalinkParts ) { + return null; + } + + const { prefix, postName, suffix } = permalinkParts; if ( isPermalinkEditable( state ) ) { return prefix + postName + suffix; @@ -2132,14 +2137,20 @@ export function getPermalink( state ) { } /** - * Returns the permalink for a post, split into it's three parts: the prefix, the postName, and the suffix. + * Returns the permalink for a post, split into it's three parts: the prefix, + * the postName, and the suffix. * * @param {Object} state Editor state. * - * @return {Object} The prefix, postName, and suffix for the permalink. + * @return {Object} An object containing the prefix, postName, and suffix for + * the permalink, or null if the post is not viewable. */ export function getPermalinkParts( state ) { const permalinkTemplate = getEditedPostAttribute( state, 'permalink_template' ); + if ( ! permalinkTemplate ) { + return null; + } + const postName = getEditedPostAttribute( state, 'slug' ) || getEditedPostAttribute( state, 'generated_slug' ); const [ prefix, suffix ] = permalinkTemplate.split( PERMALINK_POSTNAME_REGEX ); diff --git a/packages/editor/src/store/test/selectors.js b/packages/editor/src/store/test/selectors.js index 751bfccc2945f0..e8249239e4a974 100644 --- a/packages/editor/src/store/test/selectors.js +++ b/packages/editor/src/store/test/selectors.js @@ -5042,6 +5042,17 @@ describe( 'selectors', () => { expect( getPermalink( state ) ).toBe( 'http://foo.test/bar/baz/' ); } ); + + it( 'should return null if the post has no permalink template', () => { + const state = { + currentPost: {}, + editor: { + present: {}, + }, + }; + + expect( getPermalink( state ) ).toBeNull(); + } ); } ); describe( 'getPermalinkParts', () => { @@ -5087,6 +5098,17 @@ describe( 'selectors', () => { expect( getPermalinkParts( state ) ).toEqual( parts ); } ); + + it( 'should return null if the post has no permalink template', () => { + const state = { + currentPost: {}, + editor: { + present: {}, + }, + }; + + expect( getPermalinkParts( state ) ).toBeNull(); + } ); } ); describe( 'getBlockListSettings', () => { From cb0b8992a132f28714da9e9d721603482013e1dd Mon Sep 17 00:00:00 2001 From: Robert Anderson <robert@noisysocks.com> Date: Thu, 22 Nov 2018 16:34:51 +1100 Subject: [PATCH 106/254] chore(release): publish - @wordpress/block-library@2.2.7 - @wordpress/components@7.0.2 - @wordpress/edit-post@3.1.2 - @wordpress/editor@9.0.2 - @wordpress/format-library@1.2.5 - @wordpress/list-reusable-blocks@1.1.15 - @wordpress/nux@3.0.3 --- 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 33633f77deb614..700c2d9801e60e 100644 --- a/packages/block-library/package.json +++ b/packages/block-library/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-library", - "version": "2.2.6", + "version": "2.2.7", "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 d0da7943e3255f..8e470335fa50a6 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/components", - "version": "7.0.1", + "version": "7.0.2", "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 502b6541493a82..c871c899256c9e 100644 --- a/packages/edit-post/package.json +++ b/packages/edit-post/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/edit-post", - "version": "3.1.1", + "version": "3.1.2", "description": "Edit Post module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/editor/package.json b/packages/editor/package.json index 7d46585ca6aad6..45f6c934df1b1f 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/editor", - "version": "9.0.1", + "version": "9.0.2", "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 6c607684652bd6..9fa59207801c16 100644 --- a/packages/format-library/package.json +++ b/packages/format-library/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/format-library", - "version": "1.2.4", + "version": "1.2.5", "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 2e015ac6737d6c..190171efe08511 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.14", + "version": "1.1.15", "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 820b6afa670267..82cbc79c4650d5 100644 --- a/packages/nux/package.json +++ b/packages/nux/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/nux", - "version": "3.0.2", + "version": "3.0.3", "description": "NUX (New User eXperience) module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", From 5653f1095945a04e307b9c1d8b74745c121e17cd Mon Sep 17 00:00:00 2001 From: Andrea Fercia <a.fercia@gmail.com> Date: Thu, 22 Nov 2018 10:02:03 +0100 Subject: [PATCH 107/254] Make the image Link URL field readonly. (#12190) --- packages/block-library/src/image/edit.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/block-library/src/image/edit.js b/packages/block-library/src/image/edit.js index 7daffa06203b08..cb059b5bfcdc60 100644 --- a/packages/block-library/src/image/edit.js +++ b/packages/block-library/src/image/edit.js @@ -441,7 +441,7 @@ class ImageEdit extends Component { } ); const isResizable = [ 'wide', 'full' ].indexOf( align ) === -1 && isLargeViewport; - const isLinkURLInputDisabled = linkDestination !== LINK_DESTINATION_CUSTOM; + const isLinkURLInputReadOnly = linkDestination !== LINK_DESTINATION_CUSTOM; const getInspectorControls = ( imageWidth, imageHeight ) => ( <InspectorControls> @@ -529,8 +529,8 @@ class ImageEdit extends Component { label={ __( 'Link URL' ) } value={ href || '' } onChange={ this.onSetCustomHref } - placeholder={ ! isLinkURLInputDisabled ? 'https://' : undefined } - disabled={ isLinkURLInputDisabled } + placeholder={ ! isLinkURLInputReadOnly ? 'https://' : undefined } + readOnly={ isLinkURLInputReadOnly } /> <ToggleControl label={ __( 'Open in New Tab' ) } From 93262fa62aaebe13d7cc445e714b23891bbc5514 Mon Sep 17 00:00:00 2001 From: Garrett Hyder <garrett@eclipse3sixty.com> Date: Thu, 22 Nov 2018 01:31:52 -0800 Subject: [PATCH 108/254] Split the comment (#12193) * Split the comment Seperate the comment so translator comment is in it's own block. * Updated comments to address review Updated the comments to address @swisspidy's review. Removed the redundant comment and switched to a single line comment block. --- lib/client-assets.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/client-assets.php b/lib/client-assets.php index 5dfb4c18c0a777..05cd226f8b4792 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -1211,10 +1211,7 @@ function gutenberg_editor_scripts_and_styles( $hook ) { ), ); - /* - * Set a locale specific default font. - * Translators: Use this to specify the CSS font family for the default font - */ + /* Translators: Use this to specify the CSS font family for the default font */ $locale_font_family = esc_html_x( 'Noto Serif', 'CSS Font Family for Editor Font', 'gutenberg' ); $styles[] = array( 'css' => "body { font-family: '$locale_font_family' }", From 81212d2a940bfbc71f9b0d938e8dec56ead2b98d Mon Sep 17 00:00:00 2001 From: Sharaz Shahid <bc140401486@vu.edu.pk> Date: Thu, 22 Nov 2018 14:57:38 +0500 Subject: [PATCH 109/254] Adding in contributor . (#10020) I tested Gutenberg, make new block also create issues on Gutenberg repo. --- CONTRIBUTORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 3a130f10d4ddf2..114e8db14799c0 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -119,3 +119,4 @@ This list is manually curated to include valuable contributions by volunteers th | @luehrsen | @luehrsen | | @getsource | @mikeschroder | | @greatislander | @greatislander | +| @sharazghouri | @sharaz | From f090e84828b18f2b1a21bebf68a9a1bdd85324de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20=28Greg=29=20Zi=C3=B3=C5=82kowski?= <grzegorz@gziolo.pl> Date: Thu, 22 Nov 2018 11:38:56 +0100 Subject: [PATCH 110/254] Docs: Update section on transforming from shortcodes (#12213) * Docs: Update section on transforming from shortcodes Props to @jakeparis for catching. * Add @jakeparis to the list of contributors --- CONTRIBUTORS.md | 1 + .../developers/block-api/block-registration.md | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 114e8db14799c0..2de5dcefec7e0e 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -120,3 +120,4 @@ This list is manually curated to include valuable contributions by volunteers th | @getsource | @mikeschroder | | @greatislander | @greatislander | | @sharazghouri | @sharaz | +| @jakeparis | | diff --git a/docs/designers-developers/developers/block-api/block-registration.md b/docs/designers-developers/developers/block-api/block-registration.md index 9738ac482353b7..af8aaa36a75ed2 100644 --- a/docs/designers-developers/developers/block-api/block-registration.md +++ b/docs/designers-developers/developers/block-api/block-registration.md @@ -233,8 +233,8 @@ transforms: { // An attribute can be source from the shortcode attributes align: { type: 'string', - shortcode: function( named ) { - var align = named.align ? named.align : 'alignnone'; + shortcode: function( attributes ) { + var align = attributes.named.align ? attributes.named.align : 'alignnone'; return align.replace( 'align', '' ); }, }, From abf1b56793f38b562644e20077eaf12da4dc5e9c Mon Sep 17 00:00:00 2001 From: Nicola Heald <nicola@notnowlewis.com> Date: Thu, 22 Nov 2018 13:23:42 +0000 Subject: [PATCH 111/254] Remove the textdomain from block library (#12215) Fixes: https://github.com/WordPress/gutenberg/issues/12167 --- packages/block-library/src/archives/index.php | 14 +++++++------- packages/block-library/src/categories/index.php | 2 +- .../block-library/src/latest-comments/index.php | 6 +++--- packages/block-library/src/latest-posts/index.php | 2 +- phpcs.xml.dist | 4 ++++ 5 files changed, 16 insertions(+), 12 deletions(-) diff --git a/packages/block-library/src/archives/index.php b/packages/block-library/src/archives/index.php index 85186ce6123a3f..c94a4645d9c08b 100644 --- a/packages/block-library/src/archives/index.php +++ b/packages/block-library/src/archives/index.php @@ -32,7 +32,7 @@ function render_block_core_archives( $attributes ) { $class .= ' wp-block-archives-dropdown'; $dropdown_id = esc_attr( uniqid( 'wp-block-archives-' ) ); - $title = __( 'Archives', 'default' ); + $title = __( 'Archives' ); /** This filter is documented in wp-includes/widgets/class-wp-widget-archives.php */ $dropdown_args = apply_filters( @@ -50,19 +50,19 @@ function render_block_core_archives( $attributes ) { switch ( $dropdown_args['type'] ) { case 'yearly': - $label = __( 'Select Year', 'default' ); + $label = __( 'Select Year' ); break; case 'monthly': - $label = __( 'Select Month', 'default' ); + $label = __( 'Select Month' ); break; case 'daily': - $label = __( 'Select Day', 'default' ); + $label = __( 'Select Day' ); break; case 'weekly': - $label = __( 'Select Week', 'default' ); + $label = __( 'Select Week' ); break; default: - $label = __( 'Select Post', 'default' ); + $label = __( 'Select Post' ); break; } @@ -101,7 +101,7 @@ function render_block_core_archives( $attributes ) { $block_content = sprintf( '<div class="%1$s">%2$s</div>', $classnames, - __( 'No archives to show.', 'default' ) + __( 'No archives to show.' ) ); } else { diff --git a/packages/block-library/src/categories/index.php b/packages/block-library/src/categories/index.php index 478d4579207eac..38de4bd5178849 100644 --- a/packages/block-library/src/categories/index.php +++ b/packages/block-library/src/categories/index.php @@ -27,7 +27,7 @@ function render_block_core_categories( $attributes ) { if ( ! empty( $attributes['displayAsDropdown'] ) ) { $id = 'wp-block-categories-' . $block_id; $args['id'] = $id; - $args['show_option_none'] = __( 'Select Category', 'default' ); + $args['show_option_none'] = __( 'Select Category' ); $wrapper_markup = '<div class="%1$s">%2$s</div>'; $items_markup = wp_dropdown_categories( $args ); $type = 'dropdown'; diff --git a/packages/block-library/src/latest-comments/index.php b/packages/block-library/src/latest-comments/index.php index b06cccd6c0a789..05797c029e4b93 100644 --- a/packages/block-library/src/latest-comments/index.php +++ b/packages/block-library/src/latest-comments/index.php @@ -29,7 +29,7 @@ function gutenberg_draft_or_post_title( $post = 0 ) { $title = get_the_title( $post ); if ( empty( $title ) ) { - $title = __( '(no title)', 'default' ); + $title = __( '(no title)' ); } return esc_html( $title ); } @@ -98,7 +98,7 @@ function gutenberg_render_block_core_latest_comments( $attributes = array() ) { $list_items_markup .= sprintf( /* translators: 1: author name (inside <a> or <span> tag, based on if they have a URL), 2: post title related to this comment */ - __( '%1$s on %2$s', 'default' ), + __( '%1$s on %2$s' ), $author_markup, $post_title ); @@ -143,7 +143,7 @@ function gutenberg_render_block_core_latest_comments( $attributes = array() ) { ) : sprintf( '<div class="%1$s">%2$s</div>', $classnames, - __( 'No comments to show.', 'default' ) + __( 'No comments to show.' ) ); return $block_content; diff --git a/packages/block-library/src/latest-posts/index.php b/packages/block-library/src/latest-posts/index.php index 5a582957c6b7f8..72b395a06cf485 100644 --- a/packages/block-library/src/latest-posts/index.php +++ b/packages/block-library/src/latest-posts/index.php @@ -33,7 +33,7 @@ function render_block_core_latest_posts( $attributes ) { $title = get_the_title( $post_id ); if ( ! $title ) { - $title = __( '(Untitled)', 'default' ); + $title = __( '(Untitled)' ); } $list_items_markup .= sprintf( '<li><a href="%1$s">%2$s</a>', diff --git a/phpcs.xml.dist b/phpcs.xml.dist index ce0669fe42a39f..4d083175191128 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -18,6 +18,10 @@ </properties> </rule> + <rule ref="WordPress.WP.I18n.MissingArgDomainDefault"> + <exclude-pattern>packages/block-library/src/*</exclude-pattern> + </rule> + <arg value="ps"/> <arg name="extensions" value="php"/> From 0298cacb14928900b8fe6783673ad7ad0454b3f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s?= <nosolosw@users.noreply.github.com> Date: Thu, 22 Nov 2018 15:19:23 +0100 Subject: [PATCH 112/254] Fix getSelectedBlockClientId selector (#12214) * Fix getSelectedBlockClientId selector At the moment, we don't have a way to clear or update block selection for UNDO / REDO actions. By inverting the dependencies between getSelectedBlockClientId and getSelectedBlock we make sure we always return a valid clientId in getSelectedBlockClientId. We still have to fix the UNDO/REDO problem internally, but, in the meantime, this patch fixes the API. * Fix tests * Revert "Fix getSelectedBlockClientId selector" This reverts commit cfa3066cd4542cea858b5ea7c05815279bcf0eef. * Revert "Fix tests" This reverts commit 60abeab7fd902dfea017ac153ed72cffebd0d83e. * Use a more performant variant --- packages/editor/src/store/selectors.js | 5 ++++- packages/editor/src/store/test/selectors.js | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index 9cc31b46373cb8..ddbc7770a3c8e1 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -853,7 +853,10 @@ export function hasSelectedBlock( state ) { */ export function getSelectedBlockClientId( state ) { const { start, end } = state.blockSelection; - return start === end && start ? start : null; + // We need to check the block exists because the current state.blockSelection reducer + // doesn't take into account the UNDO / REDO actions to update selection. + // To be removed when that's fixed. + return start && start === end && !! state.editor.present.blocks.byClientId[ start ] ? start : null; } /** diff --git a/packages/editor/src/store/test/selectors.js b/packages/editor/src/store/test/selectors.js index e8249239e4a974..41858f28d120ea 100644 --- a/packages/editor/src/store/test/selectors.js +++ b/packages/editor/src/store/test/selectors.js @@ -2571,6 +2571,7 @@ describe( 'selectors', () => { it( 'should return the selected block ClientId', () => { const state = { + editor: { present: { blocks: { byClientId: { 23: { name: 'fake block' } } } } }, blockSelection: { start: 23, end: 23 }, }; From 668cf104cf438a67a4a9be0b0f07497cddac58be Mon Sep 17 00:00:00 2001 From: Vadim Nicolai <nicolai.vadim@gmail.com> Date: Thu, 22 Nov 2018 16:28:44 +0200 Subject: [PATCH 113/254] Added missing JSDocs in e2e test utils. (#12071) * Added/ updated JSDocs for arePrePublishChecksEnabled, clearLocalStorage, clickOnMoreMenuItem and convertBlock. * Added getUrl JSDoc. * Added JSDocs to goToWPPath,isWPPath,login,setViewport and switchToEditor. * Adjusted and added JSDocs for visitAdmin. * Adjusted arePrePublishChecksEnabled docs and returned boolean. * Adjustments based on feeback, --- test/e2e/support/utils/are-pre-publish-checks-enabled.js | 4 ++++ test/e2e/support/utils/clear-local-storage.js | 3 +++ test/e2e/support/utils/click-on-more-menu-item.js | 1 - test/e2e/support/utils/convert-block.js | 5 +++++ test/e2e/support/utils/disable-pre-publish-checks.js | 3 +++ test/e2e/support/utils/enable-pre-publish-checks.js | 3 +++ test/e2e/support/utils/get-url.js | 7 +++++++ test/e2e/support/utils/go-to-wp-path.js | 6 ++++++ test/e2e/support/utils/is-wp-path.js | 7 +++++++ test/e2e/support/utils/login.js | 6 ++++++ test/e2e/support/utils/set-viewport.js | 5 +++++ test/e2e/support/utils/switch-to-editor.js | 4 ++++ test/e2e/support/utils/visit-admin.js | 7 ++++++- 13 files changed, 59 insertions(+), 2 deletions(-) diff --git a/test/e2e/support/utils/are-pre-publish-checks-enabled.js b/test/e2e/support/utils/are-pre-publish-checks-enabled.js index fbccb04b54b2bb..ec57e1d7561c2f 100644 --- a/test/e2e/support/utils/are-pre-publish-checks-enabled.js +++ b/test/e2e/support/utils/are-pre-publish-checks-enabled.js @@ -1,3 +1,7 @@ +/** + * Verifies if publish checks are enabled. + * @return {boolean} Boolean which represents the state of prepublish checks. + */ export async function arePrePublishChecksEnabled() { return page.evaluate( () => window.wp.data.select( 'core/editor' ).isPublishSidebarEnabled() diff --git a/test/e2e/support/utils/clear-local-storage.js b/test/e2e/support/utils/clear-local-storage.js index 2770f7ce740d02..40f385f1f73ac5 100644 --- a/test/e2e/support/utils/clear-local-storage.js +++ b/test/e2e/support/utils/clear-local-storage.js @@ -1,3 +1,6 @@ +/** + * Clears the local storage. + */ export async function clearLocalStorage() { await page.evaluate( () => window.localStorage.clear() ); } diff --git a/test/e2e/support/utils/click-on-more-menu-item.js b/test/e2e/support/utils/click-on-more-menu-item.js index 7b1da2d44e9f3a..a71cf13f1e12c0 100644 --- a/test/e2e/support/utils/click-on-more-menu-item.js +++ b/test/e2e/support/utils/click-on-more-menu-item.js @@ -1,4 +1,3 @@ - /** * Clicks on More Menu item, searches for the button with the text provided and clicks it. * diff --git a/test/e2e/support/utils/convert-block.js b/test/e2e/support/utils/convert-block.js index 8d2033fd7cfc01..798ec7ab88d0a5 100644 --- a/test/e2e/support/utils/convert-block.js +++ b/test/e2e/support/utils/convert-block.js @@ -1,3 +1,8 @@ +/** + * Converts editor's block type. + * + * @param {string} name Block name. + */ export async function convertBlock( name ) { await page.mouse.move( 200, 300, { steps: 10 } ); await page.mouse.move( 250, 350, { steps: 10 } ); diff --git a/test/e2e/support/utils/disable-pre-publish-checks.js b/test/e2e/support/utils/disable-pre-publish-checks.js index 93f676c796bc9f..26c892652eb882 100644 --- a/test/e2e/support/utils/disable-pre-publish-checks.js +++ b/test/e2e/support/utils/disable-pre-publish-checks.js @@ -3,6 +3,9 @@ */ import { toggleOption } from './toggle-option'; +/** + * Disables Pre-publish checks. + */ export async function disablePrePublishChecks() { await toggleOption( 'Enable Pre-publish Checks', false ); } diff --git a/test/e2e/support/utils/enable-pre-publish-checks.js b/test/e2e/support/utils/enable-pre-publish-checks.js index 83069b40f9b1b0..a99ad3422514cc 100644 --- a/test/e2e/support/utils/enable-pre-publish-checks.js +++ b/test/e2e/support/utils/enable-pre-publish-checks.js @@ -3,6 +3,9 @@ */ import { toggleOption } from './toggle-option'; +/** + * Enables Pre-publish checks. + */ export async function enablePrePublishChecks() { await toggleOption( 'Enable Pre-publish Checks', true ); } diff --git a/test/e2e/support/utils/get-url.js b/test/e2e/support/utils/get-url.js index edd5801036978a..3f8d3fa533dceb 100644 --- a/test/e2e/support/utils/get-url.js +++ b/test/e2e/support/utils/get-url.js @@ -6,6 +6,13 @@ import { URL } from 'url'; import { WP_BASE_URL } from './config'; +/** + * Creates new URL by parsing base URL, WPPath and query string. + * + * @param {string} WPPath String to be serialized as pathname. + * @param {?string} query String to be serialized as query portion of URL. + * @return {string} String which represents full URL. + */ export function getUrl( WPPath, query = '' ) { const url = new URL( WP_BASE_URL ); diff --git a/test/e2e/support/utils/go-to-wp-path.js b/test/e2e/support/utils/go-to-wp-path.js index 69c4bbeb7dba5b..dcfe992fd83d6e 100644 --- a/test/e2e/support/utils/go-to-wp-path.js +++ b/test/e2e/support/utils/go-to-wp-path.js @@ -3,6 +3,12 @@ */ import { getUrl } from './get-url'; +/** + * Navigates to URL created from WPPath and query. + * + * @param {string} WPPath String to be serialized as pathname. + * @param {string} query String to be serialized as query portion of URL. + */ export async function goToWPPath( WPPath, query ) { await page.goto( getUrl( WPPath, query ) ); } diff --git a/test/e2e/support/utils/is-wp-path.js b/test/e2e/support/utils/is-wp-path.js index ecd1695fb52b66..3bfe446f052fa3 100644 --- a/test/e2e/support/utils/is-wp-path.js +++ b/test/e2e/support/utils/is-wp-path.js @@ -8,6 +8,13 @@ import { URL } from 'url'; */ import { getUrl } from './get-url'; +/** + * Checks if current url is a WordPress path. + * + * @param {string} WPPath String to be serialized as pathname. + * @param {?string} query String to be serialized as query portion of URL. + * @return {boolean} Boolean represents wheter current URL is or not a WordPress path. + */ export function isWPPath( WPPath, query = '' ) { const currentUrl = new URL( page.url() ); diff --git a/test/e2e/support/utils/login.js b/test/e2e/support/utils/login.js index 19b3e99d727271..87ae8af6a493f5 100644 --- a/test/e2e/support/utils/login.js +++ b/test/e2e/support/utils/login.js @@ -5,6 +5,12 @@ import { WP_USERNAME, WP_PASSWORD } from './config'; import { pressWithModifier } from './press-with-modifier'; +/** + * Performs log in with specified username and password. + * + * @param {?string} username String to be used as user credential. + * @param {?string} password String to be used as user credential. + */ export async function login( username = WP_USERNAME, password = WP_PASSWORD ) { await page.focus( '#user_login' ); await pressWithModifier( 'primary', 'a' ); diff --git a/test/e2e/support/utils/set-viewport.js b/test/e2e/support/utils/set-viewport.js index 75de557e795bdf..6ef289013763d1 100644 --- a/test/e2e/support/utils/set-viewport.js +++ b/test/e2e/support/utils/set-viewport.js @@ -3,6 +3,11 @@ */ import { waitForPageDimensions } from './wait-for-page-dimensions'; +/** + * Sets browser viewport to specified type. + * + * @param {string} type String to represent dimensions type; can be either small or large. + */ export async function setViewport( type ) { const allowedDimensions = { large: { width: 960, height: 700 }, diff --git a/test/e2e/support/utils/switch-to-editor.js b/test/e2e/support/utils/switch-to-editor.js index 2e73a5c9bc4af7..75529b8721b530 100644 --- a/test/e2e/support/utils/switch-to-editor.js +++ b/test/e2e/support/utils/switch-to-editor.js @@ -1,3 +1,7 @@ +/** + * Switches editor mode. +* @param {string} mode String editor mode. + */ export async function switchToEditor( mode ) { await page.click( '.edit-post-more-menu [aria-label="Show more tools & options"]' diff --git a/test/e2e/support/utils/visit-admin.js b/test/e2e/support/utils/visit-admin.js index ac744cca3b94a4..2c7a473998e1f5 100644 --- a/test/e2e/support/utils/visit-admin.js +++ b/test/e2e/support/utils/visit-admin.js @@ -10,11 +10,16 @@ import { goToWPPath } from './go-to-wp-path'; import { isWPPath } from './is-wp-path'; import { login } from './login'; +/** + * Visits admin page; if user is not logged in then it logging in it first, then visits admin page. + * @param {string} adminPath String to be serialized as pathname. + * @param {string} query String to be serialized as query portion of URL. + */ export async function visitAdmin( adminPath, query ) { await goToWPPath( join( 'wp-admin', adminPath ), query ); if ( isWPPath( 'wp-login.php' ) ) { await login(); - return visitAdmin( adminPath, query ); + await visitAdmin( adminPath, query ); } } From 780c98098f166cc4765bf891b0f8111415b9887f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milan=20Dini=C4=87?= <milan@srpski.biz> Date: Thu, 22 Nov 2018 15:31:04 +0100 Subject: [PATCH 114/254] Merge two similar strings. (#11708) * Merge two similar strings. * Use sentence case for "Edit Media" toolbar tooltip. --- packages/block-library/src/media-text/media-container.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-library/src/media-text/media-container.js b/packages/block-library/src/media-text/media-container.js index fa611a8406b089..863179d748b902 100644 --- a/packages/block-library/src/media-text/media-container.js +++ b/packages/block-library/src/media-text/media-container.js @@ -28,7 +28,7 @@ class MediaContainer extends Component { render={ ( { open } ) => ( <IconButton className="components-toolbar__control" - label={ __( 'Edit Media' ) } + label={ __( 'Edit media' ) } icon="edit" onClick={ open } /> From 4c35fa4fcaf8a95261fac1be7c03fc0648d5f418 Mon Sep 17 00:00:00 2001 From: Nicola Heald <nicola@notnowlewis.com> Date: Thu, 22 Nov 2018 14:44:03 +0000 Subject: [PATCH 115/254] Allow the user to Convert unembeddable URLs to links and try embedding again (#12095) * Allow the user to Convert unembeddable URLs to links * Add linebreak. * Add e2e test for link conversion button * Add a "try again" button for failed embeds --- packages/block-library/src/embed/edit.js | 11 ++-- .../src/embed/embed-placeholder.js | 9 ++- packages/block-library/src/embed/settings.js | 12 +++- packages/block-library/src/embed/util.js | 14 +++++ .../__snapshots__/embedding.test.js.snap | 7 +++ test/e2e/specs/embedding.test.js | 55 +++++++++++++++---- test/e2e/support/utils/click-button.js | 9 +++ test/e2e/support/utils/index.js | 1 + .../support/utils/set-up-response-mocking.js | 38 +++++++++---- 9 files changed, 129 insertions(+), 27 deletions(-) create mode 100644 test/e2e/specs/__snapshots__/embedding.test.js.snap create mode 100644 test/e2e/support/utils/click-button.js diff --git a/packages/block-library/src/embed/edit.js b/packages/block-library/src/embed/edit.js index d5ef390bfa34b6..0e251a8f0aafd0 100644 --- a/packages/block-library/src/embed/edit.js +++ b/packages/block-library/src/embed/edit.js @@ -1,7 +1,7 @@ /** * Internal dependencies */ -import { isFromWordPress, createUpgradedEmbedBlock, getClassNames } from './util'; +import { isFromWordPress, createUpgradedEmbedBlock, getClassNames, fallback } from './util'; import EmbedControls from './embed-controls'; import EmbedLoading from './embed-loading'; import EmbedPlaceholder from './embed-placeholder'; @@ -55,10 +55,11 @@ export function getEmbedEditComponent( title, icon, responsive = true ) { componentDidUpdate( prevProps ) { const hasPreview = undefined !== this.props.preview; const hadPreview = undefined !== prevProps.preview; - const switchedPreview = this.props.preview && this.props.attributes.url !== prevProps.attributes.url; + const previewChanged = prevProps.preview && this.props.preview && this.props.preview.html !== prevProps.preview.html; + const switchedPreview = previewChanged || ( hasPreview && ! hadPreview ); const switchedURL = this.props.attributes.url !== prevProps.attributes.url; - if ( ( hasPreview && ! hadPreview ) || switchedPreview || switchedURL ) { + if ( switchedPreview || switchedURL ) { if ( this.props.cannotEmbed ) { // Can't embed this URL, and we've just received or switched the preview. return; @@ -140,7 +141,7 @@ export function getEmbedEditComponent( title, icon, responsive = true ) { render() { const { url, editingURL } = this.state; const { caption, type, allowResponsive } = this.props.attributes; - const { fetching, setAttributes, isSelected, className, preview, cannotEmbed, themeSupportsResponsive } = this.props; + const { fetching, setAttributes, isSelected, className, preview, cannotEmbed, themeSupportsResponsive, tryAgain } = this.props; if ( fetching ) { return ( @@ -161,6 +162,8 @@ export function getEmbedEditComponent( title, icon, responsive = true ) { value={ url } cannotEmbed={ cannotEmbed } onChange={ ( event ) => this.setState( { url: event.target.value } ) } + fallback={ () => fallback( url, this.props.onReplace ) } + tryAgain={ tryAgain } /> ); } diff --git a/packages/block-library/src/embed/embed-placeholder.js b/packages/block-library/src/embed/embed-placeholder.js index 1d54279c9027fa..5545c6a3ddd3c8 100644 --- a/packages/block-library/src/embed/embed-placeholder.js +++ b/packages/block-library/src/embed/embed-placeholder.js @@ -6,7 +6,7 @@ import { Button, Placeholder } from '@wordpress/components'; import { BlockIcon } from '@wordpress/editor'; const EmbedPlaceholder = ( props ) => { - const { icon, label, value, onSubmit, onChange, cannotEmbed } = props; + const { icon, label, value, onSubmit, onChange, cannotEmbed, fallback, tryAgain } = props; return ( <Placeholder icon={ <BlockIcon icon={ icon } showColors /> } label={ label } className="wp-block-embed"> <form onSubmit={ onSubmit }> @@ -22,7 +22,12 @@ const EmbedPlaceholder = ( props ) => { type="submit"> { _x( 'Embed', 'button label' ) } </Button> - { cannotEmbed && <p className="components-placeholder__error">{ __( 'Sorry, we could not embed that content.' ) }</p> } + { cannotEmbed && + <p className="components-placeholder__error"> + { __( 'Sorry, we could not embed that content.' ) }<br /> + <Button isLarge onClick={ tryAgain }>{ _x( 'Try again', 'button label' ) }</Button> <Button isLarge onClick={ fallback }>{ _x( 'Convert to link', 'button label' ) }</Button> + </p> + } </form> </Placeholder> ); diff --git a/packages/block-library/src/embed/settings.js b/packages/block-library/src/embed/settings.js index a6d68ae80c9b20..d42a406b608ac9 100644 --- a/packages/block-library/src/embed/settings.js +++ b/packages/block-library/src/embed/settings.js @@ -14,7 +14,7 @@ import classnames from 'classnames/dedupe'; import { __, sprintf } from '@wordpress/i18n'; import { compose } from '@wordpress/compose'; import { RichText } from '@wordpress/editor'; -import { withSelect } from '@wordpress/data'; +import { withSelect, withDispatch } from '@wordpress/data'; const embedAttributes = { url: { @@ -79,6 +79,16 @@ export function getEmbedBlockSettings( { title, description, icon, category = 'e themeSupportsResponsive: themeSupports[ 'responsive-embeds' ], cannotEmbed, }; + } ), + withDispatch( ( dispatch, ownProps ) => { + const { url } = ownProps.attributes; + const coreData = dispatch( 'core/data' ); + const tryAgain = () => { + coreData.invalidateResolution( 'core', 'getEmbedPreview', [ url ] ); + }; + return { + tryAgain, + }; } ) )( edit ), diff --git a/packages/block-library/src/embed/util.js b/packages/block-library/src/embed/util.js index 58b07b0a627743..5352fce1fb58e6 100644 --- a/packages/block-library/src/embed/util.js +++ b/packages/block-library/src/embed/util.js @@ -163,3 +163,17 @@ export function getClassNames( html, existingClassNames = '', allowResponsive = return existingClassNames; } + +/** + * Fallback behaviour for unembeddable URLs. + * Creates a paragraph block containing a link to the URL, and calls `onReplace`. + * + * @param {string} url The URL that could not be embedded. + * @param {function} onReplace Function to call with the created fallback block. + */ +export function fallback( url, onReplace ) { + const link = <a href={ url }>{ url }</a>; + onReplace( + createBlock( 'core/paragraph', { content: renderToString( link ) } ) + ); +} diff --git a/test/e2e/specs/__snapshots__/embedding.test.js.snap b/test/e2e/specs/__snapshots__/embedding.test.js.snap new file mode 100644 index 00000000000000..9672627058066a --- /dev/null +++ b/test/e2e/specs/__snapshots__/embedding.test.js.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Embedding content should allow the user to convert unembeddable URLs to a paragraph with a link in it 1`] = ` +"<!-- wp:paragraph --> +<p><a href=\\"https://twitter.com/wooyaygutenberg123454312\\">https://twitter.com/wooyaygutenberg123454312</a></p> +<!-- /wp:paragraph -->" +`; diff --git a/test/e2e/specs/embedding.test.js b/test/e2e/specs/embedding.test.js index 79f7d27707b0bd..45b37006cecb83 100644 --- a/test/e2e/specs/embedding.test.js +++ b/test/e2e/specs/embedding.test.js @@ -1,7 +1,15 @@ /** * Internal dependencies */ -import { clickBlockAppender, newPost, isEmbedding, setUpResponseMocking, JSONResponse } from '../support/utils'; +import { + clickBlockAppender, + newPost, + isEmbedding, + setUpResponseMocking, + JSONResponse, + getEditedPostContent, + clickButton, +} from '../support/utils'; const MOCK_EMBED_WORDPRESS_SUCCESS_RESPONSE = { url: 'https://wordpress.org/gutenberg/handbook/block-api/attributes/', @@ -82,9 +90,7 @@ const MOCK_RESPONSES = [ }, ]; -const addEmbeds = async () => { - await newPost(); - +const addAllEmbeds = async () => { // Valid embed. await clickBlockAppender(); await page.keyboard.type( '/embed' ); @@ -135,15 +141,12 @@ const addEmbeds = async () => { await page.keyboard.press( 'Enter' ); }; -const setUp = async () => { - await setUpResponseMocking( MOCK_RESPONSES ); - await addEmbeds(); -}; - describe( 'Embedding content', () => { - beforeEach( setUp ); + beforeAll( async () => await setUpResponseMocking( MOCK_RESPONSES ) ); + beforeEach( newPost ); it( 'should render embeds in the correct state', async () => { + await addAllEmbeds(); // The successful embeds should be in a correctly classed figure element. // This tests that they have switched to the correct block. await page.waitForSelector( 'figure.wp-block-embed-twitter' ); @@ -157,4 +160,36 @@ describe( 'Embedding content', () => { await page.waitForSelector( 'input[value="https://twitter.com/thatbunty"]' ); await page.waitForSelector( 'input[value="https://wordpress.org/gutenberg/handbook/"]' ); } ); + + it( 'should allow the user to convert unembeddable URLs to a paragraph with a link in it', async () => { + // URL that can't be embedded. + await clickBlockAppender(); + await page.keyboard.type( '/embed' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( 'https://twitter.com/wooyaygutenberg123454312' ); + await page.keyboard.press( 'Enter' ); + + await clickButton( 'Convert to link' ); + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); + + it( 'should allow the user to try embedding a failed URL again', async () => { + // URL that can't be embedded. + await clickBlockAppender(); + await page.keyboard.type( '/embed' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( 'https://twitter.com/wooyaygutenberg123454312' ); + await page.keyboard.press( 'Enter' ); + // Set up a different mock to make sure that try again actually does make the request again. + await setUpResponseMocking( + [ + { + match: isEmbedding( 'https://twitter.com/wooyaygutenberg123454312' ), + onRequestMatch: JSONResponse( MOCK_EMBED_RICH_SUCCESS_RESPONSE ), + }, + ] + ); + await clickButton( 'Try again' ); + await page.waitForSelector( 'figure.wp-block-embed-twitter' ); + } ); } ); diff --git a/test/e2e/support/utils/click-button.js b/test/e2e/support/utils/click-button.js new file mode 100644 index 00000000000000..9c406c22b24af1 --- /dev/null +++ b/test/e2e/support/utils/click-button.js @@ -0,0 +1,9 @@ +/** + * Clicks a button based on the text on the button. + * + * @param {string} buttonText The text that appears on the button to click. + */ +export async function clickButton( buttonText ) { + const button = await page.waitForXPath( `//button[contains(text(), '${ buttonText }')]` ); + await button.click(); +} diff --git a/test/e2e/support/utils/index.js b/test/e2e/support/utils/index.js index a3f52f91ad79d4..f374a226ecdc52 100644 --- a/test/e2e/support/utils/index.js +++ b/test/e2e/support/utils/index.js @@ -1,6 +1,7 @@ export { arePrePublishChecksEnabled } from './are-pre-publish-checks-enabled'; export { clearLocalStorage } from './clear-local-storage'; export { clickBlockAppender } from './click-block-appender'; +export { clickButton } from './click-button'; export { clickOnCloseModalButton } from './click-on-close-modal-button'; export { clickOnMoreMenuItem } from './click-on-more-menu-item'; export { convertBlock } from './convert-block'; diff --git a/test/e2e/support/utils/set-up-response-mocking.js b/test/e2e/support/utils/set-up-response-mocking.js index 4384d634953203..8316ba9046f676 100644 --- a/test/e2e/support/utils/set-up-response-mocking.js +++ b/test/e2e/support/utils/set-up-response-mocking.js @@ -1,3 +1,13 @@ +/** + * Track if we have already initialized the request interception. + */ +let interceptionInitialized = false; + +/** + * Array of mock responses. + */ +let requestMocks = []; + /** * 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. @@ -21,15 +31,23 @@ * @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; + if ( ! interceptionInitialized ) { + // We only want to set up the request interception once, or else we get a crash + // when we try to process the same request twice. + interceptionInitialized = true; + await page.setRequestInterception( true ); + page.on( 'request', async ( request ) => { + for ( let i = 0; i < requestMocks.length; i++ ) { + const mock = requestMocks[ i ]; + if ( mock.match( request ) ) { + await mock.onRequestMatch( request ); + return; + } } - } - request.continue(); - } ); + request.continue(); + } ); + } + // Overwrite with the passed in mocks, so we can change the mocks mid-test to test + // recovery from scenarios where a request had failed, but is working again. + requestMocks = [ ...mocks ]; } From 9cc03c0652ecc6d4ccac210ba12664781d29003c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20Van=C2=A0Dorpe?= <iseulde@automattic.com> Date: Thu, 22 Nov 2018 15:55:23 +0100 Subject: [PATCH 116/254] TinyMCE: fix IE11 lost focus after init (#12206) * TinyMCE: fix IE11 lost focus after init * Attempt 3 --- packages/editor/src/components/rich-text/tinymce.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/editor/src/components/rich-text/tinymce.js b/packages/editor/src/components/rich-text/tinymce.js index 0085343bdbd27f..ca8d0ea70521ae 100644 --- a/packages/editor/src/components/rich-text/tinymce.js +++ b/packages/editor/src/components/rich-text/tinymce.js @@ -243,6 +243,15 @@ export default class TinyMCE extends Component { // Restore the original `setHTML` once initialized. editor.dom.setHTML = setHTML; + + // In IE11, focus is lost to parent after initialising + // TinyMCE, so we have to set it back. + if ( + document.activeElement !== this.editorNode && + document.activeElement.contains( this.editorNode ) + ) { + this.editorNode.focus(); + } } ); editor.on( 'keydown', this.onKeyDown, true ); From 6e8e1bc86097a4efea9427ba868e55191f9aa160 Mon Sep 17 00:00:00 2001 From: Andrea Fercia <a.fercia@gmail.com> Date: Thu, 22 Nov 2018 16:26:38 +0100 Subject: [PATCH 117/254] Simplify sidebar tabs aria-labels. (#11618) --- .../components/sidebar/settings-header/index.js | 16 ++++++++-------- test/e2e/specs/preferences.test.js | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/edit-post/src/components/sidebar/settings-header/index.js b/packages/edit-post/src/components/sidebar/settings-header/index.js index 33a3bbc27e29ab..e2866c231a1c98 100644 --- a/packages/edit-post/src/components/sidebar/settings-header/index.js +++ b/packages/edit-post/src/components/sidebar/settings-header/index.js @@ -12,16 +12,16 @@ import SidebarHeader from '../sidebar-header'; const SettingsHeader = ( { openDocumentSettings, openBlockSettings, sidebarName } ) => { const blockLabel = __( 'Block' ); const [ documentAriaLabel, documentActiveClass ] = sidebarName === 'edit-post/document' ? - // translators: ARIA label for the Document Settings sidebar tab, selected. - [ __( 'Document settings (selected)' ), 'is-active' ] : - // translators: ARIA label for the Document Settings sidebar tab, not selected. - [ __( 'Document settings' ), '' ]; + // translators: ARIA label for the Document sidebar tab, selected. + [ __( 'Document (selected)' ), 'is-active' ] : + // translators: ARIA label for the Document sidebar tab, not selected. + [ __( 'Document' ), '' ]; const [ blockAriaLabel, blockActiveClass ] = sidebarName === 'edit-post/block' ? - // translators: ARIA label for the Block Settings sidebar tab, selected. - [ __( 'Block settings (selected)' ), 'is-active' ] : - // translators: ARIA label for the Block Settings sidebar tab, not selected. - [ __( 'Block settings' ), '' ]; + // translators: ARIA label for the Block sidebar tab, selected. + [ __( 'Block (selected)' ), 'is-active' ] : + // translators: ARIA label for the Block sidebar tab, not selected. + [ __( 'Block' ), '' ]; return ( <SidebarHeader diff --git a/test/e2e/specs/preferences.test.js b/test/e2e/specs/preferences.test.js index c8fedd4e4f7750..d405009969fa20 100644 --- a/test/e2e/specs/preferences.test.js +++ b/test/e2e/specs/preferences.test.js @@ -33,7 +33,7 @@ describe( 'preferences', () => { expect( await getActiveSidebarTabText() ).toBe( 'Document' ); // Change to "Block" tab. - await page.click( '.edit-post-sidebar__panel-tab[aria-label="Block settings"]' ); + await page.click( '.edit-post-sidebar__panel-tab[aria-label="Block"]' ); expect( await getActiveSidebarTabText() ).toBe( 'Block' ); // Regression test: Reload resets to document tab. From 509fc16b209e31014c29282c1c47d369fa1041ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20Van=C2=A0Dorpe?= <iseulde@automattic.com> Date: Thu, 22 Nov 2018 17:36:38 +0100 Subject: [PATCH 118/254] Link Format: Label with selected text, not full text (#12154) * Only label with selected text * Add e2e test --- packages/format-library/src/link/inline.js | 19 +++++++++++--- .../specs/__snapshots__/links.test.js.snap | 6 +++++ test/e2e/specs/links.test.js | 25 +++++++++++++++++++ 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/packages/format-library/src/link/inline.js b/packages/format-library/src/link/inline.js index 350c94386ed857..b007076e8c7f2f 100644 --- a/packages/format-library/src/link/inline.js +++ b/packages/format-library/src/link/inline.js @@ -21,6 +21,8 @@ import { insert, isCollapsed, applyFormat, + getTextContent, + slice, } from '@wordpress/rich-text'; import { URLInput, URLPopover } from '@wordpress/editor'; @@ -51,7 +53,7 @@ function createLinkFormat( { url, opensInNewWindow, text } ) { if ( opensInNewWindow ) { // translators: accessibility label for external links, where the argument is the link text - const label = sprintf( __( '%s (opens in a new tab)' ), text ).trim(); + const label = sprintf( __( '%s (opens in a new tab)' ), text ); format.attributes.target = '_blank'; format.attributes.rel = 'noreferrer noopener'; @@ -173,7 +175,13 @@ class InlineLinkUI extends Component { // Apply now if URL is not being edited. if ( ! isShowingInput( this.props, this.state ) ) { - onChange( applyFormat( value, createLinkFormat( { url, opensInNewWindow, text: value.text } ) ) ); + const selectedText = getTextContent( slice( value ) ); + + onChange( applyFormat( value, createLinkFormat( { + url, + opensInNewWindow, + text: selectedText, + } ) ) ); } } @@ -186,7 +194,12 @@ class InlineLinkUI extends Component { const { isActive, value, onChange, speak } = this.props; const { inputValue, opensInNewWindow } = this.state; const url = prependHTTP( inputValue ); - const format = createLinkFormat( { url, opensInNewWindow, text: value.text } ); + const selectedText = getTextContent( slice( value ) ); + const format = createLinkFormat( { + url, + opensInNewWindow, + text: selectedText, + } ); event.preventDefault(); diff --git a/test/e2e/specs/__snapshots__/links.test.js.snap b/test/e2e/specs/__snapshots__/links.test.js.snap index be47f91e158f99..6b0ab2795b7af3 100644 --- a/test/e2e/specs/__snapshots__/links.test.js.snap +++ b/test/e2e/specs/__snapshots__/links.test.js.snap @@ -41,3 +41,9 @@ exports[`Links can be removed 1`] = ` <p>This is Gutenberg</p> <!-- /wp:paragraph -->" `; + +exports[`Links should contain a label when it should open in a new tab 1`] = ` +"<!-- wp:paragraph --> +<p>This is <a href=\\"http://w.org\\" target=\\"_blank\\" rel=\\"noreferrer noopener\\" aria-label=\\"WordPress (opens in a new tab)\\">WordPress</a></p> +<!-- /wp:paragraph -->" +`; diff --git a/test/e2e/specs/links.test.js b/test/e2e/specs/links.test.js index a59041f4f78259..eaa159d1bc0a38 100644 --- a/test/e2e/specs/links.test.js +++ b/test/e2e/specs/links.test.js @@ -483,4 +483,29 @@ describe( 'Links', () => { const popover = await page.$$( '.editor-url-popover' ); expect( popover ).toHaveLength( 1 ); } ); + + it( 'should contain a label when it should open in a new tab', async () => { + await clickBlockAppender(); + await page.keyboard.type( 'This is WordPress' ); + // Select "WordPress". + await pressWithModifier( 'shiftAlt', 'ArrowLeft' ); + await pressWithModifier( 'primary', 'k' ); + await waitForAutoFocus(); + await page.keyboard.type( 'w.org' ); + // Navigate to the settings toggle. + await page.keyboard.press( 'Tab' ); + await page.keyboard.press( 'Tab' ); + // Open settings. + await page.keyboard.press( 'Space' ); + // Navigate to the "Open in New Tab" checkbox. + await page.keyboard.press( 'Tab' ); + // Check the checkbox. + await page.keyboard.press( 'Space' ); + // Navigate back to the input field. + await page.keyboard.press( 'Tab' ); + // Submit the form. + await page.keyboard.press( 'Enter' ); + + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); } ); From 017f31bd092001be5e8f2afe282d8b34883d8cbd Mon Sep 17 00:00:00 2001 From: Marko Savic <savicmarko1985@gmail.com> Date: Thu, 22 Nov 2018 20:47:22 +0100 Subject: [PATCH 119/254] Rnmobile/native toolbar component ui (#11827) Implement toolbar ui --- .../block-library/src/image/edit.native.js | 4 +- .../components/src/button/index.native.js | 61 +++++++++++++++++-- .../components/src/dashicon/icon-class.js | 4 ++ .../src/dashicon/icon-class.native.js | 4 ++ packages/components/src/dashicon/index.js | 8 ++- packages/components/src/icon-button/index.js | 3 +- packages/components/src/index.native.js | 1 + .../src/primitives/svg/index.native.js | 24 ++++---- .../src/primitives/svg/style.native.scss | 14 +++++ .../components/src/toolbar/style.native.scss | 7 +++ .../src/toolbar/toolbar-container.native.js | 8 ++- packages/html-entities/package.json | 1 + 12 files changed, 113 insertions(+), 26 deletions(-) create mode 100644 packages/components/src/dashicon/icon-class.js create mode 100644 packages/components/src/dashicon/icon-class.native.js create mode 100644 packages/components/src/primitives/svg/style.native.scss create mode 100644 packages/components/src/toolbar/style.native.scss diff --git a/packages/block-library/src/image/edit.native.js b/packages/block-library/src/image/edit.native.js index fde82c5a717770..5b4132e19a2b24 100644 --- a/packages/block-library/src/image/edit.native.js +++ b/packages/block-library/src/image/edit.native.js @@ -8,7 +8,7 @@ import RNReactNativeGutenbergBridge from 'react-native-gutenberg-bridge'; * Internal dependencies */ import { MediaPlaceholder, RichText, BlockControls } from '@wordpress/editor'; -import { Toolbar, IconButton } from '@wordpress/components'; +import { Toolbar, ToolbarButton } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; export default function ImageEdit( props ) { @@ -40,7 +40,7 @@ export default function ImageEdit( props ) { const toolbarEditButton = ( <Toolbar> - <IconButton + <ToolbarButton className="components-toolbar__control" label={ __( 'Edit image' ) } icon="edit" diff --git a/packages/components/src/button/index.native.js b/packages/components/src/button/index.native.js index be7f7caf4369b9..b5a084993d21f6 100644 --- a/packages/components/src/button/index.native.js +++ b/packages/components/src/button/index.native.js @@ -1,20 +1,71 @@ /** * External dependencies */ -import { TouchableOpacity, Text, View } from 'react-native'; +import { StyleSheet, TouchableOpacity, Text, View, Platform } from 'react-native'; + +const isAndroid = Platform.OS === 'android'; +const marginBottom = isAndroid ? -0.5 : 0; +const marginLeft = -3; + +const styles = StyleSheet.create( { + container: { + flex: 1, + padding: 3, + justifyContent: 'center', + alignItems: 'center', + }, + buttonInactive: { + flex: 1, + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + aspectRatio: 1, + backgroundColor: 'white', + }, + buttonActive: { + flex: 1, + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + borderRadius: 6, + borderColor: '#2e4453', + aspectRatio: 1, + backgroundColor: '#2e4453', + }, + subscriptInactive: { + color: '#7b9ab1', + fontWeight: 'bold', + fontSize: 13, + alignSelf: 'flex-end', + marginLeft: marginLeft, + marginBottom: marginBottom, + }, + subscriptActive: { + color: 'white', + fontWeight: 'bold', + fontSize: 13, + alignSelf: 'flex-end', + marginLeft: marginLeft, + marginBottom: marginBottom, + }, +} ); export default function Button( props ) { const { children, onClick, 'aria-label': ariaLabel, 'aria-pressed': ariaPressed, 'data-subscript': subscript } = props; + return ( <TouchableOpacity + activeOpacity={ 0.7 } accessible={ true } accessibilityLabel={ ariaLabel } onPress={ onClick } - style={ { borderColor: ariaPressed ? 'black' : 'white', borderWidth: 1, borderRadius: 2 } } + style={ styles.container } > - <View style={ { height: 44, width: 44, flexDirection: 'row', justifyContent: 'center', alignItems: 'center' } }> - { children } - { subscript && ( <Text style={ { fontVariant: [ 'small-caps' ] } }>{ subscript }</Text> ) } + <View style={ ariaPressed ? styles.buttonActive : styles.buttonInactive }> + <View style={ { flexDirection: 'row' } }> + { children } + { subscript && ( <Text style={ ariaPressed ? styles.subscriptActive : styles.subscriptInactive }>{ subscript }</Text> ) } + </View> </View> </TouchableOpacity> ); diff --git a/packages/components/src/dashicon/icon-class.js b/packages/components/src/dashicon/icon-class.js new file mode 100644 index 00000000000000..f82b6453ef68e9 --- /dev/null +++ b/packages/components/src/dashicon/icon-class.js @@ -0,0 +1,4 @@ +export const IconClass = ( props ) => { + const { icon, className } = props; + return [ 'dashicon', 'dashicons-' + icon, className ].filter( Boolean ).join( ' ' ); +}; diff --git a/packages/components/src/dashicon/icon-class.native.js b/packages/components/src/dashicon/icon-class.native.js new file mode 100644 index 00000000000000..09a798008ea1c0 --- /dev/null +++ b/packages/components/src/dashicon/icon-class.native.js @@ -0,0 +1,4 @@ +export const IconClass = ( props ) => { + const { icon, className, ariaPressed } = props; + return [ ariaPressed ? 'dashicon-active' : 'dashicon', 'dashicons-' + icon, className ].filter( Boolean ).join( ' ' ); +}; diff --git a/packages/components/src/dashicon/index.js b/packages/components/src/dashicon/index.js index 8308c4a97384eb..b58f77bfe371a5 100644 --- a/packages/components/src/dashicon/index.js +++ b/packages/components/src/dashicon/index.js @@ -14,18 +14,20 @@ import { Component } from '@wordpress/element'; * Internal dependencies */ import { Path, SVG } from '../primitives'; +import { IconClass } from './icon-class'; export default class Dashicon extends Component { shouldComponentUpdate( nextProps ) { return ( this.props.icon !== nextProps.icon || this.props.size !== nextProps.size || - this.props.className !== nextProps.className + this.props.className !== nextProps.className || + this.props.ariaPressed !== nextProps.ariaPressed ); } render() { - const { icon, className, size = 20 } = this.props; + const { icon, size = 20 } = this.props; let path; switch ( icon ) { @@ -896,7 +898,7 @@ export default class Dashicon extends Component { return null; } - const iconClass = [ 'dashicon', 'dashicons-' + icon, className ].filter( Boolean ).join( ' ' ); + const iconClass = IconClass( this.props ); return ( <SVG diff --git a/packages/components/src/icon-button/index.js b/packages/components/src/icon-button/index.js index bec3d38d548646..27bcc08f79b655 100644 --- a/packages/components/src/icon-button/index.js +++ b/packages/components/src/icon-button/index.js @@ -21,6 +21,7 @@ import Dashicon from '../dashicon'; class IconButton extends Component { render() { const { icon, children, label, className, tooltip, shortcut, labelPosition, ...additionalProps } = this.props; + const { 'aria-pressed': ariaPressed } = this.props; const classes = classnames( 'components-icon-button', className ); const tooltipText = tooltip || label; @@ -42,7 +43,7 @@ class IconButton extends Component { let element = ( <Button aria-label={ label } { ...additionalProps } className={ classes }> - { isString( icon ) ? <Dashicon icon={ icon } /> : icon } + { isString( icon ) ? <Dashicon icon={ icon } ariaPressed={ ariaPressed } /> : icon } { children } </Button> ); diff --git a/packages/components/src/index.native.js b/packages/components/src/index.native.js index 47799cd872668a..3e44bdec61e509 100644 --- a/packages/components/src/index.native.js +++ b/packages/components/src/index.native.js @@ -2,6 +2,7 @@ export * from './primitives'; export { default as Dashicon } from './dashicon'; export { default as Toolbar } from './toolbar'; +export { default as ToolbarButton } from './toolbar-button'; 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'; diff --git a/packages/components/src/primitives/svg/index.native.js b/packages/components/src/primitives/svg/index.native.js index 9f6da4772c48ef..47c49b1bb61289 100644 --- a/packages/components/src/primitives/svg/index.native.js +++ b/packages/components/src/primitives/svg/index.native.js @@ -1,13 +1,12 @@ /** * External dependencies */ -import { omit } from 'lodash'; import { Svg } from 'react-native-svg'; /** * Internal dependencies */ -import styles from '../../dashicon/style.scss'; +import styles from './style.scss'; export { Circle, @@ -21,24 +20,23 @@ export const SVG = ( props ) => { // We're using the react-native-classname-to-style plugin, so when a `className` prop is passed it gets converted to `style` here. // Given it carries a string (as it was originally className) but an object is expected for `style`, // we need to check whether `style` exists and is a string, and convert it to an object - let styleKeys = new Array(); - const styleValues = new Array(); - if ( typeof props.style === 'string' || props.style instanceof String ) { - styleKeys = props.style.split( ' ' ); - styleKeys.forEach( ( element ) => { - const oneStyle = styles[ element ]; - if ( oneStyle !== undefined ) { - styleValues.push( oneStyle ); - } - } ); + + let styleValues = {}; + if ( typeof props.style === 'string' ) { + const oneStyle = props.style.split( ' ' ).map( ( element ) => styles[ element ] ).filter( Boolean ); + styleValues = Object.assign( styleValues, ...oneStyle ); } - const safeProps = styleValues.length === 0 ? { ...omit( props, [ 'style' ] ) } : { ...props, style: styleValues }; + const safeProps = { ...props, style: styleValues }; + return ( <Svg + //We want to re-render when style color is changed + key={ safeProps.style.color } height="100%" width="100%" { ...safeProps } /> ); }; + diff --git a/packages/components/src/primitives/svg/style.native.scss b/packages/components/src/primitives/svg/style.native.scss new file mode 100644 index 00000000000000..a6017e488b9228 --- /dev/null +++ b/packages/components/src/primitives/svg/style.native.scss @@ -0,0 +1,14 @@ +.dashicon { + color: #7b9ab1; + fill: currentColor; +} + +.dashicon-active { + color: #fff; + fill: currentColor; +} + +.dashicons-insert { + color: #87a6bc; + fill: currentColor; +} diff --git a/packages/components/src/toolbar/style.native.scss b/packages/components/src/toolbar/style.native.scss new file mode 100644 index 00000000000000..aca87d859b8bb6 --- /dev/null +++ b/packages/components/src/toolbar/style.native.scss @@ -0,0 +1,7 @@ +.container { + flex-direction: row; + border-right-width: 1px; + border-right-color: #e9eff3; + padding-left: 5px; + padding-right: 5px; +} diff --git a/packages/components/src/toolbar/toolbar-container.native.js b/packages/components/src/toolbar/toolbar-container.native.js index 08106a987f4089..dba6d5123c1ae7 100644 --- a/packages/components/src/toolbar/toolbar-container.native.js +++ b/packages/components/src/toolbar/toolbar-container.native.js @@ -3,8 +3,12 @@ */ import { View } from 'react-native'; -export default ( props ) => ( - <View style={ { flexDirection: 'row' } }> +import styles from './style.scss'; + +const ToolbarContainer = ( props ) => ( + <View style={ styles.container }> { props.children } </View> ); + +export default ToolbarContainer; diff --git a/packages/html-entities/package.json b/packages/html-entities/package.json index bd46481b93cf04..db944aa08cffe5 100644 --- a/packages/html-entities/package.json +++ b/packages/html-entities/package.json @@ -4,6 +4,7 @@ "description": "HTML entity utilities for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", + "react-native": "src/index", "keywords": [ "wordpress", "html", From 0823f242c7e93ad373f18b29ed54e24587b64676 Mon Sep 17 00:00:00 2001 From: Jorge <jorge.costa@developer.pt> Date: Fri, 23 Nov 2018 08:57:46 +0000 Subject: [PATCH 120/254] Fix: Classic Block: Gallery shortcode does not display images in a grid (#12228) ## Description Fixes: https://github.com/WordPress/gutenberg/issues/11940 This PR adds the necessary styles for the gallery shortcode correctly display as columns. Styles replicate what was done in /wp-includes/js/tinymce/skins/wordpress/wp-content.css. ## How has this been tested? Add a gallery shortcode inside the classic block and verify columns are correctly displayed. --- .../block-library/src/classic/editor.scss | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/packages/block-library/src/classic/editor.scss b/packages/block-library/src/classic/editor.scss index 0720c22d4a2c3d..1cda8777992fa7 100644 --- a/packages/block-library/src/classic/editor.scss +++ b/packages/block-library/src/classic/editor.scss @@ -116,6 +116,65 @@ cursor: default; border: 2px dashed rgb(186, 186, 186); } + + /** + * The following gallery styles were replicated + * from the styles applied in the tinymce skin, + * /wp-includes/js/tinymce/skins/wordpress/wp-content.css. + */ + .wpview-type-gallery::after { + content: ""; + display: table; + clear: both; + } + + .gallery img[data-mce-selected]:focus { + outline: none; + } + + .gallery a { + cursor: default; + } + + .gallery { + margin: auto -6px; + padding: 6px 0; + line-height: 1; + overflow-x: hidden; + } + + .gallery .gallery-item { + float: left; + margin: 0; + text-align: center; + padding: 6px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + } + + .gallery .gallery-caption, + .gallery .gallery-icon { + margin: 0; + } + + .gallery .gallery-caption { + font-size: 13px; + margin: 4px 0; + } + + @for $i from 1 through 9 { + .gallery-columns-#{ $i } .gallery-item { + width: #{ (100 / $i) + "%" }; + } + } + + .gallery img { + max-width: 100%; + height: auto; + border: none; + padding: 0; + } } .editor-block-list__layout .editor-block-list__block[data-type="core/freeform"] { From 70b378b1e258b3b5d908cf23fb0d2b7d41577894 Mon Sep 17 00:00:00 2001 From: Joen Asmussen <joen@automattic.com> Date: Fri, 23 Nov 2018 09:58:13 +0100 Subject: [PATCH 121/254] Improve block preview. (#12212) Hopefully fixes #11997. It polishes the preview code a bit to be leaner and more readable. But mostly it adds a fwe CSS style overrides that it might inherit from the theme. --- .../editor/src/components/block-styles/style.scss | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/editor/src/components/block-styles/style.scss b/packages/editor/src/components/block-styles/style.scss index 53a609012f74e4..8816ea0b9c4f82 100644 --- a/packages/editor/src/components/block-styles/style.scss +++ b/packages/editor/src/components/block-styles/style.scss @@ -38,14 +38,17 @@ height: 60px; background: $white; - > * { + // Actual preview contents. + .editor-block-preview__content { transform: scale(0.7); transform-origin: center center; - font-family: $editor-font; - } - - .editor-block-preview__content { width: 100%; + + // Unset some of the styles that might be inherited from the editor style. + margin: 0; + padding: 0; + overflow: visible; + min-height: auto; } } From 1f57485def4ca42fb228b88d36e3143043c660d1 Mon Sep 17 00:00:00 2001 From: Joen Asmussen <joen@automattic.com> Date: Fri, 23 Nov 2018 10:00:34 +0100 Subject: [PATCH 122/254] Improve button block styles and wrap. (#12205) This PR fixes #7843, and also simplifies the styles a bit. It also fixes an overflow issue with long text in variations. Finally, it makes a single line button have a proper pillshape. --- packages/block-library/src/button/editor.scss | 15 ++++++--------- packages/block-library/src/button/style.scss | 8 +++----- .../src/components/block-switcher/style.scss | 3 ++- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/packages/block-library/src/button/editor.scss b/packages/block-library/src/button/editor.scss index f20d565964dbcd..1330ff2cdf96f9 100644 --- a/packages/block-library/src/button/editor.scss +++ b/packages/block-library/src/button/editor.scss @@ -1,5 +1,3 @@ -$blocks-button__line-height: $big-font-size + 6px; - .editor-block-list__block[data-type="core/button"] { &[data-align="center"] { text-align: center; @@ -18,7 +16,6 @@ $blocks-button__line-height: $big-font-size + 6px; .editor-rich-text__tinymce.mce-content-body { cursor: text; - line-height: $blocks-button__line-height; } // Make placeholder text white unless custom colors or outline versions are chosen. @@ -31,15 +28,15 @@ $blocks-button__line-height: $big-font-size + 6px; opacity: 0.8; } - // Polish the empty placeholder text for the button in variation previews. - .editor-rich-text__tinymce[data-is-placeholder-visible="true"] { - height: auto; - } - // Don't let the placeholder text wrap in the variation preview. .editor-block-preview__content & { max-width: 100%; + // Polish the empty placeholder text for the button in variation previews. + .editor-rich-text__tinymce[data-is-placeholder-visible="true"] { + height: auto; + } + .wp-block-button__link { max-width: 100%; overflow: hidden; @@ -58,7 +55,7 @@ $blocks-button__line-height: $big-font-size + 6px; font-size: $default-font-size; line-height: $default-line-height; - // the width of input box plus padding plus two icon buttons. + // The width of input box plus padding plus two icon buttons. $blocks-button__link-input-width: 300px + 2px + 2 * $icon-button-size; width: $blocks-button__link-input-width; diff --git a/packages/block-library/src/button/style.scss b/packages/block-library/src/button/style.scss index fa72a8bc7835d5..2a45e2e574b38f 100644 --- a/packages/block-library/src/button/style.scss +++ b/packages/block-library/src/button/style.scss @@ -1,5 +1,4 @@ -$blocks-button__height: 46px; -$blocks-button__line-height: $big-font-size + 6px; +$blocks-button__height: 56px; .wp-block-button { color: $white; @@ -24,13 +23,12 @@ $blocks-button__line-height: $big-font-size + 6px; cursor: pointer; display: inline-block; font-size: $big-font-size; - line-height: $blocks-button__line-height; margin: 0; - padding: ($blocks-button__height - $blocks-button__line-height) / 2 24px; + padding: 12px 24px; text-align: center; text-decoration: none; white-space: normal; - word-break: break-all; + overflow-wrap: break-word; &:hover, &:focus, diff --git a/packages/editor/src/components/block-switcher/style.scss b/packages/editor/src/components/block-switcher/style.scss index daebe5a47ea72f..a994ff75424044 100644 --- a/packages/editor/src/components/block-switcher/style.scss +++ b/packages/editor/src/components/block-switcher/style.scss @@ -74,7 +74,8 @@ } .components-popover:not(.is-mobile).editor-block-switcher__popover .components-popover__content { - min-width: 320px; + min-width: 300px; + max-width: 340px; } .editor-block-switcher__popover .components-popover__content { From 93f2269e02a1842b95df341c8d8f240a11c01df3 Mon Sep 17 00:00:00 2001 From: Jorge <jorge.costa@developer.pt> Date: Fri, 23 Nov 2018 10:30:03 +0000 Subject: [PATCH 123/254] Fix: Cover Block: Move some editor specific styles from style.scss to editor.scss (#12236) During the reviews of https://github.com/WordPress/gutenberg/pull/12187 we found out that the cover block contains some editor specific styles on the front end. This is a problem because we would be transferring bytes on the frontend of websites that are not used at all. This PR just moves this styles to editor.scss so they are loaded only in the editor. ## How has this been tested? I checked that there was no noticeable change on the cover block (and its placeholder). --- packages/block-library/src/cover/editor.scss | 17 +++++++++++++++++ packages/block-library/src/cover/style.scss | 12 ------------ 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/packages/block-library/src/cover/editor.scss b/packages/block-library/src/cover/editor.scss index 35eb31ab78c40c..dffeeca07dc3cd 100644 --- a/packages/block-library/src/cover/editor.scss +++ b/packages/block-library/src/cover/editor.scss @@ -23,4 +23,21 @@ &.has-right-content .editor-rich-text__inline-toolbar { justify-content: flex-end; } + + &.components-placeholder { + // use opacity to work in various editor styles + background: $dark-opacity-light-200; + min-height: 200px; + + .is-dark-theme & { + background: $light-opacity-light-200; + } + } + + // Apply max-width to floated items that have no intrinsic width + [data-align="left"] &, + [data-align="right"] & { + max-width: $content-width / 2; + width: 100%; + } } diff --git a/packages/block-library/src/cover/style.scss b/packages/block-library/src/cover/style.scss index 6cbc5f1e3366c6..5e35c6d8ff809d 100644 --- a/packages/block-library/src/cover/style.scss +++ b/packages/block-library/src/cover/style.scss @@ -83,19 +83,7 @@ } } - &.components-placeholder { - // use opacity to work in various editor styles - background: $dark-opacity-light-200; - min-height: 200px; - - .is-dark-theme & { - background: $light-opacity-light-200; - } - } - // Apply max-width to floated items that have no intrinsic width - [data-align="left"] &, - [data-align="right"] &, &.alignleft, &.alignright { max-width: $content-width / 2; From f4fc47e7bc583bbd18b96065cbe32be420300b6c Mon Sep 17 00:00:00 2001 From: Jorge <jorge.costa@developer.pt> Date: Fri, 23 Nov 2018 11:55:50 +0000 Subject: [PATCH 124/254] Fix: Pasting a tag that is part of a transform but non matched ignores the content. (#11244) If a tag is accepted because of a raw handler transform but unmatched, we do the same as if the tag was not supported so we don't lose the content. The cause of the bug was that even after the removal of the preformatted block the pre tag was still accepted by the code block, but was unmatched if the preformatted was standard none code content. --- packages/blocks/src/api/raw-handling/utils.js | 47 +++++++++++++++---- .../non-matched-tags-handling.spec.js | 37 +++++++++++++++ 2 files changed, 74 insertions(+), 10 deletions(-) create mode 100644 test/integration/non-matched-tags-handling.spec.js diff --git a/packages/blocks/src/api/raw-handling/utils.js b/packages/blocks/src/api/raw-handling/utils.js index 1494c9b3e11a49..1859da6493a58f 100644 --- a/packages/blocks/src/api/raw-handling/utils.js +++ b/packages/blocks/src/api/raw-handling/utils.js @@ -27,7 +27,7 @@ const { ELEMENT_NODE, TEXT_NODE } = window.Node; * @return {Object} A complete block content schema. */ export function getBlockContentSchema( transforms ) { - const schemas = transforms.map( ( { blockName, schema } ) => { + const schemas = transforms.map( ( { isMatch, blockName, schema } ) => { // If the block supports the "anchor" functionality, it needs to keep its ID attribute. if ( hasBlockSupport( blockName, 'anchor' ) ) { for ( const tag in schema ) { @@ -37,18 +37,41 @@ export function getBlockContentSchema( transforms ) { schema[ tag ].attributes.push( 'id' ); } } + + // If an isMatch function exists add it to each schema tag that it applies to. + if ( isMatch ) { + for ( const tag in schema ) { + schema[ tag ].isMatch = isMatch; + } + } return schema; } ); return mergeWith( {}, ...schemas, ( objValue, srcValue, key ) => { - if ( key === 'children' ) { - if ( objValue === '*' || srcValue === '*' ) { - return '*'; - } + switch ( key ) { + case 'children': { + if ( objValue === '*' || srcValue === '*' ) { + return '*'; + } - return { ...objValue, ...srcValue }; - } else if ( key === 'attributes' || key === 'require' ) { - return [ ...( objValue || [] ), ...( srcValue || [] ) ]; + return { ...objValue, ...srcValue }; + } + case 'attributes': + case 'require': { + return [ ...( objValue || [] ), ...( srcValue || [] ) ]; + } + case 'isMatch': { + // If one of the values being merge is undefined (matches everything), + // the result of the merge will be undefined. + if ( ! objValue || ! srcValue ) { + return undefined; + } + // When merging two isMatch functions, the result is a new function + // that returns if one of the source functions returns true. + return ( ...args ) => { + return objValue( ...args ) || srcValue( ...args ); + }; + } } } ); } @@ -153,8 +176,12 @@ function cleanNodeList( nodeList, doc, schema, inline ) { Array.from( nodeList ).forEach( ( node ) => { const tag = node.nodeName.toLowerCase(); - // It's a valid child. - if ( schema.hasOwnProperty( tag ) ) { + // It's a valid child, if the tag exists in the schema without an isMatch + // function, or with an isMatch function that matches the node. + if ( + schema.hasOwnProperty( tag ) && + ( ! schema[ tag ].isMatch || schema[ tag ].isMatch( node ) ) + ) { if ( node.nodeType === ELEMENT_NODE ) { const { attributes = [], classes = [], children, require = [] } = schema[ tag ]; diff --git a/test/integration/non-matched-tags-handling.spec.js b/test/integration/non-matched-tags-handling.spec.js new file mode 100644 index 00000000000000..71ce3a13b6137c --- /dev/null +++ b/test/integration/non-matched-tags-handling.spec.js @@ -0,0 +1,37 @@ +/** + * WordPress dependencies + */ +import { + pasteHandler, + unregisterBlockType, +} from '@wordpress/blocks'; +import { registerCoreBlocks } from '@wordpress/block-library'; + +describe( 'Handling of non matched tags in block transforms', () => { + beforeAll( () => { + // Load all hooks that modify blocks + require( '../../packages/editor/src/hooks' ); + registerCoreBlocks(); + } ); + it( 'correctly pastes preformatted tag even if preformatted block is removed', () => { + unregisterBlockType( 'core/preformatted' ); + const simplePreformattedResult = pasteHandler( { + HTML: '<pre>Pre</pre>', + mode: 'AUTO', + } ); + + expect( simplePreformattedResult ).toHaveLength( 1 ); + expect( simplePreformattedResult[ 0 ].name ).toBe( 'core/paragraph' ); + expect( simplePreformattedResult[ 0 ].attributes.content ).toBe( 'Pre' ); + + const codeResult = pasteHandler( { + HTML: '<pre><code>code</code></pre>', + mode: 'AUTO', + } ); + + expect( codeResult ).toHaveLength( 1 ); + expect( codeResult[ 0 ].name ).toBe( 'core/code' ); + expect( codeResult[ 0 ].attributes.content ).toBe( 'code' ); + expect( console ).toHaveLogged(); + } ); +} ); From bd897f6cb78e7625ad264c7a487bfa950118c85c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20Van=C2=A0Dorpe?= <iseulde@automattic.com> Date: Fri, 23 Nov 2018 13:59:52 +0100 Subject: [PATCH 125/254] Add IE check for IE fix (#12234) * Add IE check * typo --- .../src/components/rich-text/tinymce.js | 29 +++++++------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/packages/editor/src/components/rich-text/tinymce.js b/packages/editor/src/components/rich-text/tinymce.js index ca8d0ea70521ae..dc6f7f46b38314 100644 --- a/packages/editor/src/components/rich-text/tinymce.js +++ b/packages/editor/src/components/rich-text/tinymce.js @@ -23,6 +23,7 @@ import { diffAriaProps, pickAriaProps } from './aria'; const { getSelection } = window; const { TEXT_NODE } = window.Node; +const { userAgent } = window.navigator; /** * Zero-width space character used by TinyMCE as a caret landing point for @@ -34,23 +35,6 @@ const { TEXT_NODE } = window.Node; */ export const TINYMCE_ZWSP = '\uFEFF'; -/** - * Determines whether we need a fix to provide `input` events for contenteditable. - * - * @param {Element} editorNode The root editor node. - * - * @return {boolean} A boolean indicating whether the fix is needed. - */ -function needsInternetExplorerInputFix( editorNode ) { - return ( - // Rely on userAgent in the absence of a reasonable feature test for contenteditable `input` events. - /Trident/.test( window.navigator.userAgent ) && - // IE11 dispatches input events for `<input>` and `<textarea>`. - ! /input/i.test( editorNode.tagName ) && - ! /textarea/i.test( editorNode.tagName ) - ); -} - /** * Applies a fix that provides `input` events for contenteditable in Internet Explorer. * @@ -113,6 +97,14 @@ function applyInternetExplorerInputFix( editorNode ) { } const IS_PLACEHOLDER_VISIBLE_ATTR_NAME = 'data-is-placeholder-visible'; + +/** + * Whether or not the user agent is Internet Explorer. + * + * @type {boolean} + */ +const IS_IE = userAgent.indexOf( 'Trident' ) >= 0; + export default class TinyMCE extends Component { constructor() { super(); @@ -247,6 +239,7 @@ export default class TinyMCE extends Component { // In IE11, focus is lost to parent after initialising // TinyMCE, so we have to set it back. if ( + IS_IE && document.activeElement !== this.editorNode && document.activeElement.contains( this.editorNode ) ) { @@ -275,7 +268,7 @@ export default class TinyMCE extends Component { this.removeInternetExplorerInputFix = null; } - if ( editorNode && needsInternetExplorerInputFix( editorNode ) ) { + if ( IS_IE ) { this.removeInternetExplorerInputFix = applyInternetExplorerInputFix( editorNode ); } } From b5a709fc63a11f042c53f67f80a2eaf50713e00a Mon Sep 17 00:00:00 2001 From: Stephen Edgar <stephen@netweb.com.au> Date: Sun, 25 Nov 2018 19:34:22 +1100 Subject: [PATCH 126/254] Travis CI: Remove `sudo: required` (#12092) This is a test to switch from _Container-Based Builds_ to _Virtual-Machine-Based_ ---- Via: https://blog.travis-ci.com/2018-11-19-required-linux-infrastructure-migration Over the next few weeks, we encourage everyone to remove any `sudo: false` configurations from your `.travis.yml`. Soon we will run all projects on the virtual-machine-based infrastructure, the `sudo` keyword will be fully deprecated. The timeline for this migration will be as follows: - 19 November, 2018 - Today we publish this post and are ready to [answer all your questions](https://travis-ci.community/t/combining-the-linux-infrastructures/310/3)! - 28 November, 2018 - We will send a service email to remind folks still using `sudo: false` on recent builds to remind you to migrate. - 03 December, 2018 - We will start randomly sampling projects on both travis-ci.org and travis-ci.com to move them permanently to using the virtual-machine-based infrastructure for all builds. The projects will be migrated incrementally over a few days - 07 December, 2018 - All projects that use a Linux build environment will be fully migrated to using the same Linux infrastructure, which runs builds in virtual-machines. ---- --- .travis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index bd4b9a57fc707e..2dc47aa01d6422 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,3 @@ -sudo: required - dist: trusty language: php From b0c31f751b56e578e1c4873e35bee67ed6227273 Mon Sep 17 00:00:00 2001 From: Tugdual de Kerviler <dekervit@gmail.com> Date: Mon, 26 Nov 2018 09:03:16 +0100 Subject: [PATCH 127/254] Add undo redo (#12231) * Expose Undo and Redo components * Make Button look differently when disabled on mobile * Use a local variable to store the aztec height instead of saving it into the block attributes --- .../block-library/src/heading/edit.native.js | 10 ++++++++-- .../src/paragraph/edit.native.js | 19 ++++++++----------- .../components/src/button/index.native.js | 19 +++++++++++++++++-- .../editor/src/components/index.native.js | 2 ++ 4 files changed, 35 insertions(+), 15 deletions(-) diff --git a/packages/block-library/src/heading/edit.native.js b/packages/block-library/src/heading/edit.native.js index 6d2ed5bf27e7d2..4ab3ad0a66c145 100644 --- a/packages/block-library/src/heading/edit.native.js +++ b/packages/block-library/src/heading/edit.native.js @@ -24,6 +24,12 @@ import './editor.scss'; const minHeight = 50; class HeadingEdit extends Component { + constructor( props ) { + super( props ); + + this.aztecHeight = 0; + } + render() { const { attributes, @@ -49,7 +55,7 @@ class HeadingEdit extends Component { value={ content } isSelected={ this.props.isSelected } style={ { - minHeight: Math.max( minHeight, typeof attributes.aztecHeight === 'undefined' ? 0 : attributes.aztecHeight ), + minHeight: Math.max( minHeight, this.aztecHeight ), } } onChange={ ( event ) => { // Create a React Tree from the new HTML @@ -72,7 +78,7 @@ class HeadingEdit extends Component { undefined } onContentSizeChange={ ( event ) => { - setAttributes( { aztecHeight: event.aztecHeight } ); + this.aztecHeight = event.aztecHeight; } } placeholder={ placeholder || __( 'Write heading…' ) } /> diff --git a/packages/block-library/src/paragraph/edit.native.js b/packages/block-library/src/paragraph/edit.native.js index 4e07a0b4560958..3f6fc3ef1abf05 100644 --- a/packages/block-library/src/paragraph/edit.native.js +++ b/packages/block-library/src/paragraph/edit.native.js @@ -19,9 +19,11 @@ import styles from './style.scss'; const name = 'core/paragraph'; class ParagraphEdit extends Component { - constructor() { - super( ...arguments ); + constructor( props ) { + super( props ); this.splitBlock = this.splitBlock.bind( this ); + + this.aztecHeight = 0; } /** @@ -90,7 +92,7 @@ class ParagraphEdit extends Component { isSelected={ this.props.isSelected } style={ { ...style, - minHeight: Math.max( minHeight, typeof attributes.aztecHeight === 'undefined' ? 0 : attributes.aztecHeight ), + minHeight: Math.max( minHeight, this.aztecHeight ), } } onChange={ ( event ) => { // Create a React Tree from the new HTML @@ -99,17 +101,12 @@ class ParagraphEdit extends Component { ...this.props.attributes, content: newParaBlock.attributes.content, } ); - } - } + } } onSplit={ this.splitBlock } onMerge={ mergeBlocks } onContentSizeChange={ ( event ) => { - setAttributes( { - ...this.props.attributes, - aztecHeight: event.aztecHeight, - } ); - } - } + this.aztecHeight = event.aztecHeight; + } } placeholder={ placeholder || __( 'Add text or type / to add content' ) } /> </View> diff --git a/packages/components/src/button/index.native.js b/packages/components/src/button/index.native.js index b5a084993d21f6..cd38fecd9ed2c2 100644 --- a/packages/components/src/button/index.native.js +++ b/packages/components/src/button/index.native.js @@ -51,7 +51,21 @@ const styles = StyleSheet.create( { } ); export default function Button( props ) { - const { children, onClick, 'aria-label': ariaLabel, 'aria-pressed': ariaPressed, 'data-subscript': subscript } = props; + const { + children, + onClick, + disabled, + 'aria-disabled': ariaDisabled, + 'aria-label': ariaLabel, + 'aria-pressed': ariaPressed, + 'data-subscript': subscript, + } = props; + + const isDisabled = ariaDisabled || disabled; + const buttonViewStyle = { + opacity: isDisabled ? 0.2 : 1, + ...( ariaPressed ? styles.buttonActive : styles.buttonInactive ), + }; return ( <TouchableOpacity @@ -60,8 +74,9 @@ export default function Button( props ) { accessibilityLabel={ ariaLabel } onPress={ onClick } style={ styles.container } + disabled={ isDisabled } > - <View style={ ariaPressed ? styles.buttonActive : styles.buttonInactive }> + <View style={ buttonViewStyle }> <View style={ { flexDirection: 'row' } }> { children } { subscript && ( <Text style={ ariaPressed ? styles.subscriptActive : styles.subscriptInactive }>{ subscript }</Text> ) } diff --git a/packages/editor/src/components/index.native.js b/packages/editor/src/components/index.native.js index 17943dee6b3ae2..dc651f9e7e0a6b 100644 --- a/packages/editor/src/components/index.native.js +++ b/packages/editor/src/components/index.native.js @@ -6,3 +6,5 @@ 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'; +export { default as EditorHistoryRedo } from './editor-history/redo'; +export { default as EditorHistoryUndo } from './editor-history/undo'; From 4a9de65d553bda3e517dd03b8c923437be3ade8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20=28Greg=29=20Zi=C3=B3=C5=82kowski?= <grzegorz@gziolo.pl> Date: Mon, 26 Nov 2018 10:29:29 +0100 Subject: [PATCH 128/254] Update published packages changelogs (#12247) --- packages/annotations/CHANGELOG.md | 2 ++ packages/block-library/CHANGELOG.md | 4 ++++ packages/blocks/CHANGELOG.md | 2 ++ packages/components/CHANGELOG.md | 4 ++++ packages/edit-post/CHANGELOG.md | 4 ++++ packages/editor/CHANGELOG.md | 4 ++++ packages/format-library/CHANGELOG.md | 4 ++++ packages/html-entities/CHANGELOG.md | 2 ++ packages/keycodes/CHANGELOG.md | 2 ++ packages/list-reusable-blocks/CHANGELOG.md | 4 ++++ packages/nux/CHANGELOG.md | 4 ++++ packages/rich-text/CHANGELOG.md | 2 ++ 12 files changed, 38 insertions(+) diff --git a/packages/annotations/CHANGELOG.md b/packages/annotations/CHANGELOG.md index 16735d5488527c..edceba997fe675 100644 --- a/packages/annotations/CHANGELOG.md +++ b/packages/annotations/CHANGELOG.md @@ -1,3 +1,5 @@ +## 1.0.3 (2018-11-21) + ## 1.0.2 (2018-11-20) ## 1.0.1 (2018-11-15) diff --git a/packages/block-library/CHANGELOG.md b/packages/block-library/CHANGELOG.md index 742b16f21091b5..8a54a337049ce4 100644 --- a/packages/block-library/CHANGELOG.md +++ b/packages/block-library/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.2.7 (2018-11-22) + +## 2.2.6 (2018-11-21) + ## 2.2.5 (2018-11-20) ## 2.2.4 (2018-11-15) diff --git a/packages/blocks/CHANGELOG.md b/packages/blocks/CHANGELOG.md index dfc293e7cd1267..f343aa557aeaa3 100644 --- a/packages/blocks/CHANGELOG.md +++ b/packages/blocks/CHANGELOG.md @@ -1,3 +1,5 @@ +## 6.0.2 (2018-11-21) + ## 6.0.1 (2018-11-20) ## 6.0.0 (2018-11-15) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 7829968a373b2b..9e3643db94c675 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -1,3 +1,7 @@ +## 7.0.2 (2018-11-22) + +## 7.0.1 (2018-11-21) + ## 7.0.0 (2018-11-20) ### Breaking Change diff --git a/packages/edit-post/CHANGELOG.md b/packages/edit-post/CHANGELOG.md index 0b741baabd375c..055d3a6b47b011 100644 --- a/packages/edit-post/CHANGELOG.md +++ b/packages/edit-post/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.1.2 (2018-11-22) + +## 3.1.1 (2018-11-21) + ## 3.1.0 (2018-11-20) ### New Feature diff --git a/packages/editor/CHANGELOG.md b/packages/editor/CHANGELOG.md index 45eb7c15d2418e..0522ffc4bc02ae 100644 --- a/packages/editor/CHANGELOG.md +++ b/packages/editor/CHANGELOG.md @@ -1,3 +1,7 @@ +## 9.0.2 (2018-11-22) + +## 9.0.1 (2018-11-21) + ## 9.0.0 (2018-11-20) ### Breaking Changes diff --git a/packages/format-library/CHANGELOG.md b/packages/format-library/CHANGELOG.md index 58bd7408389532..a010e7aba5e378 100644 --- a/packages/format-library/CHANGELOG.md +++ b/packages/format-library/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.2.5 (2018-11-22) + +## 1.2.4 (2018-11-21) + ## 1.2.3 (2018-11-20) ### Bug fixes diff --git a/packages/html-entities/CHANGELOG.md b/packages/html-entities/CHANGELOG.md index a49cefcd0d75cf..bc270815c38c88 100644 --- a/packages/html-entities/CHANGELOG.md +++ b/packages/html-entities/CHANGELOG.md @@ -1,3 +1,5 @@ +## 2.0.1 (2018-11-21) + ## 2.0.0 (2018-09-05) ### Breaking Change diff --git a/packages/keycodes/CHANGELOG.md b/packages/keycodes/CHANGELOG.md index ab84c2c4dc075a..b630cfb0799b3a 100644 --- a/packages/keycodes/CHANGELOG.md +++ b/packages/keycodes/CHANGELOG.md @@ -1,3 +1,5 @@ +## 2.0.5 (2018-11-21) + ## 2.0.4 (2018-11-20) ## 2.0.3 (2018-10-30) diff --git a/packages/list-reusable-blocks/CHANGELOG.md b/packages/list-reusable-blocks/CHANGELOG.md index 26020674f2a31e..e39a225e3366de 100644 --- a/packages/list-reusable-blocks/CHANGELOG.md +++ b/packages/list-reusable-blocks/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.1.15 (2018-11-22) + +## 1.1.14 (2018-11-21) + ## 1.1.13 (2018-11-20) ## 1.1.12 (2018-11-15) diff --git a/packages/nux/CHANGELOG.md b/packages/nux/CHANGELOG.md index 5adb00aacdd307..c087f42d7432e9 100644 --- a/packages/nux/CHANGELOG.md +++ b/packages/nux/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.0.3 (2018-11-22) + +## 3.0.2 (2018-11-21) + ## 3.0.1 (2018-11-20) ## 3.0.0 (2018-11-15) diff --git a/packages/rich-text/CHANGELOG.md b/packages/rich-text/CHANGELOG.md index 4a2ad2d43f85fd..0e9ef7674dc481 100644 --- a/packages/rich-text/CHANGELOG.md +++ b/packages/rich-text/CHANGELOG.md @@ -1,3 +1,5 @@ +## 3.0.2 (2018-11-21) + ## 3.0.1 (2018-11-20) ## 3.0.0 (2018-11-15) From 1ee20134ada0415c3affe681e0c68cd19d2668c5 Mon Sep 17 00:00:00 2001 From: Marcus Kazmierczak <marcus@mkaz.com> Date: Mon, 26 Nov 2018 01:32:58 -0800 Subject: [PATCH 129/254] Add check for css file before trying to read (#12291) * Add check for css file before trying to read Mirrors existing trac ticket https://core.trac.wordpress.org/ticket/45288 Fixes #12196 * Use other patch :-) --- lib/client-assets.php | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/client-assets.php b/lib/client-assets.php index 05cd226f8b4792..6fa221bf4c5d8c 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -1224,11 +1224,13 @@ function gutenberg_editor_scripts_and_styles( $hook ) { 'css' => file_get_contents( $style ), ); } else { - $file = get_theme_file_path( $style ); - $styles[] = array( - 'css' => file_get_contents( get_theme_file_path( $style ) ), - 'baseURL' => get_theme_file_uri( $style ), - ); + $file = get_theme_file_path( $style ); + if ( file_exists( $file ) ) { + $styles[] = array( + 'css' => file_get_contents( $file ), + 'baseURL' => get_theme_file_uri( $style ), + ); + } } } } From 6f8b4c8852dbd2026d0195703e4736ff612f1d0e Mon Sep 17 00:00:00 2001 From: Tugdual de Kerviler <dekervit@gmail.com> Date: Mon, 26 Nov 2018 11:07:54 +0100 Subject: [PATCH 130/254] Use state instead to refresh component onContentSizeChange (#12304) --- packages/block-library/src/heading/edit.native.js | 8 +++++--- packages/block-library/src/paragraph/edit.native.js | 8 +++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/block-library/src/heading/edit.native.js b/packages/block-library/src/heading/edit.native.js index 4ab3ad0a66c145..62ad35c4792945 100644 --- a/packages/block-library/src/heading/edit.native.js +++ b/packages/block-library/src/heading/edit.native.js @@ -27,7 +27,9 @@ class HeadingEdit extends Component { constructor( props ) { super( props ); - this.aztecHeight = 0; + this.state = { + aztecHeight: 0, + }; } render() { @@ -55,7 +57,7 @@ class HeadingEdit extends Component { value={ content } isSelected={ this.props.isSelected } style={ { - minHeight: Math.max( minHeight, this.aztecHeight ), + minHeight: Math.max( minHeight, this.state.aztecHeight ), } } onChange={ ( event ) => { // Create a React Tree from the new HTML @@ -78,7 +80,7 @@ class HeadingEdit extends Component { undefined } onContentSizeChange={ ( event ) => { - this.aztecHeight = event.aztecHeight; + this.setState( { aztecHeight: event.aztecHeight } ); } } placeholder={ placeholder || __( 'Write heading…' ) } /> diff --git a/packages/block-library/src/paragraph/edit.native.js b/packages/block-library/src/paragraph/edit.native.js index 3f6fc3ef1abf05..0c0d227a5fcd9f 100644 --- a/packages/block-library/src/paragraph/edit.native.js +++ b/packages/block-library/src/paragraph/edit.native.js @@ -23,7 +23,9 @@ class ParagraphEdit extends Component { super( props ); this.splitBlock = this.splitBlock.bind( this ); - this.aztecHeight = 0; + this.state = { + aztecHeight: 0, + }; } /** @@ -92,7 +94,7 @@ class ParagraphEdit extends Component { isSelected={ this.props.isSelected } style={ { ...style, - minHeight: Math.max( minHeight, this.aztecHeight ), + minHeight: Math.max( minHeight, this.state.aztecHeight ), } } onChange={ ( event ) => { // Create a React Tree from the new HTML @@ -105,7 +107,7 @@ class ParagraphEdit extends Component { onSplit={ this.splitBlock } onMerge={ mergeBlocks } onContentSizeChange={ ( event ) => { - this.aztecHeight = event.aztecHeight; + this.setState( { aztecHeight: event.aztecHeight } ); } } placeholder={ placeholder || __( 'Add text or type / to add content' ) } /> From 97d117161042ed6c7bb0acf8d1811112b024822c Mon Sep 17 00:00:00 2001 From: Joen Asmussen <joen@automattic.com> Date: Mon, 26 Nov 2018 11:49:37 +0100 Subject: [PATCH 131/254] Fix wide image jumping. (#12305) Fixes #12292. This PR fixes an issue where the block toolbar would cause an image to jump downwards when the wide or fullwide buttons were pressed. Recently as part of a floats refactor, we also refactored how the block toolbar worked. This meant the removal of a floats rule to the toolbar itself, because it was both unnecessary and interfered with adjacent floats. This PR restores that rule, but for wide and fullwide only, fixing the regression. --- .../src/components/visual-editor/style.scss | 12 ++++++++---- packages/editor/src/components/block-list/style.scss | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/edit-post/src/components/visual-editor/style.scss b/packages/edit-post/src/components/visual-editor/style.scss index 67128f843aca47..f796ed8e4c7594 100644 --- a/packages/edit-post/src/components/visual-editor/style.scss +++ b/packages/edit-post/src/components/visual-editor/style.scss @@ -35,9 +35,17 @@ height: 0; // This collapses the container to an invisible element without margin. text-align: center; + // This float rule takes the toolbar out of the flow, without it having to be absolute positioned. + // This is necessary because otherwise the mere presence of the toolbar can push down content. + // Pairs with relative rule on line 49. + float: left; + .editor-block-toolbar { max-width: $content-width; width: 100%; + + // Necessary for the toolbar to be centered. + // This unsets an absolute position that will otherwise left align the toolbar. position: relative; } } @@ -47,10 +55,6 @@ width: 100%; margin-left: 0; margin-right: 0; - - .editor-block-toolbar { - max-width: $content-width - $border-width - $border-width; - } } } } diff --git a/packages/editor/src/components/block-list/style.scss b/packages/editor/src/components/block-list/style.scss index e7e13f8a9e06aa..3ecb8dcc263b82 100644 --- a/packages/editor/src/components/block-list/style.scss +++ b/packages/editor/src/components/block-list/style.scss @@ -870,7 +870,7 @@ // 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. + // This prevents floats from messing up the position of the block toolbar on floats-adjacent blocks when selected. position: absolute; left: 0; } From 80108fb9a2a5a2d3d5eb3692d0b47a65a9bb2073 Mon Sep 17 00:00:00 2001 From: Dion Hulse <dion@wordpress.org> Date: Mon, 26 Nov 2018 23:24:10 +1000 Subject: [PATCH 132/254] Fix the URL after documentation reorganisation. See https://meta.trac.wordpress.org/ticket/3958 (#12302) --- .../block-tutorial/block-controls-toolbars-and-inspector.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/designers-developers/developers/tutorials/block-tutorial/block-controls-toolbars-and-inspector.md b/docs/designers-developers/developers/tutorials/block-tutorial/block-controls-toolbars-and-inspector.md index 236e9856197696..de6509e4c42b73 100644 --- a/docs/designers-developers/developers/tutorials/block-tutorial/block-controls-toolbars-and-inspector.md +++ b/docs/designers-developers/developers/tutorials/block-tutorial/block-controls-toolbars-and-inspector.md @@ -171,7 +171,7 @@ Note that `BlockControls` is only visible when the block is currently selected a ## Inspector -<img src="https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/blocks/inspector.png" with="281" height="527" alt="inspector"> +<img src="https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/tutorials/block-tutorial/inspector.png" with="281" height="527" alt="inspector"> The inspector is used to display less-often-used settings or settings that require more screen space. The inspector should be used for **block-level settings only**. From e9ca75f5c0c05f4d31bcf7a0d7f5ea88b658ee23 Mon Sep 17 00:00:00 2001 From: Danilo Ercoli <ercoli@gmail.com> Date: Mon, 26 Nov 2018 18:59:22 +0100 Subject: [PATCH 133/254] [RNMobile] Expose `onFocus` and wire it for correct block-toolbar management (#12316) * Expose and wire `onFocus`. Used in Toolbar management methods. * Make sure onFocus is set in PlainText, so it is clear that the listener has to be set. * Wire onFocus on Heading block * Add comments --- packages/block-library/src/code/edit.native.js | 3 ++- packages/block-library/src/heading/edit.native.js | 1 + packages/block-library/src/more/edit.native.js | 3 ++- packages/block-library/src/paragraph/edit.native.js | 1 + packages/editor/src/components/plain-text/index.native.js | 1 + packages/editor/src/components/rich-text/index.native.js | 1 + 6 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/block-library/src/code/edit.native.js b/packages/block-library/src/code/edit.native.js index 40f1225b2ae755..5433481fcfb2fb 100644 --- a/packages/block-library/src/code/edit.native.js +++ b/packages/block-library/src/code/edit.native.js @@ -21,7 +21,7 @@ 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( props ) { - const { attributes, setAttributes, style } = props; + const { attributes, setAttributes, style, onFocus } = props; return ( <View> @@ -34,6 +34,7 @@ export default function CodeEdit( props ) { placeholder={ __( 'Write code…' ) } aria-label={ __( 'Code' ) } isSelected={ props.isSelected } + onFocus={ onFocus } /> </View> ); diff --git a/packages/block-library/src/heading/edit.native.js b/packages/block-library/src/heading/edit.native.js index 62ad35c4792945..afb2fca0eaa281 100644 --- a/packages/block-library/src/heading/edit.native.js +++ b/packages/block-library/src/heading/edit.native.js @@ -56,6 +56,7 @@ class HeadingEdit extends Component { tagName={ tagName } value={ content } isSelected={ this.props.isSelected } + onFocus={ this.props.onFocus } // always assign onFocus as a props style={ { minHeight: Math.max( minHeight, this.state.aztecHeight ), } } diff --git a/packages/block-library/src/more/edit.native.js b/packages/block-library/src/more/edit.native.js index 2c4d8bf89a693a..afbfc5973b9036 100644 --- a/packages/block-library/src/more/edit.native.js +++ b/packages/block-library/src/more/edit.native.js @@ -15,7 +15,7 @@ import { PlainText } from '@wordpress/editor'; import styles from './editor.scss'; export default function MoreEdit( props ) { - const { attributes, setAttributes } = props; + const { attributes, setAttributes, onFocus } = props; const { customText } = attributes; const defaultText = __( 'Read more' ); const value = customText !== undefined ? customText : defaultText; @@ -32,6 +32,7 @@ export default function MoreEdit( props ) { onChange={ ( newValue ) => setAttributes( { customText: newValue } ) } placeholder={ defaultText } isSelected={ props.isSelected } + onFocus={ onFocus } /> <Text className={ styles[ 'block-library-more__right-marker' ] }>--&gt;</Text> </View> diff --git a/packages/block-library/src/paragraph/edit.native.js b/packages/block-library/src/paragraph/edit.native.js index 0c0d227a5fcd9f..ce3c390715e1c1 100644 --- a/packages/block-library/src/paragraph/edit.native.js +++ b/packages/block-library/src/paragraph/edit.native.js @@ -92,6 +92,7 @@ class ParagraphEdit extends Component { tagName="p" value={ content } isSelected={ this.props.isSelected } + onFocus={ this.props.onFocus } // always assign onFocus as a props style={ { ...style, minHeight: Math.max( minHeight, this.state.aztecHeight ), diff --git a/packages/editor/src/components/plain-text/index.native.js b/packages/editor/src/components/plain-text/index.native.js index ae98e6a3f9ce6a..e8155a062e25a1 100644 --- a/packages/editor/src/components/plain-text/index.native.js +++ b/packages/editor/src/components/plain-text/index.native.js @@ -31,6 +31,7 @@ export default class PlainText extends Component { ref={ ( x ) => this._input = x } className={ [ styles[ 'editor-plain-text' ], this.props.className ] } onChangeText={ ( text ) => this.props.onChange( text ) } + onFocus={ this.props.onFocus } // always assign onFocus as a props { ...this.props } /> ); diff --git a/packages/editor/src/components/rich-text/index.native.js b/packages/editor/src/components/rich-text/index.native.js index 5b378ccd447394..e2cbecad952682 100644 --- a/packages/editor/src/components/rich-text/index.native.js +++ b/packages/editor/src/components/rich-text/index.native.js @@ -356,6 +356,7 @@ export class RichText extends Component { } text={ { text: html, eventCount: this.lastEventCount } } onChange={ this.onChange } + onFocus={ this.props.onFocus } onEnter={ this.onEnter } onBackspace={ this.onBackspace } onContentSizeChange={ this.onContentSizeChange } From 6e5dc2a9beb159351b5101fff562c5f81306f1b2 Mon Sep 17 00:00:00 2001 From: Marcus Kazmierczak <marcus@mkaz.com> Date: Mon, 26 Nov 2018 10:32:41 -0800 Subject: [PATCH 134/254] Change @package to WordPress (#12319) --- packages/block-library/src/archives/index.php | 2 +- packages/block-library/src/block/index.php | 2 +- packages/block-library/src/categories/index.php | 2 +- packages/block-library/src/latest-comments/index.php | 2 +- packages/block-library/src/latest-posts/index.php | 2 +- packages/block-library/src/shortcode/index.php | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/block-library/src/archives/index.php b/packages/block-library/src/archives/index.php index c94a4645d9c08b..3d7b5b42f4ae43 100644 --- a/packages/block-library/src/archives/index.php +++ b/packages/block-library/src/archives/index.php @@ -2,7 +2,7 @@ /** * Server-side rendering of the `core/archives` block. * - * @package gutenberg + * @package WordPress */ /** diff --git a/packages/block-library/src/block/index.php b/packages/block-library/src/block/index.php index bc33929ac517ab..77a281802c50a6 100644 --- a/packages/block-library/src/block/index.php +++ b/packages/block-library/src/block/index.php @@ -2,7 +2,7 @@ /** * Server-side rendering of the `core/block` block. * - * @package gutenberg + * @package WordPress */ /** diff --git a/packages/block-library/src/categories/index.php b/packages/block-library/src/categories/index.php index 38de4bd5178849..f302d9ba745383 100644 --- a/packages/block-library/src/categories/index.php +++ b/packages/block-library/src/categories/index.php @@ -2,7 +2,7 @@ /** * Server-side rendering of the `core/categories` block. * - * @package gutenberg + * @package WordPress */ /** diff --git a/packages/block-library/src/latest-comments/index.php b/packages/block-library/src/latest-comments/index.php index 05797c029e4b93..29e17e9de50080 100644 --- a/packages/block-library/src/latest-comments/index.php +++ b/packages/block-library/src/latest-comments/index.php @@ -2,7 +2,7 @@ /** * Server-side rendering of the `core/latest-comments` block. * - * @package gutenberg + * @package WordPress */ if ( ! function_exists( 'gutenberg_draft_or_post_title' ) ) { diff --git a/packages/block-library/src/latest-posts/index.php b/packages/block-library/src/latest-posts/index.php index 72b395a06cf485..621e912013b000 100644 --- a/packages/block-library/src/latest-posts/index.php +++ b/packages/block-library/src/latest-posts/index.php @@ -2,7 +2,7 @@ /** * Server-side rendering of the `core/latest-posts` block. * - * @package gutenberg + * @package WordPress */ /** diff --git a/packages/block-library/src/shortcode/index.php b/packages/block-library/src/shortcode/index.php index d18dd4f5828b55..1c0761250d2cfd 100644 --- a/packages/block-library/src/shortcode/index.php +++ b/packages/block-library/src/shortcode/index.php @@ -2,7 +2,7 @@ /** * Server-side rendering of the `core/shortcode` block. * - * @package gutenberg + * @package WordPress */ /** From c33e9178534087db51fe9c94e8274192e1dcf143 Mon Sep 17 00:00:00 2001 From: Andrea Fercia <a.fercia@gmail.com> Date: Mon, 26 Nov 2018 23:37:23 +0100 Subject: [PATCH 135/254] Improve the top bar tools interaction and consistency (#12073) * Improve top bar tools interaction consistency. * Return early if no rootBlocks. --- .../src/components/block-navigation/dropdown.js | 12 +++++++++--- .../editor/src/components/block-navigation/index.js | 11 ++++------- .../editor/src/components/table-of-contents/index.js | 4 ++-- test/e2e/specs/block-hierarchy-navigation.test.js | 11 ----------- 4 files changed, 15 insertions(+), 23 deletions(-) diff --git a/packages/editor/src/components/block-navigation/dropdown.js b/packages/editor/src/components/block-navigation/dropdown.js index 20910e4eebb042..c6b8e976db5cc3 100644 --- a/packages/editor/src/components/block-navigation/dropdown.js +++ b/packages/editor/src/components/block-navigation/dropdown.js @@ -5,6 +5,7 @@ import { Fragment } from '@wordpress/element'; import { IconButton, Dropdown, SVG, Path, KeyboardShortcuts } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { rawShortcut, displayShortcut } from '@wordpress/keycodes'; +import { withSelect } from '@wordpress/data'; /** * Internal dependencies @@ -17,7 +18,7 @@ const MenuIcon = ( </SVG> ); -function BlockNavigationDropdown() { +function BlockNavigationDropdown( { hasBlocks } ) { return ( <Dropdown renderToggle={ ( { isOpen, onToggle } ) => ( @@ -31,10 +32,11 @@ function BlockNavigationDropdown() { <IconButton icon={ MenuIcon } aria-expanded={ isOpen } - onClick={ onToggle } + onClick={ hasBlocks ? onToggle : undefined } label={ __( 'Block Navigation' ) } className="editor-block-navigation" shortcut={ displayShortcut.access( 'o' ) } + aria-disabled={ ! hasBlocks } /> </Fragment> ) } @@ -45,4 +47,8 @@ function BlockNavigationDropdown() { ); } -export default BlockNavigationDropdown; +export default withSelect( ( select ) => { + return { + hasBlocks: !! select( 'core/editor' ).getBlockCount(), + }; +} )( BlockNavigationDropdown ); diff --git a/packages/editor/src/components/block-navigation/index.js b/packages/editor/src/components/block-navigation/index.js index 117640b5677fe2..98a55c38af7d7e 100644 --- a/packages/editor/src/components/block-navigation/index.js +++ b/packages/editor/src/components/block-navigation/index.js @@ -67,6 +67,10 @@ function BlockNavigationList( { } function BlockNavigation( { rootBlock, rootBlocks, selectedBlockClientId, selectBlock } ) { + if ( ! rootBlocks || rootBlocks.length === 0 ) { + return null; + } + const hasHierarchy = ( rootBlock && ( rootBlock.clientId !== selectedBlockClientId || @@ -95,13 +99,6 @@ function BlockNavigation( { rootBlock, rootBlocks, selectedBlockClientId, select selectBlock={ selectBlock } /> ) } - { ( ! rootBlocks || rootBlocks.length === 0 ) && ( - // If there are no blocks in this document, don't render a list of blocks. - // Instead: inform the user no blocks exist yet. - <p className="editor-block-navigation__paragraph"> - { __( 'No blocks created yet.' ) } - </p> - ) } </NavigableMenu> ); } diff --git a/packages/editor/src/components/table-of-contents/index.js b/packages/editor/src/components/table-of-contents/index.js index 59e2a2c736679e..473cc5cd5a1aa6 100644 --- a/packages/editor/src/components/table-of-contents/index.js +++ b/packages/editor/src/components/table-of-contents/index.js @@ -18,12 +18,12 @@ function TableOfContents( { hasBlocks } ) { contentClassName="table-of-contents__popover" renderToggle={ ( { isOpen, onToggle } ) => ( <IconButton - onClick={ onToggle } + onClick={ hasBlocks ? onToggle : undefined } icon="info-outline" aria-expanded={ isOpen } label={ __( 'Content structure' ) } labelPosition="bottom" - disabled={ ! hasBlocks } + aria-disabled={ ! hasBlocks } /> ) } renderContent={ () => <TableOfContentsPanel /> } diff --git a/test/e2e/specs/block-hierarchy-navigation.test.js b/test/e2e/specs/block-hierarchy-navigation.test.js index 970a6fb140ef52..2ce629b1f12e4b 100644 --- a/test/e2e/specs/block-hierarchy-navigation.test.js +++ b/test/e2e/specs/block-hierarchy-navigation.test.js @@ -102,15 +102,4 @@ describe( 'Navigating the block hierarchy', () => { expect( await getEditedPostContent() ).toMatchSnapshot(); } ); - - it( 'should report "No blocks created yet." when post is empty', async () => { - await openBlockNavigator(); - - const blockNavigationText = await page.$eval( - '.editor-block-navigation__paragraph', - ( navigationParagraph ) => navigationParagraph.textContent - ); - - expect( blockNavigationText ).toEqual( 'No blocks created yet.' ); - } ); } ); From b37b4ba737bd4e6904cc0d389b7675ddf8a8ecdc Mon Sep 17 00:00:00 2001 From: "Eduardo I. Iriarte Gahete" <eiriarte@gmail.com> Date: Tue, 27 Nov 2018 09:47:42 +0100 Subject: [PATCH 136/254] Latest Posts: typo in wp_get_recent_posts argument name (#12246) Wrong argument name broke the "filter by category" functionality in the "Latest Posts" block. --- packages/block-library/src/latest-posts/index.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-library/src/latest-posts/index.php b/packages/block-library/src/latest-posts/index.php index 621e912013b000..18a43e51828e56 100644 --- a/packages/block-library/src/latest-posts/index.php +++ b/packages/block-library/src/latest-posts/index.php @@ -21,7 +21,7 @@ function render_block_core_latest_posts( $attributes ) { ); if ( isset( $attributes['categories'] ) ) { - $args['categories'] = $attributes['categories']; + $args['category'] = $attributes['categories']; } $recent_posts = wp_get_recent_posts( $args ); From 5aacd90bc3874d964d19966c8db3bc56f48d9456 Mon Sep 17 00:00:00 2001 From: Joen Asmussen <joen@automattic.com> Date: Tue, 27 Nov 2018 10:17:39 +0100 Subject: [PATCH 137/254] Fix modals in Edge. (#12241) This PR fixes #11585. Edge has a buggy implementation of `position: sticky;`, which includes issues with flickering, z-index, and parent container paddings. See more at https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/17555420/. This is mainly an issue in deeply nested elements, it seems. This PR includes an edge only hack that overrides the sticky and uses `position: fixed;` instead. --- packages/components/src/modal/style.scss | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/components/src/modal/style.scss b/packages/components/src/modal/style.scss index 16b5e165ad1d06..99a76a85598c7c 100644 --- a/packages/components/src/modal/style.scss +++ b/packages/components/src/modal/style.scss @@ -66,13 +66,20 @@ justify-content: space-between; background: $white; align-items: center; - box-sizing: border-box; height: $header-height; position: sticky; top: 0; z-index: z-index(".components-modal__header"); margin: 0 -#{ $grid-size-large } $grid-size-large; + // Rules inside this query are only run by Microsoft Edge. + // Edge has bugs around position: sticky;, so it needs a separate top rule. + // See also https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/17555420/. + @supports (-ms-ime-align:auto) { + position: fixed; + width: 100%; + } + .components-modal__header-heading { font-size: 1em; font-weight: 400; @@ -107,4 +114,10 @@ box-sizing: border-box; height: 100%; padding: 0 $grid-size-large $grid-size-large; + + // Rules inside this query are only run by Microsoft Edge. + // This is a companion top padding to the fixed rule in line 77. + @supports (-ms-ime-align:auto) { + padding-top: $header-height; + } } From 09fe0c8861a79ca305b3a08b810d2aec4da1e72e Mon Sep 17 00:00:00 2001 From: Marcus Kazmierczak <marcus@mkaz.com> Date: Tue, 27 Nov 2018 01:56:12 -0800 Subject: [PATCH 138/254] Docs: Add additional info around dependencies to clarify (#12346) Issue #11069 highlighted that the required dependencies should be called out more in the documentation. This PR adds a line adding additional information on including. --- .../tutorials/block-tutorial/writing-your-first-block-type.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/designers-developers/developers/tutorials/block-tutorial/writing-your-first-block-type.md b/docs/designers-developers/developers/tutorials/block-tutorial/writing-your-first-block-type.md index 91680bbf7d6556..e696b73e4e911d 100644 --- a/docs/designers-developers/developers/tutorials/block-tutorial/writing-your-first-block-type.md +++ b/docs/designers-developers/developers/tutorials/block-tutorial/writing-your-first-block-type.md @@ -30,6 +30,8 @@ Note the two script dependencies: - __`wp-blocks`__ includes block type registration and related functions - __`wp-element`__ includes the [WordPress Element abstraction](https://github.com/WordPress/gutenberg/tree/master/packages/element) for describing the structure of your blocks +If you were to use a component from the `wp-editor` package, for example the RichText component, you would also need to add `wp-editor` to the dependency list. + ## Registering the Block With the script enqueued, let's look at the implementation of the block itself: From 5ac7b89f7d42bacc20f3f83b997451781149a326 Mon Sep 17 00:00:00 2001 From: Jake <jakeparis@users.noreply.github.com> Date: Tue, 27 Nov 2018 04:57:07 -0500 Subject: [PATCH 139/254] Updated contributors.md with wordpress username (#12343) --- CONTRIBUTORS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 2de5dcefec7e0e..498a8ea1c131c7 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -120,4 +120,4 @@ This list is manually curated to include valuable contributions by volunteers th | @getsource | @mikeschroder | | @greatislander | @greatislander | | @sharazghouri | @sharaz | -| @jakeparis | | +| @jakeparis | @jakeparis | From 640b2ff7b40f2a7b0fda643456dc45df656dfa6d Mon Sep 17 00:00:00 2001 From: Andrew Duthie <andrew@andrewduthie.com> Date: Tue, 27 Nov 2018 08:29:14 -0500 Subject: [PATCH 140/254] Editor: Apply IE11 input fix only when mounting TinyMCE (#12335) --- .../editor/src/components/rich-text/tinymce.js | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/packages/editor/src/components/rich-text/tinymce.js b/packages/editor/src/components/rich-text/tinymce.js index dc6f7f46b38314..833a740c690ece 100644 --- a/packages/editor/src/components/rich-text/tinymce.js +++ b/packages/editor/src/components/rich-text/tinymce.js @@ -259,17 +259,14 @@ export default class TinyMCE extends Component { this.props.setRef( editorNode ); } - /** - * A ref function can be used for cleanup because React calls it with - * `null` when unmounting. - */ - if ( this.removeInternetExplorerInputFix ) { - this.removeInternetExplorerInputFix(); - this.removeInternetExplorerInputFix = null; - } - if ( IS_IE ) { - this.removeInternetExplorerInputFix = applyInternetExplorerInputFix( editorNode ); + if ( editorNode ) { + // Mounting: + this.removeInternetExplorerInputFix = applyInternetExplorerInputFix( editorNode ); + } else { + // Unmounting: + this.removeInternetExplorerInputFix(); + } } } From ef596ca8588cfa37415ba5ffb72358bcebe50bbb Mon Sep 17 00:00:00 2001 From: Andrew Duthie <andrew@andrewduthie.com> Date: Tue, 27 Nov 2018 12:44:04 -0500 Subject: [PATCH 141/254] Editor: Merge meta attributes in getEditedPostAttribute (#12331) * Editor: Merge meta attributes in getEditedPostAttribute * Editor: Reset getEditedPostAttribute cache by assignment * Editor: Add test for changing getEditedPostAttribute currentPost value * Editor: Remove getEditedPostAttribute merge property cache * Editor: Ensure edits precedence in getEditedPostAttribute * Editor: Add CHANGELOG entry for getEditedPostAttribute merge --- packages/editor/CHANGELOG.md | 6 +++++ packages/editor/src/store/constants.js | 9 +++++++ packages/editor/src/store/reducer.js | 11 +-------- packages/editor/src/store/selectors.js | 16 ++++++++++++- packages/editor/src/store/test/selectors.js | 26 +++++++++++++++++++++ 5 files changed, 57 insertions(+), 11 deletions(-) create mode 100644 packages/editor/src/store/constants.js diff --git a/packages/editor/CHANGELOG.md b/packages/editor/CHANGELOG.md index 0522ffc4bc02ae..d5b4c4b4bae694 100644 --- a/packages/editor/CHANGELOG.md +++ b/packages/editor/CHANGELOG.md @@ -1,3 +1,9 @@ +## 9.0.3 (Unreleased) + +### Bug Fixes + +- `getEditedPostAttribute` now correctly returns the merged result of edits as a partial change when given `'meta'` as the `attributeName`. + ## 9.0.2 (2018-11-22) ## 9.0.1 (2018-11-21) diff --git a/packages/editor/src/store/constants.js b/packages/editor/src/store/constants.js new file mode 100644 index 00000000000000..f07ca417f9d6eb --- /dev/null +++ b/packages/editor/src/store/constants.js @@ -0,0 +1,9 @@ +/** + * Set of post properties for which edits should assume a merging behavior, + * assuming an object value. + * + * @type {Set} + */ +export const EDIT_MERGE_PROPERTIES = new Set( [ + 'meta', +] ); diff --git a/packages/editor/src/store/reducer.js b/packages/editor/src/store/reducer.js index d241bb008f45a1..e013cc5b7a0ed3 100644 --- a/packages/editor/src/store/reducer.js +++ b/packages/editor/src/store/reducer.js @@ -35,16 +35,7 @@ import { INITIAL_EDITS_DEFAULTS, } from './defaults'; import { insertAt, moveTo } from './array'; - -/** - * Set of post properties for which edits should assume a merging behavior, - * assuming an object value. - * - * @type {Set} - */ -const EDIT_MERGE_PROPERTIES = new Set( [ - 'meta', -] ); +import { EDIT_MERGE_PROPERTIES } from './constants'; /** * Returns a post attribute value, flattening nested rendered content using its diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index ddbc7770a3c8e1..099abab1b49d86 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -36,9 +36,10 @@ import { removep } from '@wordpress/autop'; import { addQueryArgs } from '@wordpress/url'; /** - * Dependencies + * Internal dependencies */ import { PREFERENCES_DEFAULTS } from './defaults'; +import { EDIT_MERGE_PROPERTIES } from './constants'; /*** * Module constants @@ -297,10 +298,23 @@ export function getEditedPostAttribute( state, attributeName ) { return getEditedPostContent( state ); } + // Fall back to saved post value if not edited. if ( ! edits.hasOwnProperty( attributeName ) ) { return getCurrentPostAttribute( state, attributeName ); } + // Merge properties are objects which contain only the patch edit in state, + // and thus must be merged with the current post attribute. + if ( EDIT_MERGE_PROPERTIES.has( attributeName ) ) { + // [TODO]: Since this will return a new reference on each invocation, + // consider caching in a way which would not impact non-merged property + // derivation. Alternatively, introduce a new selector for meta lookup. + return { + ...getCurrentPostAttribute( state, attributeName ), + ...edits[ attributeName ], + }; + } + return edits[ attributeName ]; } diff --git a/packages/editor/src/store/test/selectors.js b/packages/editor/src/store/test/selectors.js index 41858f28d120ea..6303dde765a920 100644 --- a/packages/editor/src/store/test/selectors.js +++ b/packages/editor/src/store/test/selectors.js @@ -602,6 +602,32 @@ describe( 'selectors', () => { expect( getEditedPostAttribute( state, 'valueOf' ) ).toBeUndefined(); } ); + + it( 'should merge mergeable properties with current post value', () => { + const state = { + currentPost: { + meta: { + a: 1, + b: 1, + }, + }, + editor: { + present: { + edits: { + meta: { + b: 2, + }, + }, + }, + }, + initialEdits: {}, + }; + + expect( getEditedPostAttribute( state, 'meta' ) ).toEqual( { + a: 1, + b: 2, + } ); + } ); } ); describe( 'getAutosaveAttribute', () => { From 9cc1e49477c8b8a8fd67070bb9d1e55650385e40 Mon Sep 17 00:00:00 2001 From: Jorge <jorge.costa@developer.pt> Date: Tue, 27 Nov 2018 19:22:05 +0000 Subject: [PATCH 142/254] Add end 2 end test to block switcher with removed blocks (#12323) This commit adds tests that make sure the block switcher works correctly even when some blocks are removed. This test changes should catch the bug fixed in #12192. --- test/e2e/specs/block-switcher.test.js | 78 +++++++++++++++++++ .../utils/get-available-block-transforms.js | 28 +++++++ test/e2e/support/utils/has-block-switcher.js | 10 +++ test/e2e/support/utils/index.js | 2 + 4 files changed, 118 insertions(+) create mode 100644 test/e2e/specs/block-switcher.test.js create mode 100644 test/e2e/support/utils/get-available-block-transforms.js create mode 100644 test/e2e/support/utils/has-block-switcher.js diff --git a/test/e2e/specs/block-switcher.test.js b/test/e2e/specs/block-switcher.test.js new file mode 100644 index 00000000000000..953117171894ad --- /dev/null +++ b/test/e2e/specs/block-switcher.test.js @@ -0,0 +1,78 @@ +/** + * Internal dependencies + */ +import { + hasBlockSwitcher, + getAvailableBlockTransforms, + newPost, + insertBlock, + pressWithModifier, +} from '../support/utils'; + +describe( 'adding blocks', () => { + beforeEach( async () => { + await newPost(); + } ); + + it( 'Should show the expected block transforms on the list block when the blocks are removed', async () => { + // Insert a list block. + await insertBlock( 'List' ); + await page.keyboard.type( 'List content' ); + await pressWithModifier( 'alt', 'F10' ); + + // Verify the block switcher exists. + expect( await hasBlockSwitcher() ).toBeTruthy(); + + // Verify the correct block transforms appear. + expect( + await getAvailableBlockTransforms() + ).toEqual( [ + 'Paragraph', + 'Quote', + ] ); + } ); + + it( 'Should show the expected block transforms on the list block when the quote block is removed', async () => { + // Remove the quote block from the list of registered blocks. + await page.evaluate( () => { + wp.blocks.unregisterBlockType( 'core/quote' ); + } ); + + // Insert a list block. + await insertBlock( 'List' ); + await page.keyboard.type( 'List content' ); + await pressWithModifier( 'alt', 'F10' ); + + // Verify the block switcher exists. + expect( await hasBlockSwitcher() ).toBeTruthy(); + + // Verify the correct block transforms appear. + expect( + await getAvailableBlockTransforms() + ).toEqual( [ + 'Paragraph', + ] ); + } ); + + it( 'Should not show the block switcher if all the blocks the list block transforms into are removed', async () => { + // Remove the paragraph and quote block from the list of registered blocks. + await page.evaluate( () => { + ( [ + 'core/quote', + 'core/paragraph', + ] ).map( ( block ) => wp.blocks.unregisterBlockType( block ) ); + } ); + + // Insert a list block. + await insertBlock( 'List' ); + await page.keyboard.type( 'List content' ); + await pressWithModifier( 'alt', 'F10' ); + + // Verify the block switcher exists. + expect( await hasBlockSwitcher() ).toBeFalsy(); + // Verify the correct block transforms appear. + expect( + await getAvailableBlockTransforms() + ).toHaveLength( 0 ); + } ); +} ); diff --git a/test/e2e/support/utils/get-available-block-transforms.js b/test/e2e/support/utils/get-available-block-transforms.js new file mode 100644 index 00000000000000..18810f41831213 --- /dev/null +++ b/test/e2e/support/utils/get-available-block-transforms.js @@ -0,0 +1,28 @@ +/** + * Internal dependencies + */ +import { hasBlockSwitcher } from './has-block-switcher'; + +/** + * Returns an array of strings with all block titles, + * that the current selected block can be transformed into. + * + * @return {Promise} Promise resolving with an array containing all possible block transforms + */ +export const getAvailableBlockTransforms = async () => { + if ( ! await hasBlockSwitcher() ) { + return []; + } + await page.click( '.editor-block-toolbar .editor-block-switcher' ); + return page.evaluate( ( buttonSelector ) => { + return Array.from( + document.querySelectorAll( + buttonSelector + ) + ).map( + ( button ) => { + return button.getAttribute( 'aria-label' ); + } + ); + }, '.editor-block-types-list .editor-block-types-list__list-item button' ); +}; diff --git a/test/e2e/support/utils/has-block-switcher.js b/test/e2e/support/utils/has-block-switcher.js new file mode 100644 index 00000000000000..8fa0acd241152a --- /dev/null +++ b/test/e2e/support/utils/has-block-switcher.js @@ -0,0 +1,10 @@ +/** + * Returns a boolean indicating if the current selected block has a block switcher or not. + * + * @return {Promise} Promise resolving with a boolean. + */ +export const hasBlockSwitcher = async () => { + return page.evaluate( ( blockSwitcherSelector ) => { + return !! document.querySelector( blockSwitcherSelector ); + }, '.editor-block-toolbar .editor-block-switcher' ); +}; diff --git a/test/e2e/support/utils/index.js b/test/e2e/support/utils/index.js index f374a226ecdc52..70d77e47b104ae 100644 --- a/test/e2e/support/utils/index.js +++ b/test/e2e/support/utils/index.js @@ -11,8 +11,10 @@ export { enablePrePublishChecks } from './enable-pre-publish-checks'; export { ensureSidebarOpened } from './ensure-sidebar-opened'; export { findSidebarPanelWithTitle } from './find-sidebar-panel-with-title'; export { getAllBlocks } from './get-all-blocks'; +export { getAvailableBlockTransforms } from './get-available-block-transforms'; export { getEditedPostContent } from './get-edited-post-content'; export { getUrl } from './get-url'; +export { hasBlockSwitcher } from './has-block-switcher'; export { insertBlock } from './insert-block'; export { isEmbedding } from './is-embedding'; export { JSONResponse } from './json-response'; From f97617fda8bc4572033e206acde41a5e0dddfe4c Mon Sep 17 00:00:00 2001 From: Jorge <jorge.costa@developer.pt> Date: Tue, 27 Nov 2018 21:33:19 +0000 Subject: [PATCH 143/254] Add end 2 end test: should not revert title during a preview right after a save draft (#7953) Adds an end 2 end test that verifies we don't regress on issue #7927. The test does the following: Adds a post. Writes aaaa in title and clicks save draft. Presses preview. See the title is correctly previewed as aaaa. Goes back to the editor ands appends bbbb to the title, presses save draft. Presses preview right after the post is saved. Verify that on the preview of the post we see 'aaaabbbb' as the title of the post. --- test/e2e/specs/preview.test.js | 38 ++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/test/e2e/specs/preview.test.js b/test/e2e/specs/preview.test.js index 225db55a901317..7cd1f57daf73f9 100644 --- a/test/e2e/specs/preview.test.js +++ b/test/e2e/specs/preview.test.js @@ -11,6 +11,7 @@ import { newPost, getUrl, publishPost, + saveDraft, } from '../support/utils'; describe( 'Preview', () => { @@ -127,4 +128,41 @@ describe( 'Preview', () => { await previewPage.close(); } ); + + it( 'Should not revert title during a preview right after a save draft', async () => { + const editorPage = page; + + // Type aaaaa in the title filed. + await editorPage.type( '.editor-post-title__input', 'aaaaa' ); + await editorPage.keyboard.press( 'Tab' ); + + // Save the post as a draft. + await editorPage.waitForSelector( '.editor-post-save-draft' ); + await saveDraft(); + + // Open the preview page. + const previewPage = await openPreviewPage( editorPage ); + + // Title in preview should match input. + let previewTitle = await previewPage.$eval( '.entry-title', ( node ) => node.textContent ); + expect( previewTitle ).toBe( 'aaaaa' ); + + // Return to editor. + await editorPage.bringToFront(); + + // Append bbbbb to the title, and tab away from the title so blur event is triggered. + await editorPage.type( '.editor-post-title__input', 'bbbbb' ); + await editorPage.keyboard.press( 'Tab' ); + + // Save draft and open the preview page right after. + await editorPage.waitForSelector( '.editor-post-save-draft' ); + await saveDraft(); + await waitForPreviewNavigation( previewPage ); + + // Title in preview should match updated input. + previewTitle = await previewPage.$eval( '.entry-title', ( node ) => node.textContent ); + expect( previewTitle ).toBe( 'aaaaabbbbb' ); + + await previewPage.close(); + } ); } ); From 6e3bb74f44f7d2f96d2665bc7c1b51ca4630aab4 Mon Sep 17 00:00:00 2001 From: Robert Anderson <robert@noisysocks.com> Date: Wed, 28 Nov 2018 18:52:07 +1100 Subject: [PATCH 144/254] Reusable blocks: Don't show trashed blocks in the editor or frontend (#12345) Don't render a reusable block in the editor or frontend if it is non-published or password protected. --- packages/block-library/src/block/index.php | 4 ++ .../src/store/effects/reusable-blocks.js | 51 ++++++++++--------- .../src/store/effects/test/reusable-blocks.js | 40 +++++++++++++++ 3 files changed, 72 insertions(+), 23 deletions(-) diff --git a/packages/block-library/src/block/index.php b/packages/block-library/src/block/index.php index 77a281802c50a6..aa235f170333ee 100644 --- a/packages/block-library/src/block/index.php +++ b/packages/block-library/src/block/index.php @@ -22,6 +22,10 @@ function render_block_core_block( $attributes ) { return ''; } + if ( 'publish' !== $reusable_block->post_status || ! empty( $reusable_block->post_password ) ) { + return ''; + } + return do_blocks( $reusable_block->post_content ); } diff --git a/packages/editor/src/store/effects/reusable-blocks.js b/packages/editor/src/store/effects/reusable-blocks.js index 07ed88ab69d520..acfa3d0aaaefb3 100644 --- a/packages/editor/src/store/effects/reusable-blocks.js +++ b/packages/editor/src/store/effects/reusable-blocks.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { castArray, map, uniqueId } from 'lodash'; +import { compact, map, uniqueId } from 'lodash'; import { BEGIN, COMMIT, REVERT } from 'redux-optimist'; /** @@ -61,30 +61,35 @@ export const fetchReusableBlocks = async ( action, store ) => { return; } - let result; - if ( id ) { - result = apiFetch( { path: `/wp/v2/${ postType.rest_base }/${ id }` } ); - } else { - result = apiFetch( { path: `/wp/v2/${ postType.rest_base }?per_page=-1` } ); - } - try { - const reusableBlockOrBlocks = await result; - dispatch( receiveReusableBlocksAction( map( - castArray( reusableBlockOrBlocks ), - ( post ) => { - const parsedBlocks = parse( post.content.raw ); - return { - reusableBlock: { - id: post.id, - title: getPostRawValue( post.title ), - }, - parsedBlock: parsedBlocks.length === 1 ? - parsedBlocks[ 0 ] : - createBlock( 'core/template', {}, parsedBlocks ), - }; + let posts; + + if ( id ) { + posts = [ await apiFetch( { path: `/wp/v2/${ postType.rest_base }/${ id }` } ) ]; + } else { + posts = await apiFetch( { path: `/wp/v2/${ postType.rest_base }?per_page=-1` } ); + } + + const results = compact( map( posts, ( post ) => { + if ( post.status !== 'publish' || post.content.protected ) { + return null; } - ) ) ); + + const parsedBlocks = parse( post.content.raw ); + return { + reusableBlock: { + id: post.id, + title: getPostRawValue( post.title ), + }, + parsedBlock: parsedBlocks.length === 1 ? + parsedBlocks[ 0 ] : + createBlock( 'core/template', {}, parsedBlocks ), + }; + } ) ); + + if ( results.length ) { + dispatch( receiveReusableBlocksAction( results ) ); + } dispatch( { type: 'FETCH_REUSABLE_BLOCKS_SUCCESS', diff --git a/packages/editor/src/store/effects/test/reusable-blocks.js b/packages/editor/src/store/effects/test/reusable-blocks.js index 1e13dba9f97142..071e4f07f4f6f4 100644 --- a/packages/editor/src/store/effects/test/reusable-blocks.js +++ b/packages/editor/src/store/effects/test/reusable-blocks.js @@ -70,11 +70,13 @@ describe( 'reusable blocks effects', () => { const blockPromise = Promise.resolve( [ { id: 123, + status: 'publish', title: { raw: 'My cool block', }, content: { raw: '<!-- wp:test-block {"name":"Big Bird"} /-->', + protected: false, }, }, ] ); @@ -118,11 +120,13 @@ describe( 'reusable blocks effects', () => { it( 'should fetch a single reusable block', async () => { const blockPromise = Promise.resolve( { id: 123, + status: 'publish', title: { raw: 'My cool block', }, content: { raw: '<!-- wp:test-block {"name":"Big Bird"} /-->', + protected: false, }, } ); const postTypePromise = Promise.resolve( { @@ -162,6 +166,42 @@ describe( 'reusable blocks effects', () => { } ); } ); + it( 'should ignore reusable blocks with a trashed post status', async () => { + const blockPromise = Promise.resolve( { + id: 123, + status: 'trash', + title: { + raw: 'My cool block', + }, + content: { + raw: '<!-- wp:test-block {"name":"Big Bird"} /-->', + protected: false, + }, + } ); + const postTypePromise = Promise.resolve( { + slug: 'wp_block', rest_base: 'blocks', + } ); + + apiFetch.mockImplementation( ( options ) => { + if ( options.path === '/wp/v2/types/wp_block' ) { + return postTypePromise; + } + + return blockPromise; + } ); + + const dispatch = jest.fn(); + const store = { getState: noop, dispatch }; + + await fetchReusableBlocks( fetchReusableBlocksAction( 123 ), store ); + + expect( dispatch ).toHaveBeenCalledTimes( 1 ); + expect( dispatch ).toHaveBeenCalledWith( { + type: 'FETCH_REUSABLE_BLOCKS_SUCCESS', + id: 123, + } ); + } ); + it( 'should handle an API error', async () => { const blockPromise = Promise.reject( { code: 'unknown_error', From 4512195fa7c3b4b90e2eac5ce256f7d9dae2405b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20=28Greg=29=20Zi=C3=B3=C5=82kowski?= <grzegorz@gziolo.pl> Date: Wed, 28 Nov 2018 10:42:33 +0100 Subject: [PATCH 145/254] Date: Optimize the usage of moment-timezone to save some kilobytes (#12356) --- lib/packages-dependencies.php | 1 - package-lock.json | 8 -------- packages/block-library/package.json | 3 --- packages/date/src/index.js | 16 +++++++++------- 4 files changed, 9 insertions(+), 19 deletions(-) diff --git a/lib/packages-dependencies.php b/lib/packages-dependencies.php index f4f1695c966a31..34c70851ff727c 100644 --- a/lib/packages-dependencies.php +++ b/lib/packages-dependencies.php @@ -25,7 +25,6 @@ 'wp-block-library' => array( 'editor', 'lodash', - 'moment', 'wp-api-fetch', 'wp-autop', 'wp-blob', diff --git a/package-lock.json b/package-lock.json index c2e08b8f0d9031..ab4562395d1a18 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2360,9 +2360,6 @@ "classnames": "^2.2.5", "lodash": "^4.17.10", "memize": "^1.0.5", - "moment": "^2.22.1", - "querystring": "^0.2.0", - "querystringify": "^1.0.0", "url": "^0.11.0" } }, @@ -17740,11 +17737,6 @@ "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", "dev": true }, - "querystringify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-1.0.0.tgz", - "integrity": "sha1-YoYkIRLFtxL6ZU5SZlK/ahP/Bcs=" - }, "quick-lru": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-1.1.0.tgz", diff --git a/packages/block-library/package.json b/packages/block-library/package.json index 700c2d9801e60e..5f6dbcd09b9321 100644 --- a/packages/block-library/package.json +++ b/packages/block-library/package.json @@ -38,9 +38,6 @@ "classnames": "^2.2.5", "lodash": "^4.17.10", "memize": "^1.0.5", - "moment": "^2.22.1", - "querystring": "^0.2.0", - "querystringify": "^1.0.0", "url": "^0.11.0" }, "devDependencies": { diff --git a/packages/date/src/index.js b/packages/date/src/index.js index 4bd87396a58aad..24eb69434fd2e8 100644 --- a/packages/date/src/index.js +++ b/packages/date/src/index.js @@ -2,9 +2,11 @@ * External dependencies */ import momentLib from 'moment'; -import 'moment-timezone'; +import 'moment-timezone/moment-timezone'; import 'moment-timezone/moment-timezone-utils'; +const WP_ZONE = 'WP'; + // Changes made here will likely need to be made in `lib/client-assets.php` as // well because it uses the `setSettings()` function to change these settings. let settings = { @@ -92,8 +94,8 @@ export function __experimentalGetSettings() { function setupWPTimezone() { // Create WP timezone based off dateSettings. momentLib.tz.add( momentLib.tz.pack( { - name: 'WP', - abbrs: [ 'WP' ], + name: WP_ZONE, + abbrs: [ WP_ZONE ], untils: [ null ], offsets: [ -settings.timezone.offset * 60 || 0 ], } ) ); @@ -371,8 +373,8 @@ export function dateI18n( dateFormat, dateValue = new Date(), gmt = false ) { * @return {boolean} Is in the future. */ export function isInTheFuture( dateValue ) { - const now = momentLib.tz( 'WP' ); - const momentObject = momentLib.tz( dateValue, 'WP' ); + const now = momentLib.tz( WP_ZONE ); + const momentObject = momentLib.tz( dateValue, WP_ZONE ); return momentObject.isAfter( now ); } @@ -386,10 +388,10 @@ export function isInTheFuture( dateValue ) { */ export function getDate( dateString ) { if ( ! dateString ) { - return momentLib.tz( 'WP' ).toDate(); + return momentLib.tz( WP_ZONE ).toDate(); } - return momentLib.tz( dateString, 'WP' ).toDate(); + return momentLib.tz( dateString, WP_ZONE ).toDate(); } setupWPTimezone(); From c014e04500fb9e7c46e1631c01a9e4ceccdfaeca Mon Sep 17 00:00:00 2001 From: Danilo Ercoli <ercoli@gmail.com> Date: Wed, 28 Nov 2018 10:51:45 +0100 Subject: [PATCH 146/254] Wire onBlur on mobile ported blocks and components. (#12370) --- packages/block-library/src/code/edit.native.js | 3 ++- packages/block-library/src/heading/edit.native.js | 1 + packages/block-library/src/more/edit.native.js | 3 ++- packages/block-library/src/paragraph/edit.native.js | 1 + packages/editor/src/components/plain-text/index.native.js | 1 + packages/editor/src/components/rich-text/index.native.js | 1 + 6 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/block-library/src/code/edit.native.js b/packages/block-library/src/code/edit.native.js index 5433481fcfb2fb..d4ee2c8071c691 100644 --- a/packages/block-library/src/code/edit.native.js +++ b/packages/block-library/src/code/edit.native.js @@ -21,7 +21,7 @@ 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( props ) { - const { attributes, setAttributes, style, onFocus } = props; + const { attributes, setAttributes, style, onFocus, onBlur } = props; return ( <View> @@ -35,6 +35,7 @@ export default function CodeEdit( props ) { aria-label={ __( 'Code' ) } isSelected={ props.isSelected } onFocus={ onFocus } + onBlur={ onBlur } /> </View> ); diff --git a/packages/block-library/src/heading/edit.native.js b/packages/block-library/src/heading/edit.native.js index afb2fca0eaa281..e613b9156b824a 100644 --- a/packages/block-library/src/heading/edit.native.js +++ b/packages/block-library/src/heading/edit.native.js @@ -57,6 +57,7 @@ class HeadingEdit extends Component { value={ content } isSelected={ this.props.isSelected } onFocus={ this.props.onFocus } // always assign onFocus as a props + onBlur={ this.props.onBlur } // always assign onBlur as a props style={ { minHeight: Math.max( minHeight, this.state.aztecHeight ), } } diff --git a/packages/block-library/src/more/edit.native.js b/packages/block-library/src/more/edit.native.js index afbfc5973b9036..5b85da44359d50 100644 --- a/packages/block-library/src/more/edit.native.js +++ b/packages/block-library/src/more/edit.native.js @@ -15,7 +15,7 @@ import { PlainText } from '@wordpress/editor'; import styles from './editor.scss'; export default function MoreEdit( props ) { - const { attributes, setAttributes, onFocus } = props; + const { attributes, setAttributes, onFocus, onBlur } = props; const { customText } = attributes; const defaultText = __( 'Read more' ); const value = customText !== undefined ? customText : defaultText; @@ -33,6 +33,7 @@ export default function MoreEdit( props ) { placeholder={ defaultText } isSelected={ props.isSelected } onFocus={ onFocus } + onBlur={ onBlur } /> <Text className={ styles[ 'block-library-more__right-marker' ] }>--&gt;</Text> </View> diff --git a/packages/block-library/src/paragraph/edit.native.js b/packages/block-library/src/paragraph/edit.native.js index ce3c390715e1c1..ecb075bca8003e 100644 --- a/packages/block-library/src/paragraph/edit.native.js +++ b/packages/block-library/src/paragraph/edit.native.js @@ -93,6 +93,7 @@ class ParagraphEdit extends Component { value={ content } isSelected={ this.props.isSelected } onFocus={ this.props.onFocus } // always assign onFocus as a props + onBlur={ this.props.onBlur } // always assign onBlur as a props style={ { ...style, minHeight: Math.max( minHeight, this.state.aztecHeight ), diff --git a/packages/editor/src/components/plain-text/index.native.js b/packages/editor/src/components/plain-text/index.native.js index e8155a062e25a1..e94f1939040f54 100644 --- a/packages/editor/src/components/plain-text/index.native.js +++ b/packages/editor/src/components/plain-text/index.native.js @@ -32,6 +32,7 @@ export default class PlainText extends Component { className={ [ styles[ 'editor-plain-text' ], this.props.className ] } onChangeText={ ( text ) => this.props.onChange( text ) } onFocus={ this.props.onFocus } // always assign onFocus as a props + onBlur={ this.props.onBlur } // always assign onBlur as a props { ...this.props } /> ); diff --git a/packages/editor/src/components/rich-text/index.native.js b/packages/editor/src/components/rich-text/index.native.js index e2cbecad952682..ba6a4673e101c2 100644 --- a/packages/editor/src/components/rich-text/index.native.js +++ b/packages/editor/src/components/rich-text/index.native.js @@ -357,6 +357,7 @@ export class RichText extends Component { text={ { text: html, eventCount: this.lastEventCount } } onChange={ this.onChange } onFocus={ this.props.onFocus } + onBlur={ this.props.onBlur } onEnter={ this.onEnter } onBackspace={ this.onBackspace } onContentSizeChange={ this.onContentSizeChange } From 3b5ab7bc135f995904c41c7a0f94421c7b3b7e0e Mon Sep 17 00:00:00 2001 From: Tugdual de Kerviler <dekervit@gmail.com> Date: Wed, 28 Nov 2018 14:58:15 +0100 Subject: [PATCH 147/254] Add more selectors of the blocks api for the mobile app (#12383) --- packages/blocks/src/api/index.native.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/blocks/src/api/index.native.js b/packages/blocks/src/api/index.native.js index aa1056f46631e5..1350df80ec2225 100644 --- a/packages/blocks/src/api/index.native.js +++ b/packages/blocks/src/api/index.native.js @@ -14,6 +14,7 @@ export { } from './serializer'; export { registerBlockType, + getFreeformContentHandlerName, setUnregisteredTypeHandlerName, getUnregisteredTypeHandlerName, getBlockType, @@ -23,5 +24,8 @@ export { setDefaultBlockName, getDefaultBlockName, } from './registration'; +export { + isUnmodifiedDefaultBlock, +} from './utils'; export { getPhrasingContentSchema } from './raw-handling'; export { default as children } from './children'; From e4fd51cb535e8300b415973d7022b16c742c0d40 Mon Sep 17 00:00:00 2001 From: Joen Asmussen <joen@automattic.com> Date: Wed, 28 Nov 2018 15:17:27 +0100 Subject: [PATCH 148/254] Polish editor style documentation (#12381) * Polish editor style documentation This includes a section on how to actually enqueue your stylesheet, because this has changed a number of times over the year and hasn't been updated. * Address feedback. --- .../developers/themes/theme-support.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/docs/designers-developers/developers/themes/theme-support.md b/docs/designers-developers/developers/themes/theme-support.md index 9c9a5cc63539c5..abc611dc5d000b 100644 --- a/docs/designers-developers/developers/themes/theme-support.md +++ b/docs/designers-developers/developers/themes/theme-support.md @@ -202,7 +202,9 @@ This flag will make sure users are only able to choose colors from the `editor-c Gutenberg supports the theme's [editor styles](https://codex.wordpress.org/Editor_Style), however it works a little differently than in the classic editor. -In the classic editor, the editor stylesheet is loaded directly into the iframe of the WYSIWYG editor, with no changes. Gutenberg, however, doesn't use iframes. To make sure your styles are applied only to the content of the editor, we automatically transform your editor styles by selectively rewriting or adjusting certain CSS selectors. +In the classic editor, the editor stylesheet is loaded directly into the iframe of the WYSIWYG editor, with no changes. Gutenberg, however, doesn't use iframes. To make sure your styles are applied only to the content of the editor, we automatically transform your editor styles by selectively rewriting or adjusting certain CSS selectors. This also allows Gutenberg to leverage your editor style in block variation previews. + +For example, if you write `body { ... }` in your editor style, this is rewritten to `.editor-styles-wrapper { ... }`. This also means that you should _not_ target any of the editor class names directly. Because it works a little differently, you need to opt-in to this by adding an extra snippet to your theme, in addition to the add_editor_style function: @@ -212,6 +214,8 @@ add_theme_support('editor-styles'); You shouldn't need to change your editor styles too much; most themes can add the snippet above and get similar results in the classic editor and inside Gutenberg. +### Dark backgrounds + If your editor style relies on a dark background, you can add the following to adjust the color of the UI to work on dark backgrounds: ```php @@ -221,6 +225,16 @@ add_theme_support( 'dark-editor-style' ); Note you don't need to add `add_theme_support( 'editor-styles' );` twice, but that rule does need to be present for the `dark-editor-style` rule to work. +### Enqueuing the editor style + +To make sure your editor style is loaded and parsed correctly, enqueue it using the following method: + +```php +add_editor_style( 'style-editor.css' ); +``` + +It is enough to paste that in your `functions.php` file, for the style to be loaded and parsed. + ### Basic colors You can style the editor like any other webpage. Here's how to change the background color and the font color to blue: From b5d4e1527af3f15e350da6be4a57963e51208bec Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Wed, 28 Nov 2018 19:17:39 +0100 Subject: [PATCH 149/254] Try fixing the tab navigation issue (#12390) * Try fixing the tab navigation issue * Fix block selection reducer --- .../editor/src/components/rich-text/index.js | 12 ------------ packages/editor/src/store/reducer.js | 3 +++ packages/editor/src/store/test/reducer.js | 19 +++++++++++++++++++ 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/packages/editor/src/components/rich-text/index.js b/packages/editor/src/components/rich-text/index.js index 2033ce14cb8f7d..688a5287052b68 100644 --- a/packages/editor/src/components/rich-text/index.js +++ b/packages/editor/src/components/rich-text/index.js @@ -682,18 +682,6 @@ export class RichText extends Component { this.savedContent = value; } - // If blocks are merged, but the content remains the same, e.g. merging - // an empty paragraph into another, then also set the selection to the - // end. - if ( isSelected && ! prevProps.isSelected && ! this.isActive() ) { - const record = this.formatToValue( value ); - const prevRecord = this.formatToValue( prevProps.value ); - const length = getTextContent( prevRecord ).length; - record.start = length; - record.end = length; - this.applyRecord( record ); - } - // If any format props update, reapply value. const shouldReapply = Object.keys( this.props ).some( ( name ) => { if ( name.indexOf( 'format_' ) !== 0 ) { diff --git a/packages/editor/src/store/reducer.js b/packages/editor/src/store/reducer.js index e013cc5b7a0ed3..385e99e8be5366 100644 --- a/packages/editor/src/store/reducer.js +++ b/packages/editor/src/store/reducer.js @@ -795,6 +795,9 @@ export function blockSelection( state = { // If there is replacement block(s), assign first's client ID as // the next selected block. If empty replacement, reset to null. const nextSelectedBlockClientId = get( action.blocks, [ 0, 'clientId' ], null ); + if ( nextSelectedBlockClientId === state.start && nextSelectedBlockClientId === state.end ) { + return state; + } return { ...state, diff --git a/packages/editor/src/store/test/reducer.js b/packages/editor/src/store/test/reducer.js index 2ddb631b5e2139..884314fe553098 100644 --- a/packages/editor/src/store/test/reducer.js +++ b/packages/editor/src/store/test/reducer.js @@ -1761,6 +1761,25 @@ describe( 'state', () => { } ); } ); + it( 'should not replace the selected block if we keep it when replacing blocks', () => { + const original = deepFreeze( { start: 'chicken', end: 'chicken' } ); + const state = blockSelection( original, { + type: 'REPLACE_BLOCKS', + clientIds: [ 'chicken' ], + blocks: [ + { + clientId: 'chicken', + name: 'core/freeform', + }, + { + clientId: 'wings', + name: 'core/freeform', + } ], + } ); + + expect( state ).toBe( original ); + } ); + it( 'should reset if replacing with empty set', () => { const original = deepFreeze( { start: 'chicken', end: 'chicken' } ); const state = blockSelection( original, { From 2a66db0fc99a9fb1ba99f61e59e6a0b2a4a5f9ef Mon Sep 17 00:00:00 2001 From: Brandon Payton <brandon@happycode.net> Date: Thu, 29 Nov 2018 05:48:50 -0700 Subject: [PATCH 150/254] Avoid changing default wpautop priority (#11722) --- lib/blocks.php | 29 +++++++++++++++++++++++++++++ lib/compat.php | 17 ----------------- 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/lib/blocks.php b/lib/blocks.php index a8083466a04053..0b11041d0576de 100644 --- a/lib/blocks.php +++ b/lib/blocks.php @@ -174,6 +174,13 @@ function gutenberg_render_block( $block ) { * @return string Updated post content. */ function do_blocks( $content ) { + // If there are blocks in this content, we shouldn't run wpautop() on it later. + $priority = has_filter( 'the_content', 'wpautop' ); + if ( false !== $priority && doing_filter( 'the_content' ) && has_blocks( $content ) ) { + remove_filter( 'the_content', 'wpautop', $priority ); + add_filter( 'the_content', '_restore_wpautop_hook', $priority + 1 ); + } + $blocks = gutenberg_parse_blocks( $content ); $output = ''; @@ -187,6 +194,28 @@ function do_blocks( $content ) { add_filter( 'the_content', 'do_blocks', 7 ); // BEFORE do_shortcode() and oembed. } +if ( ! function_exists( '_restore_wpautop_hook' ) ) { + /** + * If do_blocks() needs to remove wpautop() from the `the_content` filter, + * this re-adds it afterwards, for subsequent `the_content` usage. + * + * @access private + * + * @since 4.6.0 + * + * @param string $content The post content running through this filter. + * @return string The unmodified content. + */ + function _restore_wpautop_hook( $content ) { + $current_priority = has_filter( 'the_content', '_restore_wpautop_hook' ); + + add_filter( 'the_content', 'wpautop', $current_priority - 1 ); + remove_filter( 'the_content', '_restore_wpautop_hook', $current_priority ); + + return $content; + } +} + if ( ! function_exists( 'strip_dynamic_blocks' ) ) { /** * Remove all dynamic blocks from the given content. diff --git a/lib/compat.php b/lib/compat.php index 88b417491a89e9..03af8d9302af14 100644 --- a/lib/compat.php +++ b/lib/compat.php @@ -99,23 +99,6 @@ function gutenberg_add_rest_nonce_to_heartbeat_response_headers( $response ) { } add_filter( 'wp_refresh_nonces', 'gutenberg_add_rest_nonce_to_heartbeat_response_headers' ); -/** - * As a substitute for the default content `wpautop` filter, applies autop - * behavior only for posts where content does not contain blocks. - * - * @param string $content Post content. - * @return string Paragraph-converted text if non-block content. - */ -function gutenberg_wpautop( $content ) { - if ( has_blocks( $content ) ) { - return $content; - } - - return wpautop( $content ); -} -remove_filter( 'the_content', 'wpautop' ); -add_filter( 'the_content', 'gutenberg_wpautop', 6 ); - /** * Check if we need to load the block warning in the Classic Editor. * From c446d069fdea0e3a848bbfa5b35a33b7ff9a3317 Mon Sep 17 00:00:00 2001 From: Dion Hulse <dion@wordpress.org> Date: Fri, 30 Nov 2018 01:53:28 +1000 Subject: [PATCH 151/254] Correct the docs manifest (#12411) * Fix README filename * Fix filename * Add the Contributors handbook to the TOC * Rebuild the root manifest with the additional Contributors handbook in the TOC * Rebuild the manifest file with the contributors * Rebuild the manifest file without paths in the 'parent' field as the Importer can't handle that. * Correct the slug of the root-level index * DEBUG: This is the tool I used to re-generate the root-manifest file, altered slightly from https://github.com/WordPress/gutenberg/pull/11817/commits/a0a6349feac0be389070e36483da08481b5d17e0 * Remove generate.php tool from PR --- docs/manifest.json | 156 +++++++++++++++++++++++++++++++++------- docs/root-manifest.json | 156 +++++++++++++++++++++++++++++++++------- docs/toc.json | 23 +++++- 3 files changed, 286 insertions(+), 49 deletions(-) diff --git a/docs/manifest.json b/docs/manifest.json index c461c5d3b9a361..a15c102355f36c 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -1,14 +1,14 @@ [ { "title": "Gutenberg Handbook", - "slug": "readme", + "slug": "handbook", "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/readme.md", "parent": null }, { "title": "Designer & Developer Handbook", "slug": "designers-developers", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/README.md", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/readme.md", "parent": null }, { @@ -27,115 +27,115 @@ "title": "Block API Reference", "slug": "block-api", "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/block-api/README.md", - "parent": "designers-developers/developers" + "parent": "developers" }, { "title": "Block Registration ", "slug": "block-registration", "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/block-api/block-registration.md", - "parent": "designers-developers/developers/block-api" + "parent": "block-api" }, { "title": "Edit and Save", "slug": "block-edit-save", "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/block-api/block-edit-save.md", - "parent": "designers-developers/developers/block-api" + "parent": "block-api" }, { "title": "Attributes", "slug": "block-attributes", "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/block-api/block-attributes.md", - "parent": "designers-developers/developers/block-api" + "parent": "block-api" }, { "title": "Deprecated Blocks", "slug": "block-deprecation", "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/block-api/block-deprecation.md", - "parent": "designers-developers/developers/block-api" + "parent": "block-api" }, { "title": "Templates", "slug": "block-templates", "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/block-api/block-templates.md", - "parent": "designers-developers/developers/block-api" + "parent": "block-api" }, { "title": "Annotations", "slug": "block-annotations", "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/block-api/block-annotations.md", - "parent": "designers-developers/developers/block-api" + "parent": "block-api" }, { "title": "Filter Reference", "slug": "filters", "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/filters/README.md", - "parent": "designers-developers/developers" + "parent": "developers" }, { "title": "Block Filters", "slug": "block-filters", "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/filters/block-filters.md", - "parent": "designers-developers/developers/filters" + "parent": "filters" }, { "title": "Editor Filters (Experimental)", "slug": "editor-filters", "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/filters/editor-filters.md", - "parent": "designers-developers/developers/filters" + "parent": "filters" }, { "title": "Parser Filters", "slug": "parser-filters", "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/filters/parser-filters.md", - "parent": "designers-developers/developers/filters" + "parent": "filters" }, { "title": "Autocomplete", "slug": "autocomplete-filters", "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/filters/autocomplete-filters.md", - "parent": "designers-developers/developers/filters" + "parent": "filters" }, { "title": "Data Module Reference", "slug": "data", "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/data/README.md", - "parent": "designers-developers/developers" + "parent": "developers" }, { "title": "Packages", "slug": "packages", "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/packages.md", - "parent": "designers-developers/developers" + "parent": "developers" }, { "title": "Theming for Gutenberg", "slug": "themes", "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/themes/README.md", - "parent": "designers-developers/developers" + "parent": "developers" }, { "title": "Theme Support", "slug": "theme-support", "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/themes/theme-support.md", - "parent": "designers-developers/developers/themes" + "parent": "themes" }, { "title": "Backwards Compatibility", "slug": "backwards-compatibility", "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/backwards-compatibility/README.md", - "parent": "designers-developers/developers" + "parent": "developers" }, { "title": "Deprecations", "slug": "deprecations", "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/backwards-compatibility/deprecations.md", - "parent": "designers-developers/developers/backwards-compatibility" + "parent": "backwards-compatibility" }, { "title": "Meta Boxes", "slug": "meta-box", "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/backwards-compatibility/meta-box.md", - "parent": "designers-developers/developers/backwards-compatibility" + "parent": "backwards-compatibility" }, { "title": "Designer Documentation", @@ -147,19 +147,19 @@ "title": "Block Design", "slug": "block-design", "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/designers/block-design.md", - "parent": "designers-developers/designers" + "parent": "designers" }, { "title": "Patterns", "slug": "design-patterns", "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/designers/design-patterns.md", - "parent": "designers-developers/designers" + "parent": "designers" }, { "title": "Resources", "slug": "design-resources", "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/designers/design-resources.md", - "parent": "designers-developers/designers" + "parent": "designers" }, { "title": "Glossary", @@ -173,6 +173,114 @@ "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/faq.md", "parent": "designers-developers" }, + { + "title": "Contributors", + "slug": "contributors", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/contributors/readme.md", + "parent": null + }, + { + "title": "Coding Guidelines", + "slug": "coding-guidelines", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/contributors/coding-guidelines.md", + "parent": "contributors" + }, + { + "title": "Gutencopy Guidelines", + "slug": "copy-guide", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/contributors/copy-guide.md", + "parent": "contributors" + }, + { + "title": "Gutenberg Design Principles & Vision", + "slug": "design", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/contributors/design.md", + "parent": "contributors" + }, + { + "title": "The Gutenberg block grammar", + "slug": "grammar", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/contributors/grammar.md", + "parent": "contributors" + }, + { + "title": "History", + "slug": "history", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/contributors/history.md", + "parent": "contributors" + }, + { + "title": "Outreach", + "slug": "outreach", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/contributors/outreach.md", + "parent": "contributors" + }, + { + "title": "Articles", + "slug": "articles", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/contributors/outreach/articles.md", + "parent": "outreach" + }, + { + "title": "Meetups", + "slug": "meetups", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/contributors/outreach/meetups.md", + "parent": "outreach" + }, + { + "title": "Resources", + "slug": "resources", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/contributors/outreach/resources.md", + "parent": "outreach" + }, + { + "title": "Talks", + "slug": "talks", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/contributors/outreach/talks.md", + "parent": "outreach" + }, + { + "title": "Principles", + "slug": "principles", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/contributors/principles.md", + "parent": "contributors" + }, + { + "title": "Blocks are the Interface", + "slug": "the-block", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/contributors/principles/the-block.md", + "parent": "principles" + }, + { + "title": "Reference", + "slug": "reference", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/contributors/reference.md", + "parent": "contributors" + }, + { + "title": "Gutenberg Release Process", + "slug": "release", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/contributors/release.md", + "parent": "contributors" + }, + { + "title": "Repository Management", + "slug": "repository-management", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/contributors/repository-management.md", + "parent": "contributors" + }, + { + "title": "Scripts", + "slug": "scripts", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/contributors/scripts.md", + "parent": "contributors" + }, + { + "title": "Testing Overview", + "slug": "testing-overview", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/contributors/testing-overview.md", + "parent": "contributors" + }, { "title": "Packages", "slug": "packages", diff --git a/docs/root-manifest.json b/docs/root-manifest.json index 784008dd17bb50..0d8c79653cf331 100644 --- a/docs/root-manifest.json +++ b/docs/root-manifest.json @@ -1,14 +1,14 @@ [ { "title": "Gutenberg Handbook", - "slug": "readme", + "slug": "handbook", "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/readme.md", "parent": null }, { "title": "Designer & Developer Handbook", "slug": "designers-developers", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/README.md", + "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/readme.md", "parent": null }, { @@ -27,115 +27,115 @@ "title": "Block API Reference", "slug": "block-api", "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/block-api\/README.md", - "parent": "designers-developers\/developers" + "parent": "developers" }, { "title": "Block Registration ", "slug": "block-registration", "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/block-api\/block-registration.md", - "parent": "designers-developers\/developers\/block-api" + "parent": "block-api" }, { "title": "Edit and Save", "slug": "block-edit-save", "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/block-api\/block-edit-save.md", - "parent": "designers-developers\/developers\/block-api" + "parent": "block-api" }, { "title": "Attributes", "slug": "block-attributes", "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/block-api\/block-attributes.md", - "parent": "designers-developers\/developers\/block-api" + "parent": "block-api" }, { "title": "Deprecated Blocks", "slug": "block-deprecation", "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/block-api\/block-deprecation.md", - "parent": "designers-developers\/developers\/block-api" + "parent": "block-api" }, { "title": "Templates", "slug": "block-templates", "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/block-api\/block-templates.md", - "parent": "designers-developers\/developers\/block-api" + "parent": "block-api" }, { "title": "Annotations", "slug": "block-annotations", "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/block-api\/block-annotations.md", - "parent": "designers-developers\/developers\/block-api" + "parent": "block-api" }, { "title": "Filter Reference", "slug": "filters", "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/filters\/README.md", - "parent": "designers-developers\/developers" + "parent": "developers" }, { "title": "Block Filters", "slug": "block-filters", "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/filters\/block-filters.md", - "parent": "designers-developers\/developers\/filters" + "parent": "filters" }, { "title": "Editor Filters (Experimental)", "slug": "editor-filters", "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/filters\/editor-filters.md", - "parent": "designers-developers\/developers\/filters" + "parent": "filters" }, { "title": "Parser Filters", "slug": "parser-filters", "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/filters\/parser-filters.md", - "parent": "designers-developers\/developers\/filters" + "parent": "filters" }, { "title": "Autocomplete", "slug": "autocomplete-filters", "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/filters\/autocomplete-filters.md", - "parent": "designers-developers\/developers\/filters" + "parent": "filters" }, { "title": "Data Module Reference", "slug": "data", "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/data\/README.md", - "parent": "designers-developers\/developers" + "parent": "developers" }, { "title": "Packages", "slug": "packages", "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/packages.md", - "parent": "designers-developers\/developers" + "parent": "developers" }, { "title": "Theming for Gutenberg", "slug": "themes", "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/themes\/README.md", - "parent": "designers-developers\/developers" + "parent": "developers" }, { "title": "Theme Support", "slug": "theme-support", "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/themes\/theme-support.md", - "parent": "designers-developers\/developers\/themes" + "parent": "themes" }, { "title": "Backwards Compatibility", "slug": "backwards-compatibility", "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/backwards-compatibility\/README.md", - "parent": "designers-developers\/developers" + "parent": "developers" }, { "title": "Deprecations", "slug": "deprecations", "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/backwards-compatibility\/deprecations.md", - "parent": "designers-developers\/developers\/backwards-compatibility" + "parent": "backwards-compatibility" }, { "title": "Meta Boxes", "slug": "meta-box", "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/backwards-compatibility\/meta-box.md", - "parent": "designers-developers\/developers\/backwards-compatibility" + "parent": "backwards-compatibility" }, { "title": "Designer Documentation", @@ -147,19 +147,19 @@ "title": "Block Design", "slug": "block-design", "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/designers\/block-design.md", - "parent": "designers-developers\/designers" + "parent": "designers" }, { "title": "Patterns", "slug": "design-patterns", "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/designers\/design-patterns.md", - "parent": "designers-developers\/designers" + "parent": "designers" }, { "title": "Resources", "slug": "design-resources", "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/designers\/design-resources.md", - "parent": "designers-developers\/designers" + "parent": "designers" }, { "title": "Glossary", @@ -172,5 +172,113 @@ "slug": "faq", "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/faq.md", "parent": "designers-developers" + }, + { + "title": "Contributors", + "slug": "contributors", + "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/contributors\/readme.md", + "parent": null + }, + { + "title": "Coding Guidelines", + "slug": "coding-guidelines", + "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/contributors\/coding-guidelines.md", + "parent": "contributors" + }, + { + "title": "Gutencopy Guidelines", + "slug": "copy-guide", + "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/contributors\/copy-guide.md", + "parent": "contributors" + }, + { + "title": "Gutenberg Design Principles & Vision", + "slug": "design", + "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/contributors\/design.md", + "parent": "contributors" + }, + { + "title": "The Gutenberg block grammar", + "slug": "grammar", + "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/contributors\/grammar.md", + "parent": "contributors" + }, + { + "title": "History", + "slug": "history", + "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/contributors\/history.md", + "parent": "contributors" + }, + { + "title": "Outreach", + "slug": "outreach", + "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/contributors\/outreach.md", + "parent": "contributors" + }, + { + "title": "Articles", + "slug": "articles", + "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/contributors\/outreach\/articles.md", + "parent": "outreach" + }, + { + "title": "Meetups", + "slug": "meetups", + "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/contributors\/outreach\/meetups.md", + "parent": "outreach" + }, + { + "title": "Resources", + "slug": "resources", + "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/contributors\/outreach\/resources.md", + "parent": "outreach" + }, + { + "title": "Talks", + "slug": "talks", + "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/contributors\/outreach\/talks.md", + "parent": "outreach" + }, + { + "title": "Principles", + "slug": "principles", + "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/contributors\/principles.md", + "parent": "contributors" + }, + { + "title": "Blocks are the Interface", + "slug": "the-block", + "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/contributors\/principles\/the-block.md", + "parent": "principles" + }, + { + "title": "Reference", + "slug": "reference", + "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/contributors\/reference.md", + "parent": "contributors" + }, + { + "title": "Gutenberg Release Process", + "slug": "release", + "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/contributors\/release.md", + "parent": "contributors" + }, + { + "title": "Repository Management", + "slug": "repository-management", + "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/contributors\/repository-management.md", + "parent": "contributors" + }, + { + "title": "Scripts", + "slug": "scripts", + "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/contributors\/scripts.md", + "parent": "contributors" + }, + { + "title": "Testing Overview", + "slug": "testing-overview", + "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/contributors\/testing-overview.md", + "parent": "contributors" } ] \ No newline at end of file diff --git a/docs/toc.json b/docs/toc.json index d519837b89a8a8..b9f723a91ab54d 100644 --- a/docs/toc.json +++ b/docs/toc.json @@ -1,6 +1,6 @@ [ {"docs/readme.md": []}, - {"docs/designers-developers/README.md": [ + {"docs/designers-developers/readme.md": [ {"docs/designers-developers/key-concepts.md": []}, {"docs/designers-developers/developers/README.md": [ {"docs/designers-developers/developers/block-api/README.md": [ @@ -34,5 +34,26 @@ ]}, {"docs/designers-developers/glossary.md": []}, {"docs/designers-developers/faq.md": []} + ]}, + {"docs/contributors/readme.md": [ + {"docs/contributors/coding-guidelines.md": []}, + {"docs/contributors/copy-guide.md": []}, + {"docs/contributors/design.md": []}, + {"docs/contributors/grammar.md": []}, + {"docs/contributors/history.md": []}, + {"docs/contributors/outreach.md": [ + {"docs/contributors/outreach/articles.md": []}, + {"docs/contributors/outreach/meetups.md": []}, + {"docs/contributors/outreach/resources.md": []}, + {"docs/contributors/outreach/talks.md": []} + ]}, + {"docs/contributors/principles.md": [ + {"docs/contributors/principles/the-block.md": []} + ]}, + {"docs/contributors/reference.md": []}, + {"docs/contributors/release.md": []}, + {"docs/contributors/repository-management.md": []}, + {"docs/contributors/scripts.md": []}, + {"docs/contributors/testing-overview.md": []} ]} ] From 512318acd3ab6ce34c0e1a6cd38fb4a991fbf44a Mon Sep 17 00:00:00 2001 From: Andrew Duthie <andrew@andrewduthie.com> Date: Thu, 29 Nov 2018 16:32:37 -0500 Subject: [PATCH 152/254] Edit Post: Avoid rendering AdminNotices compatibility component (#12444) --- packages/edit-post/src/components/layout/index.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/edit-post/src/components/layout/index.js b/packages/edit-post/src/components/layout/index.js index 5a4018bc9dd1b6..1c67baed29d22c 100644 --- a/packages/edit-post/src/components/layout/index.js +++ b/packages/edit-post/src/components/layout/index.js @@ -37,7 +37,6 @@ import Sidebar from '../sidebar'; import PluginPostPublishPanel from '../sidebar/plugin-post-publish-panel'; import PluginPrePublishPanel from '../sidebar/plugin-pre-publish-panel'; import FullscreenMode from '../fullscreen-mode'; -import AdminNotices from '../admin-notices'; function Layout( { mode, @@ -71,7 +70,6 @@ function Layout( { <BrowserURL /> <UnsavedChangesWarning /> <AutosaveMonitor /> - <AdminNotices /> <Header /> <div className="edit-post-layout__content" From e080beea2ddbc779b43f32ab1779efe2e4b05638 Mon Sep 17 00:00:00 2001 From: Timothy Jacobs <timothy@ironbounddesigns.com> Date: Fri, 30 Nov 2018 02:56:35 -0500 Subject: [PATCH 153/254] Notices: Remove "files" block in package.json (#12438) --- packages/notices/package.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/notices/package.json b/packages/notices/package.json index 03ca94a9c9bb6c..f7ae5027587cdb 100644 --- a/packages/notices/package.json +++ b/packages/notices/package.json @@ -16,10 +16,6 @@ "bugs": { "url": "https://github.com/WordPress/gutenberg/issues" }, - "files": [ - "build", - "build-module" - ], "main": "build/index.js", "react-native": "src/index", "dependencies": { From f6bc5cbed250542c45a64e95052ea04fba61ce7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20Van=C2=A0Dorpe?= <iseulde@automattic.com> Date: Fri, 30 Nov 2018 09:11:26 +0100 Subject: [PATCH 154/254] RichText: convert HTML formatting whitespace to spaces (#12166) * convert HTML formatting whitespace to spaces * Adjust and add e2e tests * Visualise consecutive spaces * Update e2e tests --- .../src/components/rich-text/style.scss | 10 +++++++ packages/rich-text/src/create.js | 7 ++--- .../src/test/__snapshots__/to-dom.js.snap | 19 +++++++----- packages/rich-text/src/test/helpers/index.js | 30 +++++++++++++++---- .../specs/__snapshots__/links.test.js.snap | 2 +- .../__snapshots__/rich-text.test.js.snap | 2 +- .../e2e/specs/__snapshots__/undo.test.js.snap | 4 +-- .../__snapshots__/writing-flow.test.js.snap | 6 ++-- test/e2e/specs/adding-inline-tokens.test.js | 2 +- 9 files changed, 57 insertions(+), 25 deletions(-) diff --git a/packages/editor/src/components/rich-text/style.scss b/packages/editor/src/components/rich-text/style.scss index 731809331440cb..0907de79535d12 100644 --- a/packages/editor/src/components/rich-text/style.scss +++ b/packages/editor/src/components/rich-text/style.scss @@ -8,6 +8,16 @@ margin: 0; position: relative; line-height: $editor-line-height; + // In HTML, leading and trailing spaces are not visible, and multiple spaces + // elsewhere are visually reduced to one space. This rule prevents spaces + // from collapsing so all space is visible in the editor and can be removed. + // It also prevents some browsers from inserting non-breaking spaces at the + // end of a line to prevent the space from visually disappearing. Sometimes + // these non breaking spaces can linger in the editor causing unwanted non + // breaking spaces in between words. If also prevent Firefox from inserting + // a trailing `br` node to visualise any trailing space, causing the element + // to be saved. + white-space: pre-wrap; > p:empty { min-height: $editor-font-size * $editor-line-height; diff --git a/packages/rich-text/src/create.js b/packages/rich-text/src/create.js index fdbbde943aef1a..c45d20ce088be0 100644 --- a/packages/rich-text/src/create.js +++ b/packages/rich-text/src/create.js @@ -297,11 +297,10 @@ function createFromElement( { const length = element.childNodes.length; - // Remove any line breaks in text nodes. They are not content, but used to - // format the HTML. Line breaks in HTML are stored as BR elements. - // See https://www.w3.org/TR/html5/syntax.html#newlines. const filterStringComplete = ( string ) => { - string = string.replace( /[\r\n]/g, '' ); + // Reduce any whitespace used for HTML formatting to one space + // character, because it will also be displayed as such by the browser. + string = string.replace( /[\n\r\t]+/g, ' ' ); if ( filterString ) { string = filterString( string ); 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 ce980dcd7589b7..0153822ba7ca8c 100644 --- a/packages/rich-text/src/test/__snapshots__/to-dom.js.snap +++ b/packages/rich-text/src/test/__snapshots__/to-dom.js.snap @@ -317,13 +317,6 @@ exports[`recordToDom should ignore formats at line separator 1`] = ` </body> `; -exports[`recordToDom should ignore line breaks to format HTML 1`] = ` -<body> - - <br /> -</body> -`; - exports[`recordToDom should preserve emoji 1`] = ` <body> 🍒 @@ -339,6 +332,12 @@ exports[`recordToDom should preserve emoji in formatting 1`] = ` </body> `; +exports[`recordToDom should preserve non breaking space 1`] = ` +<body> + test  test +</body> +`; + exports[`recordToDom should remove br with settings 1`] = ` <body> @@ -359,6 +358,12 @@ exports[`recordToDom should remove with settings 1`] = ` </body> `; +exports[`recordToDom should replace characters to format HTML with space 1`] = ` +<body> + +</body> +`; + exports[`recordToDom should unwrap with settings 1`] = ` <body> te diff --git a/packages/rich-text/src/test/helpers/index.js b/packages/rich-text/src/test/helpers/index.js index 0b4a7b6e64b704..9673619d57be2e 100644 --- a/packages/rich-text/src/test/helpers/index.js +++ b/packages/rich-text/src/test/helpers/index.js @@ -29,8 +29,8 @@ export const spec = [ }, }, { - description: 'should ignore line breaks to format HTML', - html: '\n\n\r\n', + description: 'should replace characters to format HTML with space', + html: '\n\n\r\n\t', createRange: ( element ) => ( { startOffset: 0, startContainer: element, @@ -38,12 +38,30 @@ export const spec = [ endContainer: element, } ), startPath: [ 0, 0 ], - endPath: [ 0, 0 ], + endPath: [ 0, 1 ], record: { start: 0, - end: 0, - formats: [], - text: '', + end: 1, + formats: [ , ], + text: ' ', + }, + }, + { + description: 'should preserve non breaking space', + html: 'test\u00a0 test', + createRange: ( element ) => ( { + startOffset: 5, + startContainer: element.firstChild, + endOffset: 5, + endContainer: element.firstChild, + } ), + startPath: [ 0, 5 ], + endPath: [ 0, 5 ], + record: { + start: 5, + end: 5, + formats: [ , , , , , , , , , , ], + text: 'test\u00a0 test', }, }, { diff --git a/test/e2e/specs/__snapshots__/links.test.js.snap b/test/e2e/specs/__snapshots__/links.test.js.snap index 6b0ab2795b7af3..d63515e5d2ee34 100644 --- a/test/e2e/specs/__snapshots__/links.test.js.snap +++ b/test/e2e/specs/__snapshots__/links.test.js.snap @@ -20,7 +20,7 @@ exports[`Links can be created instantly when a URL is selected 1`] = ` exports[`Links can be created without any text selected 1`] = ` "<!-- wp:paragraph --> -<p>This is Gutenberg: <a href=\\"https://wordpress.org/gutenberg\\">https://wordpress.org/gutenberg</a></p> +<p>This is Gutenberg: <a href=\\"https://wordpress.org/gutenberg\\">https://wordpress.org/gutenberg</a></p> <!-- /wp:paragraph -->" `; diff --git a/test/e2e/specs/__snapshots__/rich-text.test.js.snap b/test/e2e/specs/__snapshots__/rich-text.test.js.snap index e1324add1205e7..e911f407edd680 100644 --- a/test/e2e/specs/__snapshots__/rich-text.test.js.snap +++ b/test/e2e/specs/__snapshots__/rich-text.test.js.snap @@ -2,7 +2,7 @@ exports[`RichText should apply formatting when selection is collapsed 1`] = ` "<!-- wp:paragraph --> -<p>Some <strong>bold</strong>.</p> +<p>Some <strong>bold</strong>.</p> <!-- /wp:paragraph -->" `; diff --git a/test/e2e/specs/__snapshots__/undo.test.js.snap b/test/e2e/specs/__snapshots__/undo.test.js.snap index 1ae6c8e4458b79..49a8c19fe89fae 100644 --- a/test/e2e/specs/__snapshots__/undo.test.js.snap +++ b/test/e2e/specs/__snapshots__/undo.test.js.snap @@ -28,12 +28,12 @@ exports[`undo should undo typing after a pause 2`] = ` exports[`undo should undo typing after non input change 1`] = ` "<!-- wp:paragraph --> -<p>before keyboard <strong>after keyboard</strong></p> +<p>before keyboard <strong>after keyboard</strong></p> <!-- /wp:paragraph -->" `; exports[`undo should undo typing after non input change 2`] = ` "<!-- wp:paragraph --> -<p>before keyboard </p> +<p>before keyboard </p> <!-- /wp:paragraph -->" `; diff --git a/test/e2e/specs/__snapshots__/writing-flow.test.js.snap b/test/e2e/specs/__snapshots__/writing-flow.test.js.snap index 77e8a9596b7729..a33dda228193e8 100644 --- a/test/e2e/specs/__snapshots__/writing-flow.test.js.snap +++ b/test/e2e/specs/__snapshots__/writing-flow.test.js.snap @@ -124,7 +124,7 @@ exports[`adding blocks should navigate around inline boundaries 1`] = ` exports[`adding blocks should not delete surrounding space when deleting a selected word 1`] = ` "<!-- wp:paragraph --> -<p>alpha  gamma</p> +<p>alpha gamma</p> <!-- /wp:paragraph -->" `; @@ -136,7 +136,7 @@ exports[`adding blocks should not delete surrounding space when deleting a selec exports[`adding blocks should not delete surrounding space when deleting a word with Alt+Backspace 1`] = ` "<!-- wp:paragraph --> -<p>alpha  gamma</p> +<p>alpha gamma</p> <!-- /wp:paragraph -->" `; @@ -148,7 +148,7 @@ exports[`adding blocks should not delete surrounding space when deleting a word exports[`adding blocks should not delete surrounding space when deleting a word with Backspace 1`] = ` "<!-- wp:paragraph --> -<p>1  3</p> +<p>1 3</p> <!-- /wp:paragraph -->" `; diff --git a/test/e2e/specs/adding-inline-tokens.test.js b/test/e2e/specs/adding-inline-tokens.test.js index 00fa8c45869d3c..d1504f7e35eb7d 100644 --- a/test/e2e/specs/adding-inline-tokens.test.js +++ b/test/e2e/specs/adding-inline-tokens.test.js @@ -44,7 +44,7 @@ describe( 'adding inline tokens', () => { await page.click( '.media-modal button.media-button-select' ); // Check the content. - const regex = new RegExp( `<!-- wp:paragraph -->\\s*<p>a\\u00A0<img class="wp-image-\\d+" style="width:\\s*10px;?" src="[^"]+\\/${ filename }\\.png" alt=""\\/?><\\/p>\\s*<!-- \\/wp:paragraph -->` ); + const regex = new RegExp( `<!-- wp:paragraph -->\\s*<p>a <img class="wp-image-\\d+" style="width:\\s*10px;?" src="[^"]+\\/${ filename }\\.png" alt=""\\/?><\\/p>\\s*<!-- \\/wp:paragraph -->` ); expect( await getEditedPostContent() ).toMatch( regex ); } ); } ); From d30e5eb11ba755c6c3fa167c544c4efcaaf7999a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20Van=C2=A0Dorpe?= <iseulde@automattic.com> Date: Fri, 30 Nov 2018 09:13:26 +0100 Subject: [PATCH 155/254] Only init TinyMCE once per instance (#12386) --- packages/editor/src/components/rich-text/tinymce.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/editor/src/components/rich-text/tinymce.js b/packages/editor/src/components/rich-text/tinymce.js index 833a740c690ece..7e67e108f85e61 100644 --- a/packages/editor/src/components/rich-text/tinymce.js +++ b/packages/editor/src/components/rich-text/tinymce.js @@ -111,6 +111,7 @@ export default class TinyMCE extends Component { this.bindEditorNode = this.bindEditorNode.bind( this ); this.onFocus = this.onFocus.bind( this ); this.onKeyDown = this.onKeyDown.bind( this ); + this.initialize = this.initialize.bind( this ); } onFocus() { @@ -167,7 +168,16 @@ export default class TinyMCE extends Component { } } + /** + * Initializes TinyMCE. Can only be called once per instance. + */ initialize() { + if ( this.initialize.called ) { + return; + } + + this.initialize.called = true; + const { multilineTag } = this.props; const settings = { theme: false, From e02bec8f0e57348aeaeb652507388c88566e05c2 Mon Sep 17 00:00:00 2001 From: Marin Atanasov <8436925+tyxla@users.noreply.github.com> Date: Fri, 30 Nov 2018 10:18:36 +0200 Subject: [PATCH 156/254] Autocompleters: Consider block category (#12287) --- .../editor/src/components/autocompleters/block.js | 4 ++-- .../src/components/autocompleters/test/block.js | 11 +++++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/editor/src/components/autocompleters/block.js b/packages/editor/src/components/autocompleters/block.js index e235c9a384aadb..d40b18d55c38bc 100644 --- a/packages/editor/src/components/autocompleters/block.js +++ b/packages/editor/src/components/autocompleters/block.js @@ -68,8 +68,8 @@ export function createBlockCompleter( { ); }, getOptionKeywords( inserterItem ) { - const { title, keywords = [] } = inserterItem; - return [ ...keywords, title ]; + const { title, keywords = [], category } = inserterItem; + return [ category, ...keywords, title ]; }, getOptionLabel( inserterItem ) { const { icon, title } = inserterItem; diff --git a/packages/editor/src/components/autocompleters/test/block.js b/packages/editor/src/components/autocompleters/test/block.js index 56299cba2514aa..8b853cd6931732 100644 --- a/packages/editor/src/components/autocompleters/test/block.js +++ b/packages/editor/src/components/autocompleters/test/block.js @@ -40,30 +40,33 @@ describe( 'block', () => { expect( completer.options() ).toEqual( [ option1, option3 ] ); } ); - it( 'should derive option keywords from block keywords and block title', () => { + it( 'should derive option keywords from block category, block keywords and block title', () => { const inserterItemWithTitleAndKeywords = { name: 'core/foo', title: 'foo', keywords: [ 'foo-keyword-1', 'foo-keyword-2' ], + category: 'formatting', }; const inserterItemWithTitleAndEmptyKeywords = { name: 'core/bar', title: 'bar', // Intentionally empty keyword list keywords: [], + category: 'common', }; const inserterItemWithTitleAndUndefinedKeywords = { name: 'core/baz', title: 'baz', + category: 'widgets', // Intentionally omitted keyword list }; expect( blockCompleter.getOptionKeywords( inserterItemWithTitleAndKeywords ) ) - .toEqual( [ 'foo-keyword-1', 'foo-keyword-2', 'foo' ] ); + .toEqual( [ 'formatting', 'foo-keyword-1', 'foo-keyword-2', 'foo' ] ); expect( blockCompleter.getOptionKeywords( inserterItemWithTitleAndEmptyKeywords ) ) - .toEqual( [ 'bar' ] ); + .toEqual( [ 'common', 'bar' ] ); expect( blockCompleter.getOptionKeywords( inserterItemWithTitleAndUndefinedKeywords ) ) - .toEqual( [ 'baz' ] ); + .toEqual( [ 'widgets', 'baz' ] ); } ); it( 'should render a block option label', () => { From 3ce3f0380ad77ba0b9df8c5a94741822ce8bbbb5 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, 30 Nov 2018 10:14:42 +0100 Subject: [PATCH 157/254] Add registry param to withDispatch component (#11851) * Add select param to withDispatch HOC * Refactor BlockListBlock component to use select param in withDispatch * Update data documentation to include new param used in withDispatch HOC * Update changelog for data package to include note about updated withDispatch capabilities * Add grammar corrections to data package readme * Refactor CopyHandler component to use select in withDispatch * Ensure that only functions can be returned in the object created by mapDispatchToProps * Use select param in withDispatch for Header component * Address feedback shared in review for WithDispatch component * Update withDispatch to pass registry as 3rd param * Update data package documentation to explain usage of the registry * Refactor CopyHandler to work with onCopy handler exposed from withDispatch * Pass registry as the 3rd param to mapSelectToProps in withSelect * Refactor CopyHandler to use only withDispatch --- package-lock.json | 2 +- packages/data/CHANGELOG.md | 7 ++ packages/data/README.md | 33 +++++++++- .../src/components/with-dispatch/index.js | 8 ++- .../components/with-dispatch/test/index.js | 63 ++++++++++++++++++ .../data/src/components/with-select/index.js | 2 +- .../edit-post/src/components/header/index.js | 9 ++- .../editor/src/components/block-list/block.js | 36 ++++------- .../src/components/copy-handler/index.js | 64 +++++++++---------- 9 files changed, 155 insertions(+), 69 deletions(-) diff --git a/package-lock.json b/package-lock.json index ab4562395d1a18..e90f9d449392cf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7622,7 +7622,7 @@ }, "eslint-plugin-react": { "version": "7.7.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.7.0.tgz", + "resolved": "http://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.7.0.tgz", "integrity": "sha512-KC7Snr4YsWZD5flu6A5c0AcIZidzW3Exbqp7OT67OaD2AppJtlBr/GuPrW/vaQM/yfZotEvKAdrxrO+v8vwYJA==", "dev": true, "requires": { diff --git a/packages/data/CHANGELOG.md b/packages/data/CHANGELOG.md index 18139501460cc3..d496bb6162e239 100644 --- a/packages/data/CHANGELOG.md +++ b/packages/data/CHANGELOG.md @@ -1,3 +1,10 @@ +## 4.1.0 (Unreleased) + +### New Feature + +- `withDispatch`'s `mapDispatchToProps` function takes the `registry` object as the 3rd param ([#11851](https://github.com/WordPress/gutenberg/pull/11851)). +- `withSelect`'s `mapSelectToProps` function takes the `registry` object as the 3rd param ([#11851](https://github.com/WordPress/gutenberg/pull/11851)). + ## 4.0.1 (2018-11-20) ## 4.0.0 (2018-11-15) diff --git a/packages/data/README.md b/packages/data/README.md index f6ca4c665c8fff..b5745e7719ed7e 100644 --- a/packages/data/README.md +++ b/packages/data/README.md @@ -229,7 +229,7 @@ A higher-order component is a function which accepts a [component](https://githu #### `withSelect( mapSelectToProps: Function ): Function` -Use `withSelect` to inject state-derived props into a component. Passed a function which returns an object mapping prop names to the subscribed data source, a higher-order component function is returned. The higher-order component can be used to enhance a presentational component, updating it automatically when state changes. The mapping function is passed the [`select` function](#select) and the props passed to the original component. +Use `withSelect` to inject state-derived props into a component. Passed a function which returns an object mapping prop names to the subscribed data source, a higher-order component function is returned. The higher-order component can be used to enhance a presentational component, updating it automatically when state changes. The mapping function is passed the [`select` function](#select), the props passed to the original component and the `registry` object. _Example:_ @@ -261,7 +261,7 @@ In the above example, when `HammerPriceDisplay` is rendered into an application, #### `withDispatch( mapDispatchToProps: Function ): Function` -Use `withDispatch` to inject dispatching action props into your component. Passed a function which returns an object mapping prop names to action dispatchers, a higher-order component function is returned. The higher-order component can be used to enhance a component. For example, you can define callback behaviors as props for responding to user interactions. The mapping function is passed the [`dispatch` function](#dispatch) and the props passed to the original component. +Use `withDispatch` to inject dispatching action props into your component. Passed a function which returns an object mapping prop names to action dispatchers, a higher-order component function is returned. The higher-order component can be used to enhance a component. For example, you can define callback behaviors as props for responding to user interactions. The mapping function is passed the [`dispatch` function](#dispatch), the props passed to the original component and the `registry` object. ```jsx function Button( { onClick, children } ) { @@ -272,7 +272,7 @@ const { withDispatch } = wp.data; const SaleButton = withDispatch( ( dispatch, ownProps ) => { const { startSale } = dispatch( 'my-shop' ); - const { discountPercent = 20 } = ownProps; + const { discountPercent } = ownProps; return { onClick() { @@ -281,6 +281,33 @@ const SaleButton = withDispatch( ( dispatch, ownProps ) => { }; } )( Button ); +// Rendered in the application: +// +// <SaleButton discountPercent="20">Start Sale!</SaleButton> +``` + +In the majority of cases, it will be sufficient to use only two first params passed to `mapDispatchToProps` as illustrated in the previous example. However, there might be some very advanced use cases where using the `registry` object might be used as a tool to optimize the performance of your component. Using `select` function from the registry might be useful when you need to fetch some dynamic data from the store at the time when the event is fired, but at the same time, you never use it to render your component. In such scenario, you can avoid using the `withSelect` higher order component to compute such prop, which might lead to unnecessary re-renders of you component caused by its frequent value change. Keep in mind, that `mapDispatchToProps` must return an object with functions only. + +```jsx +function Button( { onClick, children } ) { + return <button type="button" onClick={ onClick }>{ children }</button>; +} + +const { withDispatch } = wp.data; + +const SaleButton = withDispatch( ( dispatch, ownProps, { select } ) => { + // Stock number changes frequently. + const { getStockNumber } = select( 'my-shop' ); + const { startSale } = dispatch( 'my-shop' ); + + return { + onClick() { + const dicountPercent = getStockNumber() > 50 ? 10 : 20; + startSale( discountPercent ); + }, + }; +} )( Button ); + // Rendered in the application: // // <SaleButton>Start Sale!</SaleButton> diff --git a/packages/data/src/components/with-dispatch/index.js b/packages/data/src/components/with-dispatch/index.js index 3c302c557187fa..6c5944085a6942 100644 --- a/packages/data/src/components/with-dispatch/index.js +++ b/packages/data/src/components/with-dispatch/index.js @@ -39,7 +39,7 @@ const withDispatch = ( mapDispatchToProps ) => createHigherOrderComponent( proxyDispatch( propName, ...args ) { // Original dispatcher is a pre-bound (dispatching) action creator. - mapDispatchToProps( this.props.registry.dispatch, this.props.ownProps )[ propName ]( ...args ); + mapDispatchToProps( this.props.registry.dispatch, this.props.ownProps, this.props.registry )[ propName ]( ...args ); } setProxyProps( props ) { @@ -49,8 +49,12 @@ const withDispatch = ( mapDispatchToProps ) => createHigherOrderComponent( // called, it is done only to determine the keys for which // proxy functions should be created. The actual registry // dispatch does not occur until the function is called. - const propsToDispatchers = mapDispatchToProps( this.props.registry.dispatch, props.ownProps ); + const propsToDispatchers = mapDispatchToProps( this.props.registry.dispatch, props.ownProps, this.props.registry ); this.proxyProps = mapValues( propsToDispatchers, ( dispatcher, propName ) => { + if ( typeof dispatcher !== 'function' ) { + // eslint-disable-next-line no-console + console.warn( `Property ${ propName } returned from mapDispatchToProps in withDispatch must be a function.` ); + } // Prebind with prop name so we have reference to the original // dispatcher to invoke. Track between re-renders to avoid // creating new function references every render. diff --git a/packages/data/src/components/with-dispatch/test/index.js b/packages/data/src/components/with-dispatch/test/index.js index b187c5b69410c4..c7a6fe9ac034f5 100644 --- a/packages/data/src/components/with-dispatch/test/index.js +++ b/packages/data/src/components/with-dispatch/test/index.js @@ -119,4 +119,67 @@ describe( 'withDispatch', () => { expect( firstRegistryAction ).toHaveBeenCalledTimes( 2 ); expect( secondRegistryAction ).toHaveBeenCalledTimes( 2 ); } ); + + it( 'always calls select with the latest state in the handler passed to the component', () => { + const store = registry.registerStore( 'counter', { + reducer: ( state = 0, action ) => { + if ( action.type === 'update' ) { + return action.count; + } + return state; + }, + actions: { + update: ( count ) => ( { type: 'update', count } ), + }, + selectors: { + getCount: ( state ) => state, + }, + } ); + + const Component = withDispatch( ( _dispatch, ownProps, { select: _select } ) => { + const outerCount = _select( 'counter' ).getCount(); + return { + update: () => { + const innerCount = _select( 'counter' ).getCount(); + expect( innerCount ).toBe( outerCount ); + const actionReturnedFromDispatch = _dispatch( 'counter' ).update( innerCount + 1 ); + expect( actionReturnedFromDispatch ).toBe( undefined ); + }, + }; + } )( ( props ) => <button onClick={ props.update } /> ); + + const testRenderer = TestRenderer.create( + <RegistryProvider value={ registry }> + <Component /> + </RegistryProvider> + ); + + const counterUpdateHandler = testRenderer.root.findByType( 'button' ).props.onClick; + + counterUpdateHandler(); + expect( store.getState() ).toBe( 1 ); + + counterUpdateHandler(); + expect( store.getState() ).toBe( 2 ); + + counterUpdateHandler(); + expect( store.getState() ).toBe( 3 ); + } ); + + it( 'warns when mapDispatchToProps returns non-function property', () => { + const Component = withDispatch( () => { + return { + count: 3, + }; + } )( () => null ); + + TestRenderer.create( + <RegistryProvider value={ registry }> + <Component /> + </RegistryProvider> + ); + expect( console ).toHaveWarnedWith( + 'Property count returned from mapDispatchToProps in withDispatch must be a function.' + ); + } ); } ); diff --git a/packages/data/src/components/with-select/index.js b/packages/data/src/components/with-select/index.js index c471bce36388c5..d1219479d3e462 100644 --- a/packages/data/src/components/with-select/index.js +++ b/packages/data/src/components/with-select/index.js @@ -39,7 +39,7 @@ const withSelect = ( mapSelectToProps ) => createHigherOrderComponent( ( Wrapped */ function getNextMergeProps( props ) { return ( - mapSelectToProps( props.registry.select, props.ownProps ) || + mapSelectToProps( props.registry.select, props.ownProps, props.registry ) || DEFAULT_MERGE_PROPS ); } diff --git a/packages/edit-post/src/components/header/index.js b/packages/edit-post/src/components/header/index.js index 6fb3ec96eab749..0663fcd5f98b36 100644 --- a/packages/edit-post/src/components/header/index.js +++ b/packages/edit-post/src/components/header/index.js @@ -82,18 +82,17 @@ function Header( { export default compose( withSelect( ( select ) => ( { hasActiveMetaboxes: select( 'core/edit-post' ).hasMetaBoxes(), - hasBlockSelection: !! select( 'core/editor' ).getBlockSelectionStart(), isEditorSidebarOpened: select( 'core/edit-post' ).isEditorSidebarOpened(), isPublishSidebarOpened: select( 'core/edit-post' ).isPublishSidebarOpened(), isSaving: select( 'core/edit-post' ).isSavingMetaBoxes(), } ) ), - withDispatch( ( dispatch, { hasBlockSelection } ) => { + withDispatch( ( dispatch, ownProps, { select } ) => { + const { getBlockSelectionStart } = select( 'core/editor' ); const { openGeneralSidebar, closeGeneralSidebar } = dispatch( 'core/edit-post' ); - const sidebarToOpen = hasBlockSelection ? 'edit-post/block' : 'edit-post/document'; + return { - openGeneralSidebar: () => openGeneralSidebar( sidebarToOpen ), + openGeneralSidebar: () => openGeneralSidebar( getBlockSelectionStart() ? 'edit-post/block' : 'edit-post/document' ), closeGeneralSidebar: closeGeneralSidebar, - hasBlockSelection: undefined, }; } ), )( Header ); diff --git a/packages/editor/src/components/block-list/block.js b/packages/editor/src/components/block-list/block.js index e57a221531dcc0..98956980d0d555 100644 --- a/packages/editor/src/components/block-list/block.js +++ b/packages/editor/src/components/block-list/block.js @@ -71,7 +71,6 @@ 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 = { @@ -290,7 +289,7 @@ export class BlockListBlock extends Component { if ( event.shiftKey ) { if ( ! this.props.isSelected ) { - this.onShiftSelection(); + this.props.onShiftSelection(); event.preventDefault(); } } else { @@ -362,20 +361,6 @@ 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 ); - } - } - forceFocusedContextualToolbar() { this.isForcingContextualToolbar = true; // trigger a re-render @@ -649,7 +634,6 @@ const applyWithSelect = withSelect( ( select, { clientId, rootClientId, isLargeV getEditorSettings, hasSelectedInnerBlock, getTemplateLock, - getBlockSelectionStart, } = select( 'core/editor' ); const isSelected = isBlockSelected( clientId ); const { hasFixedToolbar, focusMode } = getEditorSettings(); @@ -682,13 +666,11 @@ 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, }; } ); -const applyWithDispatch = withDispatch( ( dispatch, ownProps ) => { +const applyWithDispatch = withDispatch( ( dispatch, ownProps, { select } ) => { + const { getBlockSelectionStart } = select( 'core/editor' ); const { updateBlockAttributes, selectBlock, @@ -709,7 +691,6 @@ 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 ); @@ -730,6 +711,17 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps ) => { onMetaChange( meta ) { editPost( { meta } ); }, + onShiftSelection() { + if ( ! ownProps.isSelectionEnabled ) { + return; + } + + if ( getBlockSelectionStart() ) { + multiSelect( getBlockSelectionStart(), ownProps.clientId ); + } else { + selectBlock( ownProps.clientId ); + } + }, toggleSelection( selectionEnabled ) { toggleSelection( selectionEnabled ); }, diff --git a/packages/editor/src/components/copy-handler/index.js b/packages/editor/src/components/copy-handler/index.js index e74cde91bd2aa2..f9da173bf03058 100644 --- a/packages/editor/src/components/copy-handler/index.js +++ b/packages/editor/src/components/copy-handler/index.js @@ -4,7 +4,7 @@ import { Component } from '@wordpress/element'; import { serialize } from '@wordpress/blocks'; import { documentHasSelection } from '@wordpress/dom'; -import { withSelect, withDispatch } from '@wordpress/data'; +import { withDispatch } from '@wordpress/data'; import { compose } from '@wordpress/compose'; class CopyHandler extends Component { @@ -26,33 +26,13 @@ class CopyHandler extends Component { } onCopy( event ) { - const { hasMultiSelection, selectedBlockClientIds, getBlocksByClientId } = this.props; - - if ( selectedBlockClientIds.length === 0 ) { - return; - } - - // Let native copy behaviour take over in input fields. - if ( ! hasMultiSelection && documentHasSelection() ) { - return; - } - - const serialized = serialize( getBlocksByClientId( selectedBlockClientIds ) ); - - event.clipboardData.setData( 'text/plain', serialized ); - event.clipboardData.setData( 'text/html', serialized ); - + this.props.onCopy( event.clipboardData ); event.preventDefault(); } onCut( event ) { - const { hasMultiSelection, selectedBlockClientIds } = this.props; - - this.onCopy( event ); - - if ( hasMultiSelection ) { - this.props.onRemove( selectedBlockClientIds ); - } + this.props.onCut( event.clipboardData ); + event.preventDefault(); } render() { @@ -61,27 +41,41 @@ class CopyHandler extends Component { } export default compose( [ - withSelect( ( select ) => { + withDispatch( ( dispatch, ownProps, { select } ) => { const { + getBlocksByClientId, getMultiSelectedBlockClientIds, getSelectedBlockClientId, - getBlocksByClientId, hasMultiSelection, } = select( 'core/editor' ); + const { removeBlocks } = dispatch( 'core/editor' ); const selectedBlockClientId = getSelectedBlockClientId(); const selectedBlockClientIds = selectedBlockClientId ? [ selectedBlockClientId ] : getMultiSelectedBlockClientIds(); return { - hasMultiSelection: hasMultiSelection(), - selectedBlockClientIds, - - // We only care about this value when the copy is performed - // We call it dynamically in the event handler to avoid unnecessary re-renders. - getBlocksByClientId, + onCopy( dataTransfer ) { + if ( selectedBlockClientIds.length === 0 ) { + return; + } + + // Let native copy behaviour take over in input fields. + if ( ! hasMultiSelection() && documentHasSelection() ) { + return; + } + + const serialized = serialize( getBlocksByClientId( selectedBlockClientIds ) ); + + dataTransfer.setData( 'text/plain', serialized ); + dataTransfer.setData( 'text/html', serialized ); + }, + onCut( dataTransfer ) { + this.onCopy( dataTransfer ); + + if ( hasMultiSelection() ) { + removeBlocks( selectedBlockClientIds ); + } + }, }; } ), - withDispatch( ( dispatch ) => ( { - onRemove: dispatch( 'core/editor' ).removeBlocks, - } ) ), ] )( CopyHandler ); From 77dcd005d1ee9ceb0eca34901dbce0979c7f5d61 Mon Sep 17 00:00:00 2001 From: Danilo Ercoli <ercoli@gmail.com> Date: Fri, 30 Nov 2018 10:51:42 +0100 Subject: [PATCH 158/254] [RNmobile] Fix problems with undo/redo on Android (#12417) * Always re-render RichText component when a change happens on Native side * Remove code that blocks updating of the RichText component on undo/redo * Fix lint-js * Add TODO item that better explains next steps to fix/remove re-rendering when not needed --- .../src/components/rich-text/index.native.js | 15 +++++---------- 1 file changed, 5 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 ba6a4673e101c2..4c5b57d6b83541 100644 --- a/packages/editor/src/components/rich-text/index.native.js +++ b/packages/editor/src/components/rich-text/index.native.js @@ -268,17 +268,12 @@ 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. - // The first time the component is drawn `lastContent` and `lastEventCount ` are both undefined - if ( this.lastEventCount && - nextProps.value && - this.lastContent && - 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 + // TODO: Please re-introduce the check to avoid updating the content right after an `onChange` call. + // It was removed in https://github.com/WordPress/gutenberg/pull/12417 to fix undo/redo problem. + + // If the component is changed React side (undo/redo/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 ) { From 05e5ea8440c8bbcfe631985c29fca64f53f0da73 Mon Sep 17 00:00:00 2001 From: Jorge Bernal <jbernal@gmail.com> Date: Fri, 30 Nov 2018 11:27:57 +0100 Subject: [PATCH 159/254] Hide the image upload button on native until it's implemented (#12409) --- .../editor/src/components/media-placeholder/index.native.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/editor/src/components/media-placeholder/index.native.js b/packages/editor/src/components/media-placeholder/index.native.js index 008bfba7df213e..6a64523a43962c 100644 --- a/packages/editor/src/components/media-placeholder/index.native.js +++ b/packages/editor/src/components/media-placeholder/index.native.js @@ -12,10 +12,9 @@ function MediaPlaceholder( props ) { Image </Text> <Text style={ styles.emptyStateDescription }> - Upload a new image or select a file from your library. + Select an image from your library. </Text> <View style={ styles.emptyStateButtonsContainer }> - <Button title="Upload" onPress={ props.onUploadPress } /> <Button title="Media Library" onPress={ props.onMediaLibraryPress } /> </View> </View> From d4d3e0c3ec83b0d83e6783484c2c912e349f93f4 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, 30 Nov 2018 11:31:43 +0100 Subject: [PATCH 160/254] Packages: Extract Eslint config package (#7965) * Packages: Extract Eslint config package * Copy config from eslint-config-wordpress * Docs: Expose documentation for the newly added package * Remove no longer necessary globals from the Eslint config * Apply suggestions from code review Fix ESLint typo Kudos to @ntwb for catching Co-Authored-By: gziolo <grzegorz@gziolo.pl> --- .eslintrc.js | 16 +- docs/manifest.json | 6 + package-lock.json | 145 ++++++------------ package.json | 5 +- packages/block-library/src/classic/edit.js | 2 + .../hooks/components/media-upload/index.js | 6 +- packages/eslint-config/README.md | 24 +++ packages/eslint-config/configs/es5.js | 12 ++ packages/eslint-config/configs/esnext.js | 8 + packages/eslint-config/configs/rules/es5.js | 84 ++++++++++ .../eslint-config/configs/rules/esnext.js | 66 ++++++++ .../eslint-config/index.js | 7 +- packages/eslint-config/package.json | 27 ++++ 13 files changed, 286 insertions(+), 122 deletions(-) create mode 100644 packages/eslint-config/README.md create mode 100644 packages/eslint-config/configs/es5.js create mode 100644 packages/eslint-config/configs/esnext.js create mode 100644 packages/eslint-config/configs/rules/es5.js create mode 100644 packages/eslint-config/configs/rules/esnext.js rename eslint/config.js => packages/eslint-config/index.js (97%) create mode 100644 packages/eslint-config/package.json diff --git a/.eslintrc.js b/.eslintrc.js index 45fc4c94140f1c..feced45620657e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -19,14 +19,8 @@ const majorMinorRegExp = escapeRegExp( version.replace( /\.\d+$/, '' ) ) + '(\\. module.exports = { root: true, extends: [ - './eslint/config.js', - 'plugin:jest/recommended' - ], - env: { - 'jest/globals': true, - }, - plugins: [ - 'jest', + '@wordpress/eslint-config', + 'plugin:jest/recommended', ], rules: { 'no-restricted-syntax': [ @@ -189,9 +183,13 @@ module.exports = { overrides: [ { files: [ 'test/e2e/**/*.js' ], + env: { + browser: true, + }, globals: { - page: true, browser: true, + page: true, + wp: true, }, }, ], diff --git a/docs/manifest.json b/docs/manifest.json index a15c102355f36c..398f6445c56ae2 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -443,6 +443,12 @@ "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/packages/escape-html/README.md", "parent": "packages" }, + { + "title": "@wordpress/eslint-config", + "slug": "packages-eslint-config", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/packages/eslint-config/README.md", + "parent": "packages" + }, { "title": "@wordpress/format-library", "slug": "packages-format-library", diff --git a/package-lock.json b/package-lock.json index e90f9d449392cf..92f84cbe3e34e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2590,6 +2590,15 @@ "@babel/runtime": "^7.0.0" } }, + "@wordpress/eslint-config": { + "version": "file:packages/eslint-config", + "dev": true, + "requires": { + "babel-eslint": "^8.0.3", + "eslint-plugin-jsx-a11y": "6.0.2", + "eslint-plugin-react": "7.7.0" + } + }, "@wordpress/format-library": { "version": "file:packages/format-library", "requires": { @@ -3055,16 +3064,6 @@ } } }, - "aria-query": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-0.7.1.tgz", - "integrity": "sha1-Jsu1r/ZBRLCoJb4YRuCxbPoAsR4=", - "dev": true, - "requires": { - "ast-types-flow": "0.0.7", - "commander": "^2.11.0" - } - }, "arr-diff": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", @@ -3323,15 +3322,6 @@ "integrity": "sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w==", "dev": true }, - "axobject-query": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-0.1.0.tgz", - "integrity": "sha1-YvWdvFnJ+SQnWco0mWDnov48NsA=", - "dev": true, - "requires": { - "ast-types-flow": "0.0.7" - } - }, "babel-code-frame": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", @@ -5671,15 +5661,6 @@ "integrity": "sha512-sVXqklSaotK9at437sFlFpyOcJonxe0yST/AG9DkQKUdIE6IqGIMv4SfAQSKaJbSdVEJYItASCrBiVQHq1HQew==", "dev": true }, - "comment-parser": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-0.4.2.tgz", - "integrity": "sha1-+lo/eAEwcBFIZtx7jpzzF6ljX3Q=", - "dev": true, - "requires": { - "readable-stream": "^2.0.4" - } - }, "commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", @@ -7565,34 +7546,12 @@ } } }, - "eslint-config-wordpress": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/eslint-config-wordpress/-/eslint-config-wordpress-2.0.0.tgz", - "integrity": "sha1-UgEgbGlk1kgxUjLt9t+9LpJeTNY=", - "dev": true - }, - "eslint-plugin-i18n": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-i18n/-/eslint-plugin-i18n-1.2.0.tgz", - "integrity": "sha1-SfP0OA7e/oyHbwyXlh9lw6w3zao=", - "dev": true - }, "eslint-plugin-jest": { "version": "21.5.0", "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-21.5.0.tgz", "integrity": "sha512-4fxfe2RcqzU+IVNQL5n4pqibLcUhKKxihYsA510+6kC/FTdGInszDDHgO4ntBzPWu8mcHAvKJLs8o3AQw6eHTg==", "dev": true }, - "eslint-plugin-jsdoc": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-3.5.0.tgz", - "integrity": "sha512-qoNpVicVWGjGBXAJsqRoqVuAnajgX7PWtSa2Men36XKRiXe3RS/QmRv215PXZwo4OHskYOsUoJUeiPiWtS9ULA==", - "dev": true, - "requires": { - "comment-parser": "^0.4.2", - "lodash": "^4.17.4" - } - }, "eslint-plugin-jsx-a11y": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.0.2.tgz", @@ -7606,18 +7565,33 @@ "damerau-levenshtein": "^1.0.0", "emoji-regex": "^6.1.0", "jsx-ast-utils": "^1.4.0" - } - }, - "eslint-plugin-node": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-6.0.1.tgz", - "integrity": "sha512-Q/Cc2sW1OAISDS+Ji6lZS2KV4b7ueA/WydVWd1BECTQwVvfQy5JAi3glhINoKzoMnfnuRgNP+ZWKrGAbp3QDxw==", - "dev": true, - "requires": { - "ignore": "^3.3.6", - "minimatch": "^3.0.4", - "resolve": "^1.3.3", - "semver": "^5.4.1" + }, + "dependencies": { + "aria-query": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-0.7.1.tgz", + "integrity": "sha1-Jsu1r/ZBRLCoJb4YRuCxbPoAsR4=", + "dev": true, + "requires": { + "ast-types-flow": "0.0.7", + "commander": "^2.11.0" + } + }, + "axobject-query": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-0.1.0.tgz", + "integrity": "sha1-YvWdvFnJ+SQnWco0mWDnov48NsA=", + "dev": true, + "requires": { + "ast-types-flow": "0.0.7" + } + }, + "jsx-ast-utils": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-1.4.1.tgz", + "integrity": "sha1-OGchPo3Xm/Ho8jAMDPwe+xgsDfE=", + "dev": true + } } }, "eslint-plugin-react": { @@ -7632,15 +7606,6 @@ "prop-types": "^15.6.0" }, "dependencies": { - "jsx-ast-utils": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.0.1.tgz", - "integrity": "sha1-6AGxs5mF4g//yHtA43SAgOLcrH8=", - "dev": true, - "requires": { - "array-includes": "^3.0.3" - } - }, "prop-types": { "version": "15.6.2", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz", @@ -7653,27 +7618,6 @@ } } }, - "eslint-plugin-wordpress": { - "version": "git://github.com/WordPress-Coding-Standards/eslint-plugin-wordpress.git#1774343f6226052a46b081e01db3fca8793cc9f1", - "from": "git://github.com/WordPress-Coding-Standards/eslint-plugin-wordpress.git#1774343f6226052a46b081e01db3fca8793cc9f1", - "dev": true, - "requires": { - "eslint-plugin-i18n": "~1.2.0", - "eslint-plugin-jsdoc": "~3.5.0", - "eslint-plugin-node": "~6.0.1", - "eslint-plugin-wpcalypso": "~4.0.1", - "merge": "~1.2.0" - } - }, - "eslint-plugin-wpcalypso": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-wpcalypso/-/eslint-plugin-wpcalypso-4.0.1.tgz", - "integrity": "sha512-fU5NSc0XGdel/tlEIUoESOdqphBWQN2FfSgXXNHpXKX7ftTcqXacqgzXU8OVziyhXz6s2RUzK0+JSJaNxhZ+Mw==", - "dev": true, - "requires": { - "requireindex": "^1.1.0" - } - }, "eslint-scope": { "version": "3.7.3", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.3.tgz", @@ -13298,10 +13242,13 @@ } }, "jsx-ast-utils": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-1.4.1.tgz", - "integrity": "sha1-OGchPo3Xm/Ho8jAMDPwe+xgsDfE=", - "dev": true + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.0.1.tgz", + "integrity": "sha1-6AGxs5mF4g//yHtA43SAgOLcrH8=", + "dev": true, + "requires": { + "array-includes": "^3.0.3" + } }, "keyv": { "version": "3.0.0", @@ -18617,12 +18564,6 @@ } } }, - "requireindex": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz", - "integrity": "sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==", - "dev": true - }, "resolve": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz", diff --git a/package.json b/package.json index 41b226272d1bad..4b7ebb5d84f854 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "@wordpress/babel-preset-default": "file:packages/babel-preset-default", "@wordpress/browserslist-config": "file:packages/browserslist-config", "@wordpress/custom-templated-path-webpack-plugin": "file:packages/custom-templated-path-webpack-plugin", + "@wordpress/eslint-config": "file:packages/eslint-config", "@wordpress/jest-console": "file:packages/jest-console", "@wordpress/jest-preset-default": "file:packages/jest-preset-default", "@wordpress/library-export-default-webpack-plugin": "file:packages/library-export-default-webpack-plugin", @@ -80,11 +81,7 @@ "deasync": "0.1.13", "deep-freeze": "0.0.1", "doctrine": "2.1.0", - "eslint-config-wordpress": "2.0.0", "eslint-plugin-jest": "21.5.0", - "eslint-plugin-jsx-a11y": "6.0.2", - "eslint-plugin-react": "7.7.0", - "eslint-plugin-wordpress": "git://github.com/WordPress-Coding-Standards/eslint-plugin-wordpress.git#1774343f6226052a46b081e01db3fca8793cc9f1", "espree": "3.5.4", "glob": "7.1.2", "husky": "0.14.3", diff --git a/packages/block-library/src/classic/edit.js b/packages/block-library/src/classic/edit.js index db1467b6301399..2502c0f7e0eb5f 100644 --- a/packages/block-library/src/classic/edit.js +++ b/packages/block-library/src/classic/edit.js @@ -5,6 +5,8 @@ import { Component } from '@wordpress/element'; import { __, _x } from '@wordpress/i18n'; import { BACKSPACE, DELETE, F10 } from '@wordpress/keycodes'; +const { wp } = window; + function isTmceEmpty( editor ) { // When tinyMce is empty the content seems to be: // <p><br data-mce-bogus="1"></p> 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 9160f4f84c15b5..c8552168033899 100644 --- a/packages/edit-post/src/hooks/components/media-upload/index.js +++ b/packages/edit-post/src/hooks/components/media-upload/index.js @@ -1,7 +1,7 @@ /** * External Dependencies */ -import { castArray, pick } from 'lodash'; +import { castArray, defaults, pick } from 'lodash'; /** * WordPress dependencies @@ -9,6 +9,8 @@ import { castArray, pick } from 'lodash'; import { Component } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; +const { wp } = window; + // Getter for the sake of unit tests. const getGalleryDetailsMediaFrame = () => { /** @@ -36,7 +38,7 @@ const getGalleryDetailsMediaFrame = () => { multiple: 'add', editable: false, - library: wp.media.query( _.defaults( { + library: wp.media.query( defaults( { type: 'image', }, this.options.library ) ), } ), diff --git a/packages/eslint-config/README.md b/packages/eslint-config/README.md new file mode 100644 index 00000000000000..5f2b003e765c5d --- /dev/null +++ b/packages/eslint-config/README.md @@ -0,0 +1,24 @@ +# ESLint Config + +[ESLint](https://eslint.org/) config for WordPress development. + +## Installation + +Install the module + +```bash +npm install @wordpress/eslint-config --save-dev +``` + +### Usage + +Next, extend the configuration from your project's `.eslintrc` file: + +```json +"extends": "@wordpress/eslint-config" +``` + +Refer to the [ESLint documentation on Shareable Configs](http://eslint.org/docs/developer-guide/shareable-configs) for more information. + + +<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/eslint-config/configs/es5.js b/packages/eslint-config/configs/es5.js new file mode 100644 index 00000000000000..2e56e7a2f5fcdb --- /dev/null +++ b/packages/eslint-config/configs/es5.js @@ -0,0 +1,12 @@ +/** + * The original version of this file is based on WordPress ESLint rules and shared configs: + * https://github.com/WordPress-Coding-Standards/eslint-plugin-wordpress. + */ + +module.exports = { + env: { + es6: true, + }, + + rules: require( './rules/esnext' ), +}; diff --git a/packages/eslint-config/configs/esnext.js b/packages/eslint-config/configs/esnext.js new file mode 100644 index 00000000000000..3b3a80d7d4cd82 --- /dev/null +++ b/packages/eslint-config/configs/esnext.js @@ -0,0 +1,8 @@ +/** + * The original version of this file is based on WordPress ESLint rules and shared configs: + * https://github.com/WordPress-Coding-Standards/eslint-plugin-wordpress. + */ + +module.exports = { + rules: require( './rules/es5' ), +}; diff --git a/packages/eslint-config/configs/rules/es5.js b/packages/eslint-config/configs/rules/es5.js new file mode 100644 index 00000000000000..61e3e01c343f1d --- /dev/null +++ b/packages/eslint-config/configs/rules/es5.js @@ -0,0 +1,84 @@ +module.exports = { + // Possible Errors + // Disallow assignment in conditional expressions + 'no-cond-assign': [ 'error', 'except-parens' ], + // Disallow irregular whitespace outside of strings and comments + 'no-irregular-whitespace': 'error', + // Best Practices + // Specify curly brace conventions for all control statements + curly: [ 'error', 'all' ], + // Encourages use of dot notation whenever possible + 'dot-notation': [ 'error', { + allowKeywords: true, + allowPattern: '^[a-z]+(_[a-z]+)+$', + } ], + // Disallow use of multiline strings + 'no-multi-str': 'error', + // Disallow use of the with statement + 'no-with': 'error', + // Requires to declare all vars on top of their containing scope + 'vars-on-top': 'error', + // Require immediate function invocation to be wrapped in parentheses + 'wrap-iife': 'error', + // Require or disallow Yoda conditions + yoda: [ 'error', 'always' ], + // Strict Mode + // Variables + // Stylistic Issues + // Enforce spacing inside array brackets + 'array-bracket-spacing': [ 'error', 'always' ], + // Enforce one true brace style + 'brace-style': 'error', + // Require camel case names + camelcase: [ 'error', { + properties: 'always', + } ], + // Disallow or enforce trailing commas + 'comma-dangle': [ 'error', 'never' ], + // Enforce spacing before and after comma + 'comma-spacing': 'error', + // Enforce one true comma style + 'comma-style': [ 'error', 'last' ], + // Enforce newline at the end of file, with no multiple empty lines + 'eol-last': 'error', + // Enforces spacing between keys and values in object literal properties + 'key-spacing': [ 'error', { + beforeColon: false, + afterColon: true, + } ], + // Enforce spacing before and after keywords + 'keyword-spacing': 'error', + // Disallow mixed "LF" and "CRLF" as linebreaks + 'linebreak-style': [ 'error', 'unix' ], + // Enforces empty lines around comments + 'lines-around-comment': [ 'error', { + beforeLineComment: true, + } ], + // Disallow mixed spaces and tabs for indentation + 'no-mixed-spaces-and-tabs': 'error', + // Disallow multiple empty lines + 'no-multiple-empty-lines': 'error', + // Disallow trailing whitespace at the end of lines + 'no-trailing-spaces': 'error', + // Require or disallow an newline around variable declarations + 'one-var-declaration-per-line': [ 'error', 'initializations' ], + // Enforce operators to be placed before or after line breaks + 'operator-linebreak': [ 'error', 'after' ], + // Specify whether backticks, double or single quotes should be used + quotes: [ 'error', 'single' ], + // Require or disallow use of semicolons instead of ASI + semi: [ 'error', 'always' ], + // Require or disallow space before blocks + 'space-before-blocks': [ 'error', 'always' ], + // Require or disallow space before function opening parenthesis + 'space-before-function-paren': [ 'error', 'never' ], + // Require or disallow space before blocks + 'space-in-parens': [ 'error', 'always', { exceptions: [ '{}', '[]' ] } ], + // Require spaces around operators + 'space-infix-ops': 'error', + // Require or disallow spaces before/after unary operators (words on by default, nonwords) + 'space-unary-ops': [ 'error', { + overrides: { '!': true }, + } ], + // Legacy +}; diff --git a/packages/eslint-config/configs/rules/esnext.js b/packages/eslint-config/configs/rules/esnext.js new file mode 100644 index 00000000000000..dcfc27f06554f3 --- /dev/null +++ b/packages/eslint-config/configs/rules/esnext.js @@ -0,0 +1,66 @@ +// see https://eslint.org/docs/rules/#ecmascript-6 +// +module.exports = { + // require braces around arrow function bodies + 'arrow-body-style': 'off', + // require parentheses around arrow function arguments + 'arrow-parens': 'off', + // enforce consistent spacing before and after the arrow in arrow functions + 'arrow-spacing': 'off', + // require super() calls in constructors + 'constructor-super': 'error', + // enforce consistent spacing around * operators in generator functions + 'generator-star-spacing': 'off', + // disallow reassigning class members + 'no-class-assign': 'off', + // disallow arrow functions where they could be confused with comparisons + 'no-confusing-arrow': 'off', + // disallow reassigning `const` variables + 'no-const-assign': 'error', + // disallow duplicate class members + 'no-dupe-class-members': 'error', + // disallow duplicate module imports + 'no-duplicate-imports': 'error', + // disallow `new` operators with the `Symbol` object + 'no-new-symbol': 'off', + // disallow specified modules when loaded by `import` + 'no-restricted-imports': 'off', + // disallow `this`/`super` before calling `super()` in constructors + 'no-this-before-super': 'off', + // disallow unnecessary computed property keys in object literals + 'no-useless-computed-key': 'error', + // disallow unnecessary constructors + 'no-useless-constructor': 'error', + // disallow renaming import, export, and destructured assignments to the same name + 'no-useless-rename': 'off', + // require `let` or `const` instead of `var` + 'no-var': 'error', + // require or disallow method and property shorthand syntax for object literals + 'object-shorthand': 'off', + // require arrow functions as callbacks + 'prefer-arrow-callback': 'off', + // require `const` declarations for variables that are never reassigned after declared + 'prefer-const': 'error', + // require destructuring from arrays and/or objects + 'prefer-destructuring': 'off', + // disallow `parseInt()` and `Number.parseInt()` in favor of binary, octal, and hexadecimal literals + 'prefer-numeric-literals': 'off', + // require rest parameters instead of `arguments` + 'prefer-rest-params': 'off', + // require spread operators instead of `.apply()` + 'prefer-spread': 'off', + // require template literals instead of string concatenation + 'prefer-template': 'off', + // require generator functions to contain `yield` + 'require-yield': 'off', + // enforce spacing between rest and spread operators and their expressions + 'rest-spread-spacing': 'off', + // enforce sorted import declarations within modules + 'sort-imports': 'off', + // require symbol descriptions + 'symbol-description': 'off', + // require or disallow spacing around embedded expressions of template strings + 'template-curly-spacing': [ 'error', 'always' ], + // require or disallow spacing around the `*` in `yield*` expressions + 'yield-star-spacing': 'off', +}; diff --git a/eslint/config.js b/packages/eslint-config/index.js similarity index 97% rename from eslint/config.js rename to packages/eslint-config/index.js index 5ba1ba4f43e843..aa02fdc33ff9d5 100644 --- a/eslint/config.js +++ b/packages/eslint-config/index.js @@ -1,14 +1,12 @@ module.exports = { parser: 'babel-eslint', extends: [ - 'wordpress', - 'plugin:wordpress/esnext', + './configs/es5.js', + './configs/esnext.js', 'plugin:react/recommended', 'plugin:jsx-a11y/recommended', ], env: { - browser: false, - es6: true, node: true, }, parserOptions: { @@ -22,7 +20,6 @@ module.exports = { document: true, }, plugins: [ - 'wordpress', 'react', 'jsx-a11y', ], diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json new file mode 100644 index 00000000000000..07a609d7278fa2 --- /dev/null +++ b/packages/eslint-config/package.json @@ -0,0 +1,27 @@ +{ + "name": "@wordpress/eslint-config", + "version": "1.0.0-alpha.0", + "description": "ESLint config for WordPress development.", + "author": "The WordPress Contributors", + "license": "GPL-2.0-or-later", + "keywords": [ + "wordpress", + "eslint" + ], + "homepage": "https://github.com/WordPress/gutenberg/tree/master/packages/eslint-config/README.md", + "repository": { + "type": "git", + "url": "https://github.com/WordPress/gutenberg.git" + }, + "bugs": { + "url": "https://github.com/WordPress/gutenberg/issues" + }, + "dependencies": { + "babel-eslint": "^8.0.3", + "eslint-plugin-jsx-a11y": "6.0.2", + "eslint-plugin-react": "7.7.0" + }, + "publishConfig": { + "access": "public" + } +} From bbba6f596872f449e6a338d01b2adae094b00c80 Mon Sep 17 00:00:00 2001 From: Jorge Bernal <jbernal@gmail.com> Date: Fri, 30 Nov 2018 11:40:19 +0100 Subject: [PATCH 161/254] Focus on RichText when selection changes (#12422) --- .../editor/src/components/rich-text/index.native.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/editor/src/components/rich-text/index.native.js b/packages/editor/src/components/rich-text/index.native.js index 4c5b57d6b83541..362dc4b359d1a3 100644 --- a/packages/editor/src/components/rich-text/index.native.js +++ b/packages/editor/src/components/rich-text/index.native.js @@ -283,6 +283,18 @@ export class RichText extends Component { return true; } + componentDidMount() { + if ( this.props.isSelected ) { + this._editor.focus(); + } + } + + componentDidUpdate( prevProps ) { + if ( this.props.isSelected && ! prevProps.isSelected ) { + this._editor.focus(); + } + } + isFormatActive( format ) { return this.state.formats[ format ] && this.state.formats[ format ].isActive; } From 5d2bb0c52e5b511d0cabb42f71330bf349db0370 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Wrede?= <soerenwrede@gmail.com> Date: Fri, 30 Nov 2018 11:43:31 +0100 Subject: [PATCH 162/254] Rename Gutenberg to Editor (#12456) --- packages/nux/src/components/dot-tip/index.js | 2 +- .../nux/src/components/dot-tip/test/__snapshots__/index.js.snap | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nux/src/components/dot-tip/index.js b/packages/nux/src/components/dot-tip/index.js index 022eb745352419..cd2356f8026690 100644 --- a/packages/nux/src/components/dot-tip/index.js +++ b/packages/nux/src/components/dot-tip/index.js @@ -38,7 +38,7 @@ export function DotTip( { focusOnMount="container" getAnchorRect={ getAnchorRect } role="dialog" - aria-label={ __( 'Gutenberg tips' ) } + aria-label={ __( 'Editor tips' ) } onClick={ onClick } > <p>{ children }</p> diff --git a/packages/nux/src/components/dot-tip/test/__snapshots__/index.js.snap b/packages/nux/src/components/dot-tip/test/__snapshots__/index.js.snap index eb352b831c04e7..d9639628174e7d 100644 --- a/packages/nux/src/components/dot-tip/test/__snapshots__/index.js.snap +++ b/packages/nux/src/components/dot-tip/test/__snapshots__/index.js.snap @@ -2,7 +2,7 @@ exports[`DotTip should render correctly 1`] = ` <Popover - aria-label="Gutenberg tips" + aria-label="Editor tips" className="nux-dot-tip" focusOnMount="container" getAnchorRect={[Function]} From 2a9d9a4a7823d5bb27e4088a539318a162b222f5 Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Fri, 30 Nov 2018 12:46:44 +0100 Subject: [PATCH 163/254] Bump the plugin version to 4.6 (#12461) * chore(release): publish - @wordpress/block-library@2.2.8 - @wordpress/components@7.0.3 - @wordpress/edit-post@3.1.3 - @wordpress/editor@9.0.3 - @wordpress/format-library@1.2.6 - @wordpress/list-reusable-blocks@1.1.16 - @wordpress/nux@3.0.4 * Bump Gutenberg Version to 4.6.0 --- gutenberg.php | 2 +- package-lock.json | 2 +- package.json | 2 +- 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 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/gutenberg.php b/gutenberg.php index 327a6dba8e76fc..084de1ef1a84f0 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.5.1 + * Version: 4.6.0 * Author: Gutenberg Team * * @package gutenberg diff --git a/package-lock.json b/package-lock.json index 92f84cbe3e34e9..b0c66aaf0ab234 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "4.5.1", + "version": "4.6.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 4b7ebb5d84f854..fc4236a9b3910e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "4.5.1", + "version": "4.6.0", "private": true, "description": "A new WordPress editor experience", "repository": "git+https://github.com/WordPress/gutenberg.git", diff --git a/packages/block-library/package.json b/packages/block-library/package.json index 5f6dbcd09b9321..22d04b8f506ee2 100644 --- a/packages/block-library/package.json +++ b/packages/block-library/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-library", - "version": "2.2.7", + "version": "2.2.8", "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 8e470335fa50a6..6657f37b182b61 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/components", - "version": "7.0.2", + "version": "7.0.3", "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 c871c899256c9e..4124d52ce4cdd5 100644 --- a/packages/edit-post/package.json +++ b/packages/edit-post/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/edit-post", - "version": "3.1.2", + "version": "3.1.3", "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 45f6c934df1b1f..831c8ef25adf61 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/editor", - "version": "9.0.2", + "version": "9.0.3", "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 9fa59207801c16..01f9cdd51de698 100644 --- a/packages/format-library/package.json +++ b/packages/format-library/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/format-library", - "version": "1.2.5", + "version": "1.2.6", "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 190171efe08511..8c455e8a458125 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.15", + "version": "1.1.16", "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 82cbc79c4650d5..d754dc9f891f5a 100644 --- a/packages/nux/package.json +++ b/packages/nux/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/nux", - "version": "3.0.3", + "version": "3.0.4", "description": "NUX (New User eXperience) module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", From 37016c58deb7ac3e355244a985b5dc86129339cc Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Fri, 30 Nov 2018 12:13:19 +0100 Subject: [PATCH 164/254] prevent event discovery on the editor wrapper --- packages/edit-post/src/editor.js | 4 ++++ .../edit-post/src/prevent-event-discovery.js | 17 +++++++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 packages/edit-post/src/prevent-event-discovery.js diff --git a/packages/edit-post/src/editor.js b/packages/edit-post/src/editor.js index 31a51bffcaa95d..74e035fb2fd529 100644 --- a/packages/edit-post/src/editor.js +++ b/packages/edit-post/src/editor.js @@ -4,9 +4,12 @@ import { withSelect } from '@wordpress/data'; import { EditorProvider, ErrorBoundary, PostLockedModal } from '@wordpress/editor'; import { StrictMode } from '@wordpress/element'; +import { KeyboardShortcuts } from '@wordpress/components'; + /** * Internal dependencies */ +import preventEventDiscovery from './prevent-event-discovery'; import Layout from './components/layout'; function Editor( { @@ -38,6 +41,7 @@ function Editor( { > <ErrorBoundary onError={ onError }> <Layout /> + <KeyboardShortcuts shortcuts={ preventEventDiscovery } /> </ErrorBoundary> <PostLockedModal /> </EditorProvider> diff --git a/packages/edit-post/src/prevent-event-discovery.js b/packages/edit-post/src/prevent-event-discovery.js new file mode 100644 index 00000000000000..41db63231adf3b --- /dev/null +++ b/packages/edit-post/src/prevent-event-discovery.js @@ -0,0 +1,17 @@ +export default { + 't a l e s o f g u t e n b e r g': ( event ) => { + if ( + ! document.activeElement.classList.contains( 'edit-post-visual-editor' ) && + document.activeElement !== document.body + ) { + return; + } + + event.preventDefault(); + wp.data.dispatch( 'core/editor' ).insertBlock( + wp.blocks.createBlock( 'core/paragraph', { + content: '🐡🐢🦀🐤🦋🐘🐧🐹🦁🦄🦍🐼🐿🎃🐴🐝🐆🦕🦔🌱🍇π🍌🐉💧🥨🌌🍂🍠🥦🥚🥝🎟🥥🥒🛵🥖🍒🍯🎾🎲🐺🐚🐮⌛️', + } ) + ); + }, +}; From baa849332e2c6ebab5e36258707398b47006913c Mon Sep 17 00:00:00 2001 From: Jorge <jorge.costa@developer.pt> Date: Fri, 30 Nov 2018 12:57:43 +0000 Subject: [PATCH 165/254] Skip: Adding inline tokens end 2 end test. (#12464) The file uploads are not working on travis, and our end 2 end tests are failing, we are skipping the test until we find a solution to this problem. --- test/e2e/specs/adding-inline-tokens.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/specs/adding-inline-tokens.test.js b/test/e2e/specs/adding-inline-tokens.test.js index d1504f7e35eb7d..ddc67658599dfd 100644 --- a/test/e2e/specs/adding-inline-tokens.test.js +++ b/test/e2e/specs/adding-inline-tokens.test.js @@ -21,7 +21,7 @@ describe( 'adding inline tokens', () => { await newPost(); } ); - it( 'should insert inline image', async () => { + it.skip( 'should insert inline image', async () => { // Create a paragraph. await clickBlockAppender(); await page.keyboard.type( 'a ' ); From 83f03b18ee0cc29d3c309da2c5d9a39392eefa75 Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Fri, 30 Nov 2018 14:54:56 +0100 Subject: [PATCH 166/254] Fix unit tests (linting issue) --- packages/edit-post/src/prevent-event-discovery.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/edit-post/src/prevent-event-discovery.js b/packages/edit-post/src/prevent-event-discovery.js index 41db63231adf3b..97e7b7d8a8fbbf 100644 --- a/packages/edit-post/src/prevent-event-discovery.js +++ b/packages/edit-post/src/prevent-event-discovery.js @@ -8,8 +8,8 @@ export default { } event.preventDefault(); - wp.data.dispatch( 'core/editor' ).insertBlock( - wp.blocks.createBlock( 'core/paragraph', { + window.wp.data.dispatch( 'core/editor' ).insertBlock( + window.wp.blocks.createBlock( 'core/paragraph', { content: '🐡🐢🦀🐤🦋🐘🐧🐹🦁🦄🦍🐼🐿🎃🐴🐝🐆🦕🦔🌱🍇π🍌🐉💧🥨🌌🍂🍠🥦🥚🥝🎟🥥🥒🛵🥖🍒🍯🎾🎲🐺🐚🐮⌛️', } ) ); From 4208c76da608f4aefe3712e082ea8d00490eb5a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20Van=C2=A0Dorpe?= <iseulde@automattic.com> Date: Fri, 30 Nov 2018 15:39:07 +0100 Subject: [PATCH 167/254] RichText: Do not run valueToEditableHTML on every render (#12460) --- packages/editor/src/components/rich-text/index.js | 4 +++- packages/editor/src/components/rich-text/tinymce.js | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/editor/src/components/rich-text/index.js b/packages/editor/src/components/rich-text/index.js index 688a5287052b68..57cea883c75c90 100644 --- a/packages/editor/src/components/rich-text/index.js +++ b/packages/editor/src/components/rich-text/index.js @@ -109,6 +109,7 @@ export class RichText extends Component { this.valueToFormat = this.valueToFormat.bind( this ); this.setRef = this.setRef.bind( this ); this.isActive = this.isActive.bind( this ); + this.valueToEditableHTML = this.valueToEditableHTML.bind( this ); this.formatToValue = memize( this.formatToValue.bind( this ), { size: 1 } ); @@ -874,7 +875,8 @@ export class RichText extends Component { tagName={ Tagname } onSetup={ this.onSetup } style={ style } - defaultValue={ this.valueToEditableHTML( record ) } + record={ record } + valueToEditableHTML={ this.valueToEditableHTML } isPlaceholderVisible={ isPlaceholderVisible } aria-label={ placeholder } aria-autocomplete="list" diff --git a/packages/editor/src/components/rich-text/tinymce.js b/packages/editor/src/components/rich-text/tinymce.js index 7e67e108f85e61..3a825fb995afad 100644 --- a/packages/editor/src/components/rich-text/tinymce.js +++ b/packages/editor/src/components/rich-text/tinymce.js @@ -342,7 +342,8 @@ export default class TinyMCE extends Component { const { tagName = 'div', style, - defaultValue, + record, + valueToEditableHTML, className, isPlaceholderVisible, onPaste, @@ -372,7 +373,7 @@ export default class TinyMCE extends Component { ref: this.bindEditorNode, style, suppressContentEditableWarning: true, - dangerouslySetInnerHTML: { __html: defaultValue }, + dangerouslySetInnerHTML: { __html: valueToEditableHTML( record ) }, onPaste, onInput, onFocus: this.onFocus, From 51e45baa7da6d43017bab9e5763e0c129c79ee0f Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Fri, 30 Nov 2018 15:57:35 +0100 Subject: [PATCH 168/254] Avoid getBlock in block-list/block (#11899) * Avoid getBlock in block-list/block * Update data docs * Fix unit tests post rebase :) * Rename props * Clarify the filter position * Avoid repeating the selector dependants * Fix small typo in the docs * Avoid rerending the current block if the previous block change (#12379) * Avoid rerending the current block if the previous block change * Always show the sibling inserter * Optimize the insertion point component (#12384) * Fix lock file * Fix linting --- .../developers/data/data-core-editor.md | 14 + .../developers/filters/block-filters.md | 31 +- package-lock.json | 2 +- packages/blocks/src/api/utils.js | 13 +- .../block-list/block-invalid-warning.js | 36 ++- .../editor/src/components/block-list/block.js | 289 ++++++++++-------- .../components/block-list/insertion-point.js | 51 ++-- packages/editor/src/hooks/align.js | 11 +- packages/editor/src/hooks/test/align.js | 24 +- packages/editor/src/store/selectors.js | 44 ++- 10 files changed, 287 insertions(+), 228 deletions(-) diff --git a/docs/designers-developers/developers/data/data-core-editor.md b/docs/designers-developers/developers/data/data-core-editor.md index f48dc8539dd2ef..800537ba65d225 100644 --- a/docs/designers-developers/developers/data/data-core-editor.md +++ b/docs/designers-developers/developers/data/data-core-editor.md @@ -411,6 +411,20 @@ Returns whether a block is valid or not. Is Valid. +### getBlockAttributes + +Returns a block's attributes given its client ID, or null if no block exists with +the client ID. + +*Parameters* + + * state: Editor state. + * clientId: Block client ID. + +*Returns* + +Block attributes. + ### getBlock Returns a block given its client ID. This is a parsed copy of the block, diff --git a/docs/designers-developers/developers/filters/block-filters.md b/docs/designers-developers/developers/filters/block-filters.md index 378241356578a0..5f57b9e57e97c5 100644 --- a/docs/designers-developers/developers/filters/block-filters.md +++ b/docs/designers-developers/developers/filters/block-filters.md @@ -113,10 +113,6 @@ wp.hooks.addFilter( ); ``` -#### `blocks.isUnmodifiedDefaultBlock.attributes` - -Used internally by the default block (paragraph) to exclude the attributes from the check if the block was modified. - #### `blocks.switchToBlockType.transformedBlock` Used to filters an individual transform result from block transformation. All of the original blocks are passed, since transformations are many-to-many, not one-to-one. @@ -198,19 +194,13 @@ _Example:_ ```js var el = wp.element.createElement; -var withDataAlign = wp.compose.createHigherOrderComponent( function( BlockListBlock ) { +var withClientIdClassName = wp.compose.createHigherOrderComponent( function( BlockListBlock ) { return function( props ) { var newProps = lodash.assign( {}, props, { - wrapperProps: lodash.assign( - {}, - props.wrapperProps, - { - 'data-align': props.block.attributes.align - } - ) + classsName: "block-" + props.clientId, } ); @@ -219,27 +209,22 @@ var withDataAlign = wp.compose.createHigherOrderComponent( function( BlockListBl newProps ); }; -}, 'withAlign' ); +}, 'withClientIdClassName' ); -wp.hooks.addFilter( 'editor.BlockListBlock', 'my-plugin/with-data-align', withDataAlign ); +wp.hooks.addFilter( 'editor.BlockListBlock', 'my-plugin/with-client-id-class-name', withClientIdClassName ); ``` {% ESNext %} ```js const { createHigherOrderComponent } = wp.compose; -const withDataAlign = createHigherOrderComponent( ( BlockListBlock ) => { +const withClientIdClassName = createHigherOrderComponent( ( BlockListBlock ) => { return ( props ) => { - const { align } = props.block.attributes; - - let wrapperProps = props.wrapperProps; - wrapperProps = { ...wrapperProps, 'data-align': align }; - - return <BlockListBlock { ...props } wrapperProps={ wrapperProps } />; + return <BlockListBlock { ...props } className={ "block-" + props.clientId } />; }; -}, 'withDataAlign' ); +}, 'withClientIdClassName' ); -wp.hooks.addFilter( 'editor.BlockListBlock', 'my-plugin/with-data-align', withDataAlign ); +wp.hooks.addFilter( 'editor.BlockListBlock', 'my-plugin/with-client-id-class-name', withClientIdClassName ); ``` {% end %} diff --git a/package-lock.json b/package-lock.json index b0c66aaf0ab234..09879192a1aafa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7596,7 +7596,7 @@ }, "eslint-plugin-react": { "version": "7.7.0", - "resolved": "http://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.7.0.tgz", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.7.0.tgz", "integrity": "sha512-KC7Snr4YsWZD5flu6A5c0AcIZidzW3Exbqp7OT67OaD2AppJtlBr/GuPrW/vaQM/yfZotEvKAdrxrO+v8vwYJA==", "dev": true, "requires": { diff --git a/packages/blocks/src/api/utils.js b/packages/blocks/src/api/utils.js index d5552a492aaa19..392593b440096e 100644 --- a/packages/blocks/src/api/utils.js +++ b/packages/blocks/src/api/utils.js @@ -1,13 +1,12 @@ /** * External dependencies */ -import { every, has, keys, isEqual, isFunction, isString } from 'lodash'; +import { every, has, isFunction, isString } from 'lodash'; import { default as tinycolor, mostReadable } from 'tinycolor2'; /** * WordPress dependencies */ -import { applyFilters } from '@wordpress/hooks'; import { Component, isValidElement } from '@wordpress/element'; /** @@ -40,14 +39,10 @@ export function isUnmodifiedDefaultBlock( block ) { } const newDefaultBlock = createBlock( defaultBlockName ); + const blockType = getBlockType( defaultBlockName ); - const attributeKeys = applyFilters( 'blocks.isUnmodifiedDefaultBlock.attributes', [ - ...keys( newDefaultBlock.attributes ), - ...keys( block.attributes ), - ] ); - - return every( attributeKeys, ( key ) => - isEqual( newDefaultBlock.attributes[ key ], block.attributes[ key ] ) + return every( blockType.attributes, ( value, key ) => + newDefaultBlock.attributes[ key ] === block.attributes[ key ] ); } diff --git a/packages/editor/src/components/block-list/block-invalid-warning.js b/packages/editor/src/components/block-list/block-invalid-warning.js index 27520ac4b79bd0..bc4555b1a3f0b0 100644 --- a/packages/editor/src/components/block-list/block-invalid-warning.js +++ b/packages/editor/src/components/block-list/block-invalid-warning.js @@ -9,7 +9,8 @@ import { createBlock, rawHandler, } from '@wordpress/blocks'; -import { withDispatch } from '@wordpress/data'; +import { compose } from '@wordpress/compose'; +import { withDispatch, withSelect } from '@wordpress/data'; /** * Internal dependencies @@ -96,18 +97,23 @@ const blockToBlocks = ( block ) => rawHandler( { HTML: block.originalContent, } ); -export default withDispatch( ( dispatch, { block } ) => { - const { replaceBlock } = dispatch( 'core/editor' ); +export default compose( [ + withSelect( ( select, { clientId } ) => ( { + block: select( 'core/editor' ).getBlock( clientId ), + } ) ), + withDispatch( ( dispatch, { block } ) => { + const { replaceBlock } = dispatch( 'core/editor' ); - return { - convertToClassic() { - replaceBlock( block.clientId, blockToClassic( block ) ); - }, - convertToHTML() { - replaceBlock( block.clientId, blockToHTML( block ) ); - }, - convertToBlocks() { - replaceBlock( block.clientId, blockToBlocks( block ) ); - }, - }; -} )( BlockInvalidWarning ); + return { + convertToClassic() { + replaceBlock( block.clientId, blockToClassic( block ) ); + }, + convertToHTML() { + replaceBlock( block.clientId, blockToHTML( block ) ); + }, + convertToBlocks() { + replaceBlock( block.clientId, blockToBlocks( block ) ); + }, + }; + } ), +] )( BlockInvalidWarning ); diff --git a/packages/editor/src/components/block-list/block.js b/packages/editor/src/components/block-list/block.js index 98956980d0d555..2523029c010ed2 100644 --- a/packages/editor/src/components/block-list/block.js +++ b/packages/editor/src/components/block-list/block.js @@ -165,17 +165,21 @@ export class BlockListBlock extends Component { } setAttributes( attributes ) { - const { block, onChange } = this.props; - const type = getBlockType( block.name ); - onChange( block.clientId, attributes ); - - const metaAttributes = reduce( attributes, ( result, value, key ) => { - if ( get( type, [ 'attributes', key, 'source' ] ) === 'meta' ) { - result[ type.attributes[ key ].meta ] = value; - } - - return result; - }, {} ); + const { clientId, name, onChange } = this.props; + const type = getBlockType( name ); + onChange( clientId, attributes ); + + const metaAttributes = reduce( + attributes, + ( result, value, key ) => { + if ( get( type, [ 'attributes', key, 'source' ] ) === 'meta' ) { + result[ type.attributes[ key ].meta ] = value; + } + + return result; + }, + {} + ); if ( size( metaAttributes ) ) { this.props.onMetaChange( metaAttributes ); @@ -207,8 +211,13 @@ export class BlockListBlock extends Component { const { isPartOfMultiSelection, isSelected } = this.props; const { isHovered } = this.state; - if ( isHovered || isPartOfMultiSelection || isSelected || - this.props.isMultiSelecting || this.hadTouchStart ) { + if ( + isHovered || + isPartOfMultiSelection || + isSelected || + this.props.isMultiSelecting || + this.hadTouchStart + ) { return; } @@ -227,8 +236,14 @@ export class BlockListBlock extends Component { } mergeBlocks( forward = false ) { - const { block, previousBlockClientId, nextBlockClientId, onMerge } = this.props; - + const { + clientId, + getPreviousBlockClientId, + getNextBlockClientId, + onMerge, + } = this.props; + const previousBlockClientId = getPreviousBlockClientId( clientId ); + const nextBlockClientId = getNextBlockClientId( clientId ); // Do nothing when it's the first block. if ( ( ! forward && ! previousBlockClientId ) || @@ -238,9 +253,9 @@ export class BlockListBlock extends Component { } if ( forward ) { - onMerge( block.clientId, nextBlockClientId ); + onMerge( clientId, nextBlockClientId ); } else { - onMerge( previousBlockClientId, block.clientId ); + onMerge( previousBlockClientId, clientId ); } } @@ -372,7 +387,6 @@ export class BlockListBlock extends Component { <HoverArea container={ this.wrapperNode }> { ( { hoverArea } ) => { const { - block, order, mode, isFocusMode, @@ -390,52 +404,81 @@ export class BlockListBlock extends Component { isMultiSelecting, isEmptyDefaultBlock, isMovable, - isPreviousBlockADefaultEmptyBlock, isParentOfSelectedBlock, isDraggable, className, + name, + isValid, + attributes, } = this.props; const isHovered = this.state.isHovered && ! isMultiSelecting; - const { name: blockName, isValid } = block; - const blockType = getBlockType( blockName ); + const blockType = getBlockType( name ); // 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(); + const isUnregisteredBlock = 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 && ! showSideInserter && isSelected && ! isTypingWithinBlock; - const shouldAppearHovered = ! isFocusMode && ! hasFixedToolbar && isHovered && ! isEmptyDefaultBlock; + const showEmptyBlockSideInserter = + ( isSelected || isHovered ) && isEmptyDefaultBlock && isValid; + const showSideInserter = + ( isSelected || isHovered ) && isEmptyDefaultBlock; + const shouldAppearSelected = + ! isFocusMode && + ! 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 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; + const shouldShowInsertionPoint = + ( isPartOfMultiSelection && isFirstMultiSelected ) || + ! isPartOfMultiSelection; // 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, - }, className ); + 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, + }, + className + ); const { onReplace } = this.props; @@ -444,7 +487,7 @@ export class BlockListBlock extends Component { if ( blockType.getEditWrapperProps ) { wrapperProps = { ...wrapperProps, - ...blockType.getEditWrapperProps( block.attributes ), + ...blockType.getEditWrapperProps( attributes ), }; } const blockElementId = `block-${ clientId }`; @@ -455,9 +498,9 @@ export class BlockListBlock extends Component { // `BlockHTML`, even in HTML mode. let blockEdit = ( <BlockEdit - name={ blockName } + name={ name } isSelected={ isSelected } - attributes={ block.attributes } + attributes={ attributes } setAttributes={ this.setAttributes } insertBlocksAfter={ isLocked ? undefined : this.insertBlocksAfter } onReplace={ isLocked ? undefined : onReplace } @@ -489,24 +532,20 @@ export class BlockListBlock extends Component { onMouseOverHandled={ this.hideHoverEffects } onMouseLeave={ this.hideHoverEffects } className={ wrapperClassName } - data-type={ block.name } + data-type={ name } onTouchStart={ this.onTouchStart } onFocus={ this.onFocus } onClick={ this.onClick } onKeyDown={ this.deleteOrInsertAfterWrapper } tabIndex="0" aria-label={ blockLabel } - childHandledEvents={ [ - 'onDragStart', - 'onMouseDown', - ] } + childHandledEvents={ [ 'onDragStart', 'onMouseDown' ] } { ...wrapperProps } > { shouldShowInsertionPoint && ( <BlockInsertionPoint clientId={ clientId } rootClientId={ rootClientId } - canShowInserter={ canShowInBetweenInserter } /> ) } <BlockDropZone @@ -521,7 +560,10 @@ export class BlockListBlock extends Component { isFirst={ isFirst } isLast={ isLast } isHidden={ ! ( isHovered || isSelected ) || hoverArea !== 'left' } - isDraggable={ ( isDraggable !== false ) && ( ! isPartOfMultiSelection && isMovable ) } + isDraggable={ + isDraggable !== false && + ( ! isPartOfMultiSelection && isMovable ) + } onDragStart={ this.onDragStart } onDragEnd={ this.onDragEnd } /> @@ -533,25 +575,23 @@ export class BlockListBlock extends Component { { shouldShowBreadcrumb && ( <BlockBreadcrumb clientId={ clientId } - isHidden={ ! ( isHovered || isSelected ) || hoverArea !== 'left' } + isHidden={ + ! ( isHovered || isSelected ) || hoverArea !== 'left' + } /> ) } - { ( - shouldShowContextualToolbar || - this.isForcingContextualToolbar - ) && ( + { ( 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 && + { ! shouldShowContextualToolbar && isSelected && ! hasFixedToolbar && - ! isEmptyDefaultBlock - ) && ( + ! isEmptyDefaultBlock && ( <KeyboardShortcuts bindGlobal eventName="keydown" @@ -574,17 +614,15 @@ export class BlockListBlock extends Component { { ! isValid && [ <BlockInvalidWarning key="invalid-warning" - block={ block } + clientId={ clientId } />, <div key="invalid-preview"> - { getSaveElement( blockType, block.attributes ) } + { getSaveElement( blockType, attributes ) } </div>, ] } </BlockCrashBoundary> { shouldShowMobileToolbar && ( - <BlockMobileToolbar - clientId={ clientId } - /> + <BlockMobileToolbar clientId={ clientId } /> ) } { !! error && <BlockCrashWarning /> } </IgnoreNestedEvents> @@ -615,59 +653,69 @@ export class BlockListBlock extends Component { } } -const applyWithSelect = withSelect( ( select, { clientId, rootClientId, isLargeViewport } ) => { - const { - isBlockSelected, - getPreviousBlockClientId, - getNextBlockClientId, - getBlock, - isAncestorMultiSelected, - isBlockMultiSelected, - isFirstMultiSelectedBlock, - isMultiSelecting, - isTyping, - isCaretWithinFormattedText, - getBlockIndex, - getBlockMode, - isSelectionEnabled, - getSelectedBlocksInitialCaretPosition, - getEditorSettings, - hasSelectedInnerBlock, - getTemplateLock, - } = select( 'core/editor' ); - const isSelected = isBlockSelected( clientId ); - const { hasFixedToolbar, focusMode } = getEditorSettings(); - const block = getBlock( clientId ); - const previousBlockClientId = getPreviousBlockClientId( clientId ); - const previousBlock = getBlock( previousBlockClientId ); - const templateLock = getTemplateLock( rootClientId ); - const isParentOfSelectedBlock = hasSelectedInnerBlock( clientId, true ); - - return { - nextBlockClientId: getNextBlockClientId( clientId ), - isPartOfMultiSelection: isBlockMultiSelected( clientId ) || isAncestorMultiSelected( clientId ), - isFirstMultiSelected: isFirstMultiSelectedBlock( clientId ), - isMultiSelecting: isMultiSelecting(), - // We only care about this prop when the block is selected - // Thus to avoid unnecessary rerenders we avoid updating the prop if the block is not selected. - isTypingWithinBlock: ( isSelected || isParentOfSelectedBlock ) && isTyping(), - isCaretWithinFormattedText: isCaretWithinFormattedText(), - order: getBlockIndex( clientId, rootClientId ), - mode: getBlockMode( clientId ), - isSelectionEnabled: isSelectionEnabled(), - initialPosition: getSelectedBlocksInitialCaretPosition(), - isEmptyDefaultBlock: block && isUnmodifiedDefaultBlock( block ), - isPreviousBlockADefaultEmptyBlock: previousBlock && isUnmodifiedDefaultBlock( previousBlock ), - isMovable: 'all' !== templateLock, - isLocked: !! templateLock, - isFocusMode: focusMode && isLargeViewport, - hasFixedToolbar: hasFixedToolbar && isLargeViewport, - previousBlockClientId, - block, - isSelected, - isParentOfSelectedBlock, - }; -} ); +const applyWithSelect = withSelect( + ( select, { clientId, rootClientId, isLargeViewport } ) => { + const { + isBlockSelected, + getBlockName, + isBlockValid, + getBlockAttributes, + isAncestorMultiSelected, + isBlockMultiSelected, + isFirstMultiSelectedBlock, + isMultiSelecting, + isTyping, + isCaretWithinFormattedText, + getBlockIndex, + getBlockMode, + isSelectionEnabled, + getSelectedBlocksInitialCaretPosition, + getEditorSettings, + hasSelectedInnerBlock, + getTemplateLock, + getPreviousBlockClientId, + getNextBlockClientId, + } = select( 'core/editor' ); + const isSelected = isBlockSelected( clientId ); + const { hasFixedToolbar, focusMode } = getEditorSettings(); + const templateLock = getTemplateLock( rootClientId ); + const isParentOfSelectedBlock = hasSelectedInnerBlock( clientId, true ); + const name = getBlockName( clientId ); + const attributes = getBlockAttributes( clientId ); + + return { + isPartOfMultiSelection: + isBlockMultiSelected( clientId ) || isAncestorMultiSelected( clientId ), + isFirstMultiSelected: isFirstMultiSelectedBlock( clientId ), + isMultiSelecting: isMultiSelecting(), + // We only care about this prop when the block is selected + // Thus to avoid unnecessary rerenders we avoid updating the prop if the block is not selected. + isTypingWithinBlock: + ( isSelected || isParentOfSelectedBlock ) && isTyping(), + isCaretWithinFormattedText: isCaretWithinFormattedText(), + order: getBlockIndex( clientId, rootClientId ), + mode: getBlockMode( clientId ), + isSelectionEnabled: isSelectionEnabled(), + initialPosition: getSelectedBlocksInitialCaretPosition(), + isEmptyDefaultBlock: + name && isUnmodifiedDefaultBlock( { name, attributes } ), + isValid: isBlockValid( clientId ), + isMovable: 'all' !== templateLock, + isLocked: !! templateLock, + isFocusMode: focusMode && isLargeViewport, + hasFixedToolbar: hasFixedToolbar && isLargeViewport, + name, + attributes, + isSelected, + isParentOfSelectedBlock, + + // We only care about these selectors when events are triggered. + // We call them dynamically in the event handlers to avoid unnecessary re-renders. + getPreviousBlockClientId, + getNextBlockClientId, + }; + } +); const applyWithDispatch = withDispatch( ( dispatch, ownProps, { select } ) => { const { getBlockSelectionStart } = select( 'core/editor' ); @@ -729,8 +777,9 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps, { select } ) => { } ); export default compose( + withFilters( 'editor.BlockListBlock' ), withViewportMatch( { isLargeViewport: 'medium' } ), applyWithSelect, applyWithDispatch, - withFilters( 'editor.BlockListBlock' ), + withFilters( 'editor.__experimentalBlockListBlock' ) )( BlockListBlock ); diff --git a/packages/editor/src/components/block-list/insertion-point.js b/packages/editor/src/components/block-list/insertion-point.js index ba5c98d21924f3..252925cc3a6c09 100644 --- a/packages/editor/src/components/block-list/insertion-point.js +++ b/packages/editor/src/components/block-list/insertion-point.js @@ -6,7 +6,6 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { isUnmodifiedDefaultBlock } from '@wordpress/blocks'; import { Component } from '@wordpress/element'; import { withSelect } from '@wordpress/data'; @@ -47,7 +46,6 @@ class BlockInsertionPoint extends Component { const { isInserterFocused } = this.state; const { showInsertionPoint, - canShowInserter, rootClientId, insertIndex, } = this.props; @@ -57,29 +55,27 @@ class BlockInsertionPoint extends Component { { showInsertionPoint && ( <div className="editor-block-list__insertion-point-indicator" /> ) } - { canShowInserter && ( - <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, - } ) - } - > - <Inserter - rootClientId={ rootClientId } - index={ insertIndex } - /> - </div> - ) } + <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, + } ) + } + > + <Inserter + rootClientId={ rootClientId } + index={ insertIndex } + /> + </div> </div> ); } @@ -88,18 +84,15 @@ export default withSelect( ( select, { clientId, rootClientId } ) => { const { getBlockIndex, getBlockInsertionPoint, - getBlock, isBlockInsertionPointVisible, } = select( 'core/editor' ); const blockIndex = getBlockIndex( clientId, rootClientId ); const insertIndex = blockIndex; const insertionPoint = getBlockInsertionPoint(); - const block = getBlock( clientId ); const showInsertionPoint = ( isBlockInsertionPointVisible() && insertionPoint.index === insertIndex && - insertionPoint.rootClientId === rootClientId && - ! isUnmodifiedDefaultBlock( block ) + insertionPoint.rootClientId === rootClientId ); return { showInsertionPoint, insertIndex }; diff --git a/packages/editor/src/hooks/align.js b/packages/editor/src/hooks/align.js index 7b7062ba558f5f..46adcc0ba3ede8 100644 --- a/packages/editor/src/hooks/align.js +++ b/packages/editor/src/hooks/align.js @@ -141,12 +141,11 @@ export const withToolbarControls = createHigherOrderComponent( // Exported just for testing purposes, not exported outside the module. export const insideSelectWithDataAlign = ( BlockListBlock ) => ( ( props ) => { - const { block, hasWideEnabled } = props; - const { name: blockName } = block; - const { align } = block.attributes; + const { name, attributes, hasWideEnabled } = props; + const { align } = attributes; const validAlignments = getValidAlignments( - getBlockSupport( blockName, 'align' ), - hasBlockSupport( blockName, 'alignWide', true ), + getBlockSupport( name, 'align' ), + hasBlockSupport( name, 'alignWide', true ), hasWideEnabled ); @@ -207,7 +206,7 @@ export function addAssignedAlign( props, blockType, attributes ) { } addFilter( 'blocks.registerBlockType', 'core/align/addAttribute', addAttribute ); -addFilter( 'editor.BlockListBlock', 'core/editor/align/with-data-align', withDataAlign ); +addFilter( 'editor.__experimentalBlockListBlock', 'core/editor/align/with-data-align', withDataAlign ); addFilter( 'editor.BlockEdit', 'core/editor/align/with-toolbar-controls', withToolbarControls ); addFilter( 'blocks.getSaveContent.extraProps', 'core/align/addAssignedAlign', addAssignedAlign ); diff --git a/packages/editor/src/hooks/test/align.js b/packages/editor/src/hooks/test/align.js index 5d3f3dba87c287..995d3a9a59c4c3 100644 --- a/packages/editor/src/hooks/test/align.js +++ b/packages/editor/src/hooks/test/align.js @@ -175,12 +175,10 @@ describe( 'align', () => { const wrapper = renderer.create( <EnhancedComponent - block={ { - name: 'core/foo', - attributes: { - align: 'wide', - }, + attributes={ { + align: 'wide', } } + name="core/foo" /> ); expect( wrapper.toTree().rendered.props.wrapperProps ).toEqual( { @@ -203,11 +201,9 @@ describe( 'align', () => { const wrapper = renderer.create( <EnhancedComponent - block={ { - name: 'core/foo', - attributes: { - align: 'wide', - }, + name="core/foo" + attributes={ { + align: 'wide', } } hasWideEnabled={ false } /> @@ -230,11 +226,9 @@ describe( 'align', () => { const wrapper = renderer.create( <EnhancedComponent - block={ { - name: 'core/foo', - attributes: { - align: 'wide', - }, + name="core/foo" + attributes={ { + align: 'wide', } } /> ); diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index 099abab1b49d86..d8b019930d1a07 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -627,17 +627,15 @@ export function isBlockValid( state, clientId ) { } /** - * Returns a block given its client ID. This is a parsed copy of the block, - * containing its `blockName`, `clientId`, and current `attributes` state. This - * is not the block's registration settings, which must be retrieved from the - * blocks module registration store. + * Returns a block's attributes given its client ID, or null if no block exists with + * the client ID. * * @param {Object} state Editor state. * @param {string} clientId Block client ID. * - * @return {Object} Parsed block object. + * @return {Object?} Block attributes. */ -export const getBlock = createSelector( +export const getBlockAttributes = createSelector( ( state, clientId ) => { const block = state.editor.present.blocks.byClientId[ clientId ]; if ( ! block ) { @@ -665,18 +663,44 @@ export const getBlock = createSelector( }, attributes ); } + return attributes; + }, + ( state, clientId ) => [ + state.editor.present.blocks.byClientId[ clientId ], + state.editor.present.edits.meta, + state.initialEdits.meta, + state.currentPost.meta, + ] +); + +/** + * Returns a block given its client ID. This is a parsed copy of the block, + * containing its `blockName`, `clientId`, and current `attributes` state. This + * is not the block's registration settings, which must be retrieved from the + * blocks module registration store. + * + * @param {Object} state Editor state. + * @param {string} clientId Block client ID. + * + * @return {Object} Parsed block object. + */ +export const getBlock = createSelector( + ( state, clientId ) => { + const block = state.editor.present.blocks.byClientId[ clientId ]; + if ( ! block ) { + return null; + } + return { ...block, - attributes, + attributes: getBlockAttributes( state, clientId ), innerBlocks: getBlocks( state, clientId ), }; }, ( state, clientId ) => [ state.editor.present.blocks.byClientId[ clientId ], getBlockDependantsCacheBust( state, clientId ), - state.editor.present.edits.meta, - state.initialEdits.meta, - state.currentPost.meta, + ...getBlockAttributes.getDependants( state, clientId ), ] ); From 990f640f968b9a5b50ecbc3b9734b78beb9a95a0 Mon Sep 17 00:00:00 2001 From: Jorge <jorge.costa@developer.pt> Date: Fri, 30 Nov 2018 15:09:50 +0000 Subject: [PATCH 169/254] Add: Permalink panel toggle mechanism (#12132) --- .../src/components/options-modal/index.js | 1 + .../test/__snapshots__/index.js.snap | 4 +++ .../src/components/sidebar/post-link/index.js | 6 ++-- .../e2e/specs/sidebar-permalink-panel.test.js | 35 +++++++++++++++++++ 4 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 test/e2e/specs/sidebar-permalink-panel.test.js diff --git a/packages/edit-post/src/components/options-modal/index.js b/packages/edit-post/src/components/options-modal/index.js index 2206847f19d590..51b40719eb4881 100644 --- a/packages/edit-post/src/components/options-modal/index.js +++ b/packages/edit-post/src/components/options-modal/index.js @@ -42,6 +42,7 @@ export function OptionsModal( { isModalActive, closeModal } ) { <EnableTipsOption label={ __( 'Enable Tips' ) } /> </Section> <Section title={ __( 'Document Panels' ) }> + <EnablePanelOption label={ __( 'Permalink' ) } panelName="post-link" /> <PostTaxonomies taxonomyWrapper={ ( content, taxonomy ) => ( <EnablePanelOption diff --git a/packages/edit-post/src/components/options-modal/test/__snapshots__/index.js.snap b/packages/edit-post/src/components/options-modal/test/__snapshots__/index.js.snap index f5764f8ab4accf..1367ebd0834191 100644 --- a/packages/edit-post/src/components/options-modal/test/__snapshots__/index.js.snap +++ b/packages/edit-post/src/components/options-modal/test/__snapshots__/index.js.snap @@ -25,6 +25,10 @@ exports[`OptionsModal should match snapshot when the modal is active 1`] = ` <Section title="Document Panels" > + <WithSelect(IfCondition(WithDispatch(BaseOption))) + label="Permalink" + panelName="post-link" + /> <WithSelect(PostTaxonomies) taxonomyWrapper={[Function]} /> diff --git a/packages/edit-post/src/components/sidebar/post-link/index.js b/packages/edit-post/src/components/sidebar/post-link/index.js index 64b694a21ab8b5..4b614805e03019 100644 --- a/packages/edit-post/src/components/sidebar/post-link/index.js +++ b/packages/edit-post/src/components/sidebar/post-link/index.js @@ -116,6 +116,7 @@ export default compose( [ getEditedPostAttribute, } = select( 'core/editor' ); const { + isEditorPanelEnabled, isEditorPanelOpened, } = select( 'core/edit-post' ); const { @@ -132,14 +133,15 @@ export default compose( [ isPublished: isCurrentPostPublished(), isOpened: isEditorPanelOpened( PANEL_NAME ), permalinkParts: getPermalinkParts(), + isEnabled: isEditorPanelEnabled( PANEL_NAME ), isViewable: get( postType, [ 'viewable' ], false ), postTitle: getEditedPostAttribute( 'title' ), postSlug: getEditedPostAttribute( 'slug' ), postID: id, }; } ), - ifCondition( ( { isNew, postLink, isViewable } ) => { - return ! isNew && postLink && isViewable; + ifCondition( ( { isEnabled, isNew, postLink, isViewable } ) => { + return isEnabled && ! isNew && postLink && isViewable; } ), withDispatch( ( dispatch ) => { const { toggleEditorPanelOpened } = dispatch( 'core/edit-post' ); diff --git a/test/e2e/specs/sidebar-permalink-panel.test.js b/test/e2e/specs/sidebar-permalink-panel.test.js new file mode 100644 index 00000000000000..03e24ff34ec3db --- /dev/null +++ b/test/e2e/specs/sidebar-permalink-panel.test.js @@ -0,0 +1,35 @@ +/** + * Internal dependencies + */ +import { + findSidebarPanelWithTitle, + newPost, + openDocumentSettingsSidebar, + publishPost, +} from '../support/utils'; + +// This tests are not together with the remaining sidebar tests, +// because we need to publish/save a post, to correctly test the permalink panel. +// The sidebar test suit enforces that focus is never lost, but during save operations +// the focus is lost and a new element is focused once the save is completed. +describe( 'Sidebar Permalink Panel', () => { + it( 'should not render permalink sidebar panel while the post is new', async () => { + await newPost(); + await openDocumentSettingsSidebar(); + expect( await findSidebarPanelWithTitle( 'Permalink' ) ).toBeUndefined(); + } ); + + it( 'should render permalink sidebar panel after the post is published and allow its removal', async () => { + await newPost(); + await page.keyboard.type( 'aaaaa' ); + await publishPost(); + // Start editing again. + await page.type( '.editor-post-title__input', ' (Updated)' ); + expect( await findSidebarPanelWithTitle( 'Permalink' ) ).toBeDefined(); + await page.evaluate( () => { + const { removeEditorPanel } = wp.data.dispatch( 'core/edit-post' ); + removeEditorPanel( 'post-link' ); + } ); + expect( await findSidebarPanelWithTitle( 'Permalink' ) ).toBeUndefined(); + } ); +} ); From a92216bb6f46a99712aef6124e5cf1a2ea6384a2 Mon Sep 17 00:00:00 2001 From: Jorge <jorge.costa@developer.pt> Date: Fri, 30 Nov 2018 15:30:01 +0000 Subject: [PATCH 170/254] Add: action to set category icon; Fix: Managing block categories documentation (#11640) The documentation of "Managing block categories" was wrong as on the server we can not set SVG icons. The server would not be able to use our SVG component. This PR fixes the documentation and adds a new action that allows updating the icon on the front end in a simple way. --- .../developers/data/data-core-blocks.md | 11 ++- .../developers/filters/block-filters.md | 18 +++- .../block-library/src/categories/index.php | 2 +- packages/blocks/src/api/categories.js | 10 +++ packages/blocks/src/api/index.js | 1 + packages/blocks/src/store/actions.js | 16 ++++ packages/blocks/src/store/reducer.js | 35 +++++++- packages/blocks/src/store/test/reducer.js | 83 +++++++++++++++++++ 8 files changed, 168 insertions(+), 8 deletions(-) diff --git a/docs/designers-developers/developers/data/data-core-blocks.md b/docs/designers-developers/developers/data/data-core-blocks.md index aea753fad47576..abdb30b903773c 100644 --- a/docs/designers-developers/developers/data/data-core-blocks.md +++ b/docs/designers-developers/developers/data/data-core-blocks.md @@ -224,4 +224,13 @@ Returns an action object used to set block categories. *Parameters* - * categories: Block categories. \ No newline at end of file + * categories: Block categories. + +### updateCategory + +Returns an action object used to update a category. + +*Parameters* + + * slug: Block category slug. + * category: Object containing the category properties that should be updated. \ No newline at end of file diff --git a/docs/designers-developers/developers/filters/block-filters.md b/docs/designers-developers/developers/filters/block-filters.md index 5f57b9e57e97c5..a8df36979dd1e0 100644 --- a/docs/designers-developers/developers/filters/block-filters.md +++ b/docs/designers-developers/developers/filters/block-filters.md @@ -314,7 +314,7 @@ function my_plugin_block_categories( $categories, $post ) { array( 'slug' => 'my-category', 'title' => __( 'My category', 'my-plugin' ), - 'icon' => '<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path fill="none" d="M0 0h24v24H0V0z" /><path d="M19 13H5v-2h14v2z" /></svg>', + 'icon' => 'wordpress', ), ) ); @@ -322,4 +322,18 @@ function my_plugin_block_categories( $categories, $post ) { add_filter( 'block_categories', 'my_plugin_block_categories', 10, 2 ); ``` -You can also display an icon with your block category by setting an `icon` attribute. The value can be the slug of a [WordPress Dashicon](https://developer.wordpress.org/resource/dashicons/), or a custom `svg` element. +You can also display an icon with your block category by setting an `icon` attribute.The value can be the slug of a [WordPress Dashicon](https://developer.wordpress.org/resource/dashicons/). + +It is possible to set an SVG as the icon of the category if a custom icon is needed.To do so, the icon should be rendered and set on the frontend, so it can make use of WordPress SVG, allowing mobile compatibility and making the icon more accessible. + +To set an SVG icon for the category shown in the previous example, add the following example JavaScript code to the editor calling `wp.blocks.updateCategory` e.g: +```js +( function() { + var el = wp.element.createElement; + var SVG = wp.components.SVG; + var circle = el( 'circle', { cx: 10, cy: 10, r: 10, fill: 'red', stroke: 'blue', strokeWidth: '10' } ); + var svgIcon = el( SVG, { width: 20, height: 20, viewBox: '0 0 20 20'}, circle); + wp.blocks.updateCategory( 'my-category', { icon: svgIcon } ); +} )(); +``` + diff --git a/packages/block-library/src/categories/index.php b/packages/block-library/src/categories/index.php index f302d9ba745383..57042046061d26 100644 --- a/packages/block-library/src/categories/index.php +++ b/packages/block-library/src/categories/index.php @@ -72,7 +72,7 @@ function build_dropdown_script_block_core_categories( $dropdown_id ) { ?> <script type='text/javascript'> /* <![CDATA[ */ - (function() { + ( function() { var dropdown = document.getElementById( '<?php echo esc_js( $dropdown_id ); ?>' ); function onCatChange() { if ( dropdown.options[ dropdown.selectedIndex ].value > 0 ) { diff --git a/packages/blocks/src/api/categories.js b/packages/blocks/src/api/categories.js index 51d2254943aecd..55945bc973bfcd 100644 --- a/packages/blocks/src/api/categories.js +++ b/packages/blocks/src/api/categories.js @@ -20,3 +20,13 @@ export function getCategories() { export function setCategories( categories ) { dispatch( 'core/blocks' ).setCategories( categories ); } + +/** + * Updates a category. + * + * @param {string} slug Block category slug. + * @param {Object} category Object containing the category properties that should be updated. + */ +export function updateCategory( slug, category ) { + dispatch( 'core/blocks' ).updateCategory( slug, category ); +} diff --git a/packages/blocks/src/api/index.js b/packages/blocks/src/api/index.js index ad115edd06095e..c13b28f4903ca0 100644 --- a/packages/blocks/src/api/index.js +++ b/packages/blocks/src/api/index.js @@ -24,6 +24,7 @@ export { isValidBlockContent } from './validation'; export { getCategories, setCategories, + updateCategory, } from './categories'; export { registerBlockType, diff --git a/packages/blocks/src/store/actions.js b/packages/blocks/src/store/actions.js index bef332bb2b5fc6..8f2d9c834d26dc 100644 --- a/packages/blocks/src/store/actions.js +++ b/packages/blocks/src/store/actions.js @@ -120,3 +120,19 @@ export function setCategories( categories ) { categories, }; } + +/** + * Returns an action object used to update a category. + * + * @param {string} slug Block category slug. + * @param {Object} category Object containing the category properties that should be updated. + * + * @return {Object} Action object. + */ +export function updateCategory( slug, category ) { + return { + type: 'UPDATE_CATEGORY', + slug, + category, + }; +} diff --git a/packages/blocks/src/store/reducer.js b/packages/blocks/src/store/reducer.js index 0ce2c1e88ef3f8..bfeeec10ec1e10 100644 --- a/packages/blocks/src/store/reducer.js +++ b/packages/blocks/src/store/reducer.js @@ -1,7 +1,17 @@ /** * External dependencies */ -import { keyBy, omit, mapValues, get, uniqBy, filter, map } from 'lodash'; +import { + filter, + find, + get, + isEmpty, + keyBy, + map, + mapValues, + omit, + uniqBy, +} from 'lodash'; /** * WordPress dependencies @@ -124,10 +134,27 @@ export const unregisteredFallbackBlockName = createBlockNameSetterReducer( 'SET_ * @return {Object} Updated state. */ export function categories( state = DEFAULT_CATEGORIES, action ) { - if ( action.type === 'SET_CATEGORIES' ) { - return action.categories || []; + switch ( action.type ) { + case 'SET_CATEGORIES': + return action.categories || []; + case 'UPDATE_CATEGORY': { + if ( ! action.category || isEmpty( action.category ) ) { + return state; + } + const categoryToChange = find( state, [ 'slug', action.slug ] ); + if ( categoryToChange ) { + return map( state, ( category ) => { + if ( category.slug === action.slug ) { + return { + ...category, + ...action.category, + }; + } + return category; + } ); + } + } } - return state; } diff --git a/packages/blocks/src/store/test/reducer.js b/packages/blocks/src/store/test/reducer.js index dcd2a15c18e087..249b86ea6f197a 100644 --- a/packages/blocks/src/store/test/reducer.js +++ b/packages/blocks/src/store/test/reducer.js @@ -226,4 +226,87 @@ describe( 'categories', () => { { slug: 'wings', title: 'Wings' }, ] ); } ); + + it( 'should add the category icon', () => { + const original = deepFreeze( [ { + slug: 'chicken', + title: 'Chicken', + } ] ); + + const state = categories( original, { + type: 'UPDATE_CATEGORY', + slug: 'chicken', + category: { + icon: 'new-icon', + }, + } ); + + expect( state ).toEqual( [ { + slug: 'chicken', + title: 'Chicken', + icon: 'new-icon', + } ] ); + } ); + + it( 'should update the category icon', () => { + const original = deepFreeze( [ { + slug: 'chicken', + title: 'Chicken', + icon: 'old-icon', + }, { + slug: 'wings', + title: 'Wings', + icon: 'old-icon', + } ] ); + + const state = categories( original, { + type: 'UPDATE_CATEGORY', + slug: 'chicken', + category: { + icon: 'new-icon', + }, + } ); + + expect( state ).toEqual( [ { + slug: 'chicken', + title: 'Chicken', + icon: 'new-icon', + }, { + slug: 'wings', + title: 'Wings', + icon: 'old-icon', + } ] ); + } ); + + it( 'should update multiple category properties', () => { + const original = deepFreeze( [ { + slug: 'chicken', + title: 'Chicken', + icon: 'old-icon', + }, { + slug: 'wings', + title: 'Wings', + icon: 'old-icon', + } ] ); + + const state = categories( original, { + type: 'UPDATE_CATEGORY', + slug: 'wings', + category: { + title: 'New Wings', + chicken: 'ribs', + }, + } ); + + expect( state ).toEqual( [ { + slug: 'chicken', + title: 'Chicken', + icon: 'old-icon', + }, { + slug: 'wings', + title: 'New Wings', + chicken: 'ribs', + icon: 'old-icon', + } ] ); + } ); } ); From 4178f2da468fe85505acd659bbd8fd3c95dcd7c9 Mon Sep 17 00:00:00 2001 From: Andrew Duthie <andrew@andrewduthie.com> Date: Fri, 30 Nov 2018 11:29:43 -0500 Subject: [PATCH 171/254] RichText: Reuse DOM document across calls to createEmpty (#12402) * RichText: Reuse DOM document across calls to createEmpty * RichText: Reuse document in createElement * Rich Text: Update tests to clone node to retain reference * Rich Text: Refactor createEmpty to use createElement --- packages/rich-text/CHANGELOG.md | 6 ++++++ packages/rich-text/src/create-element.js | 18 +++++++++++++++--- packages/rich-text/src/test/to-dom.js | 4 ++-- packages/rich-text/src/to-dom.js | 16 ++++++++++++---- 4 files changed, 35 insertions(+), 9 deletions(-) diff --git a/packages/rich-text/CHANGELOG.md b/packages/rich-text/CHANGELOG.md index 0e9ef7674dc481..aadcae6d607998 100644 --- a/packages/rich-text/CHANGELOG.md +++ b/packages/rich-text/CHANGELOG.md @@ -1,3 +1,9 @@ +## 3.0.3 (Unreleased) + +### Internal + +- Internal performance optimizations to avoid excessive expensive creation of DOM documents. + ## 3.0.2 (2018-11-21) ## 3.0.1 (2018-11-20) diff --git a/packages/rich-text/src/create-element.js b/packages/rich-text/src/create-element.js index 0e2f6f3572fdf8..eebbe450075e80 100644 --- a/packages/rich-text/src/create-element.js +++ b/packages/rich-text/src/create-element.js @@ -1,13 +1,25 @@ /** * Parse the given HTML into a body element. * + * Note: The current implementation will return a shared reference, reset on + * each call to `createElement`. Therefore, you should not hold a reference to + * the value to operate upon asynchronously, as it may have unexpected results. + * * @param {HTMLDocument} document The HTML document to use to parse. * @param {string} html The HTML to parse. * * @return {HTMLBodyElement} Body element with parsed HTML. */ export function createElement( { implementation }, html ) { - const { body } = implementation.createHTMLDocument( '' ); - body.innerHTML = html; - return body; + // Because `createHTMLDocument` is an expensive operation, and with this + // function being internal to `rich-text` (full control in avoiding a risk + // of asynchronous operations on the shared reference), a single document + // is reused and reset for each call to the function. + if ( ! createElement.body ) { + createElement.body = implementation.createHTMLDocument( '' ).body; + } + + createElement.body.innerHTML = html; + + return createElement.body; } diff --git a/packages/rich-text/src/test/to-dom.js b/packages/rich-text/src/test/to-dom.js index 92cc47aa28846e..16fed3f66d0a26 100644 --- a/packages/rich-text/src/test/to-dom.js +++ b/packages/rich-text/src/test/to-dom.js @@ -66,8 +66,8 @@ describe( 'applyValue', () => { cases.forEach( ( { current, future, description, movedCount } ) => { it( description, () => { - const body = createElement( document, current ); - const futureBody = createElement( document, future ); + const body = createElement( document, current ).cloneNode( true ); + const futureBody = createElement( document, future ).cloneNode( true ); const childNodes = Array.from( futureBody.childNodes ); applyValue( futureBody, body ); const count = childNodes.reduce( ( acc, { parentNode } ) => { diff --git a/packages/rich-text/src/to-dom.js b/packages/rich-text/src/to-dom.js index b582c77083d9a0..1c34d8680dd2b3 100644 --- a/packages/rich-text/src/to-dom.js +++ b/packages/rich-text/src/to-dom.js @@ -3,6 +3,7 @@ */ import { toTree } from './to-tree'; +import { createElement } from './create-element'; /** * Browser dependencies @@ -58,10 +59,17 @@ function getNodeByPath( node, path ) { }; } -function createEmpty() { - const { body } = document.implementation.createHTMLDocument( '' ); - return body; -} +/** + * Returns a new instance of a DOM tree upon which RichText operations can be + * applied. + * + * Note: The current implementation will return a shared reference, reset on + * each call to `createEmpty`. Therefore, you should not hold a reference to + * the value to operate upon asynchronously, as it may have unexpected results. + * + * @return {WPRichTextTree} RichText tree. + */ +const createEmpty = () => createElement( document, '' ); function append( element, child ) { if ( typeof child === 'string' ) { From 459984e0988d7fc0d4dd4e6696ae42f25d9bbbec Mon Sep 17 00:00:00 2001 From: Andrew Duthie <andrew@andrewduthie.com> Date: Fri, 30 Nov 2018 14:48:35 -0500 Subject: [PATCH 172/254] Edit Post: Select blocks only once multiple verified (#12477) --- packages/edit-post/src/hooks/validate-multiple-use/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/edit-post/src/hooks/validate-multiple-use/index.js b/packages/edit-post/src/hooks/validate-multiple-use/index.js index e3876b0f5fe106..74d6b183ee62e9 100644 --- a/packages/edit-post/src/hooks/validate-multiple-use/index.js +++ b/packages/edit-post/src/hooks/validate-multiple-use/index.js @@ -33,7 +33,6 @@ const enhance = compose( * @return {Component} Enhanced component with merged state data props. */ withSelect( ( select, block ) => { - const blocks = select( 'core/editor' ).getBlocks(); const multiple = hasBlockSupport( block.name, 'multiple', true ); // For block types with `multiple` support, there is no "original @@ -44,6 +43,7 @@ const enhance = compose( // Otherwise, only pass `originalBlockClientId` if it refers to a different // block from the current one. + const blocks = select( 'core/editor' ).getBlocks(); const firstOfSameType = find( blocks, ( { name } ) => block.name === name ); const isInvalid = firstOfSameType && firstOfSameType.clientId !== block.clientId; return { From b650f4ac70fc903079e40ecdc9b6120286ce17f6 Mon Sep 17 00:00:00 2001 From: Dennis Snell <dennis.snell@automattic.com> Date: Fri, 30 Nov 2018 13:13:40 -0700 Subject: [PATCH 173/254] Parser: Make attribute parsing possessive (#12342) * Parser: Make attribute parsing possessive Bug introduced in #11369 Someone discovered high CPU usage due to catastrophic backtracking on an invalid block comment delimiter. The following input crashed the parser on the server: ```html <!-- wp:block {"a":0} / --> ``` The optimization introduced in #11369 ended up opening a place for backtracking that shouldn't be there. In this patch we're grouping the attribute parsing section of the tokenizing RegExp pattern so that we can make the group itself _possessive_ so that we abort any backtracking. * add test and fix broken fix * really fix default JS parser * add explanatory comment * add @since comment with updated target version * version bumps --- .../parser.php | 8 +++- .../src/index.js | 46 ++++++++++++++++++- .../shared-tests.js | 9 ++++ 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/packages/block-serialization-default-parser/parser.php b/packages/block-serialization-default-parser/parser.php index c9fa5db1f32e9f..216cb9a5f78e87 100644 --- a/packages/block-serialization-default-parser/parser.php +++ b/packages/block-serialization-default-parser/parser.php @@ -359,6 +359,7 @@ function proceed() { * * @internal * @since 3.8.0 + * @since 4.6.1 fixed a bug in attribute parsing which caused catastrophic backtracking on invalid block comments * @return array */ function next_token() { @@ -373,13 +374,18 @@ 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, $this->offset ); + // if we get here we probably have catastrophic backtracking or out-of-memory in the PCRE + if ( false === $has_match ) { + return array( 'no-more-tokens', null, null, null, null ); + } + // we have no more tokens if ( 0 === $has_match ) { return array( 'no-more-tokens', null, null, null, null ); diff --git a/packages/block-serialization-default-parser/src/index.js b/packages/block-serialization-default-parser/src/index.js index 936c4a380969cc..489f1f76bb1bb2 100644 --- a/packages/block-serialization-default-parser/src/index.js +++ b/packages/block-serialization-default-parser/src/index.js @@ -2,7 +2,49 @@ 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; + +/** + * Matches block comment delimiters + * + * While most of this pattern is straightforward the attribute parsing + * incorporates a tricks to make sure we don't choke on specific input + * + * - since JavaScript has no possessive quantifier or atomic grouping + * we are emulating it with a trick + * + * we want a possessive quantifier or atomic group to prevent backtracking + * on the `}`s should we fail to match the remainder of the pattern + * + * we can emulate this with a positive lookahead and back reference + * (a++)*c === ((?=(a+))\1)*c + * + * let's examine an example: + * - /(a+)*c/.test('aaaaaaaaaaaaad') fails after over 49,000 steps + * - /(a++)*c/.test('aaaaaaaaaaaaad') fails after 85 steps + * - /(?>a+)*c/.test('aaaaaaaaaaaaad') fails after 126 steps + * + * this is because the possessive `++` and the atomic group `(?>)` + * tell the engine that all those `a`s belong together as a single group + * and so it won't split it up when stepping backwards to try and match + * + * if we use /((?=(a+))\1)*c/ then we get the same behavior as the atomic group + * or possessive and prevent the backtracking because the `a+` is matched but + * not captured. thus, we find the long string of `a`s and remember it, then + * reference it as a whole unit inside our pattern + * + * @cite http://instanceof.me/post/52245507631/regex-emulate-atomic-grouping-with-lookahead + * @cite http://blog.stevenlevithan.com/archives/mimic-atomic-groups + * @cite https://javascript.info/regexp-infinite-backtracking-problem + * + * once browsers reliably support atomic grouping or possessive + * quantifiers natively we should remove this trick and simplify + * + * @type RegExp + * + * @since 3.8.0 + * @since 4.6.1 added optimization to prevent backtracking on attribute parsing + */ +const tokenizer = /<!--\s+(\/)?wp:([a-z][a-z0-9_-]*\/)?([a-z][a-z0-9_-]*)\s+({(?:(?=([^}]+|}+(?=})|(?!}\s+\/?-->)[^])*)\5|[^]*?)}\s+)?(\/)?-->/g; function Block( blockName, attrs, innerBlocks, innerHTML, innerContent ) { return { @@ -189,7 +231,7 @@ function nextToken() { } const startedAt = matches.index; - const [ match, closerMatch, namespaceMatch, nameMatch, attrsMatch, voidMatch ] = matches; + const [ match, closerMatch, namespaceMatch, nameMatch, attrsMatch, /* internal/unused */, voidMatch ] = matches; const length = match.length; const isCloser = !! closerMatch; diff --git a/packages/block-serialization-spec-parser/shared-tests.js b/packages/block-serialization-spec-parser/shared-tests.js index 19f3c9b4d70791..0e5a29b75f052f 100644 --- a/packages/block-serialization-spec-parser/shared-tests.js +++ b/packages/block-serialization-spec-parser/shared-tests.js @@ -155,6 +155,15 @@ export const jsTester = ( parse ) => () => { expect( () => parsed = parse( `<!-- wp:fake {"a":"${ as }"} /-->` )[ 0 ] ).not.toThrow(); expect( parsed.attrs.a ).toHaveLength( length ); } ); + + describe( 'invalid block comment syntax', () => { + test( 'extra space after void closer', () => { + let parsed; + + expect( () => parsed = parse( '<!-- wp:block / -->' )[ 0 ] ).not.toThrow(); + expect( parsed.blockName ).toBeNull(); + } ); + } ); } ); }; From 2094200532f7e436f77ca48946feb93951abef91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20Van=C2=A0Dorpe?= <iseulde@automattic.com> Date: Fri, 30 Nov 2018 22:25:31 +0100 Subject: [PATCH 174/254] RichText: selectionChange: bind on focus, unbind on blur (#12480) * RichText: selectionChange: bind on focus, unbind on blur * Remove isActive check --- .../editor/src/components/rich-text/index.js | 26 ++++++------------- .../src/components/rich-text/tinymce.js | 2 ++ 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/packages/editor/src/components/rich-text/index.js b/packages/editor/src/components/rich-text/index.js index 57cea883c75c90..7fc04677d84b7c 100644 --- a/packages/editor/src/components/rich-text/index.js +++ b/packages/editor/src/components/rich-text/index.js @@ -93,6 +93,7 @@ export class RichText extends Component { this.onSetup = this.onSetup.bind( this ); this.onFocus = this.onFocus.bind( this ); + this.onBlur = this.onBlur.bind( this ); this.onChange = this.onChange.bind( this ); this.onDeleteKeyDown = this.onDeleteKeyDown.bind( this ); this.onKeyDown = this.onKeyDown.bind( this ); @@ -108,7 +109,6 @@ export class RichText extends Component { this.isEmpty = this.isEmpty.bind( this ); this.valueToFormat = this.valueToFormat.bind( this ); this.setRef = this.setRef.bind( this ); - this.isActive = this.isActive.bind( this ); this.valueToEditableHTML = this.valueToEditableHTML.bind( this ); this.formatToValue = memize( this.formatToValue.bind( this ), { size: 1 } ); @@ -129,22 +129,10 @@ export class RichText extends Component { this.lastHistoryValue = value; } - componentDidMount() { - document.addEventListener( 'selectionchange', this.onSelectionChange ); - } - - componentWillUnmount() { - document.removeEventListener( 'selectionchange', this.onSelectionChange ); - } - setRef( node ) { this.editableRef = node; } - isActive() { - return this.editableRef === document.activeElement; - } - /** * Handles the onSetup event for the TinyMCE component. * @@ -354,6 +342,12 @@ export class RichText extends Component { if ( unstableOnFocus ) { unstableOnFocus(); } + + document.addEventListener( 'selectionchange', this.onSelectionChange ); + } + + onBlur() { + document.removeEventListener( 'selectionchange', this.onSelectionChange ); } /** @@ -392,11 +386,6 @@ export class RichText extends Component { * Handles the `selectionchange` event: sync the selection to local state. */ onSelectionChange() { - // Ensure it's the active element. This is a global event. - if ( ! this.isActive() ) { - return; - } - const { start, end, formats } = this.createRecord(); if ( start !== this.state.start || end !== this.state.end ) { @@ -891,6 +880,7 @@ export class RichText extends Component { onCompositionEnd={ this.onCompositionEnd } onKeyDown={ this.onKeyDown } onFocus={ this.onFocus } + onBlur={ this.onBlur } multilineTag={ this.multilineTag } multilineWrapperTags={ this.multilineWrapperTags } setRef={ this.setRef } diff --git a/packages/editor/src/components/rich-text/tinymce.js b/packages/editor/src/components/rich-text/tinymce.js index 3a825fb995afad..a29117bdce24d8 100644 --- a/packages/editor/src/components/rich-text/tinymce.js +++ b/packages/editor/src/components/rich-text/tinymce.js @@ -350,6 +350,7 @@ export default class TinyMCE extends Component { onInput, onKeyDown, onCompositionEnd, + onBlur, } = this.props; /* @@ -377,6 +378,7 @@ export default class TinyMCE extends Component { onPaste, onInput, onFocus: this.onFocus, + onBlur, onKeyDown, onCompositionEnd, } ); From 68367b4b9c869900eca07b9a9b3750ee52915c46 Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Fri, 30 Nov 2018 22:32:49 +0100 Subject: [PATCH 175/254] Bump the plugin version to 4.6.1 (#12481) * Update package changelogs * Bump plugin version to 4.6.1 * chore(release): publish - @wordpress/block-library@2.2.9 - @wordpress/block-serialization-default-parser@2.0.1 - @wordpress/block-serialization-spec-parser@2.0.1 - @wordpress/blocks@6.0.3 - @wordpress/edit-post@3.1.4 - @wordpress/editor@9.0.4 - @wordpress/format-library@1.2.7 * Update changelogs after the package release --- gutenberg.php | 2 +- package-lock.json | 2 +- package.json | 2 +- packages/block-library/CHANGELOG.md | 4 ++++ packages/block-library/package.json | 2 +- packages/block-serialization-default-parser/CHANGELOG.md | 2 ++ packages/block-serialization-default-parser/package.json | 2 +- packages/block-serialization-spec-parser/CHANGELOG.md | 2 ++ packages/block-serialization-spec-parser/package.json | 2 +- packages/blocks/CHANGELOG.md | 2 ++ packages/blocks/package.json | 2 +- packages/components/CHANGELOG.md | 2 ++ packages/edit-post/CHANGELOG.md | 4 ++++ packages/edit-post/package.json | 2 +- packages/editor/CHANGELOG.md | 6 +++++- packages/editor/package.json | 2 +- packages/format-library/CHANGELOG.md | 4 ++++ packages/format-library/package.json | 2 +- packages/list-reusable-blocks/CHANGELOG.md | 2 ++ packages/nux/CHANGELOG.md | 2 ++ 20 files changed, 39 insertions(+), 11 deletions(-) diff --git a/gutenberg.php b/gutenberg.php index 084de1ef1a84f0..2f75a595903e79 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.6.0 + * Version: 4.6.1 * Author: Gutenberg Team * * @package gutenberg diff --git a/package-lock.json b/package-lock.json index 09879192a1aafa..260677072102d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "4.6.0", + "version": "4.6.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index fc4236a9b3910e..541ebbfa9df50d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "4.6.0", + "version": "4.6.1", "private": true, "description": "A new WordPress editor experience", "repository": "git+https://github.com/WordPress/gutenberg.git", diff --git a/packages/block-library/CHANGELOG.md b/packages/block-library/CHANGELOG.md index 8a54a337049ce4..e27ca871be3848 100644 --- a/packages/block-library/CHANGELOG.md +++ b/packages/block-library/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.2.9 (2018-11-30) + +## 2.2.8 (2018-11-30) + ## 2.2.7 (2018-11-22) ## 2.2.6 (2018-11-21) diff --git a/packages/block-library/package.json b/packages/block-library/package.json index 22d04b8f506ee2..ad9c4514349cc4 100644 --- a/packages/block-library/package.json +++ b/packages/block-library/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-library", - "version": "2.2.8", + "version": "2.2.9", "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/CHANGELOG.md b/packages/block-serialization-default-parser/CHANGELOG.md index e651970cc3bdcb..6c4f4c53b06453 100644 --- a/packages/block-serialization-default-parser/CHANGELOG.md +++ b/packages/block-serialization-default-parser/CHANGELOG.md @@ -1,3 +1,5 @@ +## 2.0.1 (2018-11-30) + ## 2.0.0 (2018-11-12) ### Breaking Changes diff --git a/packages/block-serialization-default-parser/package.json b/packages/block-serialization-default-parser/package.json index aed2a538f5961d..1a235b320cd06e 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": "2.0.0", + "version": "2.0.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/CHANGELOG.md b/packages/block-serialization-spec-parser/CHANGELOG.md index d31eb025e02759..12ffab6521d47e 100644 --- a/packages/block-serialization-spec-parser/CHANGELOG.md +++ b/packages/block-serialization-spec-parser/CHANGELOG.md @@ -1,3 +1,5 @@ +## 2.0.1 (2018-11-30) + ## 2.0.0 (2018-11-12) ### Breaking Change diff --git a/packages/block-serialization-spec-parser/package.json b/packages/block-serialization-spec-parser/package.json index 73a8859f5feac5..251a28045fb958 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": "2.0.0", + "version": "2.0.1", "description": "Block serialization specification parser for WordPress posts.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/blocks/CHANGELOG.md b/packages/blocks/CHANGELOG.md index f343aa557aeaa3..33ca7db882fbd1 100644 --- a/packages/blocks/CHANGELOG.md +++ b/packages/blocks/CHANGELOG.md @@ -1,3 +1,5 @@ +## 6.0.3 (2018-11-30) + ## 6.0.2 (2018-11-21) ## 6.0.1 (2018-11-20) diff --git a/packages/blocks/package.json b/packages/blocks/package.json index 0235f2c45f3e78..1a662f7a286264 100644 --- a/packages/blocks/package.json +++ b/packages/blocks/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/blocks", - "version": "6.0.2", + "version": "6.0.3", "description": "Block API for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 9e3643db94c675..fb93a60cd4712d 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -1,3 +1,5 @@ +## 7.0.3 (2018-11-30) + ## 7.0.2 (2018-11-22) ## 7.0.1 (2018-11-21) diff --git a/packages/edit-post/CHANGELOG.md b/packages/edit-post/CHANGELOG.md index 055d3a6b47b011..3fdf099954c982 100644 --- a/packages/edit-post/CHANGELOG.md +++ b/packages/edit-post/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.1.4 (2018-11-30) + +## 3.1.3 (2018-11-30) + ## 3.1.2 (2018-11-22) ## 3.1.1 (2018-11-21) diff --git a/packages/edit-post/package.json b/packages/edit-post/package.json index 4124d52ce4cdd5..6c98e2826302c5 100644 --- a/packages/edit-post/package.json +++ b/packages/edit-post/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/edit-post", - "version": "3.1.3", + "version": "3.1.4", "description": "Edit Post module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/editor/CHANGELOG.md b/packages/editor/CHANGELOG.md index d5b4c4b4bae694..69dbe834a4aeb1 100644 --- a/packages/editor/CHANGELOG.md +++ b/packages/editor/CHANGELOG.md @@ -1,9 +1,13 @@ -## 9.0.3 (Unreleased) +## 9.0.5 (Unreleased) ### Bug Fixes - `getEditedPostAttribute` now correctly returns the merged result of edits as a partial change when given `'meta'` as the `attributeName`. +## 9.0.4 (2018-11-30) + +## 9.0.3 (2018-11-30) + ## 9.0.2 (2018-11-22) ## 9.0.1 (2018-11-21) diff --git a/packages/editor/package.json b/packages/editor/package.json index 831c8ef25adf61..dfe9ac51ed3fdf 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/editor", - "version": "9.0.3", + "version": "9.0.4", "description": "Building blocks for WordPress editors.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/format-library/CHANGELOG.md b/packages/format-library/CHANGELOG.md index a010e7aba5e378..cab02a21043113 100644 --- a/packages/format-library/CHANGELOG.md +++ b/packages/format-library/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.2.7 (2018-11-30) + +## 1.2.6 (2018-11-30) + ## 1.2.5 (2018-11-22) ## 1.2.4 (2018-11-21) diff --git a/packages/format-library/package.json b/packages/format-library/package.json index 01f9cdd51de698..9c78a6ba2eedc9 100644 --- a/packages/format-library/package.json +++ b/packages/format-library/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/format-library", - "version": "1.2.6", + "version": "1.2.7", "description": "Format library for the WordPress editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/list-reusable-blocks/CHANGELOG.md b/packages/list-reusable-blocks/CHANGELOG.md index e39a225e3366de..8e8526a7f98e62 100644 --- a/packages/list-reusable-blocks/CHANGELOG.md +++ b/packages/list-reusable-blocks/CHANGELOG.md @@ -1,3 +1,5 @@ +## 1.1.16 (2018-11-30) + ## 1.1.15 (2018-11-22) ## 1.1.14 (2018-11-21) diff --git a/packages/nux/CHANGELOG.md b/packages/nux/CHANGELOG.md index c087f42d7432e9..ffe9ba7ca937ec 100644 --- a/packages/nux/CHANGELOG.md +++ b/packages/nux/CHANGELOG.md @@ -1,3 +1,5 @@ +## 3.0.4 (2018-11-30) + ## 3.0.3 (2018-11-22) ## 3.0.2 (2018-11-21) From 92eab640629ddc975b25fd5180395ed32f5550ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20=28Greg=29=20Zi=C3=B3=C5=82kowski?= <grzegorz@gziolo.pl> Date: Mon, 3 Dec 2018 10:41:27 +0100 Subject: [PATCH 176/254] Allow links in Plugins group in the More Menu (#12309) * Allow links in Plugins group in the More Menu * Docs: Add documentation for PluginMoreMenuItem * Tests: Add unit tests for PluginMoreMenuItem * Refactor PluginSidebarMoreMenuItem to use PluginMoreMenuItem * Docs: Add note about supported props for PluginMoreMenuItem * Edit post: Use props to pass children in PluginMoreMenuItem * Use href rather than url prop in PluginMoreMenuItem component --- package-lock.json | 10 +-- packages/edit-post/src/README.md | 77 +++++++++++++++++++ .../header/plugin-more-menu-item/index.js | 35 +++++++++ .../test/__snapshots__/index.js.snap | 44 +++++++++++ .../plugin-more-menu-item/test/index.js | 54 +++++++++++++ .../plugin-sidebar-more-menu-item/index.js | 23 +++--- packages/edit-post/src/index.js | 1 + 7 files changed, 225 insertions(+), 19 deletions(-) create mode 100644 packages/edit-post/src/components/header/plugin-more-menu-item/index.js create mode 100644 packages/edit-post/src/components/header/plugin-more-menu-item/test/__snapshots__/index.js.snap create mode 100644 packages/edit-post/src/components/header/plugin-more-menu-item/test/index.js diff --git a/package-lock.json b/package-lock.json index 260677072102d2..df6980879a3426 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11733,7 +11733,7 @@ }, "jest-environment-jsdom": { "version": "22.4.3", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-22.4.3.tgz", + "resolved": "http://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-22.4.3.tgz", "integrity": "sha512-FviwfR+VyT3Datf13+ULjIMO5CSeajlayhhYQwpzgunswoaLIPutdbrnfUHEMyJCwvqQFaVtTmn9+Y8WCt6n1w==", "dev": true, "requires": { @@ -11896,7 +11896,7 @@ }, "jest-get-type": { "version": "22.4.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-22.4.3.tgz", + "resolved": "http://registry.npmjs.org/jest-get-type/-/jest-get-type-22.4.3.tgz", "integrity": "sha512-/jsz0Y+V29w1chdXVygEKSz2nBoHoYqNShPe+QgxSNjAuP1i8+k4LbQNrfoliKej0P45sivkSCh7yiD6ubHS3w==", "dev": true }, @@ -12178,7 +12178,7 @@ }, "jest-message-util": { "version": "22.4.3", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-22.4.3.tgz", + "resolved": "http://registry.npmjs.org/jest-message-util/-/jest-message-util-22.4.3.tgz", "integrity": "sha512-iAMeKxhB3Se5xkSjU0NndLLCHtP4n+GtCqV0bISKA5dmOXQfEbdEmYiu2qpnWBDCQdEafNDDU6Q+l6oBMd/+BA==", "dev": true, "requires": { @@ -12267,7 +12267,7 @@ }, "jest-mock": { "version": "22.4.3", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-22.4.3.tgz", + "resolved": "http://registry.npmjs.org/jest-mock/-/jest-mock-22.4.3.tgz", "integrity": "sha512-+4R6mH5M1G4NK16CKg9N1DtCaFmuxhcIqF4lQK/Q1CIotqMs/XBemfpDPeVZBFow6iyUNu6EBT9ugdNOTT5o5Q==", "dev": true }, @@ -12821,7 +12821,7 @@ }, "jest-util": { "version": "22.4.3", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-22.4.3.tgz", + "resolved": "http://registry.npmjs.org/jest-util/-/jest-util-22.4.3.tgz", "integrity": "sha512-rfDfG8wyC5pDPNdcnAlZgwKnzHvZDu8Td2NJI/jAGKEGxJPYiE4F0ss/gSAkG4778Y23Hvbz+0GMrDJTeo7RjQ==", "dev": true, "requires": { diff --git a/packages/edit-post/src/README.md b/packages/edit-post/src/README.md index 7dceb8f8b08a73..91d5c14ed93e14 100644 --- a/packages/edit-post/src/README.md +++ b/packages/edit-post/src/README.md @@ -181,6 +181,83 @@ The [Dashicon](https://developer.wordpress.org/resource/dashicons/) icon slug st - Required: No - Default: _inherits from the plugin_ +### `PluginMoreMenuItem` + +Renders a menu item in `Plugins` group in `More Menu` drop down, and can be used to as a button or link depending on the props provided. +The text within the component appears as the menu item label. + +_Example:_ + +{% codetabs %} + +{% ES5 %} +```js +var __ = wp.i18n.__; +var PluginMoreMenuItem = wp.editPost.PluginMoreMenuItem; +var el = wp.element.createElement; + +function onButtonClick() { + alert( 'Button clicked.' ); +} + +function MyButtonMoreMenuItem() { + return el( + PluginMoreMenuItem, + { + icon: 'smiley', + onClick: onButtonClick + }, + __( 'My button title' ) + ) +} +``` + +{% ESNext %} +```jsx +const { __ } = wp.i18n; +const { PluginMoreMenuItem } = wp.editPost; + +function onButtonClick() { + alert( 'Button clicked.' ); +} + +const MyButtonMoreMenuItem = () => ( + <PluginMoreMenuItem + icon="smiley" + onClick={ onButtonClick } + > + { __( 'My button title' ) } + </PluginMoreMenuItem> +); +``` +{% end %} + +#### Props + +`PluginMoreMenuItem` supports the following props. Any additional props are passed through to the underlying [MenuItem](../../components/src/menu-item) component. + +##### href + +When `href` is provided then the menu item is represented as an anchor rather than button. It corresponds to the `href` attribute of the anchor. + +- Type: `String` +- Required: No + +##### icon + +The [Dashicon](https://developer.wordpress.org/resource/dashicons/) icon slug string, or an SVG WP element, to be rendered to the left of the menu item label. + +- Type: `String` | `Element` +- Required: No +- Default: _inherits from the plugin_ + +##### onClick + +The callback function to be executed when the user clicks the menu item. + +- Type: `function` +- Required: No +- Default: _function which does nothing_ ### `PluginSidebarMoreMenuItem` diff --git a/packages/edit-post/src/components/header/plugin-more-menu-item/index.js b/packages/edit-post/src/components/header/plugin-more-menu-item/index.js new file mode 100644 index 00000000000000..2f2e279c7616ac --- /dev/null +++ b/packages/edit-post/src/components/header/plugin-more-menu-item/index.js @@ -0,0 +1,35 @@ +/** + * External dependencies + */ +import { noop } from 'lodash'; + +/** + * WordPress dependencies + */ +import { compose } from '@wordpress/compose'; +import { MenuItem } from '@wordpress/components'; +import { withPluginContext } from '@wordpress/plugins'; + +/** + * Internal dependencies + */ +import PluginsMoreMenuGroup from '../plugins-more-menu-group'; + +const PluginMoreMenuItem = ( { onClick = noop, ...props } ) => ( + <PluginsMoreMenuGroup> + { ( fillProps ) => ( + <MenuItem + { ...props } + onClick={ compose( onClick, fillProps.onClose ) } + /> + ) } + </PluginsMoreMenuGroup> +); + +export default compose( + withPluginContext( ( context, ownProps ) => { + return { + icon: ownProps.icon || context.icon, + }; + } ), +)( PluginMoreMenuItem ); diff --git a/packages/edit-post/src/components/header/plugin-more-menu-item/test/__snapshots__/index.js.snap b/packages/edit-post/src/components/header/plugin-more-menu-item/test/__snapshots__/index.js.snap new file mode 100644 index 00000000000000..33e7e8889c3e5a --- /dev/null +++ b/packages/edit-post/src/components/header/plugin-more-menu-item/test/__snapshots__/index.js.snap @@ -0,0 +1,44 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PluginMoreMenuItem renders menu item as button properly 1`] = ` +<div + className="components-menu-group" +> + <div + className="components-menu-group__label" + id="components-menu-group-label-0" + > + Plugins + </div> + <div + aria-labelledby="components-menu-group-label-0" + aria-orientation="vertical" + onKeyDown={[Function]} + role="menu" + > + <button + aria-label="My plugin button menu item" + className="components-button components-icon-button components-menu-item__button has-icon" + onClick={[Function]} + role="menuitem" + type="button" + > + <svg + aria-hidden="true" + className="dashicon dashicons-smiley" + focusable="false" + height={20} + role="img" + viewBox="0 0 20 20" + width={20} + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M7 5.2c1.1 0 2 .89 2 2 0 .37-.11.71-.28 1C8.72 8.2 8 8 7 8s-1.72.2-1.72.2c-.17-.29-.28-.63-.28-1 0-1.11.9-2 2-2zm6 0c1.11 0 2 .89 2 2 0 .37-.11.71-.28 1 0 0-.72-.2-1.72-.2s-1.72.2-1.72.2c-.17-.29-.28-.63-.28-1 0-1.11.89-2 2-2zm-3 13.7c3.72 0 7.03-2.36 8.23-5.88l-1.32-.46C15.9 15.52 13.12 17.5 10 17.5s-5.9-1.98-6.91-4.94l-1.32.46c1.2 3.52 4.51 5.88 8.23 5.88z" + /> + </svg> + My plugin button menu item + </button> + </div> +</div> +`; diff --git a/packages/edit-post/src/components/header/plugin-more-menu-item/test/index.js b/packages/edit-post/src/components/header/plugin-more-menu-item/test/index.js new file mode 100644 index 00000000000000..9ab1b35920bcc8 --- /dev/null +++ b/packages/edit-post/src/components/header/plugin-more-menu-item/test/index.js @@ -0,0 +1,54 @@ +/** + * External dependencies + */ +import { noop } from 'lodash'; +import ReactTestRenderer from 'react-test-renderer'; + +/** + * WordPress dependencies + */ +import { SlotFillProvider } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import PluginMoreMenuItem from '../'; +import PluginsMoreMenuGroup from '../../plugins-more-menu-group'; + +describe( 'PluginMoreMenuItem', () => { + const fillProps = { + onClose: noop, + }; + + test( 'renders menu item as button properly', () => { + const component = ReactTestRenderer.create( + <SlotFillProvider> + <PluginMoreMenuItem + icon="smiley" + > + My plugin button menu item + </PluginMoreMenuItem> + <PluginsMoreMenuGroup.Slot fillProps={ fillProps } /> + </SlotFillProvider> + ); + + expect( component.toJSON() ).toMatchSnapshot(); + } ); + + test( 'renders menu item as link properly', () => { + const url = 'https://make.wordpress.org'; + const component = ReactTestRenderer.create( + <SlotFillProvider> + <PluginMoreMenuItem + icon="smiley" + href={ url } + > + My plugin link menu item + </PluginMoreMenuItem> + <PluginsMoreMenuGroup.Slot fillProps={ fillProps } /> + </SlotFillProvider> + ); + + expect( component.root.findByType( 'a' ).props.href ).toBe( url ); + } ); +} ); diff --git a/packages/edit-post/src/components/header/plugin-sidebar-more-menu-item/index.js b/packages/edit-post/src/components/header/plugin-sidebar-more-menu-item/index.js index 403e8aeb1c3e7e..0579ccec8a3b07 100644 --- a/packages/edit-post/src/components/header/plugin-sidebar-more-menu-item/index.js +++ b/packages/edit-post/src/components/header/plugin-sidebar-more-menu-item/index.js @@ -3,27 +3,22 @@ */ import { compose } from '@wordpress/compose'; import { withSelect, withDispatch } from '@wordpress/data'; -import { MenuItem } from '@wordpress/components'; import { withPluginContext } from '@wordpress/plugins'; /** * Internal dependencies */ -import PluginsMoreMenuGroup from '../plugins-more-menu-group'; +import PluginMoreMenuItem from '../plugin-more-menu-item'; const PluginSidebarMoreMenuItem = ( { children, icon, isSelected, onClick } ) => ( - <PluginsMoreMenuGroup> - { ( fillProps ) => ( - <MenuItem - icon={ isSelected ? 'yes' : icon } - isSelected={ isSelected } - role="menuitemcheckbox" - onClick={ compose( onClick, fillProps.onClose ) } - > - { children } - </MenuItem> - ) } - </PluginsMoreMenuGroup> + <PluginMoreMenuItem + icon={ isSelected ? 'yes' : icon } + isSelected={ isSelected } + role="menuitemcheckbox" + onClick={ onClick } + > + { children } + </PluginMoreMenuItem> ); export default compose( diff --git a/packages/edit-post/src/index.js b/packages/edit-post/src/index.js index 26a2c71ea446c6..bce8285f72b0b4 100644 --- a/packages/edit-post/src/index.js +++ b/packages/edit-post/src/index.js @@ -88,6 +88,7 @@ export function initializeEditor( id, postType, postId, settings, initialEdits ) } export { default as PluginBlockSettingsMenuItem } from './components/block-settings-menu/plugin-block-settings-menu-item'; +export { default as PluginMoreMenuItem } from './components/header/plugin-more-menu-item'; export { default as PluginPostPublishPanel } from './components/sidebar/plugin-post-publish-panel'; export { default as PluginPostStatusInfo } from './components/sidebar/plugin-post-status-info'; export { default as PluginPrePublishPanel } from './components/sidebar/plugin-pre-publish-panel'; From 522d683492d756eb250d6c8c0ac97650b81a3e19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20=28Greg=29=20Zi=C3=B3=C5=82kowski?= <grzegorz@gziolo.pl> Date: Mon, 3 Dec 2018 16:20:21 +0100 Subject: [PATCH 177/254] Docs: Stop using `el` variable in edit post docs (#12534) * Docs: Stop using el variable in edit post docs * Docs: Move extending the post editor UI to package README --- packages/edit-post/README.md | 505 ++++++++++++++++++++++++++++++ packages/edit-post/src/README.md | 508 ------------------------------- 2 files changed, 505 insertions(+), 508 deletions(-) delete mode 100644 packages/edit-post/src/README.md diff --git a/packages/edit-post/README.md b/packages/edit-post/README.md index c37c4b87904452..a150193a78a61a 100644 --- a/packages/edit-post/README.md +++ b/packages/edit-post/README.md @@ -14,4 +14,509 @@ npm install @wordpress/edit-post _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)._ +## Extending the post editor UI + +Extending the editor UI can be accomplished with the `registerPlugin` API, allowing you to define all your plugin's UI elements in one place. + +Refer to [the plugins module documentation](../plugins/) for more information. + +## Plugin Components + +The following components can be used with the `registerPlugin` ([see documentation](../plugins)) API. +They can be found in the global variable `wp.editPost` when defining `wp-edit-post` as a script dependency. + +### `PluginBlockSettingsMenuItem` + +Renders a new item in the block settings menu. + +Example: + +{% codetabs %} + +{% ES5 %} +```js +var __ = wp.i18n.__; +var PluginBlockSettingsMenuItem = wp.editPost.PluginBlockSettingsMenuItem; + +function doOnClick(){ + // To be called when the user clicks the menu item. +} + +function MyPluginBlockSettingsMenuItem() { + return wp.element.createElement( + PluginBlockSettingsMenuItem, + { + allowedBlockNames: [ 'core/paragraph' ], + icon: 'dashicon-name', + label: __( 'Menu item text' ), + onClick: doOnClick, + } + ); +} +``` + +{% ESNext %} +```jsx +import { __ } from wp.i18n; +import { PluginBlockSettingsMenuItem } from wp.editPost; + +const doOnClick = ( ) => { + // To be called when the user clicks the menu item. +}; + +const MyPluginBlockSettingsMenuItem = () => ( + <PluginBlockSettingsMenuItem + allowedBlockNames=[ 'core/paragraph' ] + icon='dashicon-name' + label=__( 'Menu item text' ) + onClick={ doOnClick } /> +); +``` + +{% end %} + +#### Props + +##### allowedBlockNames + +An array containing a whitelist of block names for which the item should be shown. If this prop is not present the item will be rendered for any block. If multiple blocks are selected, it'll be shown if and only if all of them are in the whitelist. + +- Type: `Array` +- Required: No +- Default: Menu item is shown for any block + +##### icon + +The [Dashicon](https://developer.wordpress.org/resource/dashicons/) icon slug string, or an SVG WP element, to be rendered to the left of the menu item label. + +- Type: `String` | `Element` +- Required: No +- Default: Menu item wil be rendered without icon + +##### label + +A string containing the menu item text. + +- Type: `String` +- Required: Yes + +##### onClick + +The callback function to be executed when the user clicks the menu item. + +- Type: `function` +- Required: Yes + +### `PluginSidebar` + +Renders a sidebar when activated. The contents within the `PluginSidebar` will appear as content within the sidebar. + +If you wish to display the sidebar, you can with use the [`PluginSidebarMoreMenuItem`](#pluginsidebarmoremenuitem) component or the `wp.data.dispatch` API: +```js +wp.data.dispatch( 'core/edit-post' ).openGeneralSidebar( 'plugin-name/sidebar-name' ); +``` + +_Example:_ + +{% codetabs %} + +{% ES5 %} +```js +var __ = wp.i18n.__; +var el = wp.element.createElement; +var PanelBody = wp.components.PanelBody; +var PluginSidebar = wp.editPost.PluginSidebar; + + +function MyPluginSidebar() { + return el( + PluginSidebar, + { + name: 'my-sidebar', + title: 'My sidebar title', + icon: 'smiley', + }, + el( + PanelBody, + {}, + __( 'My sidebar content' ) + ) + ); +} +``` + +{% ESNext %} +```jsx +const { __ } = wp.i18n; +const { PanelBody } = wp.components; +const { PluginSidebar } = wp.editPost; + +const MyPluginSidebar = () => ( + <PluginSidebar + name="my-sidebar" + title="My sidebar title" + icon="smiley" + > + <PanelBody> + { __( 'My sidebar content' ) } + </PanelBody> + </PluginSidebar> +); +``` +{% end %} + +#### Props + +##### name + +A string identifying the sidebar. Must be unique for every sidebar registered within the scope of your plugin. + +- Type: `String` +- Required: Yes + +##### title + +Title displayed at the top of the sidebar. + +- Type: `String` +- Required: Yes + +##### isPinnable + +Whether to allow to pin sidebar to toolbar. + +- Type: `Boolean` +- Required: No +- Default: `true` + +##### icon + +The [Dashicon](https://developer.wordpress.org/resource/dashicons/) icon slug string, or an SVG WP element, to be rendered when the sidebar is pinned to toolbar. + +- Type: `String` | `Element` +- Required: No +- Default: _inherits from the plugin_ + +### `PluginMoreMenuItem` + +Renders a menu item in `Plugins` group in `More Menu` drop down, and can be used to as a button or link depending on the props provided. +The text within the component appears as the menu item label. + +_Example:_ + +{% codetabs %} + +{% ES5 %} +```js +var __ = wp.i18n.__; +var PluginMoreMenuItem = wp.editPost.PluginMoreMenuItem; + +function onButtonClick() { + alert( 'Button clicked.' ); +} + +function MyButtonMoreMenuItem() { + return wp.element.createElement( + PluginMoreMenuItem, + { + icon: 'smiley', + onClick: onButtonClick + }, + __( 'My button title' ) + ) +} +``` + +{% ESNext %} +```jsx +const { __ } = wp.i18n; +const { PluginMoreMenuItem } = wp.editPost; + +function onButtonClick() { + alert( 'Button clicked.' ); +} + +const MyButtonMoreMenuItem = () => ( + <PluginMoreMenuItem + icon="smiley" + onClick={ onButtonClick } + > + { __( 'My button title' ) } + </PluginMoreMenuItem> +); +``` +{% end %} + +#### Props + +`PluginMoreMenuItem` supports the following props. Any additional props are passed through to the underlying [MenuItem](../components/src/menu-item) component. + +##### href + +When `href` is provided then the menu item is represented as an anchor rather than button. It corresponds to the `href` attribute of the anchor. + +- Type: `String` +- Required: No + +##### icon + +The [Dashicon](https://developer.wordpress.org/resource/dashicons/) icon slug string, or an SVG WP element, to be rendered to the left of the menu item label. + +- Type: `String` | `Element` +- Required: No +- Default: _inherits from the plugin_ + +##### onClick + +The callback function to be executed when the user clicks the menu item. + +- Type: `function` +- Required: No +- Default: _function which does nothing_ + +### `PluginSidebarMoreMenuItem` + +Renders a menu item in `Plugins` group in `More Menu` drop down, and can be used to activate the corresponding `PluginSidebar` component. +The text within the component appears as the menu item label. + +_Example:_ + +{% codetabs %} + +{% ES5 %} +```js +var __ = wp.i18n.__; +var PluginSidebarMoreMenuItem = wp.editPost.PluginSidebarMoreMenuItem; + +function MySidebarMoreMenuItem() { + return wp.element.createElement( + PluginSidebarMoreMenuItem, + { + target: 'my-sidebar', + icon: 'smiley', + }, + __( 'My sidebar title' ) + ) +} +``` + +{% ESNext %} +```jsx +const { __ } = wp.i18n; +const { PluginSidebarMoreMenuItem } = wp.editPost; + +const MySidebarMoreMenuItem = () => ( + <PluginSidebarMoreMenuItem + target="my-sidebar" + icon="smiley" + > + { __( 'My sidebar title' ) } + </PluginSidebarMoreMenuItem> +); +``` +{% end %} + +#### Props + +##### target + +A string identifying the target sidebar you wish to be activated by this menu item. Must be the same as the `name` prop you have given to that sidebar. + +- Type: `String` +- Required: Yes + +##### icon + +The [Dashicon](https://developer.wordpress.org/resource/dashicons/) icon slug string, or an SVG WP element, to be rendered to the left of the menu item label. + +- Type: `String` | `Element` +- Required: No +- Default: _inherits from the plugin_ + + +### `PluginPostStatusInfo` + +Renders a row in the Status & Visibility panel of the Document sidebar. +It should be noted that this is named and implemented around the function it serves and not its location, which may change in future iterations. + +_Example:_ + +{% codetabs %} + +{% ES5 %} +```js +var __ = wp.i18n.__; +var PluginPostStatusInfo = wp.editPost.PluginPostStatusInfo; + +function MyPluginPostStatusInfo() { + return wp.element.createElement( + PluginPostStatusInfo, + { + className: 'my-plugin-post-status-info', + }, + __( 'My post status info' ) + ) +} +``` + +{% ESNext %} +```jsx +const { __ } = wp.i18n; +const { PluginPostStatusInfo } = wp.editPost; + +const MyPluginPostStatusInfo = () => ( + <PluginPostStatusInfo + className="my-plugin-post-status-info" + > + { __( 'My post status info' ) } + </PluginPostStatusInfo> +); +``` +{% end %} + +#### Props + +##### className + +An optional class name added to the row. + +- Type: `String` +- Required: No + +### `PluginPrePublishPanel` + +Renders provided content to the pre-publish side panel in the publish flow (side panel that opens when a user first pushes "Publish" from the main editor). + +_Example:_ + +{% codetabs %} + +{% ES5 %} +```js +var __ = wp.i18n.__; +var PluginPrePublishPanel = wp.editPost.PluginPrePublishPanel; + +function MyPluginPrePublishPanel() { + return wp.element.createElement( + PluginPrePublishPanel, + { + className: 'my-plugin-pre-publish-panel', + title: __( 'My panel title' ), + initialOpen: true, + }, + __( 'My panel content' ) + ); +} +``` + +{% ESNext %} +```jsx +const { __ } = wp.i18n; +const { PluginPrePublishPanel } = wp.editPost; + +const MyPluginPrePublishPanel = () => ( + <PluginPrePublishPanel + className="my-plugin-pre-publish-panel" + title={ __( 'My panel title' ) } + initialOpen={ true } + > + { __( 'My panel content' ) } + </PluginPrePublishPanel> +); +``` +{% end %} + +#### Props + +##### className + +An optional class name added to the panel. + +- Type: `String` +- Required: No + +##### title + +Title displayed at the top of the panel. + +- Type: `String` +- Required: No + +##### initialOpen + +Whether to have the panel initially opened. When no title is provided it is always opened. + +- Type: `Boolean` +- Required: No +- Default: `false` + + +### `PluginPostPublishPanel` + +Renders provided content to the post-publish panel in the publish flow (side panel that opens after a user publishes the post). + +_Example:_ + +{% codetabs %} + +{% ES5 %} +```js +var __ = wp.i18n.__; +var PluginPostPublishPanel = wp.editPost.PluginPostPublishPanel; + +function MyPluginPostPublishPanel() { + return wp.element.createElement( + PluginPostPublishPanel, + { + className: 'my-plugin-post-publish-panel', + title: __( 'My panel title' ), + initialOpen: true, + }, + __( 'My panel content' ) + ); +} +``` + +{% ESNext %} +```jsx +const { __ } = wp.i18n; +const { PluginPostPublishPanel } = wp.editPost; + +const MyPluginPostPublishPanel = () => ( + <PluginPostPublishPanel + className="my-plugin-post-publish-panel" + title={ __( 'My panel title' ) } + initialOpen={ true } + > + { __( 'My panel content' ) } + </PluginPostPublishPanel> +); +``` +{% end %} + +#### Props + +##### className + +An optional class name added to the panel. + +- Type: `String` +- Required: No + +##### title + +Title displayed at the top of the panel. + +- Type: `String` +- Required: No + +##### initialOpen + +Whether to have the panel initially opened. When no title is provided it is always opened. + +- Type: `Boolean` +- Required: No +- Default: `false` + + <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/edit-post/src/README.md b/packages/edit-post/src/README.md deleted file mode 100644 index 91d5c14ed93e14..00000000000000 --- a/packages/edit-post/src/README.md +++ /dev/null @@ -1,508 +0,0 @@ -## Extending the post editor UI - -Extending the editor UI can be accomplished with the `registerPlugin` API, allowing you to define all your plugin's UI elements in one place. - -Refer to [the plugins module documentation](../../plugins/) for more information. - -## Plugin Components - -The following components can be used with the `registerPlugin` ([see documentation](../../plugins)) API. -They can be found in the global variable `wp.editPost` when defining `wp-edit-post` as a script dependency. - -### `PluginBlockSettingsMenuItem` - -Renders a new item in the block settings menu. - -Example: - -{% codetabs %} - -{% ES5 %} -```js -var __ = wp.i18n.__; -var PluginBlockSettingsMenuItem = wp.editPost.PluginBlockSettingsMenuItem; - -function doOnClick(){ - // To be called when the user clicks the menu item. -} - -function MyPluginBlockSettingsMenuItem() { - return el( - PluginBlockSettingsMenuItem, - { - allowedBlockNames: [ 'core/paragraph' ], - icon: 'dashicon-name', - label: __( 'Menu item text' ), - onClick: doOnClick, - } - ); -} -``` - -{% ESNext %} -```jsx -import { __ } from wp.i18n; -import { PluginBlockSettingsMenuItem } from wp.editPost; - -const doOnClick = ( ) => { - // To be called when the user clicks the menu item. -}; - -const MyPluginBlockSettingsMenuItem = () => ( - <PluginBlockSettingsMenuItem - allowedBlockNames=[ 'core/paragraph' ] - icon='dashicon-name' - label=__( 'Menu item text' ) - onClick={ doOnClick } /> -); -``` - -{% end %} - -#### Props - -##### allowedBlockNames - -An array containing a whitelist of block names for which the item should be shown. If this prop is not present the item will be rendered for any block. If multiple blocks are selected, it'll be shown if and only if all of them are in the whitelist. - -- Type: `Array` -- Required: No -- Default: Menu item is shown for any block - -##### icon - -The [Dashicon](https://developer.wordpress.org/resource/dashicons/) icon slug string, or an SVG WP element, to be rendered to the left of the menu item label. - -- Type: `String` | `Element` -- Required: No -- Default: Menu item wil be rendered without icon - -##### label - -A string containing the menu item text. - -- Type: `String` -- Required: Yes - -##### onClick - -The callback function to be executed when the user clicks the menu item. - -- Type: `function` -- Required: Yes - -### `PluginSidebar` - -Renders a sidebar when activated. The contents within the `PluginSidebar` will appear as content within the sidebar. - -If you wish to display the sidebar, you can with use the [`PluginSidebarMoreMenuItem`](#pluginsidebarmoremenuitem) component or the `wp.data.dispatch` API: -```js -wp.data.dispatch( 'core/edit-post' ).openGeneralSidebar( 'plugin-name/sidebar-name' ); -``` - -_Example:_ - -{% codetabs %} - -{% ES5 %} -```js -var __ = wp.i18n.__; -var el = wp.element.createElement; -var PanelBody = wp.components.PanelBody; -var PluginSidebar = wp.editPost.PluginSidebar; - - -function MyPluginSidebar() { - return el( - PluginSidebar, - { - name: 'my-sidebar', - title: 'My sidebar title', - icon: 'smiley', - }, - el( - PanelBody, - {}, - __( 'My sidebar content' ) - ) - ); -} -``` - -{% ESNext %} -```jsx -const { __ } = wp.i18n; -const { PanelBody } = wp.components; -const { PluginSidebar } = wp.editPost; - -const MyPluginSidebar = () => ( - <PluginSidebar - name="my-sidebar" - title="My sidebar title" - icon="smiley" - > - <PanelBody> - { __( 'My sidebar content' ) } - </PanelBody> - </PluginSidebar> -); -``` -{% end %} - -#### Props - -##### name - -A string identifying the sidebar. Must be unique for every sidebar registered within the scope of your plugin. - -- Type: `String` -- Required: Yes - -##### title - -Title displayed at the top of the sidebar. - -- Type: `String` -- Required: Yes - -##### isPinnable - -Whether to allow to pin sidebar to toolbar. - -- Type: `Boolean` -- Required: No -- Default: `true` - -##### icon - -The [Dashicon](https://developer.wordpress.org/resource/dashicons/) icon slug string, or an SVG WP element, to be rendered when the sidebar is pinned to toolbar. - -- Type: `String` | `Element` -- Required: No -- Default: _inherits from the plugin_ - -### `PluginMoreMenuItem` - -Renders a menu item in `Plugins` group in `More Menu` drop down, and can be used to as a button or link depending on the props provided. -The text within the component appears as the menu item label. - -_Example:_ - -{% codetabs %} - -{% ES5 %} -```js -var __ = wp.i18n.__; -var PluginMoreMenuItem = wp.editPost.PluginMoreMenuItem; -var el = wp.element.createElement; - -function onButtonClick() { - alert( 'Button clicked.' ); -} - -function MyButtonMoreMenuItem() { - return el( - PluginMoreMenuItem, - { - icon: 'smiley', - onClick: onButtonClick - }, - __( 'My button title' ) - ) -} -``` - -{% ESNext %} -```jsx -const { __ } = wp.i18n; -const { PluginMoreMenuItem } = wp.editPost; - -function onButtonClick() { - alert( 'Button clicked.' ); -} - -const MyButtonMoreMenuItem = () => ( - <PluginMoreMenuItem - icon="smiley" - onClick={ onButtonClick } - > - { __( 'My button title' ) } - </PluginMoreMenuItem> -); -``` -{% end %} - -#### Props - -`PluginMoreMenuItem` supports the following props. Any additional props are passed through to the underlying [MenuItem](../../components/src/menu-item) component. - -##### href - -When `href` is provided then the menu item is represented as an anchor rather than button. It corresponds to the `href` attribute of the anchor. - -- Type: `String` -- Required: No - -##### icon - -The [Dashicon](https://developer.wordpress.org/resource/dashicons/) icon slug string, or an SVG WP element, to be rendered to the left of the menu item label. - -- Type: `String` | `Element` -- Required: No -- Default: _inherits from the plugin_ - -##### onClick - -The callback function to be executed when the user clicks the menu item. - -- Type: `function` -- Required: No -- Default: _function which does nothing_ - -### `PluginSidebarMoreMenuItem` - -Renders a menu item in `Plugins` group in `More Menu` drop down, and can be used to activate the corresponding `PluginSidebar` component. -The text within the component appears as the menu item label. - -_Example:_ - -{% codetabs %} - -{% ES5 %} -```js -var __ = wp.i18n.__; -var PluginSidebarMoreMenuItem = wp.editPost.PluginSidebarMoreMenuItem; -var el = wp.element.createElement; - -function MySidebarMoreMenuItem() { - return el( - PluginSidebarMoreMenuItem, - { - target: 'my-sidebar', - icon: 'smiley', - }, - __( 'My sidebar title' ) - ) -} -``` - -{% ESNext %} -```jsx -const { __ } = wp.i18n; -const { PluginSidebarMoreMenuItem } = wp.editPost; - -const MySidebarMoreMenuItem = () => ( - <PluginSidebarMoreMenuItem - target="my-sidebar" - icon="smiley" - > - { __( 'My sidebar title' ) } - </PluginSidebarMoreMenuItem> -); -``` -{% end %} - -#### Props - -##### target - -A string identifying the target sidebar you wish to be activated by this menu item. Must be the same as the `name` prop you have given to that sidebar. - -- Type: `String` -- Required: Yes - -##### icon - -The [Dashicon](https://developer.wordpress.org/resource/dashicons/) icon slug string, or an SVG WP element, to be rendered to the left of the menu item label. - -- Type: `String` | `Element` -- Required: No -- Default: _inherits from the plugin_ - - -### `PluginPostStatusInfo` - -Renders a row in the Status & Visibility panel of the Document sidebar. -It should be noted that this is named and implemented around the function it serves and not its location, which may change in future iterations. - -_Example:_ - -{% codetabs %} - -{% ES5 %} -```js -var __ = wp.i18n.__; -var PluginPostStatusInfo = wp.editPost.PluginPostStatusInfo; -var el = wp.element.createElement; - -function MyPluginPostStatusInfo() { - return el( - PluginPostStatusInfo, - { - className: 'my-plugin-post-status-info', - }, - __( 'My post status info' ) - ) -} -``` - -{% ESNext %} -```jsx -const { __ } = wp.i18n; -const { PluginPostStatusInfo } = wp.editPost; - -const MyPluginPostStatusInfo = () => ( - <PluginPostStatusInfo - className="my-plugin-post-status-info" - > - { __( 'My post status info' ) } - </PluginPostStatusInfo> -); -``` -{% end %} - -#### Props - -##### className - -An optional class name added to the row. - -- Type: `String` -- Required: No - -### `PluginPrePublishPanel` - -Renders provided content to the pre-publish side panel in the publish flow (side panel that opens when a user first pushes "Publish" from the main editor). - -_Example:_ - -{% codetabs %} - -{% ES5 %} -```js -var __ = wp.i18n.__; -var PluginPrePublishPanel = wp.editPost.PluginPrePublishPanel; -var el = wp.element.createElement; - -function MyPluginPrePublishPanel() { - return el( - PluginPrePublishPanel, - { - className: 'my-plugin-pre-publish-panel', - title: __( 'My panel title' ), - initialOpen: true, - }, - __( 'My panel content' ) - ); -} -``` - -{% ESNext %} -```jsx -const { __ } = wp.i18n; -const { PluginPrePublishPanel } = wp.editPost; - -const MyPluginPrePublishPanel = () => ( - <PluginPrePublishPanel - className="my-plugin-pre-publish-panel" - title={ __( 'My panel title' ) } - initialOpen={ true } - > - { __( 'My panel content' ) } - </PluginPrePublishPanel> -); -``` -{% end %} - -#### Props - -##### className - -An optional class name added to the panel. - -- Type: `String` -- Required: No - -##### title - -Title displayed at the top of the panel. - -- Type: `String` -- Required: No - -##### initialOpen - -Whether to have the panel initially opened. When no title is provided it is always opened. - -- Type: `Boolean` -- Required: No -- Default: `false` - - -### `PluginPostPublishPanel` - -Renders provided content to the post-publish panel in the publish flow (side panel that opens after a user publishes the post). - -_Example:_ - -{% codetabs %} - -{% ES5 %} -```js -var __ = wp.i18n.__; -var PluginPostPublishPanel = wp.editPost.PluginPostPublishPanel; -var el = wp.element.createElement; - -function MyPluginPostPublishPanel() { - return el( - PluginPostPublishPanel, - { - className: 'my-plugin-post-publish-panel', - title: __( 'My panel title' ), - initialOpen: true, - }, - __( 'My panel content' ) - ); -} -``` - -{% ESNext %} -```jsx -const { __ } = wp.i18n; -const { PluginPostPublishPanel } = wp.editPost; - -const MyPluginPostPublishPanel = () => ( - <PluginPostPublishPanel - className="my-plugin-post-publish-panel" - title={ __( 'My panel title' ) } - initialOpen={ true } - > - { __( 'My panel content' ) } - </PluginPostPublishPanel> -); -``` -{% end %} - -#### Props - -##### className - -An optional class name added to the panel. - -- Type: `String` -- Required: No - -##### title - -Title displayed at the top of the panel. - -- Type: `String` -- Required: No - -##### initialOpen - -Whether to have the panel initially opened. When no title is provided it is always opened. - -- Type: `Boolean` -- Required: No -- Default: `false` From 652a662a8aba2b621978871e56706990d5deb653 Mon Sep 17 00:00:00 2001 From: Andrea Fercia <a.fercia@gmail.com> Date: Mon, 3 Dec 2018 17:35:53 +0100 Subject: [PATCH 178/254] Remove aria-expanded from RichText. (#12524) --- .../block-library/src/button/test/__snapshots__/index.js.snap | 1 - .../block-library/src/heading/test/__snapshots__/index.js.snap | 1 - .../block-library/src/list/test/__snapshots__/index.js.snap | 1 - .../src/paragraph/test/__snapshots__/index.js.snap | 1 - .../src/preformatted/test/__snapshots__/index.js.snap | 1 - .../src/pullquote/test/__snapshots__/index.js.snap | 1 - .../block-library/src/quote/test/__snapshots__/index.js.snap | 1 - .../src/text-columns/test/__snapshots__/index.js.snap | 2 -- .../block-library/src/verse/test/__snapshots__/index.js.snap | 1 - packages/editor/src/components/rich-text/index.js | 3 +-- 10 files changed, 1 insertion(+), 12 deletions(-) diff --git a/packages/block-library/src/button/test/__snapshots__/index.js.snap b/packages/block-library/src/button/test/__snapshots__/index.js.snap index 9d96ec60114b22..e1532798910960 100644 --- a/packages/block-library/src/button/test/__snapshots__/index.js.snap +++ b/packages/block-library/src/button/test/__snapshots__/index.js.snap @@ -16,7 +16,6 @@ exports[`core/button block edit matches snapshot 1`] = ` > <div aria-autocomplete="list" - aria-expanded="false" aria-label="Add text…" aria-multiline="true" class="wp-block-button__link editor-rich-text__tinymce" diff --git a/packages/block-library/src/heading/test/__snapshots__/index.js.snap b/packages/block-library/src/heading/test/__snapshots__/index.js.snap index 238570a774d726..0131b6104ae6ab 100644 --- a/packages/block-library/src/heading/test/__snapshots__/index.js.snap +++ b/packages/block-library/src/heading/test/__snapshots__/index.js.snap @@ -11,7 +11,6 @@ exports[`core/heading block edit matches snapshot 1`] = ` > <h2 aria-autocomplete="list" - aria-expanded="false" aria-label="Write heading…" aria-multiline="true" class="editor-rich-text__tinymce" 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 80e123d44c6b4d..d2da3e3413525b 100644 --- a/packages/block-library/src/list/test/__snapshots__/index.js.snap +++ b/packages/block-library/src/list/test/__snapshots__/index.js.snap @@ -11,7 +11,6 @@ exports[`core/list block edit matches snapshot 1`] = ` > <ul aria-autocomplete="list" - aria-expanded="false" aria-label="Write list…" aria-multiline="true" class="editor-rich-text__tinymce" 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 4e273f676b1b32..036646b041a21e 100644 --- a/packages/block-library/src/paragraph/test/__snapshots__/index.js.snap +++ b/packages/block-library/src/paragraph/test/__snapshots__/index.js.snap @@ -13,7 +13,6 @@ exports[`core/paragraph block edit matches snapshot 1`] = ` > <p aria-autocomplete="list" - aria-expanded="false" 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" diff --git a/packages/block-library/src/preformatted/test/__snapshots__/index.js.snap b/packages/block-library/src/preformatted/test/__snapshots__/index.js.snap index 9e9fc90e01d09c..d89042667a97c2 100644 --- a/packages/block-library/src/preformatted/test/__snapshots__/index.js.snap +++ b/packages/block-library/src/preformatted/test/__snapshots__/index.js.snap @@ -11,7 +11,6 @@ exports[`core/preformatted block edit matches snapshot 1`] = ` > <pre aria-autocomplete="list" - aria-expanded="false" aria-label="Write preformatted text…" aria-multiline="true" class="editor-rich-text__tinymce" 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 4c13da3da08b10..aed466a2081420 100644 --- a/packages/block-library/src/pullquote/test/__snapshots__/index.js.snap +++ b/packages/block-library/src/pullquote/test/__snapshots__/index.js.snap @@ -15,7 +15,6 @@ exports[`core/pullquote block edit matches snapshot 1`] = ` > <div aria-autocomplete="list" - aria-expanded="false" aria-label="Write quote…" aria-multiline="true" class="editor-rich-text__tinymce" 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 ecc391ed5da7f1..1365b8581bcff8 100644 --- a/packages/block-library/src/quote/test/__snapshots__/index.js.snap +++ b/packages/block-library/src/quote/test/__snapshots__/index.js.snap @@ -14,7 +14,6 @@ exports[`core/quote block edit matches snapshot 1`] = ` > <div aria-autocomplete="list" - aria-expanded="false" aria-label="Write quote…" aria-multiline="true" class="editor-rich-text__tinymce" diff --git a/packages/block-library/src/text-columns/test/__snapshots__/index.js.snap b/packages/block-library/src/text-columns/test/__snapshots__/index.js.snap index 19e26bedd4e4f5..aabf7c36e121ff 100644 --- a/packages/block-library/src/text-columns/test/__snapshots__/index.js.snap +++ b/packages/block-library/src/text-columns/test/__snapshots__/index.js.snap @@ -17,7 +17,6 @@ exports[`core/text-columns block edit matches snapshot 1`] = ` > <p aria-autocomplete="list" - aria-expanded="false" aria-label="New Column" aria-multiline="true" class="editor-rich-text__tinymce" @@ -52,7 +51,6 @@ exports[`core/text-columns block edit matches snapshot 1`] = ` > <p aria-autocomplete="list" - aria-expanded="false" aria-label="New Column" aria-multiline="true" class="editor-rich-text__tinymce" diff --git a/packages/block-library/src/verse/test/__snapshots__/index.js.snap b/packages/block-library/src/verse/test/__snapshots__/index.js.snap index 4141acd82063f3..8595e740857acb 100644 --- a/packages/block-library/src/verse/test/__snapshots__/index.js.snap +++ b/packages/block-library/src/verse/test/__snapshots__/index.js.snap @@ -11,7 +11,6 @@ exports[`core/verse block edit matches snapshot 1`] = ` > <pre aria-autocomplete="list" - aria-expanded="false" aria-label="Write…" aria-multiline="true" 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 7fc04677d84b7c..872049c948e6a2 100644 --- a/packages/editor/src/components/rich-text/index.js +++ b/packages/editor/src/components/rich-text/index.js @@ -858,7 +858,7 @@ export class RichText extends Component { record={ record } onChange={ this.onChange } > - { ( { isExpanded, listBoxId, activeId } ) => ( + { ( { listBoxId, activeId } ) => ( <Fragment> <TinyMCE tagName={ Tagname } @@ -869,7 +869,6 @@ export class RichText extends Component { isPlaceholderVisible={ isPlaceholderVisible } aria-label={ placeholder } aria-autocomplete="list" - aria-expanded={ isExpanded } aria-owns={ listBoxId } aria-activedescendant={ activeId } { ...ariaProps } From 69b55c70443762c1d302e982a5f91196ff0892f4 Mon Sep 17 00:00:00 2001 From: Dion Hulse <dion@wordpress.org> Date: Tue, 4 Dec 2018 15:37:55 +1000 Subject: [PATCH 179/254] Restore missing handbook pages after #12411 (#12452) ## Description Following on from #12411 and #11817 it was noticed that some pages were skipped, as they weren't included in the `toc.json` file previously. This PR includes several specific fixes which are all interconnected: 1. Adds the Internationalization page along with the Block Tutorial to the handbook hierarchy 1. An additional landing page for the Tutorials index was added 1. `Components` was added to the developers handbook alongside `Packages` and `Data Package Reference` pages. 1. In order to handle the above move, the duplicative declarations of `Packages`, `Data Package Reference` and `Components` were removed from `docs/tool/manifest.js` to prevent them adding as top-level-items. (Are these intended to be top-level-handbook-pages or deep within the developers handbook as the TOC seems to suggest?) I also took the opportunity to convert `generate.php` from the previous PR into a JS command that allows for `docs/root-manifest.json` to be removed entirely and dynamically generated from `docs/toc.json` so it only needs updating in a single location. That's the `getRootManifest()` and `generateRootManifestFromTOCItems()` functions which are rather messy, but work (I'm sure someone else could clean that up significantly). ## How has this been tested? The only testing done so far is the manual review of the final `docs/manifest.json`. --- .../designers-developers/developers/README.md | 2 +- .../block-tutorial/{intro.md => readme.md} | 0 .../developers/tutorials/readme.md | 3 + docs/designers-developers/faq.md | 2 +- docs/manifest.json | 78 +++-- docs/root-manifest.json | 284 ------------------ docs/toc.json | 12 + docs/tool/config.js | 2 +- docs/tool/index.js | 4 +- docs/tool/manifest.js | 121 ++++---- 10 files changed, 149 insertions(+), 359 deletions(-) rename docs/designers-developers/developers/tutorials/block-tutorial/{intro.md => readme.md} (100%) create mode 100644 docs/designers-developers/developers/tutorials/readme.md delete mode 100644 docs/root-manifest.json diff --git a/docs/designers-developers/developers/README.md b/docs/designers-developers/developers/README.md index e88dd2cd6b2ea1..5c552b2bd835ee 100644 --- a/docs/designers-developers/developers/README.md +++ b/docs/designers-developers/developers/README.md @@ -6,7 +6,7 @@ Gutenberg is highly flexible, like most of WordPress. You can build custom block Gutenberg is about blocks, and the main extensibility API of Gutenberg is the Block API. It allows you to create your own static blocks, dynamic blocks rendered on the server and also blocks capable of saving data to Post Meta for more structured content. -If you want to learn more about block creation, the [Blocks Tutorial](../../../docs/designers-developers/developers/tutorials/block-tutorial/intro.md) is the best place to start. +If you want to learn more about block creation, the [Blocks Tutorial](../../../docs/designers-developers/developers/tutorials/block-tutorial/readme.md) is the best place to start. ## Extending Blocks diff --git a/docs/designers-developers/developers/tutorials/block-tutorial/intro.md b/docs/designers-developers/developers/tutorials/block-tutorial/readme.md similarity index 100% rename from docs/designers-developers/developers/tutorials/block-tutorial/intro.md rename to docs/designers-developers/developers/tutorials/block-tutorial/readme.md diff --git a/docs/designers-developers/developers/tutorials/readme.md b/docs/designers-developers/developers/tutorials/readme.md new file mode 100644 index 00000000000000..d12dcd1c4982d5 --- /dev/null +++ b/docs/designers-developers/developers/tutorials/readme.md @@ -0,0 +1,3 @@ +# Tutorials + +If you want to learn more about block creation, the [Blocks Tutorial](../../../../docs/designers-developers/developers/tutorials/block-tutorial/readme.md) is the best place to start. diff --git a/docs/designers-developers/faq.md b/docs/designers-developers/faq.md index 547395acb1eb23..2cc8e6684a6be1 100644 --- a/docs/designers-developers/faq.md +++ b/docs/designers-developers/faq.md @@ -251,7 +251,7 @@ Our [list of supported browsers can be found in the Make WordPress handbook](htt ## How do I make my own block? -The API for creating blocks is a crucial aspect of the project. We are working on improved documentation and tutorials. Check out the [Creating Block Types](../../docs/designers-developers/developers/tutorials/block-tutorial/intro.md) section to get started. +The API for creating blocks is a crucial aspect of the project. We are working on improved documentation and tutorials. Check out the [Creating Block Types](../../docs/designers-developers/developers/tutorials/block-tutorial/readme.md) section to get started. ## Does Gutenberg involve editing posts/pages in the front-end? diff --git a/docs/manifest.json b/docs/manifest.json index 398f6445c56ae2..2dae91884153aa 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -95,6 +95,12 @@ "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/filters/autocomplete-filters.md", "parent": "filters" }, + { + "title": "Internationalization", + "slug": "internationalization", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/internationalization.md", + "parent": "developers" + }, { "title": "Data Module Reference", "slug": "data", @@ -107,6 +113,12 @@ "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/packages.md", "parent": "developers" }, + { + "title": "Components", + "slug": "components", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/packages/components/README.md", + "parent": "developers" + }, { "title": "Theming for Gutenberg", "slug": "themes", @@ -137,6 +149,54 @@ "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/backwards-compatibility/meta-box.md", "parent": "backwards-compatibility" }, + { + "title": "Tutorials", + "slug": "tutorials", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/tutorials/readme.md", + "parent": "developers" + }, + { + "title": "Blocks", + "slug": "block-tutorial", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/tutorials/block-tutorial/readme.md", + "parent": "tutorials" + }, + { + "title": "Writing Your First Block Type", + "slug": "writing-your-first-block-type", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/tutorials/block-tutorial/writing-your-first-block-type.md", + "parent": "block-tutorial" + }, + { + "title": "Applying Styles From a Stylesheet", + "slug": "applying-styles-with-stylesheets", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/tutorials/block-tutorial/applying-styles-with-stylesheets.md", + "parent": "block-tutorial" + }, + { + "title": "Introducing Attributes and Editable Fields", + "slug": "introducing-attributes-and-editable-fields", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/tutorials/block-tutorial/introducing-attributes-and-editable-fields.md", + "parent": "block-tutorial" + }, + { + "title": "Block Controls: Toolbars and Inspector", + "slug": "block-controls-toolbars-and-inspector", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/tutorials/block-tutorial/block-controls-toolbars-and-inspector.md", + "parent": "block-tutorial" + }, + { + "title": "Creating dynamic blocks", + "slug": "creating-dynamic-blocks", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/tutorials/block-tutorial/creating-dynamic-blocks.md", + "parent": "block-tutorial" + }, + { + "title": "Generate Blocks with WP-CLI", + "slug": "generate-blocks-with-wp-cli", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/tutorials/block-tutorial/generate-blocks-with-wp-cli.md", + "parent": "block-tutorial" + }, { "title": "Designer Documentation", "slug": "designers", @@ -281,12 +341,6 @@ "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/contributors/testing-overview.md", "parent": "contributors" }, - { - "title": "Packages", - "slug": "packages", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/packages.md", - "parent": null - }, { "title": "@wordpress/a11y", "slug": "packages-a11y", @@ -587,12 +641,6 @@ "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/packages/wordcount/README.md", "parent": "packages" }, - { - "title": "Components Package Reference", - "slug": "components", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/packages/components.md", - "parent": null - }, { "title": "Autocomplete", "slug": "autocomplete", @@ -971,12 +1019,6 @@ "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/packages/components/src/tree-select/README.md", "parent": "components" }, - { - "title": "Data Package Reference", - "slug": "data", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/data/README.md", - "parent": null - }, { "title": "WordPress Core Data", "slug": "data-core", diff --git a/docs/root-manifest.json b/docs/root-manifest.json deleted file mode 100644 index 0d8c79653cf331..00000000000000 --- a/docs/root-manifest.json +++ /dev/null @@ -1,284 +0,0 @@ -[ - { - "title": "Gutenberg Handbook", - "slug": "handbook", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/readme.md", - "parent": null - }, - { - "title": "Designer & Developer Handbook", - "slug": "designers-developers", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/readme.md", - "parent": null - }, - { - "title": "Key Concepts", - "slug": "key-concepts", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/key-concepts.md", - "parent": "designers-developers" - }, - { - "title": "Developer Documentation", - "slug": "developers", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/README.md", - "parent": "designers-developers" - }, - { - "title": "Block API Reference", - "slug": "block-api", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/block-api\/README.md", - "parent": "developers" - }, - { - "title": "Block Registration ", - "slug": "block-registration", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/block-api\/block-registration.md", - "parent": "block-api" - }, - { - "title": "Edit and Save", - "slug": "block-edit-save", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/block-api\/block-edit-save.md", - "parent": "block-api" - }, - { - "title": "Attributes", - "slug": "block-attributes", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/block-api\/block-attributes.md", - "parent": "block-api" - }, - { - "title": "Deprecated Blocks", - "slug": "block-deprecation", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/block-api\/block-deprecation.md", - "parent": "block-api" - }, - { - "title": "Templates", - "slug": "block-templates", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/block-api\/block-templates.md", - "parent": "block-api" - }, - { - "title": "Annotations", - "slug": "block-annotations", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/block-api\/block-annotations.md", - "parent": "block-api" - }, - { - "title": "Filter Reference", - "slug": "filters", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/filters\/README.md", - "parent": "developers" - }, - { - "title": "Block Filters", - "slug": "block-filters", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/filters\/block-filters.md", - "parent": "filters" - }, - { - "title": "Editor Filters (Experimental)", - "slug": "editor-filters", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/filters\/editor-filters.md", - "parent": "filters" - }, - { - "title": "Parser Filters", - "slug": "parser-filters", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/filters\/parser-filters.md", - "parent": "filters" - }, - { - "title": "Autocomplete", - "slug": "autocomplete-filters", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/filters\/autocomplete-filters.md", - "parent": "filters" - }, - { - "title": "Data Module Reference", - "slug": "data", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/data\/README.md", - "parent": "developers" - }, - { - "title": "Packages", - "slug": "packages", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/packages.md", - "parent": "developers" - }, - { - "title": "Theming for Gutenberg", - "slug": "themes", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/themes\/README.md", - "parent": "developers" - }, - { - "title": "Theme Support", - "slug": "theme-support", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/themes\/theme-support.md", - "parent": "themes" - }, - { - "title": "Backwards Compatibility", - "slug": "backwards-compatibility", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/backwards-compatibility\/README.md", - "parent": "developers" - }, - { - "title": "Deprecations", - "slug": "deprecations", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/backwards-compatibility\/deprecations.md", - "parent": "backwards-compatibility" - }, - { - "title": "Meta Boxes", - "slug": "meta-box", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/developers\/backwards-compatibility\/meta-box.md", - "parent": "backwards-compatibility" - }, - { - "title": "Designer Documentation", - "slug": "designers", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/designers\/README.md", - "parent": "designers-developers" - }, - { - "title": "Block Design", - "slug": "block-design", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/designers\/block-design.md", - "parent": "designers" - }, - { - "title": "Patterns", - "slug": "design-patterns", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/designers\/design-patterns.md", - "parent": "designers" - }, - { - "title": "Resources", - "slug": "design-resources", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/designers\/design-resources.md", - "parent": "designers" - }, - { - "title": "Glossary", - "slug": "glossary", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/glossary.md", - "parent": "designers-developers" - }, - { - "title": "Frequently Asked Questions", - "slug": "faq", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/designers-developers\/faq.md", - "parent": "designers-developers" - }, - { - "title": "Contributors", - "slug": "contributors", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/contributors\/readme.md", - "parent": null - }, - { - "title": "Coding Guidelines", - "slug": "coding-guidelines", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/contributors\/coding-guidelines.md", - "parent": "contributors" - }, - { - "title": "Gutencopy Guidelines", - "slug": "copy-guide", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/contributors\/copy-guide.md", - "parent": "contributors" - }, - { - "title": "Gutenberg Design Principles & Vision", - "slug": "design", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/contributors\/design.md", - "parent": "contributors" - }, - { - "title": "The Gutenberg block grammar", - "slug": "grammar", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/contributors\/grammar.md", - "parent": "contributors" - }, - { - "title": "History", - "slug": "history", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/contributors\/history.md", - "parent": "contributors" - }, - { - "title": "Outreach", - "slug": "outreach", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/contributors\/outreach.md", - "parent": "contributors" - }, - { - "title": "Articles", - "slug": "articles", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/contributors\/outreach\/articles.md", - "parent": "outreach" - }, - { - "title": "Meetups", - "slug": "meetups", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/contributors\/outreach\/meetups.md", - "parent": "outreach" - }, - { - "title": "Resources", - "slug": "resources", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/contributors\/outreach\/resources.md", - "parent": "outreach" - }, - { - "title": "Talks", - "slug": "talks", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/contributors\/outreach\/talks.md", - "parent": "outreach" - }, - { - "title": "Principles", - "slug": "principles", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/contributors\/principles.md", - "parent": "contributors" - }, - { - "title": "Blocks are the Interface", - "slug": "the-block", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/contributors\/principles\/the-block.md", - "parent": "principles" - }, - { - "title": "Reference", - "slug": "reference", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/contributors\/reference.md", - "parent": "contributors" - }, - { - "title": "Gutenberg Release Process", - "slug": "release", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/contributors\/release.md", - "parent": "contributors" - }, - { - "title": "Repository Management", - "slug": "repository-management", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/contributors\/repository-management.md", - "parent": "contributors" - }, - { - "title": "Scripts", - "slug": "scripts", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/contributors\/scripts.md", - "parent": "contributors" - }, - { - "title": "Testing Overview", - "slug": "testing-overview", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/contributors\/testing-overview.md", - "parent": "contributors" - } -] \ No newline at end of file diff --git a/docs/toc.json b/docs/toc.json index b9f723a91ab54d..211c74631ec700 100644 --- a/docs/toc.json +++ b/docs/toc.json @@ -17,14 +17,26 @@ {"docs/designers-developers/developers/filters/parser-filters.md": []}, {"docs/designers-developers/developers/filters/autocomplete-filters.md": []} ]}, + {"docs/designers-developers/developers/internationalization.md": []}, {"docs/designers-developers/developers/data/README.md": "{{data}}"}, {"docs/designers-developers/developers/packages.md": "{{packages}}"}, + {"packages/components/README.md": "{{components}}"}, {"docs/designers-developers/developers/themes/README.md": [ {"docs/designers-developers/developers/themes/theme-support.md": []} ]}, {"docs/designers-developers/developers/backwards-compatibility/README.md": [ {"docs/designers-developers/developers/backwards-compatibility/deprecations.md": []}, {"docs/designers-developers/developers/backwards-compatibility/meta-box.md": []} + ]}, + {"docs/designers-developers/developers/tutorials/readme.md": [ + {"docs/designers-developers/developers/tutorials/block-tutorial/readme.md" :[ + {"docs/designers-developers/developers/tutorials/block-tutorial/writing-your-first-block-type.md" :[]}, + {"docs/designers-developers/developers/tutorials/block-tutorial/applying-styles-with-stylesheets.md" :[]}, + {"docs/designers-developers/developers/tutorials/block-tutorial/introducing-attributes-and-editable-fields.md" :[]}, + {"docs/designers-developers/developers/tutorials/block-tutorial/block-controls-toolbars-and-inspector.md" :[]}, + {"docs/designers-developers/developers/tutorials/block-tutorial/creating-dynamic-blocks.md" :[]}, + {"docs/designers-developers/developers/tutorials/block-tutorial/generate-blocks-with-wp-cli.md" :[]} + ]} ]} ]}, {"docs/designers-developers/designers/README.md": [ diff --git a/docs/tool/config.js b/docs/tool/config.js index 1a3e69450c9a8e..87900c4c624fa7 100644 --- a/docs/tool/config.js +++ b/docs/tool/config.js @@ -56,6 +56,6 @@ module.exports = { packageFileNames: glob( 'packages/*/package.json' ) .map( ( fileName ) => fileName.split( '/' )[ 1 ] ), - rootManifest: path.resolve( __dirname, '../root-manifest.json' ), + tocFileName: path.resolve( __dirname, '../toc.json' ), manifestOutput: path.resolve( __dirname, '../manifest.json' ), }; diff --git a/docs/tool/index.js b/docs/tool/index.js index 5b901a0fb8efe6..c0bb2c1101c7e8 100644 --- a/docs/tool/index.js +++ b/docs/tool/index.js @@ -9,12 +9,12 @@ const fs = require( 'fs' ); const config = require( './config' ); const parser = require( './parser' ); const generator = require( './generator' ); -const { getPackageManifest, getComponentManifest, getDataManifest } = require( './manifest' ); +const { getPackageManifest, getComponentManifest, getDataManifest, getRootManifest } = require( './manifest' ); const parsedModules = parser( config.dataNamespaces ); generator( parsedModules, config.dataDocsOutput ); -const rootManifest = require( config.rootManifest ); +const rootManifest = getRootManifest( config.tocFileName ); const packageManifest = getPackageManifest( config.packageFileNames ); const componentManifest = getComponentManifest( config.componentPaths ); const dataManifest = getDataManifest( parsedModules ); diff --git a/docs/tool/manifest.js b/docs/tool/manifest.js index 22d98734278161..06732edc108dc9 100644 --- a/docs/tool/manifest.js +++ b/docs/tool/manifest.js @@ -3,6 +3,8 @@ */ const { camelCase, kebabCase, nth, upperFirst } = require( 'lodash' ); +const fs = require( 'fs' ); + const baseRepoUrl = `https://raw.githubusercontent.com/WordPress/gutenberg/master`; /** @@ -13,24 +15,15 @@ const baseRepoUrl = `https://raw.githubusercontent.com/WordPress/gutenberg/maste * @return {Array} Manifest */ function getPackageManifest( packageFolderNames ) { - return [ - { - title: 'Packages', - slug: 'packages', - markdown_source: `${ baseRepoUrl }/docs/designers-developers/developers/packages.md`, - parent: null, - }, - ].concat( - packageFolderNames.map( ( folderName ) => { - const path = `${ baseRepoUrl }/packages/${ folderName }/README.md`; - return { - title: `@wordpress/${ folderName }`, - slug: `packages-${ folderName }`, - markdown_source: path, - parent: 'packages', - }; - } ) - ); + return packageFolderNames.map( ( folderName ) => { + const path = `${ baseRepoUrl }/packages/${ folderName }/README.md`; + return { + title: `@wordpress/${ folderName }`, + slug: `packages-${ folderName }`, + markdown_source: path, + parent: 'packages', + }; + } ); } /** @@ -41,24 +34,15 @@ function getPackageManifest( packageFolderNames ) { * @return {Array} Manifest */ function getComponentManifest( componentPaths ) { - return [ - { - title: 'Components Package Reference', - slug: 'components', - markdown_source: `${ baseRepoUrl }/packages/components.md`, - parent: null, - }, - ...componentPaths - .map( ( filePath ) => { - const slug = nth( filePath.split( '/' ), -2 ); - return { - title: upperFirst( camelCase( slug ) ), - slug, - markdown_source: `${ baseRepoUrl }/${ filePath }`, - parent: 'components', - }; - } ), - ]; + return componentPaths.map( ( filePath ) => { + const slug = nth( filePath.split( '/' ), -2 ); + return { + title: upperFirst( camelCase( slug ) ), + slug, + markdown_source: `${ baseRepoUrl }/${ filePath }`, + parent: 'components', + }; + } ); } /** @@ -69,26 +53,59 @@ function getComponentManifest( componentPaths ) { * @return {Array} Manifest */ function getDataManifest( parsedNamespaces ) { - return [ { - title: 'Data Package Reference', - slug: 'data', - markdown_source: `${ baseRepoUrl }/docs/designers-developers/developers/data/README.md`, - parent: null, - } ].concat( - Object.values( parsedNamespaces ).map( ( parsedNamespace ) => { - const slug = `data-${ kebabCase( parsedNamespace.name ) }`; - return { - title: parsedNamespace.title, - slug, - markdown_source: `${ baseRepoUrl }/docs/designers-developers/developers/data/${ slug }.md`, - parent: 'data', - }; - } ) - ); + return Object.values( parsedNamespaces ).map( ( parsedNamespace ) => { + const slug = `data-${ kebabCase( parsedNamespace.name ) }`; + return { + title: parsedNamespace.title, + slug, + markdown_source: `${ baseRepoUrl }/docs/designers-developers/developers/data/${ slug }.md`, + parent: 'data', + }; + } ); +} + +function getRootManifest( tocFileName ) { + return generateRootManifestFromTOCItems( require( tocFileName ) ); +} + +function generateRootManifestFromTOCItems( items, parent = null ) { + let pageItems = []; + items.map( ( obj ) => { + const fileName = Object.keys( obj )[ 0 ]; + const children = obj[ fileName ]; + + let slug = nth( fileName.split( '/' ), -1 ).replace( '.md', '' ); + if ( 'readme' === slug.toLowerCase() ) { + slug = nth( fileName.split( '/' ), -2 ); + + // Special case - the root 'docs' readme needs the 'handbook' slug. + if ( parent === null && 'docs' === slug ) { + slug = 'handbook'; + } + } + let title = upperFirst( camelCase( slug ) ); + const markdownSource = fs.readFileSync( fileName, 'utf8' ); + const titleMarkdown = markdownSource.match( /^#\s(.+)$/m ); + if ( titleMarkdown ) { + title = titleMarkdown[ 1 ]; + } + + pageItems.push( { + title: title, + slug: slug, + markdown_source: `${ baseRepoUrl }\/${ fileName }`, + parent: parent, + } ); + if ( Array.isArray( children ) && children.length ) { + pageItems = pageItems.concat( generateRootManifestFromTOCItems( children, slug ) ); + } + } ); + return pageItems; } module.exports = { getPackageManifest, getComponentManifest, getDataManifest, + getRootManifest, }; From 2fc34bc151bd53b72dc9a5fb7d4112be3ae8828c Mon Sep 17 00:00:00 2001 From: Stephen Edgar <stephen@netweb.com.au> Date: Tue, 4 Dec 2018 21:53:06 +1100 Subject: [PATCH 180/254] Docs: Make contributors history links clcikable (#12574) ## Description <!-- Please describe what you have changed or added --> Makes existing HTML links clcikable ## How has this been tested? <!-- Please describe in detail how you tested your changes. --> <!-- Include details of your testing environment, tests ran to see how --> <!-- your change affects other areas of the code, etc. --> Links are not clickable at https://wordpress.org/gutenberg/handbook/contributors/history/ ## Screenshots <!-- if applicable --> ## Types of changes <!-- What types of changes does your code introduce? --> <!-- Bug fix (non-breaking change which fixes an issue) --> <!-- New feature (non-breaking change which adds functionality) --> <!-- Breaking change (fix or feature that would cause existing functionality to not work as expected) --> Docs ## Checklist: - [x] My code is tested. - [x] My code follows the WordPress code style. <!-- Check code: `npm run lint`, Guidelines: https://make.wordpress.org/core/handbook/best-practices/coding-standards/javascript/ --> - [x] My code follows the accessibility standards. <!-- Guidelines: https://make.wordpress.org/core/handbook/best-practices/coding-standards/accessibility-coding-standards/ --> - [x] My code has proper inline documentation. <!-- Guidelines: https://make.wordpress.org/core/handbook/best-practices/inline-documentation-standards/javascript/ --> --- docs/contributors/history.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/contributors/history.md b/docs/contributors/history.md index a1d755e2994db8..b38aacc43ecce9 100644 --- a/docs/contributors/history.md +++ b/docs/contributors/history.md @@ -1,19 +1,19 @@ # History ## Survey -There was a survey done: https://make.wordpress.org/core/2017/04/07/editor-experience-survey-results/ +There was a survey done: [https://make.wordpress.org/core/2017/04/07/editor-experience-survey-results/](https://make.wordpress.org/core/2017/04/07/editor-experience-survey-results/) ## Inspiration This includes a list of historical articles and influences on Gutenberg. -- LivingDocs: https://beta.livingdocs.io/articles -- Parrot: https://intenseminimalism.com/2017/parrot-an-integrated-site-builder-and-editor-concept-for-wordpress/ +- LivingDocs: [https://beta.livingdocs.io/articles](https://beta.livingdocs.io/articles) +- Parrot: [https://intenseminimalism.com/2017/parrot-an-integrated-site-builder-and-editor-concept-for-wordpress/](https://intenseminimalism.com/2017/parrot-an-integrated-site-builder-and-editor-concept-for-wordpress/) - Apple Keynote - Slack - Google Sites v2 ## Blog posts by the team -- Gutenberg tag on make/core: updates and much more: https://make.wordpress.org/core/tag/gutenberg/ -- Suggested revised timeline: https://make.wordpress.org/core/2017/08/11/revised-suggested-roadmap-for-gutenberg-and-customization/ -- Discovering Gutenberg: https://make.wordpress.org/core/2017/08/08/discovering-gutenberg-and-next-steps/ +- Gutenberg tag on make/core: updates and much more: [https://make.wordpress.org/core/tag/gutenberg/](https://make.wordpress.org/core/tag/gutenberg/) +- Suggested revised timeline: [https://make.wordpress.org/core/2017/08/11/revised-suggested-roadmap-for-gutenberg-and-customization/](https://make.wordpress.org/core/2017/08/11/revised-suggested-roadmap-for-gutenberg-and-customization/) +- Discovering Gutenberg: [https://make.wordpress.org/core/2017/08/08/discovering-gutenberg-and-next-steps/](https://make.wordpress.org/core/2017/08/08/discovering-gutenberg-and-next-steps/) From 46f789a5c873ba0b23436983545052e0f09df507 Mon Sep 17 00:00:00 2001 From: Jorge <jorge.costa@developer.pt> Date: Tue, 4 Dec 2018 21:49:37 +0000 Subject: [PATCH 181/254] Try: fix end 2 end inline tokens test (#12459) The adding-inline-tokens.test.js test was failing on Travis, instead of an image upload being successful we got an error reporting that no permissions exist. This commit uses 2 commands on the docker container to do the following actions: Create an uploads folder inside wp-content. Change the folder permissions to 766. 766 was chosen because according to the hierarchy we should follow when trying to fix permission problems available in https://codex.wordpress.org/Changing_File_Permissions is the most adequate value. The value bellow it (765) does not work. --- bin/install-wordpress.sh | 4 ++++ test/e2e/specs/adding-inline-tokens.test.js | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/bin/install-wordpress.sh b/bin/install-wordpress.sh index 643c42b972f347..a346152a579bda 100755 --- a/bin/install-wordpress.sh +++ b/bin/install-wordpress.sh @@ -77,5 +77,9 @@ fi echo -e $(status_message "Activating Gutenberg...") docker-compose $DOCKER_COMPOSE_FILE_OPTIONS run --rm $CLI plugin activate gutenberg >/dev/null +# Make sure the uploads folder exist and we have permissions to add files there. +docker-compose $DOCKER_COMPOSE_FILE_OPTIONS run --rm $CONTAINER mkdir -p /var/www/html/wp-content/uploads +docker-compose $DOCKER_COMPOSE_FILE_OPTIONS run --rm $CONTAINER chmod -v 767 /var/www/html/wp-content/uploads + # 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/test/e2e/specs/adding-inline-tokens.test.js b/test/e2e/specs/adding-inline-tokens.test.js index ddc67658599dfd..d1504f7e35eb7d 100644 --- a/test/e2e/specs/adding-inline-tokens.test.js +++ b/test/e2e/specs/adding-inline-tokens.test.js @@ -21,7 +21,7 @@ describe( 'adding inline tokens', () => { await newPost(); } ); - it.skip( 'should insert inline image', async () => { + it( 'should insert inline image', async () => { // Create a paragraph. await clickBlockAppender(); await page.keyboard.type( 'a ' ); From 20edbf2593af3dc1c1a8fcfccdc41a710ebb6806 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20=28Greg=29=20Zi=C3=B3=C5=82kowski?= <grzegorz@gziolo.pl> Date: Wed, 5 Dec 2018 10:35:08 +0100 Subject: [PATCH 182/254] Editor: Fix regression introduced in CopyHandler (#12543) * Edior: Fix regression introduced in CopyHandler * Refactor CopyHandler to be less dependent on withData implementation --- .../src/components/copy-handler/index.js | 57 +++++++++---------- 1 file changed, 28 insertions(+), 29 deletions(-) diff --git a/packages/editor/src/components/copy-handler/index.js b/packages/editor/src/components/copy-handler/index.js index f9da173bf03058..90d8324d033fc1 100644 --- a/packages/editor/src/components/copy-handler/index.js +++ b/packages/editor/src/components/copy-handler/index.js @@ -11,8 +11,8 @@ class CopyHandler extends Component { constructor() { super( ...arguments ); - this.onCopy = this.onCopy.bind( this ); - this.onCut = this.onCut.bind( this ); + this.onCopy = ( event ) => this.props.onCopy( event ); + this.onCut = ( event ) => this.props.onCut( event ); } componentDidMount() { @@ -25,16 +25,6 @@ class CopyHandler extends Component { document.removeEventListener( 'cut', this.onCut ); } - onCopy( event ) { - this.props.onCopy( event.clipboardData ); - event.preventDefault(); - } - - onCut( event ) { - this.props.onCut( event.clipboardData ); - event.preventDefault(); - } - render() { return null; } @@ -50,29 +40,38 @@ export default compose( [ } = select( 'core/editor' ); const { removeBlocks } = dispatch( 'core/editor' ); - const selectedBlockClientId = getSelectedBlockClientId(); - const selectedBlockClientIds = selectedBlockClientId ? [ selectedBlockClientId ] : getMultiSelectedBlockClientIds(); + const onCopy = ( event ) => { + const selectedBlockClientIds = getSelectedBlockClientId() ? + [ getSelectedBlockClientId() ] : + getMultiSelectedBlockClientIds(); - return { - onCopy( dataTransfer ) { - if ( selectedBlockClientIds.length === 0 ) { - return; - } + if ( selectedBlockClientIds.length === 0 ) { + return; + } - // Let native copy behaviour take over in input fields. - if ( ! hasMultiSelection() && documentHasSelection() ) { - return; - } + // Let native copy behaviour take over in input fields. + if ( ! hasMultiSelection() && documentHasSelection() ) { + return; + } - const serialized = serialize( getBlocksByClientId( selectedBlockClientIds ) ); + const serialized = serialize( getBlocksByClientId( selectedBlockClientIds ) ); - dataTransfer.setData( 'text/plain', serialized ); - dataTransfer.setData( 'text/html', serialized ); - }, - onCut( dataTransfer ) { - this.onCopy( dataTransfer ); + event.clipboardData.setData( 'text/plain', serialized ); + event.clipboardData.setData( 'text/html', serialized ); + + event.preventDefault(); + }; + + return { + onCopy, + onCut( event ) { + onCopy( event ); if ( hasMultiSelection() ) { + const selectedBlockClientIds = getSelectedBlockClientId() ? + [ getSelectedBlockClientId() ] : + getMultiSelectedBlockClientIds(); + removeBlocks( selectedBlockClientIds ); } }, From fccbb4bb69e8b36aff0c8b2bf1d47c2a0cbd0847 Mon Sep 17 00:00:00 2001 From: Pascal Birchler <pascal.birchler@gmail.com> Date: Wed, 5 Dec 2018 12:08:41 +0100 Subject: [PATCH 183/254] Handbook: improve I18N docs (#12576) * Fixes for i18n docs. Clarify wording/examples. * Clarify sentence about babel plugin * Always capitalize I18N abbreviaton. --- .../developers/internationalization.md | 48 ++++++++++++------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/docs/designers-developers/developers/internationalization.md b/docs/designers-developers/developers/internationalization.md index 602b7494880855..68cafea0e4257f 100644 --- a/docs/designers-developers/developers/internationalization.md +++ b/docs/designers-developers/developers/internationalization.md @@ -1,33 +1,47 @@ # Internationalization -## PHP - -WordPress has long offered a number of ways to create translatable strings in PHP. +This document aims to give an overview of the possibilities for both internationalization and localization when developing with Gutenberg. -### Common methods +## PHP -- `__( $string_to_translate, $text_domain )` - translatable string wrapper, denoting translation namespace -- `_e( $string_to_translate, $text_domain )` - transltable string wrapper, with echo to print. -- `esc_html__( $string_to_translate, $text_domain )` - escapes and returns translation -- `esc_html_e( $string_to_translate, $text_domain )` - escapes, translates, and prints -- `_n( $singular, $plural, $number, $text_domain )` - Translatable singular/plural string, using %d to inject changing number digit. +For years, WordPress has been providing the necessary tools and functions to internationalize plugins and themes. This includes helper functions like `__()` and similar. -### More Resources +### Common Methods -- i18n for Developers - Covers numbers in translatable strings, best practices. -- WP-CLI can be used to generate translation files. +- `__( 'Hello World', 'my-text-domain' )`: Translate a certain string. +- `_x( 'Block', 'noun', 'my-text-domain' )`: Translate a certain string with some additional context. +- `_e( 'Hello World', 'my-text-domain' )`: Translate and print a certain string. +- `esc_html__( 'Hello World', 'my-text-domain' )`: Translate a certain string and escape it for safe use in HTML output. +- `esc_html_e( 'Hello World', 'my-text-domain' )`: Translate a certain string, escape it for safe use in HTML output, and print it. +- `_n( '%s Comment', '%s Comments', $number, 'my-text-domain' )`: Translate and retrieve the singular or plural form based on the supplied number. + Usually used in combination with `sprintf()` and `number_format_i18n()`. ## JavaScript Historically, `wp_localize_script()` has been used to put server-side PHP data into a properly-escaped native JavaScript object. -The new editor introduces a new approach to translating strings for the editor through a new package called `@wordpress/i18n` and a build tool for Babel called `@wordpress/babel-plugin-makepot` to create the necessary translation file (requires use of babel to compile code to extract the i18n methods). +The new editor introduces a new approach to translating strings for the editor through a new package called `@wordpress/i18n`. The new script package is registered with WordPress as `wp-i18n` and should be declared as a dependency during `wp_register_script()` and imported as a global off the Window object as `wp.i18n`. +Depending on your developer workflow, you might want to use WP-CLI's `wp i18n make-pot` command or a build tool for Babel called `@wordpress/babel-plugin-makepot` to create the necessary translation file. The latter approach integrates with Babel to extract the I18N methods. + ### Common methods in wp.i18n (may look similar) -- `setLocaleData( data: Object, domain: string )` - Create new Jed instance providing translation data for a domain (probably writing this to the DOM in escaped in PHP function). -- `__( stringToTranslate, textDomain )` - translatable string wrapper, denoting translation namespace -- `_n( singular, plural, number, textDomain )` - Translatable singular/plural string, using %d to inject changing number digit. -- `_x( singular, plural, number, textDomain )` - gettext equivalent for translation +- `setLocaleData( data: Object, domain: string )`: Creates a new I18N instance providing translation data for a domain. +- `__( 'Hello World', 'my-text-domain' )`: Translate a certain string. +- `_n( '%s Comment', '%s Comments', numberOfComments, 'my-text-domain' )`: Translate and retrieve the singular or plural form based on the supplied number. +- `_x( 'Default', 'block style', 'my-text-domain' )`: Translate a certain string with some additional context. +- `sprintf()`: JavaScript port of the PHP function with the same name. + +### Loading Translations + +WordPress 5.0 introduces a new function called `wp_set_script_translations( 'my-script-handle', 'my-text-domain' )` to load translation files for a given script handle. + +You can learn more about it in [the JavaScript I18N dev note](https://make.wordpress.org/core/2018/11/09/new-javascript-i18n-support-in-wordpress/). + +## More Resources + +- [WP-CLI I18N command to generate translation catalogues](https://github.com/wp-cli/i18n-command) +- [Plugin Developer Handbook](https://developer.wordpress.org/plugins/internationalization/) +- [Theme Developer Handbook](https://developer.wordpress.org/themes/internationalization/) From 12f1ac9f4fd0c9fadceda81a836f40081c8c1032 Mon Sep 17 00:00:00 2001 From: Matthew Riley MacPherson <hi@tofumatt.com> Date: Wed, 5 Dec 2018 12:04:07 +0000 Subject: [PATCH 184/254] docs: Remove "1440" line (#11882) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: Remove "1440" line The first line in a project's repo should be more explanative than quippy 😅 Besides: the GitHub description already contains that exact sentence. I find it sort of obscure and not very helpful; it probably confuses plenty of younger people unaware that we used to read these things called "books", "printed" on "paper". 😆 * docs: Tweak alt attribute --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 1f8ae7ce5b83f3..03251ceba2160e 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,7 @@ [![Build Status](https://img.shields.io/travis/WordPress/gutenberg/master.svg)](https://travis-ci.org/WordPress/gutenberg) [![lerna](https://img.shields.io/badge/maintained%20with-lerna-cc00ff.svg)](https://lernajs.io/) -Printing since 1440. - -![Gutenberg editing](https://cldup.com/H0oKBfpidk.png) +![Screenshot of the Gutenberg Editor, editing a post in WordPress](https://cldup.com/H0oKBfpidk.png) This repo is the development hub for the <a href="https://make.wordpress.org/core/2017/01/04/focus-tech-and-design-leads/">editor focus in WordPress Core</a>. `Gutenberg` is the project name. From 47a5e492b463186de5a59b38973938210820cd34 Mon Sep 17 00:00:00 2001 From: Sheri Bigelow <sheri@designsimply.com> Date: Wed, 5 Dec 2018 05:04:59 -0700 Subject: [PATCH 185/254] Update CONTRIBUTORS.md (#12594) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This patch adds myself to the list of Gutenberg contributors list for an e2e test I committed on Dec 3 🎉 --- CONTRIBUTORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 498a8ea1c131c7..8f7b5d83932b64 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -121,3 +121,4 @@ This list is manually curated to include valuable contributions by volunteers th | @greatislander | @greatislander | | @sharazghouri | @sharaz | | @jakeparis | @jakeparis | +| @designsimply | @designsimply | From 874321773aff9be87b591e63c5fc606ede9872ac Mon Sep 17 00:00:00 2001 From: Sheri Bigelow <sheri@designsimply.com> Date: Wed, 5 Dec 2018 05:07:01 -0700 Subject: [PATCH 186/254] Testing: add e2e test for the HTML block (#12550) * Testing: add e2e test for the HTML block * chore: Tweak test spec name * chore: Tweak test spec name --- .../blocks/__snapshots__/html.test.js.snap | 8 ++++++ test/e2e/specs/blocks/html.test.js | 26 +++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 test/e2e/specs/blocks/__snapshots__/html.test.js.snap create mode 100644 test/e2e/specs/blocks/html.test.js diff --git a/test/e2e/specs/blocks/__snapshots__/html.test.js.snap b/test/e2e/specs/blocks/__snapshots__/html.test.js.snap new file mode 100644 index 00000000000000..b72664d0212768 --- /dev/null +++ b/test/e2e/specs/blocks/__snapshots__/html.test.js.snap @@ -0,0 +1,8 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`HTML block can be created by typing "/html" 1`] = ` +"<!-- wp:html --> +<p>Pythagorean theorem: +<var>a</var><sup>2</sup> + <var>b</var><sup>2</sup> = <var>c</var><sup>2</sup> </p> +<!-- /wp:html -->" +`; diff --git a/test/e2e/specs/blocks/html.test.js b/test/e2e/specs/blocks/html.test.js new file mode 100644 index 00000000000000..8f1996c00435a2 --- /dev/null +++ b/test/e2e/specs/blocks/html.test.js @@ -0,0 +1,26 @@ +/** + * Internal dependencies + */ +import { + clickBlockAppender, + getEditedPostContent, + newPost, +} from '../../support/utils'; + +describe( 'HTML block', () => { + beforeEach( async () => { + await newPost(); + } ); + + it( 'can be created by typing "/html"', async () => { + // Create a Custom HTML block with the slash shortcut. + await clickBlockAppender(); + await page.keyboard.type( '/html' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( '<p>Pythagorean theorem: ' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( '<var>a</var><sup>2</sup> + <var>b</var><sup>2</sup> = <var>c</var><sup>2</sup> </p>' ); + + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); +} ); From 67a67e5290f36441a20debfd2947e786d436fa5f Mon Sep 17 00:00:00 2001 From: jankimoradiya <janki.moradiya@gmail.com> Date: Thu, 6 Dec 2018 04:34:14 +0530 Subject: [PATCH 187/254] Removed Link from ## User Handbook Section (#12589) Removed "Visit the User Handbook" link from ##User Handbook Section. ## Description We have removed "Visit the User Handbook" link from User Handbook section. Reff. #12585. --- docs/readme.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/readme.md b/docs/readme.md index 93b58f0b17cda7..5164003a654a87 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -12,7 +12,6 @@ Learn how to build blocks and extend the editor, best practices for designing bl Discover the new features Gutenberg offers, learn how your site will be affected by the new editor and how to keep using the old interface, and tips for creating beautiful posts and pages. -[Visit the User Handbook](../docs/users/readme.md) ## Contributor Handbook From 5565c566bb1e75534418e5b51ddf857e668561e1 Mon Sep 17 00:00:00 2001 From: Marcus Kazmierczak <marcus@mkaz.com> Date: Wed, 5 Dec 2018 15:05:53 -0800 Subject: [PATCH 188/254] Docs: Add details on installing/using packages (#12595) * Add details on installing/using packages * docs: a few tweaks --- .../developers/packages.md | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/docs/designers-developers/developers/packages.md b/docs/designers-developers/developers/packages.md index cde5bf98553aeb..7096f67115a28e 100644 --- a/docs/designers-developers/developers/packages.md +++ b/docs/designers-developers/developers/packages.md @@ -2,8 +2,39 @@ Gutenberg exposes a list of JavaScript packages and tools for WordPress development. -## Using the packages +## Using the packages via WordPress global JavaScript packages are available as a registered script in WordPress and can be accessed using the `wp` global variable. +If you wanted to use the `PlainText` component from the editor module, first you would specify `wp-editor` as a dependency when you enqueue your script: + +```php +wp_enqueue_script( + 'my-custom-block', + plugins_url( $block_path, __FILE__ ), + array( 'wp-blocks', 'wp-editor', 'wp-element', 'wp-i18n' ) +); +``` + +After the dependency is declared, you can access the module in your JavaScript code using the global `wp` like so: +```js +const { PlainText } = wp.editor; + +``` + +## Using the packages via npm + All the packages are also available on [npm](https://www.npmjs.com/org/wordpress) if you want to bundle them in your code. + +Using the same `PlainText` example, you would install the editor module with npm: + +```bash +npm install @wordpress/editor --save +``` + +Once installed, you can access the component in your code using: + +```js +import { PlainText } from '@wordpress/editor'; +``` + From f62f336176001ddb0a8e63275529a732a2fc11aa Mon Sep 17 00:00:00 2001 From: Marcus Kazmierczak <marcus@mkaz.com> Date: Wed, 5 Dec 2018 15:09:23 -0800 Subject: [PATCH 189/254] Docs: Taking a stab at updating FAQ (#12486) ## Description The FAQ appears to be originally written in 2017 when the project was a bit younger. I've taken a stab at updating with more current information and what we now know. I also included a link to Matt's own FAQ around Gutenberg and WordPress 5.0, which likely has more recent information and questions. Fixes #8535 ## Types of changes Documentation update. --- docs/designers-developers/faq.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/designers-developers/faq.md b/docs/designers-developers/faq.md index 2cc8e6684a6be1..fb2d16dfe68fce 100644 --- a/docs/designers-developers/faq.md +++ b/docs/designers-developers/faq.md @@ -1,5 +1,7 @@ # Frequently Asked Questions +This initial set of questions were created when the Gutenberg project was relatively new, for a more recent set of questions, please see Matt's November 2018 post [WordPress 5.0: A Gutenberg FAQ](https://ma.tt/2018/11/a-gutenberg-faq/). + ## What is Gutenberg? “Gutenberg” is the name of the project to create a new editor experience for WordPress. The goal is to create a new post and page editing experience that makes it easy for anyone to create rich post layouts. This was the kickoff goal: @@ -12,16 +14,14 @@ Key takeaways include the following points: - By embracing blocks as an interaction paradigm, we can unify multiple different interfaces into one. Instead of learning how to write shortcodes and custom HTML, or pasting URLs to embed media, there's a common, reliable flow for inserting any kind of content. - “Mystery meat” refers to hidden features in software, features that you have to discover. WordPress already supports a large number of blocks and 30+ embeds, so let's surface them. -Gutenberg is being developed on [GitHub](https://github.com/WordPress/gutenberg) under the WordPress organization, and you can try [a beta version today—it's available in the plugin repository](https://wordpress.org/plugins/gutenberg/). It's important to keep in mind that Gutenberg is not yet fully functional, feature complete, or production ready. But we'd love your help to make that a reality. +Gutenberg is being developed on [GitHub](https://github.com/WordPress/gutenberg) under the WordPress organization, and you can use it today — [available in the plugin repository](https://wordpress.org/plugins/gutenberg/). ## When will Gutenberg be merged into WordPress? -We are hoping that Gutenberg will be sufficiently polished, tested, iterated, and proven enough to be [merged into WordPress 5.0](https://ma.tt/2017/06/4-8-and-whats-coming/), with an estimated release date of 2018. +Gutenberg will be merged into WordPress 5.0, and will be released at the end of 2018, or early 2019. Please follow [WordPress.org News](https://wordpress.org/news/) for the latest information. The editor focus started in early 2017 with the first three months spent designing, planning, prototyping, and testing prototypes, to help us inform how to approach this project. The actual plugin, which you can install from the repository, was launched during WordCamp Europe in June. -There is still plenty of work to do, but we are moving fast. New updates tend to be released on a weekly basis. Please help us by giving feedback, surfacing issues, testing, or even contributing, so we can be ready in time for WordPress 5.0. - ## What are “blocks” and why are we using them? The current WordPress editor is an open text window—it’s always been a wonderful blank canvas for writing, but when it comes to building posts and pages with images, multimedia, embedded content from social media, polls, and other elements, it required a mix of different approaches that were not always intuitive: @@ -273,9 +273,7 @@ Indeed. There are multiple ways in which custom post types can leverage Gutenber ## Will there be columns? -Our primary goal is on a solid block foundation before exploring column support. - -*See:* [Issue #219](https://github.com/WordPress/gutenberg/issues/219) +Yes, a columns block is available in Gutenberg. ## Will there be nested blocks? @@ -285,7 +283,7 @@ See also [Issue #428](https://github.com/WordPress/gutenberg/issues/428) ## Will drag and drop be used for rearranging blocks? -A key value for Gutenberg has been to see drag and drop as an _additive enhancement_. When explicit actions (_click_ or _tab_ + _space_) exist can we add drag and drop as an enhancement on top of it. So yes, we expect drag and drop to be part of Gutenberg, even if it isn't today. +Yes, you can drag and drop blocks to rearrange their order. ## Can themes _style_ blocks? @@ -322,9 +320,10 @@ We realize it's a big change. We also think there will be many new opportunities ## Will I be able to opt out of Gutenberg for my site? -We are looking at ways to make Gutenberg configurable for many use cases, including disabling different aspects (like blocks, panels, etc.). +There is a “Classic” block, which is virtually the same as the current editor, except in block form. + +There is also the [Classic Editor Plugin](https://wordpress.org/plugins/classic-editor/) which restores the previous editor, see the plugin for more information. The WordPress Core team has committed to supporting the Classic Editor Plugin [until December 2021](https://make.wordpress.org/core/2018/11/07/classic-editor-plugin-support-window/). -There is also be a “Classic” block, which is virtually the same as the current editor, except in block form. There’s also likely to be a very popular plugin in the repository to replace Gutenberg with the classic editor. ## How will custom TinyMCE buttons work in Gutenberg? @@ -366,7 +365,8 @@ var blocks = wp.blocks.parse( postContent ); In PHP: ```php -$blocks = gutenberg_parse_blocks( $post_content ); +$blocks = gutenberg_parse_blocks( $post_content ); // plugin +$blocks = parse_blocks( $post_content ); // WordPress 5.0 ``` ## Why should I start using this once released? From 387bb77b0f21c73399f71445f85ad9db91750d7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20Van=C2=A0Dorpe?= <iseulde@automattic.com> Date: Thu, 6 Dec 2018 11:17:29 +0100 Subject: [PATCH 190/254] RichText: fix onSetup doc (#12607) --- packages/editor/src/components/rich-text/index.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/editor/src/components/rich-text/index.js b/packages/editor/src/components/rich-text/index.js index 872049c948e6a2..de0e7dd5aa3633 100644 --- a/packages/editor/src/components/rich-text/index.js +++ b/packages/editor/src/components/rich-text/index.js @@ -134,12 +134,9 @@ export class RichText extends Component { } /** - * Handles the onSetup event for the TinyMCE component. + * Sets a reference to the TinyMCE editor instance. * - * Will setup event handlers for the TinyMCE instance. - * An `onSetup` function in the props will be called if it is present. - * - * @param {tinymce} editor The editor instance as passed by TinyMCE. + * @param {Editor} editor The editor instance as passed by TinyMCE. */ onSetup( editor ) { this.editor = editor; From d7eaeb916779bd31d9abe657ac14f7f20cbc4a73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=87a=C4=9Fda=C5=9F=20Da=C4=9F?= <cagdasdag81@gmail.com> Date: Fri, 7 Dec 2018 18:09:50 +0300 Subject: [PATCH 191/254] Update broken links (#12660) * Update broken links There was 2 broken links. I changed same way with Data Module Reference page. * Update docs/designers-developers/developers/block-api/README.md Co-Authored-By: cagdasdag <cagdasdag81@gmail.com> * Update docs/designers-developers/developers/block-api/README.md Co-Authored-By: cagdasdag <cagdasdag81@gmail.com> --- docs/designers-developers/developers/block-api/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/designers-developers/developers/block-api/README.md b/docs/designers-developers/developers/block-api/README.md index 1301449ecdf2c1..2c18a261d8fc5c 100644 --- a/docs/designers-developers/developers/block-api/README.md +++ b/docs/designers-developers/developers/block-api/README.md @@ -4,8 +4,8 @@ Blocks are the fundamental element of the Gutenberg editor. They are the primary ## Registering a block -All blocks must be registered before they can be used in the editor. You can learn about block registration, and the available options, in the [block registration](block-api/block-registration.md) documentation. +All blocks must be registered before they can be used in the editor. You can learn about block registration, and the available options, in the [block registration](../../../../docs/designers-developers/developers/block-api/block-registration.md) documentation. ## Block `edit` and `save` -The `edit` and `save` functions define the editor interface with which a user would interact, and the markup to be serialized back when a post is saved. They are the heart of how a block operates, so they are [covered separately](block-api/block-edit-save.md). +The `edit` and `save` functions define the editor interface with which a user would interact, and the markup to be serialized back when a post is saved. They are the heart of how a block operates, so they are [covered separately](../../../../docs/designers-developers/developers/block-api/block-edit-save.md). From 50cafddcc593fbb37c99f54c878079bd9aa5bfe7 Mon Sep 17 00:00:00 2001 From: Marcus Kazmierczak <marcus@mkaz.com> Date: Fri, 7 Dec 2018 07:10:21 -0800 Subject: [PATCH 192/254] Docs: Update Glossary (#12479) * Update glossar with missing terms * Convert glossary to use dl/dt/dd dtags. Fixes #9976 * Fix TinyMCE link * remove spacing around tags * Add template definition, with link * Updates per review * Update docs/designers-developers/glossary.md Co-Authored-By: mkaz <marcus@mkaz.com> --- docs/designers-developers/glossary.md | 78 ++++++++++++++++++++------- 1 file changed, 59 insertions(+), 19 deletions(-) diff --git a/docs/designers-developers/glossary.md b/docs/designers-developers/glossary.md index afa574179db276..f51c0d16d5dbab 100644 --- a/docs/designers-developers/glossary.md +++ b/docs/designers-developers/glossary.md @@ -1,21 +1,61 @@ # Glossary -- **Attribute sources**: An object describing the attributes shape of a block. The keys can be named as most appropriate to describe the state of a block type. The value for each key is a function which describes the strategy by which the attribute value should be extracted from the content of a saved post's content. When processed, a new object is created, taking the form of the keys defined in the attribute sources, where each value is the result of the attribute source function. -- **Attributes**: The object representation of the current state of a block in post content. When loading a saved post, this is determined by the attribute sources for the block type. These values can change over time during an editing session when the user modifies a block, and are used when determining how to serialize the block. -- **Block**: The abstract term used to describe units of markup that, composed together, form the content or layout of a webpage. The idea combines concepts of what in WordPress today we achieve with shortcodes, custom HTML, and embed discovery into a single consistent API and user experience. -- **Block Categories**: These are not a WordPress taxonomy, but instead used internally to sort blocks in the Block Inserter. -- **Block Inserter**: Primary interface for selecting from the available blocks, triggered by plus icon buttons on Blocks or in the top-left of the editor interface. -- **Block name**: A unique identifier for a block type, consisting of a plugin-specific namespace and a short label describing the block's intent. e.g. `core/image` -- **Block type**: In contrast with the blocks composing a particular post, a block type describes the blueprint by which any block of that type should behave. So while there may be many images within a post, each behaves consistent with a unified image block type definition. -- **Classic block**: -- **Dynamic block**: A type of block where the content of which may change and cannot be determined at the time of saving a post, instead calculated any time the post is shown on the front of a site. These blocks may save fallback content or no content at all in their JavaScript implementation, instead deferring to a PHP block implementation for runtime rendering. -- **RichText**: A common component enabling rich content editing including bold, italics, hyperlinks, etc. It is not too much unlike the single editor region of the legacy post editor, and is in fact powered by the same TinyMCE library. -- **Inspector**: A block settings region shown in place of the post settings when a block is selected. Fields may be shown here to allow the user to customize the selected block. -- **Post settings**: A sidebar region containing metadata fields for the post, including scheduling, visibility, terms, and featured image. -- **Reusable block**: -- **Sidebar**: -- **Serialization**: The process of converting a block's attributes object into HTML markup, typically occurring when saving the post. -- **Static block**: A type of block where the content of which is known at the time of saving a post. A static block will be saved with HTML markup directly in post content. -- **TinyMCE**: [TinyMCE](https://www.tinymce.com/) is a web-based JavaScript WYSIWYG (What You See Is What You Get) editor. -- **Toolbar**: A set of button controls. In the context of a block, usually referring to the toolbar of block controls shown above the selected block. -- **Template**: +<dl> +<dt>Attribute sources</dt> +<dd>An object describing the attributes shape of a block. The keys can be named as most appropriate to describe the state of a block type. The value for each key is a function which describes the strategy by which the attribute value should be extracted from the content of a saved post's content. When processed, a new object is created, taking the form of the keys defined in the attribute sources, where each value is the result of the attribute source function.</dd> + +<dt>Attributes</dt> +<dd>The object representation of the current state of a block in post content. When loading a saved post, this is determined by the attribute sources for the block type. These values can change over time during an editing session when the user modifies a block, and are used when determining how to serialize the block.</dd> + +<dt>Block</dt> +<dd>The abstract term used to describe units of markup that, composed together, form the content or layout of a webpage. The idea combines concepts of what in WordPress today we achieve with shortcodes, custom HTML, and embed discovery into a single consistent API and user experience.</dd> + +<dt>Block Categories</dt> +<dd>These are not a WordPress taxonomy, but instead used internally to sort blocks in the Block Inserter.</dd> + +<dt>Block Inserter</dt> +<dd>Primary interface for selecting from the available blocks, triggered by plus icon buttons on Blocks or in the top-left of the editor interface.</dd> + +<dt>Block name</dt> +<dd>A unique identifier for a block type, consisting of a plugin-specific namespace and a short label describing the block's intent. e.g. <code>core/image</code></dd> + +<dt>Block type</dt> +<dd>In contrast with the blocks composing a particular post, a block type describes the blueprint by which any block of that type should behave. So while there may be many images within a post, each behaves consistent with a unified image block type definition.</dd> + +<dt>Classic block</dt> +<dd>A block which embeds the TinyMCE editor as a block, TinyMCE was the base of the previous core editor. Older content created prior to the block editor will be loaded in to a single Classic block.</dd> + +<dt>Dynamic block</dt> +<dd>A type of block where the content of which may change and cannot be determined at the time of saving a post, instead calculated any time the post is shown on the front of a site. These blocks may save fallback content or no content at all in their JavaScript implementation, instead deferring to a PHP block implementation for runtime rendering.</dd> + +<dt>Inspector</dt> +<dd>A block settings region shown in place of the post settings when a block is selected. Fields may be shown here to allow the user to customize the selected block.</dd> + +<dt>Post settings</dt> +<dd>A sidebar region containing metadata fields for the post, including scheduling, visibility, terms, and featured image.</dd> + +<dt>RichText</dt> +<dd>A common component enabling rich content editing including bold, italics, hyperlinks, etc. It is not too much unlike the single editor region of the legacy post editor, and is in fact powered by the same TinyMCE library.</dd> + +<dt>Reusable block</dt> +<dd>A block that is saved and then can be shared as a reusable, repeatable piece of content.</dd> + +<dt>Sidebar</dt> +<dd>The panel on the right which contains the document and block settings. The sidebar is toggled using the Settings gear icon.</dd> + +<dt>Serialization</dt> +<dd>The process of converting a block's attributes object into HTML markup, which occurs each time a block is edited.</dd> + +<dt>Static block</dt> +<dd>A type of block where the content of which is known at the time of saving a post. A static block will be saved with HTML markup directly in post content.</dd> + +<dt>TinyMCE</dt> +<dd><a href="https://www.tinymce.com/">TinyMCE</a> is a web-based JavaScript WYSIWYG (What You See Is What You Get) editor.</dd> + +<dt>Toolbar</dt> +<dd>A set of button controls. In the context of a block, usually referring to the toolbar of block controls shown above the selected block.</dd> + +<dt>Template</dt> +<dd> A template is a pre-defined arrangement of blocks, possibly with predefined attributes or placeholder content. You can provide a template for a post type, to give users a starting point when creating a new piece of content, or inside a custom block with the <code>InnerBlocks</code> component. See the templates documentation for more information. See <a href="../../developers/block-api/block-templates.md">templates documentation</a> for more information.</dd> + +</dl> From 6b8cd54fde910aee9928679a18bb8daf84daf03d Mon Sep 17 00:00:00 2001 From: George Hotelling <george@hotelling.net> Date: Fri, 7 Dec 2018 10:10:48 -0500 Subject: [PATCH 193/254] Add documentation for `safeDecodeURI()` and `filterURLForDisplay()` (#12570) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add documentation for `safeDecodeURI()` and `filterURLForDisplay()` * Whitespace * Consistant Capit… i mean capitalization * Oxford comma Co-Authored-By: georgeh <george@hotelling.net> --- packages/url/README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/url/README.md b/packages/url/README.md index 4c092d670fc9fd..fb78d8808bb2be 100644 --- a/packages/url/README.md +++ b/packages/url/README.md @@ -167,4 +167,20 @@ const newUrl = removeQueryArgs( 'https://wordpress.org?foo=bar&bar=baz&baz=fooba Removes one or more query string arguments from the given URL. +### safeDecodeURI + +```js +const badUri = safeDecodeURI( '%z' ); // does not throw an Error, simply returns '%z' +``` + +Safely decodes a URI with `decodeURI`. Returns the URI unmodified if `decodeURI` throws an Error. + +### filterURLForDisplay + +```js +const displayUrl = filterURLForDisplay( 'https://www.wordpress.org/gutenberg/' ); // wordpress.org/gutenberg +``` + +Returns a URL for display, without protocol, www subdomain, or trailing slash. + <br/><br/><p align="center"><img src="https://s.w.org/style/images/codeispoetry.png?1" alt="Code is Poetry." /></p> From f5b18c2a1e8ea7a5be8183b78fbaa71bc3da262e Mon Sep 17 00:00:00 2001 From: Joseph Chesterton <37393284+josephchesterton@users.noreply.github.com> Date: Sat, 8 Dec 2018 01:13:08 +1000 Subject: [PATCH 194/254] Update theme-support.md (#12661) --- docs/designers-developers/developers/themes/theme-support.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/designers-developers/developers/themes/theme-support.md b/docs/designers-developers/developers/themes/theme-support.md index abc611dc5d000b..a5da2c7199e97b 100644 --- a/docs/designers-developers/developers/themes/theme-support.md +++ b/docs/designers-developers/developers/themes/theme-support.md @@ -4,7 +4,7 @@ The new Blocks include baseline support in all themes, enhancements to opt-in to There are a few new concepts to consider when building themes: -- **Editor Color Palette** - A default set of colors is provided, but themes and register their own and optionally lock users into picking from the defined palette. +- **Editor Color Palette** - A default set of colors is provided, but themes can register their own and optionally lock users into picking from the defined palette. - **Editor Text Size Palette** - A default set of sizes is provided, but themes and register their own and optionally lock users into picking from preselected sizes. - **Responsive Embeds** - Themes must opt-in to responsive embeds. - **Frontend & Editor Styles** - To get the most out of blocks, theme authors will want to make sure Core styles look good and opt-in, or write their own styles to best fit their theme. From 7f118ddbb66452ccbcf820ba36240330845df9d7 Mon Sep 17 00:00:00 2001 From: Jorge Costa <jorge.costa@developer.pt> Date: Sat, 8 Dec 2018 06:50:33 +0000 Subject: [PATCH 195/254] Fix: Undoing Image Selection in Image Block results in Broken Image (#12567) --- packages/block-library/src/image/edit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-library/src/image/edit.js b/packages/block-library/src/image/edit.js index cb059b5bfcdc60..551e3dadda255a 100644 --- a/packages/block-library/src/image/edit.js +++ b/packages/block-library/src/image/edit.js @@ -414,7 +414,7 @@ class ImageEdit extends Component { </BlockControls> ); - if ( isEditing ) { + if ( isEditing || ! url ) { const src = isExternal ? url : undefined; return ( <Fragment> From 715af6b4980ae75c8dd505f06765f610894ef0ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20Van=C2=A0Dorpe?= <iseulde@automattic.com> Date: Sat, 8 Dec 2018 11:38:14 +0100 Subject: [PATCH 196/254] Optimize isViewportMatch (#12542) --- packages/viewport/src/store/selectors.js | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/packages/viewport/src/store/selectors.js b/packages/viewport/src/store/selectors.js index ea8308d6ee156a..d379beeb5052fa 100644 --- a/packages/viewport/src/store/selectors.js +++ b/packages/viewport/src/store/selectors.js @@ -1,8 +1,3 @@ -/** - * External dependencies - */ -import { takeRight } from 'lodash'; - /** * Returns true if the viewport matches the given query, or false otherwise. * @@ -20,9 +15,10 @@ import { takeRight } from 'lodash'; * @return {boolean} Whether viewport matches query. */ export function isViewportMatch( state, query ) { - // Pad to _at least_ two elements to take from the right, effectively - // defaulting the left-most value. - const key = takeRight( [ '>=', ...query.split( ' ' ) ], 2 ).join( ' ' ); + // Default to `>=` if no operator is present. + if ( query.indexOf( ' ' ) === -1 ) { + query = '>= ' + query; + } - return !! state[ key ]; + return !! state[ query ]; } From 8e772be25ad16891db000351a929c4957c54029f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20Van=C2=A0Dorpe?= <iseulde@automattic.com> Date: Sat, 8 Dec 2018 11:39:02 +0100 Subject: [PATCH 197/254] Cache createBlock call in isUnmodifiedDefaultBlock (#12521) * Cache createBlock call in isUnmodifiedDefaultBlock * Invalidate cache when default block name changes * Merge ifs --- packages/blocks/src/api/test/utils.js | 29 +++++++++++++++++++++++++++ packages/blocks/src/api/utils.js | 11 +++++++++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/packages/blocks/src/api/test/utils.js b/packages/blocks/src/api/test/utils.js index 0bfeb8b0fe2586..ea3fe579084410 100644 --- a/packages/blocks/src/api/test/utils.js +++ b/packages/blocks/src/api/test/utils.js @@ -67,5 +67,34 @@ describe( 'block helpers', () => { expect( isUnmodifiedDefaultBlock( block ) ).toBe( false ); } ); + + it( 'should invalidate cache if the default block name changed', () => { + registerBlockType( 'core/test-block1', { + attributes: { + includesDefault1: { + type: 'boolean', + default: true, + }, + }, + save: noop, + category: 'common', + title: 'test block', + } ); + registerBlockType( 'core/test-block2', { + attributes: { + includesDefault2: { + type: 'boolean', + default: true, + }, + }, + save: noop, + category: 'common', + title: 'test block', + } ); + setDefaultBlockName( 'core/test-block1' ); + isUnmodifiedDefaultBlock( createBlock( 'core/test-block1' ) ); + setDefaultBlockName( 'core/test-block2' ); + expect( isUnmodifiedDefaultBlock( createBlock( 'core/test-block2' ) ) ).toBe( true ); + } ); } ); } ); diff --git a/packages/blocks/src/api/utils.js b/packages/blocks/src/api/utils.js index 392593b440096e..6dcabbcbcea012 100644 --- a/packages/blocks/src/api/utils.js +++ b/packages/blocks/src/api/utils.js @@ -38,7 +38,16 @@ export function isUnmodifiedDefaultBlock( block ) { return false; } - const newDefaultBlock = createBlock( defaultBlockName ); + // Cache a created default block if no cache exists or the default block + // name changed. + if ( + ! isUnmodifiedDefaultBlock.block || + isUnmodifiedDefaultBlock.block.name !== defaultBlockName + ) { + isUnmodifiedDefaultBlock.block = createBlock( defaultBlockName ); + } + + const newDefaultBlock = isUnmodifiedDefaultBlock.block; const blockType = getBlockType( defaultBlockName ); return every( blockType.attributes, ( value, key ) => From d385f3d76f7574a8284c41bfbe12ab1c909ff66c Mon Sep 17 00:00:00 2001 From: Andrea Fercia <a.fercia@gmail.com> Date: Sat, 8 Dec 2018 11:40:06 +0100 Subject: [PATCH 198/254] Font Size Picker: Use a menuitemradio role and better labels. (#12372) * Use a menuitemradio role and better labels. * Restore Button and remove MenuItem import. * Use template literals. --- .../components/src/font-size-picker/index.js | 45 +++++++++++-------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/packages/components/src/font-size-picker/index.js b/packages/components/src/font-size-picker/index.js index 16329316d5ad1a..2908b3879c1ebf 100644 --- a/packages/components/src/font-size-picker/index.js +++ b/packages/components/src/font-size-picker/index.js @@ -6,8 +6,7 @@ import { map } from 'lodash'; /** * WordPress dependencies */ -import { __, _x } from '@wordpress/i18n'; -import { withInstanceId } from '@wordpress/compose'; +import { __, _x, sprintf } from '@wordpress/i18n'; /** * Internal dependencies @@ -37,6 +36,7 @@ function FontSizePicker( { }; const currentFont = fontSizes.find( ( font ) => font.size === value ); + const currentFontSizeName = ( currentFont && currentFont.name ) || ( ! value && _x( 'Normal', 'font size name' ) ) || _x( 'Custom', 'font size name' ); return ( <BaseControl label={ __( 'Font Size' ) }> @@ -51,26 +51,34 @@ function FontSizePicker( { isLarge onClick={ onToggle } aria-expanded={ isOpen } - aria-label={ __( 'Custom font size' ) } + aria-label={ sprintf( + /* translators: %s: font size name */ + __( 'Font size: %s' ), currentFontSizeName + ) } > - { ( currentFont && currentFont.name ) || ( ! value && _x( 'Normal', 'font size name' ) ) || _x( 'Custom', 'font size name' ) } + { currentFontSizeName } </Button> ) } renderContent={ () => ( <NavigableMenu> - { map( fontSizes, ( { name, size, slug } ) => ( - <Button - key={ slug } - onClick={ () => onChange( slug === 'normal' ? undefined : size ) } - className={ 'is-font-' + slug } - role="menuitem" - > - { ( value === size || ( ! value && slug === 'normal' ) ) && <Dashicon icon="saved" /> } - <span className="components-font-size-picker__dropdown-text-size" style={ { fontSize: size } }> - { name } - </span> - </Button> - ) ) } + { map( fontSizes, ( { name, size, slug } ) => { + const isSelected = ( value === size || ( ! value && slug === 'normal' ) ); + + return ( + <Button + key={ slug } + onClick={ () => onChange( slug === 'normal' ? undefined : size ) } + className={ `is-font-${ slug }` } + role="menuitemradio" + aria-checked={ isSelected } + > + { isSelected && <Dashicon icon="saved" /> } + <span className="components-font-size-picker__dropdown-text-size" style={ { fontSize: size } }> + { name } + </span> + </Button> + ); + } ) } </NavigableMenu> ) } /> @@ -90,7 +98,6 @@ function FontSizePicker( { onClick={ () => onChange( undefined ) } isSmall isDefault - aria-label={ __( 'Reset font size' ) } > { __( 'Reset' ) } </Button> @@ -112,4 +119,4 @@ function FontSizePicker( { ); } -export default withInstanceId( FontSizePicker ); +export default FontSizePicker; From 13459da2de90fba6279668b8ab6771020e8f4a2b Mon Sep 17 00:00:00 2001 From: Pascal Birchler <pascal.birchler@gmail.com> Date: Sat, 8 Dec 2018 04:40:40 -0600 Subject: [PATCH 199/254] Set document title for preview interstitial (#12466) --- packages/editor/src/components/post-preview-button/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/editor/src/components/post-preview-button/index.js b/packages/editor/src/components/post-preview-button/index.js index 5c31e9e271de8f..b735ebcd5324ed 100644 --- a/packages/editor/src/components/post-preview-button/index.js +++ b/packages/editor/src/components/post-preview-button/index.js @@ -80,6 +80,7 @@ function writeInterstitialMessage( targetDocument ) { `; targetDocument.write( markup ); + targetDocument.title = __( 'Generating preview…' ); targetDocument.close(); } From 1d5fa326dce3998c8fef809645121ba5a6b75384 Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Sun, 9 Dec 2018 09:52:04 -0600 Subject: [PATCH 200/254] Fix e2e tests after the WordPress 5.0 upgrade (#12715) * Fix e2e tests after the WordPress 5.0 upgrade * Remove the php unit tests testing the WP5.0 core instead of the plugin --- phpunit/class-parsing-test.php | 105 ---- ...ss-rest-block-renderer-controller-test.php | 454 --------------- phpunit/class-rest-blocks-controller-test.php | 204 ------- phpunit/class-rest-search-controller-test.php | 521 ------------------ test/e2e/specs/classic-editor.test.js | 25 - test/e2e/specs/font-size-picker.test.js | 12 +- test/e2e/specs/new-post.test.js | 7 + test/e2e/specs/templates.test.js | 10 +- test/e2e/specs/writing-flow.test.js | 3 + test/e2e/support/plugins.js | 2 +- test/e2e/test-plugins/post-formats.php | 15 + 11 files changed, 42 insertions(+), 1316 deletions(-) delete mode 100644 phpunit/class-parsing-test.php delete mode 100644 phpunit/class-rest-block-renderer-controller-test.php delete mode 100644 phpunit/class-rest-blocks-controller-test.php delete mode 100644 phpunit/class-rest-search-controller-test.php delete mode 100644 test/e2e/specs/classic-editor.test.js create mode 100644 test/e2e/test-plugins/post-formats.php diff --git a/phpunit/class-parsing-test.php b/phpunit/class-parsing-test.php deleted file mode 100644 index b854f8e306a2bd..00000000000000 --- a/phpunit/class-parsing-test.php +++ /dev/null @@ -1,105 +0,0 @@ -<?php -/** - * PEG.js parser (PHP code target) tests - * - * @package Gutenberg - */ - -class Parsing_Test extends WP_UnitTestCase { - protected static $fixtures_dir; - - function parsing_test_filenames() { - self::$fixtures_dir = dirname( dirname( __FILE__ ) ) . '/test/integration/full-content/fixtures'; - - require_once dirname( dirname( __FILE__ ) ) . '/lib/parser.php'; - - $fixture_filenames = array_merge( - glob( self::$fixtures_dir . '/*.json' ), - glob( self::$fixtures_dir . '/*.html' ) - ); - $fixture_filenames = array_values( - array_unique( - array_map( - array( $this, 'clean_fixture_filename' ), - $fixture_filenames - ) - ) - ); - - return array_map( - array( $this, 'pass_parser_fixture_filenames' ), - $fixture_filenames - ); - } - - function clean_fixture_filename( $filename ) { - $filename = basename( $filename ); - $filename = preg_replace( '/\..+$/', '', $filename ); - return $filename; - } - - function pass_parser_fixture_filenames( $filename ) { - return array( - "$filename.html", - "$filename.parsed.json", - ); - } - - function strip_r( $input ) { - return str_replace( "\r", '', $input ); - } - - /** - * @dataProvider parsing_test_filenames - */ - function test_spec_parser_output( $html_filename, $parsed_json_filename ) { - $html_path = self::$fixtures_dir . '/' . $html_filename; - $parsed_json_path = self::$fixtures_dir . '/' . $parsed_json_filename; - - foreach ( array( $html_path, $parsed_json_path ) as $filename ) { - if ( ! file_exists( $filename ) ) { - throw new Exception( "Missing fixture file: '$filename'" ); - } - } - - $html = self::strip_r( file_get_contents( $html_path ) ); - $expected_parsed = json_decode( self::strip_r( file_get_contents( $parsed_json_path ) ), true ); - - $parser = new Gutenberg_PEG_Parser; - $result = $parser->parse( _gutenberg_utf8_split( $html ) ); - - $this->assertEquals( - $expected_parsed, - $result, - "File '$parsed_json_filename' does not match expected value" - ); - } - - /** - * @dataProvider parsing_test_filenames - */ - function test_default_parser_output( $html_filename, $parsed_json_filename ) { - // include the parser if it was not yet loaded. - require_once dirname( __FILE__ ) . '/../packages/block-serialization-default-parser/parser.php'; - $html_path = self::$fixtures_dir . '/' . $html_filename; - $parsed_json_path = self::$fixtures_dir . '/' . $parsed_json_filename; - - foreach ( array( $html_path, $parsed_json_path ) as $filename ) { - if ( ! file_exists( $filename ) ) { - throw new Exception( "Missing fixture file: '$filename'" ); - } - } - - $html = self::strip_r( file_get_contents( $html_path ) ); - $expected_parsed = json_decode( self::strip_r( file_get_contents( $parsed_json_path ) ), true ); - - $parser = new WP_Block_Parser(); - $result = json_decode( json_encode( $parser->parse( $html ) ), true ); - - $this->assertEquals( - $expected_parsed, - $result, - "File '$parsed_json_filename' does not match expected value" - ); - } -} diff --git a/phpunit/class-rest-block-renderer-controller-test.php b/phpunit/class-rest-block-renderer-controller-test.php deleted file mode 100644 index 3691c4d162af4f..00000000000000 --- a/phpunit/class-rest-block-renderer-controller-test.php +++ /dev/null @@ -1,454 +0,0 @@ -<?php -/** - * WP_REST_Block_Renderer_Controller tests. - * - * @package gutenberg - */ - -/** - * Tests for WP_REST_Block_Renderer_Controller. - * - * @covers WP_REST_Block_Renderer_Controller - */ -class REST_Block_Renderer_Controller_Test extends WP_Test_REST_Controller_Testcase { - - /** - * The REST API route for the block renderer. - * - * @var string - */ - protected static $rest_api_route = '/wp/v2/block-renderer/'; - - /** - * Test block's name. - * - * @var string - */ - protected static $block_name = 'core/test-block'; - - /** - * Test post context block's name. - * - * @var string - */ - protected static $context_block_name = 'core/context-test-block'; - - /** - * Test API user's ID. - * - * @var int - */ - protected static $user_id; - - /** - * Test post ID. - * - * @var int - */ - protected static $post_id; - - /** - * Author test user ID. - * - * @var int - */ - protected static $author_id; - - /** - * Create test data before the tests run. - * - * @param WP_UnitTest_Factory $factory Helper that lets us create fake data. - */ - public static function wpSetUpBeforeClass( $factory ) { - self::$user_id = $factory->user->create( - array( - 'role' => 'editor', - ) - ); - - self::$author_id = $factory->user->create( - array( - 'role' => 'author', - ) - ); - - self::$post_id = $factory->post->create( - array( - 'post_title' => 'Test Post', - ) - ); - } - - /** - * Delete test data after our tests run. - */ - public static function wpTearDownAfterClass() { - self::delete_user( self::$user_id ); - } - - /** - * Set up. - * - * @see gutenberg_register_rest_routes() - */ - public function setUp() { - $this->register_test_block(); - $this->register_post_context_test_block(); - parent::setUp(); - } - - /** - * Tear down. - */ - public function tearDown() { - WP_Block_Type_Registry::get_instance()->unregister( self::$block_name ); - WP_Block_Type_Registry::get_instance()->unregister( self::$context_block_name ); - parent::tearDown(); - } - - /** - * Register test block. - */ - public function register_test_block() { - register_block_type( - self::$block_name, - array( - 'attributes' => array( - 'some_string' => array( - 'type' => 'string', - 'default' => 'some_default', - ), - 'some_int' => array( - 'type' => 'integer', - ), - 'some_array' => array( - 'type' => 'array', - 'items' => array( - 'type' => 'integer', - ), - ), - ), - 'render_callback' => array( $this, 'render_test_block' ), - ) - ); - } - - /** - * Register test block with post_id as attribute for post context test. - */ - public function register_post_context_test_block() { - register_block_type( - self::$context_block_name, - array( - 'attributes' => array(), - 'render_callback' => array( $this, 'render_post_context_test_block' ), - ) - ); - } - - /** - * Test render callback. - * - * @param array $attributes Props. - * @return string Rendered attributes, which is here just JSON. - */ - public function render_test_block( $attributes ) { - return wp_json_encode( $attributes ); - } - - /** - * Test render callback for testing post context. - * - * @return string - */ - public function render_post_context_test_block() { - return get_the_title(); - } - - /** - * Check that the route was registered properly. - * - * @covers WP_REST_Block_Renderer_Controller::register_routes() - */ - public function test_register_routes() { - $dynamic_block_names = get_dynamic_block_names(); - $this->assertContains( self::$block_name, $dynamic_block_names ); - - $routes = rest_get_server()->get_routes(); - foreach ( $dynamic_block_names as $dynamic_block_name ) { - $this->assertArrayHasKey( self::$rest_api_route . "(?P<name>$dynamic_block_name)", $routes ); - } - } - - /** - * Test getting item without permissions. - * - * @covers WP_REST_Block_Renderer_Controller::get_item() - */ - public function test_get_item_without_permissions() { - wp_set_current_user( 0 ); - - $request = new WP_REST_Request( 'GET', self::$rest_api_route . self::$block_name ); - $request->set_param( 'context', 'edit' ); - - $response = rest_get_server()->dispatch( $request ); - - $this->assertErrorResponse( 'gutenberg_block_cannot_read', $response, rest_authorization_required_code() ); - } - - /** - * Test getting item without 'edit' context. - */ - public function test_get_item_with_invalid_context() { - wp_set_current_user( self::$user_id ); - - $request = new WP_REST_Request( 'GET', self::$rest_api_route . self::$block_name ); - $response = rest_get_server()->dispatch( $request ); - - $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); - } - - /** - * Test getting item with invalid block name. - * - * @covers WP_REST_Block_Renderer_Controller::get_item() - */ - public function test_get_item_invalid_block_name() { - wp_set_current_user( self::$user_id ); - $request = new WP_REST_Request( 'GET', self::$rest_api_route . 'core/123' ); - - $request->set_param( 'context', 'edit' ); - $response = rest_get_server()->dispatch( $request ); - - $this->assertErrorResponse( 'rest_no_route', $response, 404 ); - } - - /** - * Check getting item with an invalid param provided. - * - * @covers WP_REST_Block_Renderer_Controller::get_item() - */ - public function test_get_item_invalid_attribute() { - wp_set_current_user( self::$user_id ); - $request = new WP_REST_Request( 'GET', self::$rest_api_route . self::$block_name ); - $request->set_param( 'context', 'edit' ); - $request->set_param( - 'attributes', - array( - 'some_string' => array( 'no!' ), - ) - ); - $response = rest_get_server()->dispatch( $request ); - $this->assertEquals( 400, $response->get_status() ); - } - - /** - * Check getting item with an invalid param provided. - * - * @covers WP_REST_Block_Renderer_Controller::get_item() - */ - public function test_get_item_unrecognized_attribute() { - wp_set_current_user( self::$user_id ); - $request = new WP_REST_Request( 'GET', self::$rest_api_route . self::$block_name ); - $request->set_param( 'context', 'edit' ); - $request->set_param( - 'attributes', - array( - 'unrecognized' => 'yes', - ) - ); - $response = rest_get_server()->dispatch( $request ); - $this->assertEquals( 400, $response->get_status() ); - } - - /** - * Check getting item with default attributes provided. - * - * @covers WP_REST_Block_Renderer_Controller::get_item() - */ - public function test_get_item_default_attributes() { - wp_set_current_user( self::$user_id ); - - $block_type = WP_Block_Type_Registry::get_instance()->get_registered( self::$block_name ); - $defaults = array(); - foreach ( $block_type->attributes as $key => $attribute ) { - if ( isset( $attribute['default'] ) ) { - $defaults[ $key ] = $attribute['default']; - } - } - - $request = new WP_REST_Request( 'GET', self::$rest_api_route . self::$block_name ); - $request->set_param( 'context', 'edit' ); - $request->set_param( 'attributes', array() ); - $response = rest_get_server()->dispatch( $request ); - $this->assertEquals( 200, $response->get_status() ); - $data = $response->get_data(); - - $this->assertEquals( $defaults, json_decode( $data['rendered'], true ) ); - $this->assertEquals( - json_decode( $block_type->render( $defaults ) ), - json_decode( $data['rendered'] ) - ); - } - - /** - * Check getting item with attributes provided. - * - * @covers WP_REST_Block_Renderer_Controller::get_item() - */ - public function test_get_item() { - wp_set_current_user( self::$user_id ); - - $block_type = WP_Block_Type_Registry::get_instance()->get_registered( self::$block_name ); - $attributes = array( - 'some_int' => '123', - 'some_string' => 'foo', - 'some_array' => array( 1, '2', 3 ), - ); - - $expected_attributes = $attributes; - $expected_attributes['some_int'] = (int) $expected_attributes['some_int']; - $expected_attributes['some_array'] = array_map( 'intval', $expected_attributes['some_array'] ); - - $request = new WP_REST_Request( 'GET', self::$rest_api_route . self::$block_name ); - $request->set_param( 'context', 'edit' ); - $request->set_param( 'attributes', $attributes ); - $response = rest_get_server()->dispatch( $request ); - $this->assertEquals( 200, $response->get_status() ); - $data = $response->get_data(); - - $this->assertEquals( $expected_attributes, json_decode( $data['rendered'], true ) ); - $this->assertEquals( - json_decode( $block_type->render( $attributes ), true ), - json_decode( $data['rendered'], true ) - ); - } - - - - /** - * Check success response for getting item with layout attribute provided. - */ - public function test_get_item_with_layout() { - wp_set_current_user( self::$user_id ); - - $attributes = array( - 'layout' => 'foo', - ); - - $request = new WP_REST_Request( 'GET', self::$rest_api_route . self::$block_name ); - $request->set_param( 'context', 'edit' ); - $request->set_param( 'attributes', $attributes ); - $response = rest_get_server()->dispatch( $request ); - $this->assertEquals( 200, $response->get_status() ); - } - - /** - * Test getting item with post context. - */ - public function test_get_item_with_post_context() { - wp_set_current_user( self::$user_id ); - - $expected_title = 'Test Post'; - $request = new WP_REST_Request( 'GET', self::$rest_api_route . self::$context_block_name ); - $request->set_param( 'context', 'edit' ); - - // Test without post ID. - $response = rest_get_server()->dispatch( $request ); - - $this->assertEquals( 200, $response->get_status() ); - $data = $response->get_data(); - - $this->assertTrue( empty( $data['rendered'] ) ); - - // Now test with post ID. - $request->set_param( 'post_id', self::$post_id ); - $response = rest_get_server()->dispatch( $request ); - - $this->assertEquals( 200, $response->get_status() ); - $data = $response->get_data(); - - $this->assertEquals( $expected_title, $data['rendered'] ); - } - - /** - * Test getting item with invalid post ID. - */ - public function test_get_item_without_permissions_invalid_post() { - wp_set_current_user( self::$user_id ); - - $request = new WP_REST_Request( 'GET', self::$rest_api_route . self::$context_block_name ); - $request->set_param( 'context', 'edit' ); - - // Test with invalid post ID. - $request->set_param( 'post_id', PHP_INT_MAX ); - $response = rest_get_server()->dispatch( $request ); - - $this->assertErrorResponse( 'gutenberg_block_cannot_read', $response, 403 ); - } - - /** - * Test getting item without permissions to edit context post. - */ - public function test_get_item_without_permissions_cannot_edit_post() { - wp_set_current_user( self::$author_id ); - - $request = new WP_REST_Request( 'GET', self::$rest_api_route . self::$context_block_name ); - $request->set_param( 'context', 'edit' ); - - // Test with private post ID. - $request->set_param( 'post_id', self::$post_id ); - $response = rest_get_server()->dispatch( $request ); - - $this->assertErrorResponse( 'gutenberg_block_cannot_read', $response, 403 ); - } - - /** - * Get item schema. - * - * @covers WP_REST_Block_Renderer_Controller::get_item_schema() - */ - public function test_get_item_schema() { - $request = new WP_REST_Request( 'OPTIONS', self::$rest_api_route . self::$block_name ); - $response = rest_get_server()->dispatch( $request ); - $data = $response->get_data(); - - $this->assertEqualSets( array( 'GET' ), $data['endpoints'][0]['methods'] ); - $this->assertEqualSets( - array( 'name', 'context', 'attributes', 'post_id' ), - array_keys( $data['endpoints'][0]['args'] ) - ); - $this->assertEquals( 'object', $data['endpoints'][0]['args']['attributes']['type'] ); - - $this->assertArrayHasKey( 'schema', $data ); - $this->assertEquals( 'rendered-block', $data['schema']['title'] ); - $this->assertEquals( 'object', $data['schema']['type'] ); - $this->arrayHasKey( 'rendered', $data['schema']['properties'] ); - $this->arrayHasKey( 'string', $data['schema']['properties']['rendered']['type'] ); - $this->assertEquals( array( 'edit' ), $data['schema']['properties']['rendered']['context'] ); - } - - public function test_update_item() { - $this->markTestSkipped( 'Controller doesn\'t implement update_item().' ); - } - - public function test_create_item() { - $this->markTestSkipped( 'Controller doesn\'t implement create_item().' ); - } - - public function test_delete_item() { - $this->markTestSkipped( 'Controller doesn\'t implement delete_item().' ); - } - - public function test_get_items() { - $this->markTestSkipped( 'Controller doesn\'t implement get_items().' ); - } - - public function test_context_param() { - $this->markTestSkipped( 'Controller doesn\'t implement context_param().' ); - } - - public function test_prepare_item() { - $this->markTestSkipped( 'Controller doesn\'t implement prepare_item().' ); - } -} diff --git a/phpunit/class-rest-blocks-controller-test.php b/phpunit/class-rest-blocks-controller-test.php deleted file mode 100644 index 5a9e71af7e33e9..00000000000000 --- a/phpunit/class-rest-blocks-controller-test.php +++ /dev/null @@ -1,204 +0,0 @@ -<?php -/** - * WP_REST_Blocks_Controller tests - * - * @package gutenberg - */ - -/** - * Tests for WP_REST_Blocks_Controller. - */ -class REST_Blocks_Controller_Test extends WP_UnitTestCase { - - /** - * Our fake block's post ID. - * - * @var int - */ - protected static $post_id; - - /** - * Our fake user IDs, keyed by their role. - * - * @var array - */ - protected static $user_ids; - - /** - * Create fake data before our tests run. - * - * @param WP_UnitTest_Factory $factory Helper that lets us create fake data. - */ - public static function wpSetUpBeforeClass( $factory ) { - self::$post_id = wp_insert_post( - array( - 'post_type' => 'wp_block', - 'post_status' => 'publish', - 'post_title' => 'My cool block', - 'post_content' => '<!-- wp:paragraph --><p>Hello!</p><!-- /wp:paragraph -->', - ) - ); - - self::$user_ids = array( - 'editor' => $factory->user->create( array( 'role' => 'editor' ) ), - 'author' => $factory->user->create( array( 'role' => 'author' ) ), - 'contributor' => $factory->user->create( array( 'role' => 'contributor' ) ), - ); - } - - /** - * Delete our fake data after our tests run. - */ - public static function wpTearDownAfterClass() { - wp_delete_post( self::$post_id ); - - foreach ( self::$user_ids as $user_id ) { - self::delete_user( $user_id ); - } - } - - /** - * Test cases for test_capabilities(). - */ - public function data_capabilities() { - return array( - array( 'create', 'editor', 201 ), - array( 'create', 'author', 201 ), - array( 'create', 'contributor', 403 ), - array( 'create', null, 401 ), - - array( 'read', 'editor', 200 ), - array( 'read', 'author', 200 ), - array( 'read', 'contributor', 200 ), - array( 'read', null, 401 ), - - array( 'update_delete_own', 'editor', 200 ), - array( 'update_delete_own', 'author', 200 ), - array( 'update_delete_own', 'contributor', 403 ), - - array( 'update_delete_others', 'editor', 200 ), - array( 'update_delete_others', 'author', 403 ), - array( 'update_delete_others', 'contributor', 403 ), - array( 'update_delete_others', null, 401 ), - ); - } - - /** - * Exhaustively check that each role either can or cannot create, edit, - * update, and delete reusable blocks. - * - * @dataProvider data_capabilities - */ - public function test_capabilities( $action, $role, $expected_status ) { - if ( $role ) { - $user_id = self::$user_ids[ $role ]; - wp_set_current_user( $user_id ); - } else { - wp_set_current_user( 0 ); - } - - switch ( $action ) { - case 'create': - $request = new WP_REST_Request( 'POST', '/wp/v2/blocks' ); - $request->set_body_params( - array( - 'title' => 'Test', - 'content' => '<!-- wp:paragraph --><p>Test</p><!-- /wp:paragraph -->', - ) - ); - - $response = rest_get_server()->dispatch( $request ); - $this->assertEquals( $expected_status, $response->get_status() ); - - break; - - case 'read': - $request = new WP_REST_Request( 'GET', '/wp/v2/blocks/' . self::$post_id ); - - $response = rest_get_server()->dispatch( $request ); - $this->assertEquals( $expected_status, $response->get_status() ); - - break; - - case 'update_delete_own': - $post_id = wp_insert_post( - array( - 'post_type' => 'wp_block', - 'post_status' => 'publish', - 'post_title' => 'My cool block', - 'post_content' => '<!-- wp:paragraph --><p>Hello!</p><!-- /wp:paragraph -->', - 'post_author' => $user_id, - ) - ); - - $request = new WP_REST_Request( 'PUT', '/wp/v2/blocks/' . $post_id ); - $request->set_body_params( - array( - 'title' => 'Test', - 'content' => '<!-- wp:paragraph --><p>Test</p><!-- /wp:paragraph -->', - ) - ); - - $response = rest_get_server()->dispatch( $request ); - $this->assertEquals( $expected_status, $response->get_status() ); - - $request = new WP_REST_Request( 'DELETE', '/wp/v2/blocks/' . $post_id ); - - $response = rest_get_server()->dispatch( $request ); - $this->assertEquals( $expected_status, $response->get_status() ); - - wp_delete_post( $post_id ); - - break; - - case 'update_delete_others': - $request = new WP_REST_Request( 'PUT', '/wp/v2/blocks/' . self::$post_id ); - $request->set_body_params( - array( - 'title' => 'Test', - 'content' => '<!-- wp:paragraph --><p>Test</p><!-- /wp:paragraph -->', - ) - ); - - $response = rest_get_server()->dispatch( $request ); - $this->assertEquals( $expected_status, $response->get_status() ); - - $request = new WP_REST_Request( 'DELETE', '/wp/v2/blocks/' . self::$post_id ); - - $response = rest_get_server()->dispatch( $request ); - $this->assertEquals( $expected_status, $response->get_status() ); - - break; - - default: - $this->fail( "'$action' is not a valid action." ); - } - } - - /** - * Check that the raw title and content of a block can be accessed when there - * is no set schema, and that the rendered content of a block is not included - * in the response. - */ - public function test_content() { - wp_set_current_user( self::$user_ids['author'] ); - - $request = new WP_REST_Request( 'GET', '/wp/v2/blocks/' . self::$post_id ); - $response = rest_get_server()->dispatch( $request ); - $data = $response->get_data(); - - $this->assertEquals( - array( - 'raw' => 'My cool block', - ), - $data['title'] - ); - $this->assertEquals( - array( - 'raw' => '<!-- wp:paragraph --><p>Hello!</p><!-- /wp:paragraph -->', - 'protected' => false, - ), - $data['content'] - ); - } -} diff --git a/phpunit/class-rest-search-controller-test.php b/phpunit/class-rest-search-controller-test.php deleted file mode 100644 index 26649037e8ff22..00000000000000 --- a/phpunit/class-rest-search-controller-test.php +++ /dev/null @@ -1,521 +0,0 @@ -<?php -/** - * WP_REST_Search_Controller tests - * - * @package gutenberg - */ - -/** - * Tests for WP_REST_Search_Controller. - */ -class REST_Search_Controller_Test extends WP_Test_REST_Controller_Testcase { - - /** - * Posts with title 'my-footitle'. - * - * @var array - */ - private static $my_title_post_ids = array(); - - /** - * Pages with title 'my-footitle'. - * - * @var array - */ - private static $my_title_page_ids = array(); - - /** - * Posts with content 'my-foocontent'. - * - * @var array - */ - private static $my_content_post_ids = array(); - - /** - * Create fake data before our tests run. - * - * @param WP_UnitTest_Factory $factory Helper that lets us create fake data. - */ - public static function wpSetUpBeforeClass( $factory ) { - self::$my_title_post_ids = $factory->post->create_many( - 4, - array( - 'post_title' => 'my-footitle', - 'post_type' => 'post', - ) - ); - - self::$my_title_page_ids = $factory->post->create_many( - 4, - array( - 'post_title' => 'my-footitle', - 'post_type' => 'page', - ) - ); - - self::$my_content_post_ids = $factory->post->create_many( - 6, - array( - 'post_content' => 'my-foocontent', - ) - ); - } - - /** - * Delete our fake data after our tests run. - */ - public static function wpTearDownAfterClass() { - $post_ids = array_merge( - self::$my_title_post_ids, - self::$my_title_page_ids, - self::$my_content_post_ids - ); - - foreach ( $post_ids as $post_id ) { - wp_delete_post( $post_id, true ); - } - } - - /** - * Check that our routes get set up properly. - */ - public function test_register_routes() { - $routes = rest_get_server()->get_routes(); - - $this->assertArrayHasKey( '/wp/v2/search', $routes ); - $this->assertCount( 1, $routes['/wp/v2/search'] ); - } - - /** - * Check the context parameter. - */ - public function test_context_param() { - $response = $this->do_request_with_params( array(), 'OPTIONS' ); - $data = $response->get_data(); - - $this->assertEquals( 'view', $data['endpoints'][0]['args']['context']['default'] ); - $this->assertEquals( array( 'view', 'embed' ), $data['endpoints'][0]['args']['context']['enum'] ); - } - - /** - * Search through all content. - */ - public function test_get_items() { - $response = $this->do_request_with_params( - array( - 'per_page' => 100, - ) - ); - - $this->assertEquals( 200, $response->get_status() ); - $this->assertEqualSets( - array_merge( - self::$my_title_post_ids, - self::$my_title_page_ids, - self::$my_content_post_ids - ), - wp_list_pluck( $response->get_data(), 'id' ) - ); - } - - /** - * Search through all content with a low limit. - */ - public function test_get_items_with_limit() { - $response = $this->do_request_with_params( - array( - 'per_page' => 3, - ) - ); - - $this->assertEquals( 200, $response->get_status() ); - $this->assertEquals( 3, count( $response->get_data() ) ); - } - - /** - * Search through posts of any post type. - */ - public function test_get_items_search_type_post() { - $response = $this->do_request_with_params( - array( - 'per_page' => 100, - 'type' => 'post', - ) - ); - - $this->assertEquals( 200, $response->get_status() ); - $this->assertEqualSets( - array_merge( - self::$my_title_post_ids, - self::$my_title_page_ids, - self::$my_content_post_ids - ), - wp_list_pluck( $response->get_data(), 'id' ) - ); - } - - /** - * Search through posts of post type 'post'. - */ - public function test_get_items_search_type_post_subtype_post() { - $response = $this->do_request_with_params( - array( - 'per_page' => 100, - 'type' => 'post', - 'subtype' => 'post', - ) - ); - - $this->assertEquals( 200, $response->get_status() ); - $this->assertEqualSets( - array_merge( - self::$my_title_post_ids, - self::$my_content_post_ids - ), - wp_list_pluck( $response->get_data(), 'id' ) - ); - } - - /** - * Search through posts of post type 'page'. - */ - public function test_get_items_search_type_post_subtype_page() { - $response = $this->do_request_with_params( - array( - 'per_page' => 100, - 'type' => 'post', - 'subtype' => 'page', - ) - ); - - $this->assertEquals( 200, $response->get_status() ); - $this->assertEqualSets( - self::$my_title_page_ids, - wp_list_pluck( $response->get_data(), 'id' ) - ); - } - - /** - * Search through an invalid type - */ - public function test_get_items_search_type_invalid() { - $response = $this->do_request_with_params( - array( - 'per_page' => 100, - 'type' => 'invalid', - ) - ); - - $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); - } - - /** - * Search through posts of an invalid post type. - */ - public function test_get_items_search_type_post_subtype_invalid() { - $response = $this->do_request_with_params( - array( - 'per_page' => 100, - 'type' => 'post', - 'subtype' => 'invalid', - ) - ); - - $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); - } - - /** - * Search through posts and pages. - */ - public function test_get_items_search_posts_and_pages() { - $response = $this->do_request_with_params( - array( - 'per_page' => 100, - 'type' => 'post', - 'subtype' => 'post,page', - ) - ); - - $this->assertEquals( 200, $response->get_status() ); - $this->assertEqualSets( - array_merge( - self::$my_title_post_ids, - self::$my_title_page_ids, - self::$my_content_post_ids - ), - wp_list_pluck( $response->get_data(), 'id' ) - ); - } - - /** - * Search through all that matches a 'footitle' search. - */ - public function test_get_items_search_for_footitle() { - $response = $this->do_request_with_params( - array( - 'per_page' => 100, - 'search' => 'footitle', - ) - ); - - $this->assertEquals( 200, $response->get_status() ); - $this->assertEqualSets( - array_merge( - self::$my_title_post_ids, - self::$my_title_page_ids - ), - wp_list_pluck( $response->get_data(), 'id' ) - ); - } - - /** - * Search through all that matches a 'foocontent' search. - */ - public function test_get_items_search_for_foocontent() { - $response = $this->do_request_with_params( - array( - 'per_page' => 100, - 'search' => 'foocontent', - ) - ); - - $this->assertEquals( 200, $response->get_status() ); - $this->assertEqualSets( - self::$my_content_post_ids, - wp_list_pluck( $response->get_data(), 'id' ) - ); - } - - /** - * Test retrieving a single item isn't possible. - */ - public function test_get_item() { - /** The search controller does not allow getting individual item content */ - $request = new WP_REST_Request( 'GET', '/wp/v2/search' . self::$my_title_post_ids[0] ); - $response = rest_get_server()->dispatch( $request ); - $this->assertEquals( 404, $response->get_status() ); - } - - /** - * Test creating an item isn't possible. - */ - public function test_create_item() { - /** The search controller does not allow creating content */ - $request = new WP_REST_Request( 'POST', '/wp/v2/search' ); - $response = rest_get_server()->dispatch( $request ); - $this->assertEquals( 404, $response->get_status() ); - } - - /** - * Test updating an item isn't possible. - */ - public function test_update_item() { - /** The search controller does not allow upading content */ - $request = new WP_REST_Request( 'POST', '/wp/v2/search' . self::$my_title_post_ids[0] ); - $response = rest_get_server()->dispatch( $request ); - $this->assertEquals( 404, $response->get_status() ); - } - - /** - * Test deleting an item isn't possible. - */ - public function test_delete_item() { - /** The search controller does not allow deleting content */ - $request = new WP_REST_Request( 'DELETE', '/wp/v2/search' . self::$my_title_post_ids[0] ); - $response = rest_get_server()->dispatch( $request ); - $this->assertEquals( 404, $response->get_status() ); - } - - /** - * Test preparing the data contains the correct fields. - */ - public function test_prepare_item() { - $response = $this->do_request_with_params(); - $this->assertEquals( 200, $response->get_status() ); - - $data = $response->get_data(); - $this->assertEquals( - array( - 'id', - 'title', - 'url', - 'type', - 'subtype', - '_links', - ), - array_keys( $data[0] ) - ); - } - - /** - * Test preparing the data with limited fields contains the correct fields. - */ - public function test_prepare_item_limit_fields() { - if ( ! method_exists( 'WP_REST_Controller', 'get_fields_for_response' ) ) { - $this->markTestSkipped( 'Limiting fields requires the WP_REST_Controller::get_fields_for_response() method.' ); - } - - $response = $this->do_request_with_params( - array( - '_fields' => 'id,title', - ) - ); - $this->assertEquals( 200, $response->get_status() ); - - $data = $response->get_data(); - $this->assertEquals( - array( - 'id', - 'title', - '_links', - ), - array_keys( $data[0] ) - ); - } - - /** - * Tests the item schema is correct. - */ - public function test_get_item_schema() { - $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/search' ); - $response = rest_get_server()->dispatch( $request ); - $data = $response->get_data(); - $properties = $data['schema']['properties']; - - $this->assertArrayHasKey( 'id', $properties ); - $this->assertArrayHasKey( 'title', $properties ); - $this->assertArrayHasKey( 'url', $properties ); - $this->assertArrayHasKey( 'type', $properties ); - $this->assertArrayHasKey( 'subtype', $properties ); - } - - /** - * Tests that non-public post types are not allowed. - */ - public function test_non_public_post_type() { - $response = $this->do_request_with_params( - array( - 'type' => 'post', - 'subtype' => 'post,nav_menu_item', - ) - ); - $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); - } - - /** - * Test getting items directly with a custom search handler. - */ - public function test_custom_search_handler_get_items() { - $controller = new WP_REST_Search_Controller( array( new WP_REST_Dummy_Search_Handler( 10 ) ) ); - - $request = $this->get_request( - array( - 'page' => 1, - 'per_page' => 10, - 'type' => 'dummy', - 'subtype' => array( WP_REST_Search_Controller::TYPE_ANY ), - ) - ); - $response = $controller->get_items( $request ); - $this->assertEqualSets( range( 1, 10 ), wp_list_pluck( $response->get_data(), 'id' ) ); - - $request = $this->get_request( - array( - 'page' => 1, - 'per_page' => 10, - 'type' => 'dummy', - 'subtype' => array( 'dummy_first_type' ), - ) - ); - $response = $controller->get_items( $request ); - $this->assertEqualSets( range( 1, 5 ), wp_list_pluck( $response->get_data(), 'id' ) ); - } - - /** - * Test preparing an item directly with a custom search handler. - */ - public function test_custom_search_handler_prepare_item() { - $controller = new WP_REST_Search_Controller( array( new WP_REST_Dummy_Search_Handler( 10 ) ) ); - - $request = $this->get_request( - array( - 'type' => 'dummy', - 'subtype' => array( WP_REST_Search_Controller::TYPE_ANY ), - ) - ); - $response = $controller->prepare_item_for_response( 1, $request ); - $data = $response->get_data(); - $this->assertEquals( - array( - 'id', - 'title', - 'url', - 'type', - 'subtype', - ), - array_keys( $data ) - ); - } - - /** - * Test preparing an item directly with a custom search handler with limited fields. - */ - public function test_custom_search_handler_prepare_item_limit_fields() { - if ( ! method_exists( 'WP_REST_Controller', 'get_fields_for_response' ) ) { - $this->markTestSkipped( 'Limiting fields requires the WP_REST_Controller::get_fields_for_response() method.' ); - } - - $controller = new WP_REST_Search_Controller( array( new WP_REST_Dummy_Search_Handler( 10 ) ) ); - - $request = $this->get_request( - array( - 'type' => 'dummy', - 'subtype' => array( WP_REST_Search_Controller::TYPE_ANY ), - '_fields' => 'id,title', - ) - ); - $response = $controller->prepare_item_for_response( 1, $request ); - $data = $response->get_data(); - $this->assertEquals( - array( - 'id', - 'title', - ), - array_keys( $data ) - ); - } - - /** - * Test getting the collection params directly with a custom search handler. - */ - public function test_custom_search_handler_get_collection_params() { - $controller = new WP_REST_Search_Controller( array( new WP_REST_Dummy_Search_Handler( 10 ) ) ); - - $params = $controller->get_collection_params(); - $this->assertEquals( 'dummy', $params[ WP_REST_Search_Controller::PROP_TYPE ]['default'] ); - $this->assertEqualSets( array( 'dummy' ), $params[ WP_REST_Search_Controller::PROP_TYPE ]['enum'] ); - $this->assertEqualSets( array( 'dummy_first_type', 'dummy_second_type', WP_REST_Search_Controller::TYPE_ANY ), $params[ WP_REST_Search_Controller::PROP_SUBTYPE ]['items']['enum'] ); - } - - /** - * Perform a REST request to our search endpoint with given parameters. - */ - private function do_request_with_params( $params = array(), $method = 'GET' ) { - $request = $this->get_request( $params, $method ); - - return rest_get_server()->dispatch( $request ); - } - - /** - * Get a REST request object for given parameters. - */ - private function get_request( $params = array(), $method = 'GET' ) { - $request = new WP_REST_Request( $method, '/wp/v2/search' ); - - foreach ( $params as $param => $value ) { - $request->set_param( $param, $value ); - } - - return $request; - } -} diff --git a/test/e2e/specs/classic-editor.test.js b/test/e2e/specs/classic-editor.test.js deleted file mode 100644 index 0d51c93e5b3420..00000000000000 --- a/test/e2e/specs/classic-editor.test.js +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Internal dependencies - */ -import { visitAdmin } from '../support/utils'; - -describe( 'classic editor', () => { - beforeAll( async () => { - await visitAdmin( 'post-new.php', 'classic-editor' ); - } ); - - it( 'Should work properly', async () => { - // Click visual editor - await expect( page ).toClick( '#content-tmce' ); - await expect( page ).toClick( '#content_ifr' ); - - // type some random text - await page.keyboard.type( 'Typing in classic editor' ); - - // Switch to HTML mode - await expect( page ).toClick( '#content-html' ); - - const textEditorContent = await page.$eval( '.wp-editor-area', ( element ) => element.value ); - expect( textEditorContent ).toEqual( 'Typing in classic editor' ); - } ); -} ); diff --git a/test/e2e/specs/font-size-picker.test.js b/test/e2e/specs/font-size-picker.test.js index 4a55dd3e0dbc83..42bfe7e17f5a49 100644 --- a/test/e2e/specs/font-size-picker.test.js +++ b/test/e2e/specs/font-size-picker.test.js @@ -5,6 +5,7 @@ import { clickBlockAppender, getEditedPostContent, newPost, + pressTimes, } from '../support/utils'; describe( 'Font Size Picker', () => { @@ -32,7 +33,8 @@ describe( 'Font Size Picker', () => { await page.keyboard.type( 'Paragraph to be made "small"' ); await page.click( '.blocks-font-size .components-range-control__number' ); - await page.keyboard.type( '13' ); + // This should be the "small" font-size of the current theme. + await page.keyboard.type( '19.5' ); // Ensure content matches snapshot. const content = await getEditedPostContent(); @@ -58,7 +60,8 @@ describe( 'Font Size Picker', () => { await page.keyboard.type( 'Paragraph with font size reset using button' ); await page.click( '.blocks-font-size .components-range-control__number' ); - await page.keyboard.type( '13' ); + // This should be the default font-size of the current theme. + await page.keyboard.type( '22' ); // Blur the range control await page.click( '.components-base-control__label' ); @@ -80,9 +83,10 @@ describe( 'Font Size Picker', () => { const changeSizeButton = await page.waitForSelector( '.components-button.is-font-large' ); await changeSizeButton.click(); + // Clear the custom font size input. await page.click( '.blocks-font-size .components-range-control__number' ); - await page.keyboard.press( 'Backspace' ); - await page.keyboard.press( 'Backspace' ); + await pressTimes( 'ArrowRight', 4 ); + await pressTimes( 'Backspace', 4 ); // Ensure content matches snapshot. const content = await getEditedPostContent(); diff --git a/test/e2e/specs/new-post.test.js b/test/e2e/specs/new-post.test.js index da171371330db4..44e08f1902f2dd 100644 --- a/test/e2e/specs/new-post.test.js +++ b/test/e2e/specs/new-post.test.js @@ -2,12 +2,19 @@ * Internal dependencies */ import { newPost } from '../support/utils'; +import { activatePlugin, deactivatePlugin } from '../support/plugins'; describe( 'new editor state', () => { beforeAll( async () => { + await activatePlugin( 'gutenberg-test-plugin-post-formats-support' ); await newPost(); } ); + afterAll( async () => { + await newPost(); + await deactivatePlugin( 'gutenberg-test-plugin-post-formats-support' ); + } ); + it( 'should show the New Post page in Gutenberg', async () => { expect( page.url() ).toEqual( expect.stringContaining( 'post-new.php' ) ); // Should display the blank title. diff --git a/test/e2e/specs/templates.test.js b/test/e2e/specs/templates.test.js index 9b5d22080e0d42..4d8d16e9ce9513 100644 --- a/test/e2e/specs/templates.test.js +++ b/test/e2e/specs/templates.test.js @@ -72,8 +72,14 @@ describe( 'templates', () => { await switchToTestUser(); } - beforeAll( async () => await setPostFormat( 'image' ) ); - afterAll( async () => await setPostFormat( STANDARD_FORMAT_VALUE ) ); + beforeAll( async () => { + await activatePlugin( 'gutenberg-test-plugin-post-formats-support' ); + await setPostFormat( 'image' ); + } ); + afterAll( async () => { + await setPostFormat( STANDARD_FORMAT_VALUE ); + await deactivatePlugin( 'gutenberg-test-plugin-post-formats-support' ); + } ); it( 'should populate new post with default block for format', async () => { await newPost(); diff --git a/test/e2e/specs/writing-flow.test.js b/test/e2e/specs/writing-flow.test.js index bcad1f3d55bb0c..7c4197755bab81 100644 --- a/test/e2e/specs/writing-flow.test.js +++ b/test/e2e/specs/writing-flow.test.js @@ -44,6 +44,8 @@ describe( 'adding blocks', () => { // Arrow up in inner blocks should navigate through (1) column wrapper, // (2) text fields. + // We need to arrow up key presses in the paragraph block because it shows up in two lines. + await page.keyboard.press( 'ArrowUp' ); await page.keyboard.press( 'ArrowUp' ); await page.keyboard.press( 'ArrowUp' ); activeElementText = await page.evaluate( () => document.activeElement.textContent ); @@ -53,6 +55,7 @@ describe( 'adding blocks', () => { // columns wrappers before escaping out. let activeElementBlockType; await page.keyboard.press( 'ArrowUp' ); + await page.keyboard.press( 'ArrowUp' ); activeElementBlockType = await page.evaluate( () => ( document.activeElement.getAttribute( 'data-type' ) ) ); diff --git a/test/e2e/support/plugins.js b/test/e2e/support/plugins.js index da9edd3656860f..f8d64a7a1ec175 100644 --- a/test/e2e/support/plugins.js +++ b/test/e2e/support/plugins.js @@ -11,7 +11,7 @@ import { visitAdmin, switchToAdminUser, switchToTestUser } from './utils'; */ export async function installPlugin( slug, searchTerm ) { await switchToAdminUser(); - await visitAdmin( 'plugin-install.php?s=' + encodeURIComponent( searchTerm || slug ) + '&tab=search&type=term' ); + 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(); diff --git a/test/e2e/test-plugins/post-formats.php b/test/e2e/test-plugins/post-formats.php new file mode 100644 index 00000000000000..b0fdec68947fcb --- /dev/null +++ b/test/e2e/test-plugins/post-formats.php @@ -0,0 +1,15 @@ +<?php +/** + * Plugin Name: Gutenberg Test Plugin, Post Formats Support + * Plugin URI: https://github.com/WordPress/gutenberg + * Author: Gutenberg Team + * + * @package gutenberg-test-plugin-post-formats + */ + +add_theme_support( 'post-formats', array( 'image', 'gallery' ) ); +add_action( 'init', 'gutenberg_test_plugin_post_formats_add_post_support', 11 ); + +function gutenberg_test_plugin_post_formats_add_post_support() { + add_post_type_support( 'page', 'post-formats' ); +} From 06c4dc3e1e92d739012137c1c7a3624fc6a49fcd Mon Sep 17 00:00:00 2001 From: Robert Anderson <robert@noisysocks.com> Date: Mon, 10 Dec 2018 03:17:48 +1100 Subject: [PATCH 201/254] Meta Boxes: Don't hide disabled meta boxes by modifying DOM (#12628) Hiding disabled meta boxes by setting `element.style.display = 'none'` interferes with plugins like ACF which rely on being able to show and hide meta boxes using `$.hide()` and `$.show()`. Hiding the meta box using a new `.edit-post-meta-boxes-area .is-hidden` class ensures that we don't interfere with third party code. --- .../src/components/meta-boxes/meta-box-visibility.js | 11 +++++++++-- .../components/meta-boxes/meta-boxes-area/style.scss | 6 ++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/edit-post/src/components/meta-boxes/meta-box-visibility.js b/packages/edit-post/src/components/meta-boxes/meta-box-visibility.js index 31bc0becf225f2..970cf26355e6ca 100644 --- a/packages/edit-post/src/components/meta-boxes/meta-box-visibility.js +++ b/packages/edit-post/src/components/meta-boxes/meta-box-visibility.js @@ -17,9 +17,16 @@ class MetaBoxVisibility extends Component { updateDOM() { const { id, isVisible } = this.props; + const element = document.getElementById( id ); - if ( element ) { - element.style.display = isVisible ? '' : 'none'; + if ( ! element ) { + return; + } + + if ( isVisible ) { + element.classList.remove( 'is-hidden' ); + } else { + element.classList.add( 'is-hidden' ); } } diff --git a/packages/edit-post/src/components/meta-boxes/meta-boxes-area/style.scss b/packages/edit-post/src/components/meta-boxes/meta-boxes-area/style.scss index f336804fa38de1..b86d15952683b0 100644 --- a/packages/edit-post/src/components/meta-boxes/meta-boxes-area/style.scss +++ b/packages/edit-post/src/components/meta-boxes/meta-boxes-area/style.scss @@ -75,6 +75,12 @@ right: 20px; z-index: z-index(".edit-post-meta-boxes-area .spinner"); } + + // Hide disabled meta boxes using CSS so that we don't interfere with plugins + // that modify `element.style.display` on the meta box. + .is-hidden { + display: none; + } } .edit-post-meta-boxes-area__clear { From 29cfa83674a663c2ee8bd64372483aefd9ed4bbb Mon Sep 17 00:00:00 2001 From: Brent Swisher <brent@brentswisher.com> Date: Sun, 9 Dec 2018 11:21:07 -0500 Subject: [PATCH 202/254] Add a word-wrap style to the facebook embed preview screen (#11890) * Add a word-break style to the facebook embed preview screen to prevent the long embed url from breaking the block boundary * Fix typo and missing space in scss comment --- packages/block-library/src/embed/editor.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/block-library/src/embed/editor.scss b/packages/block-library/src/embed/editor.scss index 0b79d5b0414beb..29cbd9fb932cff 100644 --- a/packages/block-library/src/embed/editor.scss +++ b/packages/block-library/src/embed/editor.scss @@ -25,4 +25,9 @@ font-size: $default-font-size; } } + + // Stops long URLs from breaking out of the no preview available screen + .components-placeholder__error { + word-break: break-word; + } } From 607c46f73804efbb19d47a8d4849dfbaae29b97d Mon Sep 17 00:00:00 2001 From: Alda Vigdis Skarphedinsdottir <191583+aldavigdis@users.noreply.github.com> Date: Sun, 9 Dec 2018 17:26:17 +0100 Subject: [PATCH 203/254] Adding @aldavigdis to the contributors list (#12686) --- CONTRIBUTORS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 8f7b5d83932b64..2c58e53ed7685a 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -122,3 +122,5 @@ This list is manually curated to include valuable contributions by volunteers th | @sharazghouri | @sharaz | | @jakeparis | @jakeparis | | @designsimply | @designsimply | +| @aldavigdis | @aldavigdis | + From 85d83ad4adae33501463cc31f9bb5c791cb75736 Mon Sep 17 00:00:00 2001 From: Pascal Birchler <pascal.birchler@gmail.com> Date: Sun, 9 Dec 2018 11:07:19 -0600 Subject: [PATCH 204/254] Make media & text block placeholder translatable (#12706) --- packages/block-library/src/media-text/edit.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/block-library/src/media-text/edit.js b/packages/block-library/src/media-text/edit.js index c1cabf248e57ca..9f9b626f837fd2 100644 --- a/packages/block-library/src/media-text/edit.js +++ b/packages/block-library/src/media-text/edit.js @@ -7,7 +7,7 @@ import { get } from 'lodash'; /** * WordPress dependencies */ -import { __ } from '@wordpress/i18n'; +import { __, _x } from '@wordpress/i18n'; import { BlockControls, InnerBlocks, @@ -32,7 +32,7 @@ import MediaContainer from './media-container'; */ const ALLOWED_BLOCKS = [ 'core/button', 'core/paragraph', 'core/heading', 'core/list' ]; const TEMPLATE = [ - [ 'core/paragraph', { fontSize: 'large', placeholder: 'Content…' } ], + [ 'core/paragraph', { fontSize: 'large', placeholder: _x( 'Content…', 'content placeholder' ) } ], ]; class MediaTextEdit extends Component { From 9f6dc6dddde1c9c4f80bcaa79903849fcd266b69 Mon Sep 17 00:00:00 2001 From: Jorge Costa <jorge.costa@developer.pt> Date: Sun, 9 Dec 2018 17:19:38 +0000 Subject: [PATCH 205/254] Fix: Problems on Media & Text block resizing; Load wp-block-library styles before wp-edit-blocks. (#12619) --- lib/client-assets.php | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/client-assets.php b/lib/client-assets.php index 6fa221bf4c5d8c..13672f6516bcdd 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -461,6 +461,7 @@ function gutenberg_register_scripts_and_styles() { array( 'wp-components', 'wp-editor', + 'wp-block-library', // Always include visual styles so the editor never appears broken. 'wp-block-library-theme', ), From 779a18eb2f08b2c113a54d0162bc88abccb59d7a Mon Sep 17 00:00:00 2001 From: Takayuki Miyauchi <miya0001@users.noreply.github.com> Date: Mon, 10 Dec 2018 02:27:41 +0900 Subject: [PATCH 206/254] Get wordcount type from translation (#12586) * get wordcount type from translation * Add description to explain the options for wordcount type * Added mylsef into contributors.md. :) --- CONTRIBUTORS.md | 2 +- packages/editor/src/components/word-count/index.js | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 2c58e53ed7685a..8279df075b66bb 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -123,4 +123,4 @@ This list is manually curated to include valuable contributions by volunteers th | @jakeparis | @jakeparis | | @designsimply | @designsimply | | @aldavigdis | @aldavigdis | - +| @miya0001 | @miyauchi | diff --git a/packages/editor/src/components/word-count/index.js b/packages/editor/src/components/word-count/index.js index 176435cdc76ebb..719351ba323829 100644 --- a/packages/editor/src/components/word-count/index.js +++ b/packages/editor/src/components/word-count/index.js @@ -2,11 +2,19 @@ * WordPress dependencies */ import { withSelect } from '@wordpress/data'; +import { _x } from '@wordpress/i18n'; import { count as wordCount } from '@wordpress/wordcount'; function WordCount( { content } ) { + /* + * translators: If your word count is based on single characters (e.g. East Asian characters), + * enter 'characters_excluding_spaces' or 'characters_including_spaces'. Otherwise, enter 'words'. + * Do not translate into your own language. + */ + const wordCountType = _x( 'words', 'Word count type. Do not translate!' ); + return ( - <span className="word-count">{ wordCount( content, 'words' ) }</span> + <span className="word-count">{ wordCount( content, wordCountType ) }</span> ); } From a12fcec4e77b15b661402dd27d24d0efbe7b6d51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20Van=C2=A0Dorpe?= <iseulde@automattic.com> Date: Sun, 9 Dec 2018 13:04:42 -0600 Subject: [PATCH 207/254] Only render InserterWithShortcuts on hover (#12510) --- .../src/components/default-block-appender/index.js | 14 +++++++++++--- .../test/__snapshots__/index.js.snap | 9 ++++++--- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/packages/editor/src/components/default-block-appender/index.js b/packages/editor/src/components/default-block-appender/index.js index ecfe98d351a905..d4b95c4d961bae 100644 --- a/packages/editor/src/components/default-block-appender/index.js +++ b/packages/editor/src/components/default-block-appender/index.js @@ -7,7 +7,7 @@ import TextareaAutosize from 'react-autosize-textarea'; * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { compose } from '@wordpress/compose'; +import { compose, withState } from '@wordpress/compose'; import { getDefaultBlockName } from '@wordpress/blocks'; import { decodeEntities } from '@wordpress/html-entities'; import { withSelect, withDispatch } from '@wordpress/data'; @@ -26,6 +26,8 @@ export function DefaultBlockAppender( { showPrompt, placeholder, rootClientId, + hovered, + setState, } ) { if ( isLocked || ! isVisible ) { return null; @@ -49,7 +51,12 @@ export function DefaultBlockAppender( { // The wp-block className is important for editor styles. return ( - <div data-root-client-id={ rootClientId || '' } className="wp-block editor-default-block-appender"> + <div + data-root-client-id={ rootClientId || '' } + className="wp-block editor-default-block-appender" + onMouseEnter={ () => setState( { hovered: true } ) } + onMouseLeave={ () => setState( { hovered: false } ) } + > <BlockDropZone rootClientId={ rootClientId } /> <TextareaAutosize role="button" @@ -59,12 +66,13 @@ export function DefaultBlockAppender( { onFocus={ onAppend } value={ showPrompt ? value : '' } /> - <InserterWithShortcuts rootClientId={ rootClientId } /> + { hovered && <InserterWithShortcuts rootClientId={ rootClientId } /> } <Inserter position="top right" /> </div> ); } export default compose( + withState( { hovered: false } ), withSelect( ( select, ownProps ) => { const { getBlockCount, getBlockName, isBlockValid, getEditorSettings, getTemplateLock } = select( 'core/editor' ); 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 df9daeebd54f21..dc26d4405d8636 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 @@ -4,6 +4,8 @@ exports[`DefaultBlockAppender should append a default block when input focused 1 <div className="wp-block editor-default-block-appender" data-root-client-id="" + onMouseEnter={[Function]} + onMouseLeave={[Function]} > <WithDispatch(WithSelect(WithFilters(BlockDropZone))) /> <TextareaAutosize @@ -22,7 +24,6 @@ exports[`DefaultBlockAppender should append a default block when input focused 1 rows={1} value="Start writing or type / to choose a block" /> - <WithSelect(WithDispatch(InserterWithShortcuts)) /> <WithSelect(IfCondition(Inserter)) position="top right" /> @@ -33,6 +34,8 @@ exports[`DefaultBlockAppender should match snapshot 1`] = ` <div className="wp-block editor-default-block-appender" data-root-client-id="" + onMouseEnter={[Function]} + onMouseLeave={[Function]} > <WithDispatch(WithSelect(WithFilters(BlockDropZone))) /> <TextareaAutosize @@ -44,7 +47,6 @@ exports[`DefaultBlockAppender should match snapshot 1`] = ` rows={1} value="Start writing or type / to choose a block" /> - <WithSelect(WithDispatch(InserterWithShortcuts)) /> <WithSelect(IfCondition(Inserter)) position="top right" /> @@ -55,6 +57,8 @@ exports[`DefaultBlockAppender should optionally show without prompt 1`] = ` <div className="wp-block editor-default-block-appender" data-root-client-id="" + onMouseEnter={[Function]} + onMouseLeave={[Function]} > <WithDispatch(WithSelect(WithFilters(BlockDropZone))) /> <TextareaAutosize @@ -66,7 +70,6 @@ exports[`DefaultBlockAppender should optionally show without prompt 1`] = ` rows={1} value="" /> - <WithSelect(WithDispatch(InserterWithShortcuts)) /> <WithSelect(IfCondition(Inserter)) position="top right" /> From 42bf37553bf18a23835f686c35d37b222b2ea639 Mon Sep 17 00:00:00 2001 From: Joen Asmussen <joen@automattic.com> Date: Sun, 9 Dec 2018 20:11:14 +0100 Subject: [PATCH 208/254] Fix issue where default appender has icons overlaying the text (#12536) * Fix issue where default appender has icons overlaying the text This fixes #11425. It adds padding to the right of the default block appender to fit 3 icons. * chore: Tweak spelling --- packages/block-library/src/paragraph/editor.scss | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/block-library/src/paragraph/editor.scss b/packages/block-library/src/paragraph/editor.scss index 98ea5e4e2b85a4..1110e5d5832d4d 100644 --- a/packages/block-library/src/paragraph/editor.scss +++ b/packages/block-library/src/paragraph/editor.scss @@ -1,6 +1,11 @@ // Specific to the empty paragraph placeholder: -// when shown on mobile and in nested contexts, the plus to add blocks shows up on the right. +// when shown on mobile and in nested contexts, one or more icons show up on the right. // This padding makes sure it doesn't overlap text. .editor-rich-text__tinymce[data-is-placeholder-visible="true"] + .editor-rich-text__tinymce.wp-block-paragraph { - padding-right: $icon-button-size; + padding-right: $icon-button-size * 3; + + // In nested contexts only one icon shows up. + .wp-block .wp-block & { + padding-right: $icon-button-size; + } } From 2f4317cedafdbff5cf09d6d6726c528dcf6d14ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20Van=C2=A0Dorpe?= <iseulde@automattic.com> Date: Sun, 9 Dec 2018 13:54:43 -0600 Subject: [PATCH 209/254] Classic Block: set correct focus back after blur (#12415) * Classic Block: set correct focus back after blur * Add e2e test * reset bookmark on mousedown and touchstart * e2e: Look for aria-label="Add Media" rather than "Insert Media" --- packages/block-library/src/classic/edit.js | 14 ++++ .../blocks/__snapshots__/classic.test.js.snap | 3 + test/e2e/specs/blocks/classic.test.js | 73 +++++++++++++++++++ 3 files changed, 90 insertions(+) create mode 100644 test/e2e/specs/blocks/__snapshots__/classic.test.js.snap create mode 100644 test/e2e/specs/blocks/classic.test.js diff --git a/packages/block-library/src/classic/edit.js b/packages/block-library/src/classic/edit.js index 2502c0f7e0eb5f..f0eb37ff30ca9b 100644 --- a/packages/block-library/src/classic/edit.js +++ b/packages/block-library/src/classic/edit.js @@ -78,6 +78,7 @@ export default class ClassicEdit extends Component { onSetup( editor ) { const { attributes: { content }, setAttributes } = this.props; const { ref } = this; + let bookmark; this.editor = editor; @@ -86,12 +87,25 @@ export default class ClassicEdit extends Component { } editor.on( 'blur', () => { + bookmark = editor.selection.getBookmark( 2, true ); + setAttributes( { content: editor.getContent(), } ); + + editor.once( 'focus', () => { + if ( bookmark ) { + editor.selection.moveToBookmark( bookmark ); + } + } ); + return false; } ); + editor.on( 'mousedown touchstart', () => { + bookmark = null; + } ); + editor.on( 'keydown', ( event ) => { if ( ( event.keyCode === BACKSPACE || event.keyCode === DELETE ) && isTmceEmpty( editor ) ) { // delete the block diff --git a/test/e2e/specs/blocks/__snapshots__/classic.test.js.snap b/test/e2e/specs/blocks/__snapshots__/classic.test.js.snap new file mode 100644 index 00000000000000..a4461344bb4384 --- /dev/null +++ b/test/e2e/specs/blocks/__snapshots__/classic.test.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Classic should be inserted 1`] = `"test"`; diff --git a/test/e2e/specs/blocks/classic.test.js b/test/e2e/specs/blocks/classic.test.js new file mode 100644 index 00000000000000..f0b733b4a9947d --- /dev/null +++ b/test/e2e/specs/blocks/classic.test.js @@ -0,0 +1,73 @@ +/** + * External dependencies + */ +import path from 'path'; +import fs from 'fs'; +import os from 'os'; +import uuid from 'uuid/v4'; + +/** + * Internal dependencies + */ +import { + getEditedPostContent, + newPost, + insertBlock, + pressWithModifier, +} from '../../support/utils'; + +describe( 'Classic', () => { + beforeEach( async () => { + await newPost(); + } ); + + it( 'should be inserted', async () => { + await insertBlock( 'Classic' ); + // Wait for TinyMCE to initialise. + await page.waitForSelector( '.mce-content-body' ); + // Ensure there is focus. + await page.focus( '.mce-content-body' ); + await page.keyboard.type( 'test' ); + // Move focus away. + await pressWithModifier( 'shift', 'Tab' ); + + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); + + it( 'should insert media', async () => { + await insertBlock( 'Classic' ); + // Wait for TinyMCE to initialise. + await page.waitForSelector( '.mce-content-body' ); + // Ensure there is focus. + await page.focus( '.mce-content-body' ); + await page.keyboard.type( 'test' ); + + // Click the image button. + await page.waitForSelector( 'div[aria-label="Add Media"]' ); + await page.click( 'div[aria-label="Add Media"]' ); + + // Wait for media modal to appear and upload image. + await page.waitForSelector( '.media-modal input[type=file]' ); + const inputElement = await page.$( '.media-modal input[type=file]' ); + const testImagePath = path.join( __dirname, '..', '..', 'assets', '10x10_e2e_test_image_z9T8jK.png' ); + const filename = uuid(); + const tmpFileName = path.join( os.tmpdir(), filename + '.png' ); + fs.copyFileSync( testImagePath, tmpFileName ); + await inputElement.uploadFile( tmpFileName ); + + // Wait for upload. + await page.waitForSelector( `.media-modal li[aria-label="${ filename }"]` ); + + // Insert the uploaded image. + await page.click( '.media-modal button.media-button-insert' ); + + // Wait for image to be inserted. + await page.waitForSelector( '.mce-content-body img' ); + + // Move focus away. + await pressWithModifier( 'shift', 'Tab' ); + + const regExp = new RegExp( `test<img class="alignnone size-full wp-image-\\d+" src="[^"]+\\/${ filename }\\.png" alt="" width="10" height="10" \\/>` ); + expect( await getEditedPostContent() ).toMatch( regExp ); + } ); +} ); From f23282bfa292e457ea5cc6d704a76fee99de93d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20Van=C2=A0Dorpe?= <iseulde@automattic.com> Date: Sun, 9 Dec 2018 14:35:07 -0600 Subject: [PATCH 210/254] RichText: only replace range and nodes if different (#12547) * RichText: only set range if different * Check rangeCount * Also compare nodes * Add e2e test * Simplify * RichText: Document isRangeEqual * Testing: RichText: Assure subscriber removal * Unsubscribe in page.evaluate --- packages/rich-text/src/to-dom.js | 46 +++++++++--- .../__snapshots__/rich-text.test.js.snap | 6 ++ test/e2e/specs/rich-text.test.js | 71 +++++++++++++++++++ 3 files changed, 112 insertions(+), 11 deletions(-) diff --git a/packages/rich-text/src/to-dom.js b/packages/rich-text/src/to-dom.js index 1c34d8680dd2b3..c811bbc6180595 100644 --- a/packages/rich-text/src/to-dom.js +++ b/packages/rich-text/src/to-dom.js @@ -226,21 +226,17 @@ export function apply( { export function applyValue( future, current ) { let i = 0; + let futureChild; - while ( future.firstChild ) { + while ( ( futureChild = future.firstChild ) ) { const currentChild = current.childNodes[ i ]; - const futureNodeType = future.firstChild.nodeType; if ( ! currentChild ) { - current.appendChild( future.firstChild ); - } else if ( - futureNodeType !== currentChild.nodeType || - futureNodeType !== TEXT_NODE || - future.firstChild.nodeValue !== currentChild.nodeValue - ) { - current.replaceChild( future.firstChild, currentChild ); + current.appendChild( futureChild ); + } else if ( ! currentChild.isEqualNode( futureChild ) ) { + current.replaceChild( futureChild, currentChild ); } else { - future.removeChild( future.firstChild ); + future.removeChild( futureChild ); } i++; @@ -251,6 +247,25 @@ export function applyValue( future, current ) { } } +/** + * Returns true if two ranges are equal, or false otherwise. Ranges are + * considered equal if their start and end occur in the same container and + * offset. + * + * @param {Range} a First range object to test. + * @param {Range} b First range object to test. + * + * @return {boolean} Whether the two ranges are equal. + */ +function isRangeEqual( a, b ) { + return ( + a.startContainer === b.startContainer && + a.startOffset === b.startOffset && + a.endContainer === b.endContainer && + a.endOffset === b.endOffset + ); +} + export function applySelection( selection, current ) { const { node: startContainer, offset: startOffset } = getNodeByPath( current, selection.startPath ); const { node: endContainer, offset: endOffset } = getNodeByPath( current, selection.endPath ); @@ -283,6 +298,15 @@ export function applySelection( selection, current ) { range.setEnd( endContainer, endOffset ); } - windowSelection.removeAllRanges(); + if ( windowSelection.rangeCount > 0 ) { + // If the to be added range and the live range are the same, there's no + // need to remove the live range and add the equivalent range. + if ( isRangeEqual( range, windowSelection.getRangeAt( 0 ) ) ) { + return; + } + + windowSelection.removeAllRanges(); + } + windowSelection.addRange( range ); } diff --git a/test/e2e/specs/__snapshots__/rich-text.test.js.snap b/test/e2e/specs/__snapshots__/rich-text.test.js.snap index e911f407edd680..275080b436f2da 100644 --- a/test/e2e/specs/__snapshots__/rich-text.test.js.snap +++ b/test/e2e/specs/__snapshots__/rich-text.test.js.snap @@ -24,6 +24,12 @@ exports[`RichText should handle change in tag name gracefully 1`] = ` <!-- /wp:heading -->" `; +exports[`RichText should only mutate text data on input 1`] = ` +"<!-- wp:paragraph --> +<p>1<strong>2</strong>34</p> +<!-- /wp:paragraph -->" +`; + exports[`RichText should transform backtick to code 1`] = ` "<!-- wp:paragraph --> <p>A <code>backtick</code></p> diff --git a/test/e2e/specs/rich-text.test.js b/test/e2e/specs/rich-text.test.js index dca248c8289314..2f1012766444fd 100644 --- a/test/e2e/specs/rich-text.test.js +++ b/test/e2e/specs/rich-text.test.js @@ -67,4 +67,75 @@ describe( 'RichText', () => { expect( await getEditedPostContent() ).toMatchSnapshot(); } ); + + it( 'should only mutate text data on input', async () => { + await clickBlockAppender(); + await page.keyboard.type( '1' ); + await pressWithModifier( 'primary', 'b' ); + await page.keyboard.type( '2' ); + await pressWithModifier( 'primary', 'b' ); + await page.keyboard.type( '3' ); + + await page.evaluate( () => { + let called; + const { body } = document; + const config = { + attributes: true, + childList: true, + characterData: true, + subtree: true, + }; + + const mutationObserver = new MutationObserver( ( records ) => { + if ( called || records.length > 1 ) { + throw new Error( 'Typing should only mutate once.' ); + } + + records.forEach( ( record ) => { + if ( record.type !== 'characterData' ) { + throw new Error( + `Typing mutated more than character data: ${ record.type }` + ); + } + } ); + + called = true; + } ); + + mutationObserver.observe( body, config ); + + window.unsubscribes = [ () => mutationObserver.disconnect() ]; + + document.addEventListener( 'selectionchange', () => { + function throwMultipleSelectionChange() { + throw new Error( 'Typing should only emit one selection change event.' ); + } + + document.addEventListener( + 'selectionchange', + throwMultipleSelectionChange, + { once: true } + ); + + window.unsubscribes.push( () => { + document.removeEventListener( 'selectionchange', throwMultipleSelectionChange ); + } ); + }, { once: true } ); + } ); + + await page.keyboard.type( '4' ); + + await page.evaluate( () => { + // The selection change event should be called once. If there's only + // one item in `window.unsubscribes`, it means that only one + // function is present to disconnect the `mutationObserver`. + if ( window.unsubscribes.length === 1 ) { + throw new Error( 'The selection change event listener was never called.' ); + } + + window.unsubscribes.forEach( ( unsubscribe ) => unsubscribe() ); + } ); + + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); } ); From c63bb7d5836132a743a47302106e88d0b0a8ac51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20=28Greg=29=20Zi=C3=B3=C5=82kowski?= <grzegorz@gziolo.pl> Date: Sun, 9 Dec 2018 14:59:48 -0600 Subject: [PATCH 211/254] Mark temporary eslint-config package as private (#12734) --- packages/eslint-config/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json index 07a609d7278fa2..8d5c6c93c35eee 100644 --- a/packages/eslint-config/package.json +++ b/packages/eslint-config/package.json @@ -1,5 +1,6 @@ { "name": "@wordpress/eslint-config", + "private": true, "version": "1.0.0-alpha.0", "description": "ESLint config for WordPress development.", "author": "The WordPress Contributors", From 4ea5094b4b24c65bc2783fddd6c4e53409f6749c Mon Sep 17 00:00:00 2001 From: Jason Coleman <33220397+ideadude@users.noreply.github.com> Date: Sun, 9 Dec 2018 15:03:21 -0600 Subject: [PATCH 212/254] When a post is saved, check for tinymce and save any editors. (#12568) * When a post is saved, check for tinymce and save any editors. * Importing tinymce and using tinyMCE vs the object stored in window.tinymce. * Updated version number and changelog. * no longer importing tinymce since we use the tinyMCE global. tinyMCE.triggerSave works now. checking if tinyMCE exists before making the call just in case. * Using typeof to check for tinyMCE and fixed issues brought up in travis run. * using window.tinyMCE again to avoid warning RE undefined var * Restore the package.json version. * Add e2e tests for the custom wp_editor metaboxes --- packages/edit-post/CHANGELOG.md | 5 +++ packages/edit-post/src/store/effects.js | 5 +++ .../wp-editor-meta-box.test.js.snap | 3 ++ test/e2e/specs/wp-editor-meta-box.test.js | 38 +++++++++++++++++++ test/e2e/test-plugins/wp-editor-metabox.php | 27 +++++++++++++ 5 files changed, 78 insertions(+) create mode 100644 test/e2e/specs/__snapshots__/wp-editor-meta-box.test.js.snap create mode 100644 test/e2e/specs/wp-editor-meta-box.test.js create mode 100644 test/e2e/test-plugins/wp-editor-metabox.php diff --git a/packages/edit-post/CHANGELOG.md b/packages/edit-post/CHANGELOG.md index 3fdf099954c982..e4b0c1ad32c680 100644 --- a/packages/edit-post/CHANGELOG.md +++ b/packages/edit-post/CHANGELOG.md @@ -1,3 +1,8 @@ +## 3.1.5 (Unreleased) + +### Bug Fixes + - Fix saving WYSIWYG Meta Boxes + ## 3.1.4 (2018-11-30) ## 3.1.3 (2018-11-30) diff --git a/packages/edit-post/src/store/effects.js b/packages/edit-post/src/store/effects.js index 699b08f0636ed2..70ebd8cbb53051 100644 --- a/packages/edit-post/src/store/effects.js +++ b/packages/edit-post/src/store/effects.js @@ -72,6 +72,11 @@ const effects = { } ); }, REQUEST_META_BOX_UPDATES( action, store ) { + // Saves the wp_editor fields + if ( window.tinyMCE ) { + window.tinyMCE.triggerSave(); + } + const state = store.getState(); // Additional data needed for backwards compatibility. diff --git a/test/e2e/specs/__snapshots__/wp-editor-meta-box.test.js.snap b/test/e2e/specs/__snapshots__/wp-editor-meta-box.test.js.snap new file mode 100644 index 00000000000000..485862873b648d --- /dev/null +++ b/test/e2e/specs/__snapshots__/wp-editor-meta-box.test.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`WP Editor Meta Boxes Should save the changes 1`] = `"<p>Typing in a metabox</p>"`; diff --git a/test/e2e/specs/wp-editor-meta-box.test.js b/test/e2e/specs/wp-editor-meta-box.test.js new file mode 100644 index 00000000000000..496057bb55c1cf --- /dev/null +++ b/test/e2e/specs/wp-editor-meta-box.test.js @@ -0,0 +1,38 @@ +/** + * Internal dependencies + */ +import { newPost, publishPost } from '../support/utils'; +import { activatePlugin, deactivatePlugin } from '../support/plugins'; + +describe( 'WP Editor Meta Boxes', () => { + beforeAll( async () => { + await activatePlugin( 'gutenberg-test-plugin-wp-editor-meta-box' ); + await newPost(); + } ); + + afterAll( async () => { + await deactivatePlugin( 'gutenberg-test-plugin-wp-editor-meta-box' ); + } ); + + it( 'Should save the changes', async () => { + // Add title to enable valid non-empty post save. + await page.type( '.editor-post-title__input', 'Hello Meta' ); + + // Type something + await page.click( '#test_tinymce_id-html' ); + await page.type( '#test_tinymce_id', 'Typing in a metabox' ); + await page.click( '#test_tinymce_id-tmce' ); + + await publishPost(); + + await page.reload(); + + await page.click( '#test_tinymce_id-html' ); + const content = await page.$eval( + '#test_tinymce_id', + ( textarea ) => textarea.value + ); + + expect( content ).toMatchSnapshot(); + } ); +} ); diff --git a/test/e2e/test-plugins/wp-editor-metabox.php b/test/e2e/test-plugins/wp-editor-metabox.php new file mode 100644 index 00000000000000..088fd2ee6e87a2 --- /dev/null +++ b/test/e2e/test-plugins/wp-editor-metabox.php @@ -0,0 +1,27 @@ +<?php +/** + * Plugin Name: Gutenberg Test Plugin, WP Editor Meta Box + * Plugin URI: https://github.com/WordPress/gutenberg + * Author: Gutenberg Team + * + * @package gutenberg-test-wp-editor-metabox + */ + +add_action( 'add_meta_boxes', function(){ + add_meta_box( 'test_tinymce', 'Test TinyMCE', function( $post ){ + $field_value = get_post_meta( $post->ID, 'test_tinymce', true ); + wp_editor( $field_value, 'test_tinymce_id', array( + 'wpautop' => true, + 'media_buttons' => false, + 'textarea_name' => 'test_tinymce', + 'textarea_rows' => 10, + 'teeny' => true + ) ); + }, null, 'advanced', 'high' ); +}); +add_action( 'save_post', function( $post_id ){ + if ( ! isset( $_POST['test_tinymce'] ) ) { + return; + } + update_post_meta( $post_id, 'test_tinymce', $_POST['test_tinymce'] ); +}); From 4bb3b0cf79e7aca2bd1724ab2aa13142919d1e70 Mon Sep 17 00:00:00 2001 From: Marcus Kazmierczak <marcus@mkaz.com> Date: Sun, 9 Dec 2018 13:05:04 -0800 Subject: [PATCH 213/254] Rename functions, removing gutenberg_ prefix (#12326) * Rename functions, removing gutenberg_ and prefixing with wp_ * Remove wp_ prefix to match core * Remove function check per review --- lib/load.php | 5 +- .../src/latest-comments/index.php | 58 +++++++++---------- 2 files changed, 29 insertions(+), 34 deletions(-) diff --git a/lib/load.php b/lib/load.php index ee1c973f99568a..0c55db3a37f6ca 100644 --- a/lib/load.php +++ b/lib/load.php @@ -62,10 +62,7 @@ if ( ! function_exists( 'render_block_core_categories' ) ) { require dirname( __FILE__ ) . '/../packages/block-library/src/categories/index.php'; } -// Currently merged in core as `gutenberg_render_block_core_latest_comments`, -// expected to change soon. -if ( ! function_exists( 'render_block_core_latest_comments' ) - && ! function_exists( 'gutenberg_render_block_core_latest_comments' ) ) { +if ( ! function_exists( 'render_block_core_latest_comments' ) ) { require dirname( __FILE__ ) . '/../packages/block-library/src/latest-comments/index.php'; } if ( ! function_exists( 'render_block_core_latest_posts' ) ) { diff --git a/packages/block-library/src/latest-comments/index.php b/packages/block-library/src/latest-comments/index.php index 29e17e9de50080..fcc17c3cd6a004 100644 --- a/packages/block-library/src/latest-comments/index.php +++ b/packages/block-library/src/latest-comments/index.php @@ -5,34 +5,32 @@ * @package WordPress */ -if ( ! function_exists( 'gutenberg_draft_or_post_title' ) ) { - /** - * Get the post title. - * - * The post title is fetched and if it is blank then a default string is - * returned. - * - * Copied from `wp-admin/includes/template.php`, but we can't include that - * file because: - * - * 1. It causes bugs with test fixture generation and strange Docker 255 error - * codes. - * 2. It's in the admin; ideally we *shouldn't* be including files from the - * admin for a block's output. It's a very small/simple function as well, - * so duplicating it isn't too terrible. - * - * @since 3.3.0 - * - * @param int|WP_Post $post Optional. Post ID or WP_Post object. Default is global $post. - * @return string The post title if set; "(no title)" if no title is set. - */ - function gutenberg_draft_or_post_title( $post = 0 ) { - $title = get_the_title( $post ); - if ( empty( $title ) ) { - $title = __( '(no title)' ); - } - return esc_html( $title ); +/** + * Get the post title. + * + * The post title is fetched and if it is blank then a default string is + * returned. + * + * Copied from `wp-admin/includes/template.php`, but we can't include that + * file because: + * + * 1. It causes bugs with test fixture generation and strange Docker 255 error + * codes. + * 2. It's in the admin; ideally we *shouldn't* be including files from the + * admin for a block's output. It's a very small/simple function as well, + * so duplicating it isn't too terrible. + * + * @since 3.3.0 + * + * @param int|WP_Post $post Optional. Post ID or WP_Post object. Default is global $post. + * @return string The post title if set; "(no title)" if no title is set. + */ +function wp_latest_comments_draft_or_post_title( $post = 0 ) { + $title = get_the_title( $post ); + if ( empty( $title ) ) { + $title = __( '(no title)' ); } + return esc_html( $title ); } /** @@ -42,7 +40,7 @@ function gutenberg_draft_or_post_title( $post = 0 ) { * * @return string Returns the post content with latest comments added. */ -function gutenberg_render_block_core_latest_comments( $attributes = array() ) { +function render_block_core_latest_comments( $attributes = array() ) { // This filter is documented in wp-includes/widgets/class-wp-widget-recent-comments.php. $comments = get_comments( apply_filters( @@ -94,7 +92,7 @@ function gutenberg_render_block_core_latest_comments( $attributes = array() ) { // `_draft_or_post_title` calls `esc_html()` so we don't need to wrap that call in // `esc_html`. - $post_title = '<a class="wp-block-latest-comments__comment-link" href="' . esc_url( get_comment_link( $comment ) ) . '">' . gutenberg_draft_or_post_title( $comment->comment_post_ID ) . '</a>'; + $post_title = '<a class="wp-block-latest-comments__comment-link" href="' . esc_url( get_comment_link( $comment ) ) . '">' . wp_latest_comments_draft_or_post_title( $comment->comment_post_ID ) . '</a>'; $list_items_markup .= sprintf( /* translators: 1: author name (inside <a> or <span> tag, based on if they have a URL), 2: post title related to this comment */ @@ -179,6 +177,6 @@ function gutenberg_render_block_core_latest_comments( $attributes = array() ) { 'enum' => array( 'center', 'left', 'right', 'wide', 'full', '' ), ), ), - 'render_callback' => 'gutenberg_render_block_core_latest_comments', + 'render_callback' => 'render_block_core_latest_comments', ) ); From e7fb179e41710df72a57e681c8dab4aea63c42a0 Mon Sep 17 00:00:00 2001 From: Andrew Duthie <andrew@andrewduthie.com> Date: Sun, 9 Dec 2018 16:22:03 -0600 Subject: [PATCH 214/254] Annotations: Apply annotation className as string (#12741) --- packages/annotations/src/block/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/annotations/src/block/index.js b/packages/annotations/src/block/index.js index 5095fc473d67e6..e2677bbf6a9b3a 100644 --- a/packages/annotations/src/block/index.js +++ b/packages/annotations/src/block/index.js @@ -17,7 +17,7 @@ const addAnnotationClassName = ( OriginalComponent ) => { return { className: annotations.map( ( annotation ) => { return 'is-annotated-by-' + annotation.source; - } ), + } ).join( ' ' ), }; } )( OriginalComponent ); }; From b44a2f114e99b6f6f774f9b1cb0be781d9a4d569 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20Van=C2=A0Dorpe?= <iseulde@automattic.com> Date: Sun, 9 Dec 2018 17:47:53 -0600 Subject: [PATCH 215/254] RichText: Ensure instance is selected before setting back selection (#12737) --- packages/editor/src/components/rich-text/index.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/editor/src/components/rich-text/index.js b/packages/editor/src/components/rich-text/index.js index de0e7dd5aa3633..0731916d6a1a73 100644 --- a/packages/editor/src/components/rich-text/index.js +++ b/packages/editor/src/components/rich-text/index.js @@ -688,9 +688,12 @@ export class RichText extends Component { if ( shouldReapply ) { const record = this.formatToValue( value ); - // Maintain the previous selection: - record.start = this.state.start; - record.end = this.state.end; + // Maintain the previous selection if the instance is currently + // selected. + if ( isSelected ) { + record.start = this.state.start; + record.end = this.state.end; + } this.applyRecord( record ); } From 47736a4d25a7d69d83389ec1521458a57011907a Mon Sep 17 00:00:00 2001 From: Saleeh <saleeh93@gmail.com> Date: Sun, 9 Dec 2018 21:13:29 -0600 Subject: [PATCH 216/254] Fix for #11663 (#12728) * Fixed Deleting an HTML Anchor attribute leaves an empty HTML id attribute * Fixed Deleting an HTML Anchor attribute leaves an empty --- packages/editor/src/hooks/anchor.js | 2 +- packages/editor/src/hooks/test/anchor.js | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/editor/src/hooks/anchor.js b/packages/editor/src/hooks/anchor.js index 66b1c4f01eef4a..3af32e39ef2ac4 100644 --- a/packages/editor/src/hooks/anchor.js +++ b/packages/editor/src/hooks/anchor.js @@ -98,7 +98,7 @@ export const withInspectorControl = createHigherOrderComponent( ( BlockEdit ) => */ export function addSaveProps( extraProps, blockType, attributes ) { if ( hasBlockSupport( blockType, 'anchor' ) ) { - extraProps.id = attributes.anchor; + extraProps.id = attributes.anchor === '' ? null : attributes.anchor; } return extraProps; diff --git a/packages/editor/src/hooks/test/anchor.js b/packages/editor/src/hooks/test/anchor.js index 74bfa2e20dfc25..21a5c65be83cb0 100644 --- a/packages/editor/src/hooks/test/anchor.js +++ b/packages/editor/src/hooks/test/anchor.js @@ -62,5 +62,17 @@ describe( 'anchor', () => { expect( extraProps.id ).toBe( 'foo' ); } ); + + it( 'should remove an anchor attribute ID when feild is cleared', () => { + const attributes = { anchor: '' }; + const extraProps = getSaveContentExtraProps( {}, { + ...blockSettings, + supports: { + anchor: true, + }, + }, attributes ); + + expect( extraProps.id ).toBe( null ); + } ); } ); } ); From 8a5486d748160772c10763fcd13ca4fa58ec200c Mon Sep 17 00:00:00 2001 From: Robert Anderson <robert@noisysocks.com> Date: Mon, 10 Dec 2018 17:06:21 +1100 Subject: [PATCH 217/254] Update plugin version to 4.7.0-rc.1 (#12752) --- 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 2f75a595903e79..9f9d7b183aeb55 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.6.1 + * Version: 4.7.0-rc.1 * Author: Gutenberg Team * * @package gutenberg diff --git a/package-lock.json b/package-lock.json index df6980879a3426..8ced31ee5d85a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "4.6.1", + "version": "4.7.0-rc.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 541ebbfa9df50d..7351c13255f052 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "4.6.1", + "version": "4.7.0-rc.1", "private": true, "description": "A new WordPress editor experience", "repository": "git+https://github.com/WordPress/gutenberg.git", From b6949b9169f92ad71b32f320119c28a61894fb71 Mon Sep 17 00:00:00 2001 From: Brent Swisher <brent@brentswisher.com> Date: Mon, 10 Dec 2018 02:15:40 -0500 Subject: [PATCH 218/254] Add an error state to the image block to allow upload errors to display (#10224) --- packages/block-library/src/image/edit.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/block-library/src/image/edit.js b/packages/block-library/src/image/edit.js index 551e3dadda255a..f7fdd73a9aaa54 100644 --- a/packages/block-library/src/image/edit.js +++ b/packages/block-library/src/image/edit.js @@ -121,7 +121,7 @@ class ImageEdit extends Component { } componentDidMount() { - const { attributes, setAttributes } = this.props; + const { attributes, setAttributes, noticeOperations } = this.props; const { id, url = '' } = attributes; if ( isTemporaryImage( id, url ) ) { @@ -134,6 +134,10 @@ class ImageEdit extends Component { setAttributes( pickRelevantMediaFiles( image ) ); }, allowedTypes: ALLOWED_MEDIA_TYPES, + onError: ( message ) => { + noticeOperations.createErrorNotice( message ); + this.setState( { isEditing: true } ); + }, } ); } } From 40d1282a992702e1933118ef626f0666692baf2b Mon Sep 17 00:00:00 2001 From: Joen Asmussen <joen@automattic.com> Date: Mon, 10 Dec 2018 08:26:23 +0100 Subject: [PATCH 219/254] Try: JS Console warning for when in Quirks Mode (#12575) * Try: JS Console warning for when in Quirks Mode This PR detects whether the browser is in Quirks Mode. Quirks Mode is a rendering method used when the doctype definition is missing or incorrectly placed in the HTML source, causing the browser to have difficulty detecting the type of document it is to render. This is usually caused by a PHP error, or even just a style tag that is output incorrectly on the page. See discussion in https://github.com/WordPress/gutenberg/pull/12455 and https://github.com/WordPress/gutenberg/issues/11378. The usual result is Gutenberg rendering incorrectly, notably with metaboxes overlapping content. The purpose of this PR is to help developers debug the issue and fix it at the root. As such, it adds a console warning, props @nickcernis for the text: ``` [Warning] Your browser is using Quirks Mode. This can cause rendering issues such as blocks overlaying meta boxes in the editor. Quirks Mode can be triggered by PHP errors or HTML code appearing before the opening <!DOCTYPE html>. Try checking the raw page source or your site's PHP error log and resolving errors there, removing any HTML before the doctype, or disabling plugins. ``` It also augments the documentation to add a note about this. * Move warning to index.js * Remove try/catch. * Tweak: Remove redundant [warning] in warn call --- .../developers/backwards-compatibility/meta-box.md | 2 ++ packages/edit-post/src/index.js | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/docs/designers-developers/developers/backwards-compatibility/meta-box.md b/docs/designers-developers/developers/backwards-compatibility/meta-box.md index eb96bfc38e0ffb..2f28f48ed77cb2 100644 --- a/docs/designers-developers/developers/backwards-compatibility/meta-box.md +++ b/docs/designers-developers/developers/backwards-compatibility/meta-box.md @@ -82,3 +82,5 @@ Most PHP meta boxes should continue to work in Gutenberg, but some meta boxes th - Plugins relying on selectors that target the post title, post content fields, and other metaboxes (of the old editor). - Plugins relying on TinyMCE's API because there's no longer a single TinyMCE instance to talk to in Gutenberg. - Plugins making updates to their DOM on "submit" or on "save". + +Please also note that if your plugin triggers a PHP warning or notice to be output on the page, this will cause the HTML document type (`<!DOCTYPE html>`) to be output incorrectly. This will cause the browser to render using "Quirks Mode", which is a compatibility layer that gets enabled when the browser doesn't know what type of document it is parsing. The block editor is not meant to work in this mode, but it can _appear_ to be working just fine. If you encounter issues such as *meta boxes overlaying the editor* or other layout issues, please check the raw page source of your document to see that the document type definition is the first thing output on the page. There will also be a warning in the JavaScript console, noting the issue. diff --git a/packages/edit-post/src/index.js b/packages/edit-post/src/index.js index bce8285f72b0b4..59e870e7929da2 100644 --- a/packages/edit-post/src/index.js +++ b/packages/edit-post/src/index.js @@ -68,6 +68,13 @@ export function initializeEditor( id, postType, postId, settings, initialEdits ) registerCoreBlocks(); + // Show a console log warning if the browser is not in Standards rendering mode. + const documentMode = document.compatMode === 'CSS1Compat' ? 'Standards' : 'Quirks'; + if ( documentMode !== 'Standards' ) { + // eslint-disable-next-line no-console + console.warn( "Your browser is using Quirks Mode. \nThis can cause rendering issues such as blocks overlaying meta boxes in the editor. Quirks Mode can be triggered by PHP errors or HTML code appearing before the opening <!DOCTYPE html>. Try checking the raw page source or your site's PHP error log and resolving errors there, removing any HTML before the doctype, or disabling plugins." ); + } + dispatch( 'core/nux' ).triggerGuide( [ 'core/editor.inserter', 'core/editor.settings', From b7e343d892fbcbe471e0438de64c3b9b373a44b0 Mon Sep 17 00:00:00 2001 From: Terri Ann <terri.ann.s@gmail.com> Date: Mon, 10 Dec 2018 09:48:41 -0600 Subject: [PATCH 220/254] Organizing screenshot assets for the block tutorial inside the designers-developers directory in the repo (#12745) --- .../block-tutorial => assets}/inspector.png | Bin .../assets/toolbar-text.png | Bin 0 -> 22683 bytes .../block-controls-toolbars-and-inspector.md | 4 ++-- 3 files changed, 2 insertions(+), 2 deletions(-) rename docs/designers-developers/{developers/tutorials/block-tutorial => assets}/inspector.png (100%) create mode 100644 docs/designers-developers/assets/toolbar-text.png diff --git a/docs/designers-developers/developers/tutorials/block-tutorial/inspector.png b/docs/designers-developers/assets/inspector.png similarity index 100% rename from docs/designers-developers/developers/tutorials/block-tutorial/inspector.png rename to docs/designers-developers/assets/inspector.png diff --git a/docs/designers-developers/assets/toolbar-text.png b/docs/designers-developers/assets/toolbar-text.png new file mode 100644 index 0000000000000000000000000000000000000000..76b18c6b8f368f3abd5d2559e5a6f71c75913ed4 GIT binary patch literal 22683 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 literal 0 HcmV?d00001 diff --git a/docs/designers-developers/developers/tutorials/block-tutorial/block-controls-toolbars-and-inspector.md b/docs/designers-developers/developers/tutorials/block-tutorial/block-controls-toolbars-and-inspector.md index de6509e4c42b73..1bad64b3d95329 100644 --- a/docs/designers-developers/developers/tutorials/block-tutorial/block-controls-toolbars-and-inspector.md +++ b/docs/designers-developers/developers/tutorials/block-tutorial/block-controls-toolbars-and-inspector.md @@ -4,7 +4,7 @@ To simplify block customization and ensure a consistent experience for users, th ## Toolbar -<img src="https://cldup.com/jUslj672CK.png" width="391" height="79" alt="toolbar"> +![Screenshot of the rich text toolbar applied to a paragraph block inside the block editor](https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/assets/toolbar-text.png) When the user selects a block, a number of control buttons may be shown in a toolbar above the selected block. Some of these block-level controls are included automatically if the editor is able to transform the block to another type, or if the focused element is an RichText component. @@ -171,7 +171,7 @@ Note that `BlockControls` is only visible when the block is currently selected a ## Inspector -<img src="https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/tutorials/block-tutorial/inspector.png" with="281" height="527" alt="inspector"> +![Screenshot of the inspector panel focused on the settings for a paragraph block](https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/assets/inspector.png) The inspector is used to display less-often-used settings or settings that require more screen space. The inspector should be used for **block-level settings only**. From 8790bce7ae56191d00fe7384ea1832df10624961 Mon Sep 17 00:00:00 2001 From: Marcus Kazmierczak <marcus@mkaz.com> Date: Mon, 10 Dec 2018 12:55:19 -0800 Subject: [PATCH 221/254] Rename backwards compatiblity to backward compatibility (#12751) * Rename backwards compatiblity to backward compatibility * Remove package-lock from commit * Update CONTRIBUTING.md Co-Authored-By: mkaz <marcus@mkaz.com> * Update CONTRIBUTING.md Co-Authored-By: mkaz <marcus@mkaz.com> * Whitespace in manifest --- CONTRIBUTING.md | 6 +++--- README.md | 4 ++-- docs/designers-developers/developers/README.md | 2 +- .../developers/backward-compatibility/README.md | 1 + .../deprecations.md | 2 +- .../meta-box.md | 6 +++--- .../developers/backwards-compatibility/README.md | 1 - docs/manifest.json | 14 +++++++------- docs/toc.json | 6 +++--- lib/client-assets.php | 2 +- .../components/src/server-side-render/README.md | 4 ++-- packages/edit-post/src/store/effects.js | 2 +- 12 files changed, 25 insertions(+), 25 deletions(-) create mode 100644 docs/designers-developers/developers/backward-compatibility/README.md rename docs/designers-developers/developers/{backwards-compatibility => backward-compatibility}/deprecations.md (97%) rename docs/designers-developers/developers/{backwards-compatibility => backward-compatibility}/meta-box.md (95%) delete mode 100644 docs/designers-developers/developers/backwards-compatibility/README.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4ccb59aa2df443..e9a47c7322e476 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -166,9 +166,9 @@ Maintaining dozens of npm packages is difficult—it can be tough to keep track The developer who proposes a change (pull request) is responsible to choose the correct version increment (`major`, `minor`, or `patch`) according to the following guidelines: -- Major version X (X.y.z | X > 0) should be changed with any backwards-incompatible/"breaking" change. This will usually occur at the final stage of deprecating and removing of a feature. -- Minor version Y (x.Y.z | x > 0) should be changed when you add functionality or change functionality in a backwards-compatible manner. It must be incremented if any public API functionality is marked as deprecated. -- Patch version Z (x.y.Z | x > 0) should be incremented when you make backwards-compatible bug fixes. +- Major version X (X.y.z | X > 0) should be changed with any backward incompatible/"breaking" change. This will usually occur at the final stage of deprecating and removing of a feature. +- Minor version Y (x.Y.z | x > 0) should be changed when you add functionality or change functionality in a backward compatible manner. It must be incremented if any public API functionality is marked as deprecated. +- Patch version Z (x.y.Z | x > 0) should be incremented when you make backward compatible bug fixes. When in doubt, refer to [Semantic Versioning specification](https://semver.org/). diff --git a/README.md b/README.md index 03251ceba2160e..855958ef9635ef 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ This repo is the development hub for the <a href="https://make.wordpress.org/core/2017/01/04/focus-tech-and-design-leads/">editor focus in WordPress Core</a>. `Gutenberg` is the project name. ## Getting started -- **Download:** If you want to use the latest release with your WordPress site, <a href="https://wordpress.org/plugins/gutenberg/">download the latest release from the WordPress.org plugins repository</a>. +- **Download:** If you want to use the latest release with your WordPress site, <a href="https://wordpress.org/plugins/gutenberg/">download the latest release from the WordPress.org plugins repository</a>. - **Discuss:** Conversations and discussions take place in <a href="https://wordpress.slack.com/messages/C02QB2JS7">`#core-editor` channel on the Making WordPress Slack</a>. - **Contribute:** Development of Gutenberg happens in this GitHub repo. Get started by <a href="https://github.com/WordPress/gutenberg/blob/master/CONTRIBUTING.md">reading the contributing guidelines</a>. - **Learn:** <a href="https://wordpress.org/gutenberg/">Discover more about the project on WordPress.org</a>. @@ -44,7 +44,7 @@ Check out the <a href="https://github.com/WordPress/gutenberg/blob/master/docs/r ## Compatibility -Posts are backwards compatible, and shortcodes will still work. We are continuously exploring how highly-tailored meta boxes can be accommodated, and are looking at solutions ranging from a plugin to disable Gutenberg to automatically detecting whether to load Gutenberg or not. While we want to make sure the new editing experience from writing to publishing is user-friendly, we’re committed to finding a good solution for highly-tailored existing sites. +Posts are backward compatible, and shortcodes will still work. We are continuously exploring how highly-tailored meta boxes can be accommodated, and are looking at solutions ranging from a plugin to disable Gutenberg to automatically detecting whether to load Gutenberg or not. While we want to make sure the new editing experience from writing to publishing is user-friendly, we’re committed to finding a good solution for highly-tailored existing sites. ## The stages of Gutenberg diff --git a/docs/designers-developers/developers/README.md b/docs/designers-developers/developers/README.md index 5c552b2bd835ee..8dbc72195d05cf 100644 --- a/docs/designers-developers/developers/README.md +++ b/docs/designers-developers/developers/README.md @@ -26,7 +26,7 @@ You can also filter certain aspects of the editor; this is documented on the [Ed **Porting PHP meta boxes to blocks and Gutenberg plugins is highly encouraged!** -Discover how [Meta Box](../../../docs/designers-developers/developers/backwards-compatibility/meta-box.md) support works in Gutenberg. +Discover how [Meta Box](../../../docs/designers-developers/developers/backward-compatibility/meta-box.md) support works in Gutenberg. ## Theme Support diff --git a/docs/designers-developers/developers/backward-compatibility/README.md b/docs/designers-developers/developers/backward-compatibility/README.md new file mode 100644 index 00000000000000..bd559617c5b17f --- /dev/null +++ b/docs/designers-developers/developers/backward-compatibility/README.md @@ -0,0 +1 @@ +# Backward Compatibility diff --git a/docs/designers-developers/developers/backwards-compatibility/deprecations.md b/docs/designers-developers/developers/backward-compatibility/deprecations.md similarity index 97% rename from docs/designers-developers/developers/backwards-compatibility/deprecations.md rename to docs/designers-developers/developers/backward-compatibility/deprecations.md index 804f55c0a2c19a..7b690e995c5169 100644 --- a/docs/designers-developers/developers/backwards-compatibility/deprecations.md +++ b/docs/designers-developers/developers/backward-compatibility/deprecations.md @@ -1,6 +1,6 @@ # Deprecations -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. +Gutenberg's deprecation policy is intended to support backward 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. diff --git a/docs/designers-developers/developers/backwards-compatibility/meta-box.md b/docs/designers-developers/developers/backward-compatibility/meta-box.md similarity index 95% rename from docs/designers-developers/developers/backwards-compatibility/meta-box.md rename to docs/designers-developers/developers/backward-compatibility/meta-box.md index 2f28f48ed77cb2..8e722b4cb31565 100644 --- a/docs/designers-developers/developers/backwards-compatibility/meta-box.md +++ b/docs/designers-developers/developers/backward-compatibility/meta-box.md @@ -21,7 +21,7 @@ WordPress will fall back to the Classic editor, where the meta box will continue Explicitly setting `__block_editor_compatible_meta_box` to `true` will cause WordPress to stay in Gutenberg (assuming another meta box doesn't cause a fallback). -After a meta box is converted to a block, it can be declared as existing for backwards compatibility: +After a meta box is converted to a block, it can be declared as existing for backward compatibility: ```php add_meta_box( 'my-meta-box', 'My Meta Box', 'my_meta_box_callback', @@ -32,7 +32,7 @@ add_meta_box( 'my-meta-box', 'My Meta Box', 'my_meta_box_callback', ); ``` -When Gutenberg is used, this meta box will no longer be displayed in the meta box area, as it now only exists for backwards compatibility purposes. It will continue to display correctly in the Classic editor, should some other meta box cause a fallback. +When Gutenberg is used, this meta box will no longer be displayed in the meta box area, as it now only exists for backward compatibility purposes. It will continue to display correctly in the Classic editor, should some other meta box cause a fallback. ### Meta Box Data Collection @@ -42,7 +42,7 @@ See `lib/register.php gutenberg_trick_plugins_into_registering_meta_boxes()` `gutenberg_collect_meta_box_data()` is hooked in later on `admin_head`. It will run through the functions and hooks that `post.php` runs to register meta boxes; namely `add_meta_boxes`, `add_meta_boxes_{$post->post_type}`, and `do_meta_boxes`. -A copy of the global `$wp_meta_boxes` is made then filtered through `apply_filters( 'filter_gutenberg_meta_boxes', $_meta_boxes_copy );`, which will strip out any core meta boxes, standard custom taxonomy meta boxes, and any meta boxes that have declared themselves as only existing for backwards compatibility purposes. +A copy of the global `$wp_meta_boxes` is made then filtered through `apply_filters( 'filter_gutenberg_meta_boxes', $_meta_boxes_copy );`, which will strip out any core meta boxes, standard custom taxonomy meta boxes, and any meta boxes that have declared themselves as only existing for backward compatibility purposes. Then each location for this particular type of meta box is checked for whether it is active. If it is not empty a value of true is stored, if it is empty a value of false is stored. This meta box location data is then dispatched by the editor Redux store in `INITIALIZE_META_BOX_STATE`. diff --git a/docs/designers-developers/developers/backwards-compatibility/README.md b/docs/designers-developers/developers/backwards-compatibility/README.md deleted file mode 100644 index bd453cba0e56a4..00000000000000 --- a/docs/designers-developers/developers/backwards-compatibility/README.md +++ /dev/null @@ -1 +0,0 @@ -# Backwards Compatibility diff --git a/docs/manifest.json b/docs/manifest.json index 2dae91884153aa..e8925eec9fb9ce 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -132,22 +132,22 @@ "parent": "themes" }, { - "title": "Backwards Compatibility", - "slug": "backwards-compatibility", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/backwards-compatibility/README.md", + "title": "Backward Compatibility", + "slug": "backward-compatibility", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/backward-compatibility/README.md", "parent": "developers" }, { "title": "Deprecations", "slug": "deprecations", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/backwards-compatibility/deprecations.md", - "parent": "backwards-compatibility" + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/backward-compatibility/deprecations.md", + "parent": "backward-compatibility" }, { "title": "Meta Boxes", "slug": "meta-box", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/backwards-compatibility/meta-box.md", - "parent": "backwards-compatibility" + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/backward-compatibility/meta-box.md", + "parent": "backward-compatibility" }, { "title": "Tutorials", diff --git a/docs/toc.json b/docs/toc.json index 211c74631ec700..6dd5e876cbc00c 100644 --- a/docs/toc.json +++ b/docs/toc.json @@ -24,9 +24,9 @@ {"docs/designers-developers/developers/themes/README.md": [ {"docs/designers-developers/developers/themes/theme-support.md": []} ]}, - {"docs/designers-developers/developers/backwards-compatibility/README.md": [ - {"docs/designers-developers/developers/backwards-compatibility/deprecations.md": []}, - {"docs/designers-developers/developers/backwards-compatibility/meta-box.md": []} + {"docs/designers-developers/developers/backward-compatibility/README.md": [ + {"docs/designers-developers/developers/backward-compatibility/deprecations.md": []}, + {"docs/designers-developers/developers/backward-compatibility/meta-box.md": []} ]}, {"docs/designers-developers/developers/tutorials/readme.md": [ {"docs/designers-developers/developers/tutorials/block-tutorial/readme.md" :[ diff --git a/lib/client-assets.php b/lib/client-assets.php index 13672f6516bcdd..c1a6e847bba5e9 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -391,7 +391,7 @@ function gutenberg_register_scripts_and_styles() { wp_add_inline_script( 'wp-block-library', $script, 'before' ); // Editor Styles. - // This empty stylesheet is defined to ensure backwards compatibility. + // This empty stylesheet is defined to ensure backward compatibility. gutenberg_override_style( 'wp-blocks', false ); $fonts_url = ''; diff --git a/packages/components/src/server-side-render/README.md b/packages/components/src/server-side-render/README.md index cccde285c645df..ee18a7f4294ed6 100644 --- a/packages/components/src/server-side-render/README.md +++ b/packages/components/src/server-side-render/README.md @@ -2,7 +2,7 @@ ServerSideRender is a component used for server-side rendering a preview of dynamic blocks to display in the editor. Server-side rendering in a block's `edit` function should be limited to blocks that are heavily dependent on existing PHP rendering logic that is heavily intertwined with data, particularly when there are no endpoints available. -ServerSideRender may also be used when a legacy block is provided as a backwards compatibility measure, rather than needing to re-write the deprecated code that the block may depend on. +ServerSideRender may also be used when a legacy block is provided as a backward compatibility measure, rather than needing to re-write the deprecated code that the block may depend on. ServerSideRender should be regarded as a fallback or legacy mechanism, it is not appropriate for developing new features against. @@ -20,7 +20,7 @@ const MyServerSideRender = () => ( block="core/archives" attributes={ { showPostCounts: true, - displayAsDropdown: false, + displayAsDropdown: false, } } /> ); diff --git a/packages/edit-post/src/store/effects.js b/packages/edit-post/src/store/effects.js index 70ebd8cbb53051..a177c36c1b761b 100644 --- a/packages/edit-post/src/store/effects.js +++ b/packages/edit-post/src/store/effects.js @@ -79,7 +79,7 @@ const effects = { const state = store.getState(); - // Additional data needed for backwards compatibility. + // Additional data needed for backward compatibility. // If we do not provide this data, the post will be overridden with the default values. const post = select( 'core/editor' ).getCurrentPost( state ); const additionalData = [ From 1a1dc7c15f556a5298c2549e219fdc79fb951304 Mon Sep 17 00:00:00 2001 From: Pascal Birchler <pascal.birchler@gmail.com> Date: Tue, 11 Dec 2018 03:21:35 -0600 Subject: [PATCH 222/254] Update node-sass to 4.11.0 to support Node.js 11 (#12541) ## Description Fixes #12539 by updating node-sass to support Node.js 11. ## How has this been tested? Running `npm install` on macOS 10.14 with Node.js 11.2 without problems. ## Types of changes Minor dependency bump to support Node.js 11. ## Checklist: - [x] My code is tested. - [x] My code follows the WordPress code style. <!-- Check code: `npm run lint`, Guidelines: https://make.wordpress.org/core/handbook/best-practices/coding-standards/javascript/ --> - [x] My code follows the accessibility standards. <!-- Guidelines: https://make.wordpress.org/core/handbook/best-practices/coding-standards/accessibility-coding-standards/ --> - [x] My code has proper inline documentation. <!-- Guidelines: https://make.wordpress.org/core/handbook/best-practices/inline-documentation-standards/javascript/ --> --- package-lock.json | 158 +++++++++++++++++++++++++++++++++------------- package.json | 2 +- 2 files changed, 115 insertions(+), 45 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8ced31ee5d85a2..347cbcd5337484 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15030,9 +15030,9 @@ } }, "node-sass": { - "version": "4.9.2", - "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.9.2.tgz", - "integrity": "sha512-LdxoJLZutx0aQXHtWIYwJKMj+9pTjneTcLWJgzf2XbGu0q5pRNqW5QvFCEdm3mc5rJOdru/mzln5d0EZLacf6g==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.11.0.tgz", + "integrity": "sha512-bHUdHTphgQJZaF1LASx0kAviPH7sGlcyNhWade4eVIpFp6tsn7SV8xNMTbsQFpEV9VXpnwTTnNYlfsZXgGgmkA==", "dev": true, "requires": { "async-foreach": "^0.1.3", @@ -15048,14 +15048,26 @@ "meow": "^3.7.0", "mkdirp": "^0.5.1", "nan": "^2.10.0", - "node-gyp": "^3.3.1", + "node-gyp": "^3.8.0", "npmlog": "^4.0.0", - "request": "2.87.0", + "request": "^2.88.0", "sass-graph": "^2.2.4", "stdout-stream": "^1.4.0", "true-case-path": "^1.0.2" }, "dependencies": { + "ajv": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.6.1.tgz", + "integrity": "sha512-ZoJjft5B+EJBjUyu9C9Hc0OZyPZSSlOF+plzouTrg6UlA8f+e/n8NIgBFG/9tppJtpPWfthHakK7juJdNDODww==", + "dev": true, + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", @@ -15068,6 +15080,12 @@ "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", "dev": true }, + "aws4": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", + "dev": true + }, "camelcase": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", @@ -15076,7 +15094,7 @@ }, "camelcase-keys": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "resolved": "http://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", "dev": true, "requires": { @@ -15086,7 +15104,7 @@ }, "chalk": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "dev": true, "requires": { @@ -15107,6 +15125,28 @@ "which": "^1.2.9" } }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "dev": true + }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "dev": true, + "requires": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + } + }, "indent-string": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", @@ -15116,15 +15156,11 @@ "repeating": "^2.0.0" } }, - "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" - } + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true }, "map-obj": { "version": "1.0.1", @@ -15134,7 +15170,7 @@ }, "meow": { "version": "3.7.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", + "resolved": "http://registry.npmjs.org/meow/-/meow-3.7.0.tgz", "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", "dev": true, "requires": { @@ -15150,12 +15186,33 @@ "trim-newlines": "^1.0.0" } }, + "mime-db": { + "version": "1.37.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz", + "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==", + "dev": true + }, + "mime-types": { + "version": "2.1.21", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz", + "integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==", + "dev": true, + "requires": { + "mime-db": "~1.37.0" + } + }, "minimist": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "dev": true + }, "redent": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", @@ -15166,9 +15223,37 @@ "strip-indent": "^1.0.1" } }, + "request": { + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "dev": true, + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + } + }, "strip-ansi": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, "requires": { @@ -18883,7 +18968,7 @@ }, "os-locale": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "resolved": "http://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", "dev": true, "requires": { @@ -18903,7 +18988,7 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, "requires": { @@ -19038,7 +19123,7 @@ "dependencies": { "source-map": { "version": "0.4.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "resolved": "http://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", "dev": true, "requires": { @@ -19674,9 +19759,9 @@ "dev": true }, "stdout-stream": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.0.tgz", - "integrity": "sha1-osfIWH5U2UJ+qe2zrD8s1SLfN4s=", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.1.tgz", + "integrity": "sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==", "dev": true, "requires": { "readable-stream": "^2.0.1" @@ -20723,27 +20808,12 @@ "dev": true }, "true-case-path": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.2.tgz", - "integrity": "sha1-fskRMJJHZsf1c74wIMNPj9/QDWI=", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.3.tgz", + "integrity": "sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew==", "dev": true, "requires": { - "glob": "^6.0.4" - }, - "dependencies": { - "glob": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", - "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", - "dev": true, - "requires": { - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "2 || 3", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } + "glob": "^7.1.2" } }, "tryer": { diff --git a/package.json b/package.json index 7351c13255f052..432662628f9e2e 100644 --- a/package.json +++ b/package.json @@ -90,7 +90,7 @@ "lint-staged": "7.2.0", "lodash": "4.17.10", "mkdirp": "0.5.1", - "node-sass": "4.9.2", + "node-sass": "4.11.0", "path-type": "3.0.0", "pegjs": "0.10.0", "phpegjs": "1.0.0-beta7", From 0fba129ab9dd7755d4e94a275f738657470fc170 Mon Sep 17 00:00:00 2001 From: Nate Wright <NateWr@users.noreply.github.com> Date: Tue, 11 Dec 2018 18:14:51 +0000 Subject: [PATCH 223/254] Add attributes to ServerSideRender readme (#12793) * Add attributes to ServerSideRender readme Adds a code example demonstrating how to define attributes when registering a block that will use attributes in a ServerSideRender component. * Add whitespace and inline code markup to ServerSideRender readme Implements requested changes from code review. --- .../src/server-side-render/README.md | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/packages/components/src/server-side-render/README.md b/packages/components/src/server-side-render/README.md index ee18a7f4294ed6..55b0be7630dabb 100644 --- a/packages/components/src/server-side-render/README.md +++ b/packages/components/src/server-side-render/README.md @@ -32,5 +32,25 @@ Output uses the block's `render_callback` function, set when defining the block. ## API Endpoint -The API endpoint for getting the output for ServerSideRender is `/wp/v2/block-renderer/:block`. It accepts any params, which are used as `attributes` for the block's `render_callback` method. - +The API endpoint for getting the output for ServerSideRender is `/wp/v2/block-renderer/:block`. It will use the block's `render_callback` method. + +If you pass `attributes` to `ServerSideRender`, the block must also be registered and have its attributes defined in PHP. + +```php +register_block_type( + 'core/archives', + array( + 'attributes' => array( + 'showPostCounts' => array( + 'type' => 'boolean', + 'default' => false, + ), + 'displayAsDropdown' => array( + 'type' => 'boolean', + 'default' => false, + ), + ), + 'render_callback' => 'render_block_core_archives', + ) +); +``` From 70a48ea2b25c3af621563b42c32dc936a0a945f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20=28Greg=29=20Zi=C3=B3=C5=82kowski?= <grzegorz@gziolo.pl> Date: Wed, 12 Dec 2018 03:44:54 -0500 Subject: [PATCH 224/254] Scripts: Add check-engines script to the package (#12721) * Scripts: Add check-engines script to the package * Update packages/scripts/CHANGELOG.md Co-Authored-By: gziolo <grzegorz@gziolo.pl> * Update packages/scripts/README.md Co-Authored-By: gziolo <grzegorz@gziolo.pl> * Update minimal node version to 10.x Co-Authored-By: gziolo <grzegorz@gziolo.pl> --- package-lock.json | 1 + package.json | 7 +---- packages/scripts/CHANGELOG.md | 6 ++++ packages/scripts/README.md | 19 +++++++++++++ packages/scripts/package.json | 1 + packages/scripts/scripts/check-engines.js | 34 +++++++++++++++++++++++ 6 files changed, 62 insertions(+), 6 deletions(-) create mode 100644 packages/scripts/scripts/check-engines.js diff --git a/package-lock.json b/package-lock.json index 347cbcd5337484..161b9cdfaf0be8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2763,6 +2763,7 @@ "@wordpress/npm-package-json-lint-config": "file:packages/npm-package-json-lint-config", "babel-eslint": "8.0.3", "chalk": "^2.4.1", + "check-node-version": "^3.1.1", "cross-spawn": "^5.1.0", "eslint": "^4.19.1", "jest": "^23.6.0", diff --git a/package.json b/package.json index 432662628f9e2e..b5be65bc97bcb2 100644 --- a/package.json +++ b/package.json @@ -10,10 +10,6 @@ "WordPress", "editor" ], - "engines": { - "node": ">=8.0.0", - "npm": ">=6.0.0" - }, "dependencies": { "@wordpress/a11y": "file:packages/a11y", "@wordpress/annotations": "file:packages/annotations", @@ -72,7 +68,6 @@ "autoprefixer": "8.2.0", "babel-loader": "8.0.0", "chalk": "2.4.1", - "check-node-version": "3.1.1", "concurrently": "3.5.0", "copy-webpack-plugin": "4.5.2", "core-js": "2.5.7", @@ -145,7 +140,7 @@ "prebuild:packages": "npm run clean:packages && lerna run build && cross-env INCLUDE_PACKAGES=babel-plugin-import-jsx-pragma,postcss-themes,jest-console SKIP_JSX_PRAGMA_TRANSFORM=1 node ./bin/packages/build.js", "build:packages": "cross-env EXCLUDE_PACKAGES=babel-plugin-import-jsx-pragma,jest-console,postcss-themes node ./bin/packages/build.js", "build": "npm run build:packages && cross-env NODE_ENV=production webpack", - "check-engines": "check-node-version --package", + "check-engines": "wp-scripts check-engines", "check-licenses": "concurrently \"wp-scripts check-licenses --prod --gpl2\" \"wp-scripts check-licenses --dev\"", "precheck-local-changes": "npm run docs:build", "check-local-changes": "( git diff -U0 | xargs -0 node bin/process-git-diff ) || ( echo \"There are local uncommitted changes after one or both of 'npm install' or 'npm run docs:build'!\" && exit 1 );", diff --git a/packages/scripts/CHANGELOG.md b/packages/scripts/CHANGELOG.md index c07fe9a2e6d566..6f2977dc07f901 100644 --- a/packages/scripts/CHANGELOG.md +++ b/packages/scripts/CHANGELOG.md @@ -1,3 +1,9 @@ +## 2.5.0 (Unreleased) + +### New Feature + +- Added support for `check-engines` script ([#12721](https://github.com/WordPress/gutenberg/pull/12721)) + ## 2.4.4 (2018-11-20) ## 2.4.3 (2018-11-09) diff --git a/packages/scripts/README.md b/packages/scripts/README.md index 4126cdd34cc9b2..04ddf958782e02 100644 --- a/packages/scripts/README.md +++ b/packages/scripts/README.md @@ -19,6 +19,7 @@ _Example:_ ```json { "scripts": { + "check-engines": "wp-scripts check-engines", "lint:pkg-json": "wp-scripts lint-pkg-json .", "test": "wp-scripts test-unit-js" } @@ -27,6 +28,24 @@ _Example:_ ## Available Scripts +### `check-engines` + +Check if the current `node`, `npm` (or `yarn`) versions match the given [semantic version](https://semver.org/) ranges. If the given version is not satisfied, information about installing the needed version is printed and the program exits with an error code. It uses [check-node-version](https://www.npmjs.com/package/check-node-version) behind the scenes with the default configuration provided. You can specify your own ranges as described in [check-node-version docs](https://www.npmjs.com/package/check-node-version). + +_Example:_ + +```json +{ + "scripts": { + "check-engines": "wp-scripts check-engines" + } +} +``` + +This is how you execute the script with presented setup: +* `npm run check-engines` - checks installed version of `node` and `npm`. + + ### `wp-scripts lint-js` Helps enforce coding style guidelines for your JavaScript files. It uses [eslint](https://eslint.org/) with no rules provided (we plan to add zero config support in the near future). You can specify your own rules as described in [eslint docs](https://eslint.org/docs/rules/). diff --git a/packages/scripts/package.json b/packages/scripts/package.json index fbbbff832e7360..6a0bc5e1986a92 100644 --- a/packages/scripts/package.json +++ b/packages/scripts/package.json @@ -36,6 +36,7 @@ "@wordpress/npm-package-json-lint-config": "file:../npm-package-json-lint-config", "babel-eslint": "8.0.3", "chalk": "^2.4.1", + "check-node-version": "^3.1.1", "cross-spawn": "^5.1.0", "eslint": "^4.19.1", "jest": "^23.6.0", diff --git a/packages/scripts/scripts/check-engines.js b/packages/scripts/scripts/check-engines.js new file mode 100644 index 00000000000000..5440e27ccaffba --- /dev/null +++ b/packages/scripts/scripts/check-engines.js @@ -0,0 +1,34 @@ +/** + * External dependencies + */ +const { sync: spawn } = require( 'cross-spawn' ); +const { sync: resolveBin } = require( 'resolve-bin' ); + +/** + * Internal dependencies + */ +const { + getCliArgs, + hasCliArg, +} = require( '../utils' ); + +const args = getCliArgs(); + +const hasConfig = hasCliArg( '--package' ) || + hasCliArg( '--node' ) || + hasCliArg( '--npm' ) || + hasCliArg( '--yarn' ); +const config = ! hasConfig ? + [ + '--node', '>=10.0.0', + '--npm', '>=6.0.0', + ] : + []; + +const result = spawn( + resolveBin( 'check-node-version' ), + [ ...config, ...args ], + { stdio: 'inherit' } +); + +process.exit( result.status ); From 0416bae17c52b0a11ec1075c0928f879264b7d75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20=28Greg=29=20Zi=C3=B3=C5=82kowski?= <grzegorz@gziolo.pl> Date: Wed, 12 Dec 2018 04:21:31 -0500 Subject: [PATCH 225/254] Move devDependencies to root package.json file (#12720) * Chore: Remove unused npm dependencies from the root package.json file * Move devDependencies to root package.json file --- package-lock.json | 60 ++++++++++++++----- package.json | 20 +++++-- .../package.json | 4 -- packages/babel-plugin-makepot/package.json | 4 -- packages/block-library/package.json | 5 -- .../package.json | 3 - .../package.json | 3 - packages/blocks/package.json | 3 - packages/browserslist-config/package.json | 3 - packages/components/package.json | 5 -- packages/compose/package.json | 5 -- packages/core-data/package.json | 3 - .../package.json | 3 - packages/data/package.json | 5 -- packages/edit-post/package.json | 4 -- packages/editor/package.json | 6 -- packages/element/package.json | 3 - packages/hooks/package.json | 3 - packages/i18n/package.json | 3 - packages/is-shallow-equal/package.json | 8 --- .../package.json | 4 -- packages/notices/package.json | 3 - .../npm-package-json-lint-config/package.json | 3 - packages/nux/package.json | 3 - packages/postcss-themes/package.json | 4 +- packages/redux-routine/package.json | 3 - packages/rich-text/package.json | 4 -- packages/viewport/package.json | 4 -- 28 files changed, 63 insertions(+), 118 deletions(-) diff --git a/package-lock.json b/package-lock.json index 161b9cdfaf0be8..086978a9ea5c95 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2732,7 +2732,9 @@ "dev": true, "requires": { "@babel/runtime": "^7.0.0", - "postcss": "^6.0.16" + "autoprefixer": "^8.2.0", + "postcss": "^6.0.16", + "postcss-color-function": "^4.0.1" } }, "@wordpress/redux-routine": { @@ -4503,6 +4505,16 @@ "tweetnacl": "^0.14.3" } }, + "benchmark": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/benchmark/-/benchmark-2.1.4.tgz", + "integrity": "sha1-CfPeMckWQl1JjMLuVloOvzwqVik=", + "dev": true, + "requires": { + "lodash": "^4.17.4", + "platform": "^1.3.3" + } + }, "bfj": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/bfj/-/bfj-6.1.1.tgz", @@ -15095,7 +15107,7 @@ }, "camelcase-keys": { "version": "2.1.0", - "resolved": "http://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", "dev": true, "requires": { @@ -15105,7 +15117,7 @@ }, "chalk": { "version": "1.1.3", - "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "dev": true, "requires": { @@ -15171,7 +15183,7 @@ }, "meow": { "version": "3.7.0", - "resolved": "http://registry.npmjs.org/meow/-/meow-3.7.0.tgz", + "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", "dev": true, "requires": { @@ -15204,7 +15216,7 @@ }, "minimist": { "version": "1.2.0", - "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true }, @@ -15254,7 +15266,7 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, "requires": { @@ -16371,6 +16383,12 @@ "find-up": "^2.1.0" } }, + "platform": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.5.tgz", + "integrity": "sha512-TuvHS8AOIZNAlE77WUDiR4rySV/VMptyMfcfeoMgs4P8apaZM3JrnbzBiixKUv+XR6i+BXrQh8WAnjaSPFO65Q==", + "dev": true + }, "please-upgrade-node": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.1.1.tgz", @@ -18969,7 +18987,7 @@ }, "os-locale": { "version": "1.4.0", - "resolved": "http://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", "dev": true, "requires": { @@ -18989,7 +19007,7 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, "requires": { @@ -19124,7 +19142,7 @@ "dependencies": { "source-map": { "version": "0.4.4", - "resolved": "http://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", "dev": true, "requires": { @@ -19280,6 +19298,24 @@ } } }, + "shallow-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.0.0.tgz", + "integrity": "sha1-UI0YOLPeWQq4dXsBGyXkMJAJRfc=", + "dev": true + }, + "shallow-equals": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shallow-equals/-/shallow-equals-1.0.0.tgz", + "integrity": "sha1-JLdL8cY0wR7Uxxgqbfb7MA3OQ5A=", + "dev": true + }, + "shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", + "dev": true + }, "shebang-command": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", @@ -20436,12 +20472,6 @@ "integrity": "sha1-rifbOPZgp64uHDt9G8KQgZuFGeY=", "dev": true }, - "symlink-or-copy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/symlink-or-copy/-/symlink-or-copy-1.2.0.tgz", - "integrity": "sha512-W31+GLiBmU/ZR02Ii0mVZICuNEN9daZ63xZMPDsYgPgNjMtg+atqLEGI7PPI936jYSQZxoLb/63xos8Adrx4Eg==", - "dev": true - }, "table": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/table/-/table-4.0.3.tgz", diff --git a/package.json b/package.json index b5be65bc97bcb2..c9a2723cb90084 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,9 @@ }, "devDependencies": { "@babel/core": "7.0.0", + "@babel/plugin-syntax-jsx": "7.0.0", "@babel/runtime-corejs2": "7.0.0", + "@babel/traverse": "7.0.0", "@wordpress/babel-plugin-import-jsx-pragma": "file:packages/babel-plugin-import-jsx-pragma", "@wordpress/babel-plugin-makepot": "file:packages/babel-plugin-makepot", "@wordpress/babel-preset-default": "file:packages/babel-preset-default", @@ -65,41 +67,49 @@ "@wordpress/npm-package-json-lint-config": "file:packages/npm-package-json-lint-config", "@wordpress/postcss-themes": "file:packages/postcss-themes", "@wordpress/scripts": "file:packages/scripts", - "autoprefixer": "8.2.0", "babel-loader": "8.0.0", + "benchmark": "2.1.4", + "browserslist": "3.2.8", "chalk": "2.4.1", "concurrently": "3.5.0", "copy-webpack-plugin": "4.5.2", "core-js": "2.5.7", "cross-env": "3.2.4", "cssnano": "4.0.3", + "enzyme": "3.7.0", "deasync": "0.1.13", "deep-freeze": "0.0.1", "doctrine": "2.1.0", "eslint-plugin-jest": "21.5.0", "espree": "3.5.4", + "fbjs": "0.8.17", "glob": "7.1.2", "husky": "0.14.3", + "is-plain-obj": "1.1.0", + "is-equal-shallow": "0.1.3", "jest-puppeteer": "3.2.1", + "jsdom": "11.12.0", "lerna": "3.4.3", "lint-staged": "7.2.0", "lodash": "4.17.10", "mkdirp": "0.5.1", "node-sass": "4.11.0", - "path-type": "3.0.0", "pegjs": "0.10.0", "phpegjs": "1.0.0-beta7", - "postcss-color-function": "4.0.1", "puppeteer": "1.6.1", + "react-dom": "16.6.3", "react-test-renderer": "16.6.3", + "redux": "4.0.0", "rimraf": "2.6.2", "rtlcss": "2.4.0", "sass-loader": "6.0.7", - "source-map-loader": "0.2.3", + "shallow-equal": "1.0.0", + "shallow-equals": "1.0.0", + "shallowequal": "1.1.0", "sprintf-js": "1.1.1", + "source-map-loader": "0.2.3", "stylelint": "9.5.0", "stylelint-config-wordpress": "13.1.0", - "symlink-or-copy": "1.2.0", "uuid": "3.3.2", "webpack": "4.8.3", "webpack-bundle-analyzer": "3.0.2", diff --git a/packages/babel-plugin-import-jsx-pragma/package.json b/packages/babel-plugin-import-jsx-pragma/package.json index 5cd00bf163f86f..1c2bb7de9bb959 100644 --- a/packages/babel-plugin-import-jsx-pragma/package.json +++ b/packages/babel-plugin-import-jsx-pragma/package.json @@ -28,10 +28,6 @@ "dependencies": { "@babel/runtime": "^7.0.0" }, - "devDependencies": { - "@babel/core": "^7.0.0", - "@babel/plugin-syntax-jsx": "^7.0.0" - }, "peerDependencies": { "@babel/core": "^7.0.0" }, diff --git a/packages/babel-plugin-makepot/package.json b/packages/babel-plugin-makepot/package.json index e21a023be7d3d3..819415f3b97320 100644 --- a/packages/babel-plugin-makepot/package.json +++ b/packages/babel-plugin-makepot/package.json @@ -29,10 +29,6 @@ "gettext-parser": "^1.3.1", "lodash": "^4.17.10" }, - "devDependencies": { - "@babel/core": "^7.0.0", - "@babel/traverse": "^7.0.0" - }, "peerDependencies": { "@babel/core": "^7.0.0" }, diff --git a/packages/block-library/package.json b/packages/block-library/package.json index ad9c4514349cc4..5d3e510bbfe93c 100644 --- a/packages/block-library/package.json +++ b/packages/block-library/package.json @@ -40,11 +40,6 @@ "memize": "^1.0.5", "url": "^0.11.0" }, - "devDependencies": { - "deep-freeze": "^0.0.1", - "enzyme": "^3.7.0", - "react-test-renderer": "^16.6.3" - }, "publishConfig": { "access": "public" } diff --git a/packages/block-serialization-default-parser/package.json b/packages/block-serialization-default-parser/package.json index 1a235b320cd06e..8cf5725dfd2d2d 100644 --- a/packages/block-serialization-default-parser/package.json +++ b/packages/block-serialization-default-parser/package.json @@ -23,9 +23,6 @@ "dependencies": { "@babel/runtime": "^7.0.0" }, - "devDependencies": { - "@wordpress/block-serialization-spec-parser": "file:../block-serialization-spec-parser" - }, "publishConfig": { "access": "public" } diff --git a/packages/block-serialization-spec-parser/package.json b/packages/block-serialization-spec-parser/package.json index 251a28045fb958..7739f0bf041774 100644 --- a/packages/block-serialization-spec-parser/package.json +++ b/packages/block-serialization-spec-parser/package.json @@ -18,9 +18,6 @@ "bugs": { "url": "https://github.com/WordPress/gutenberg/issues" }, - "devDependencies": { - "pegjs": "0.10.0" - }, "publishConfig": { "access": "public" }, diff --git a/packages/blocks/package.json b/packages/blocks/package.json index 1a662f7a286264..af7151b58d8065 100644 --- a/packages/blocks/package.json +++ b/packages/blocks/package.json @@ -41,9 +41,6 @@ "tinycolor2": "^1.4.1", "uuid": "^3.3.2" }, - "devDependencies": { - "deep-freeze": "^0.0.1" - }, "publishConfig": { "access": "public" } diff --git a/packages/browserslist-config/package.json b/packages/browserslist-config/package.json index a554d875997e83..928f2c2dffb236 100644 --- a/packages/browserslist-config/package.json +++ b/packages/browserslist-config/package.json @@ -21,9 +21,6 @@ "node": ">=8" }, "main": "index.js", - "devDependencies": { - "browserslist": "^3.1.0" - }, "publishConfig": { "access": "public" } diff --git a/packages/components/package.json b/packages/components/package.json index 6657f37b182b61..9ad1244df0dffa 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -47,11 +47,6 @@ "tinycolor2": "^1.4.1", "uuid": "^3.3.2" }, - "devDependencies": { - "@wordpress/token-list": "file:../token-list", - "enzyme": "^3.7.0", - "react-test-renderer": "^16.6.3" - }, "publishConfig": { "access": "public" } diff --git a/packages/compose/package.json b/packages/compose/package.json index dc7a4ea89316ee..ace35bcce8ba3e 100644 --- a/packages/compose/package.json +++ b/packages/compose/package.json @@ -26,11 +26,6 @@ "@wordpress/is-shallow-equal": "file:../is-shallow-equal", "lodash": "^4.17.10" }, - "devDependencies": { - "enzyme": "^3.7.0", - "react-dom": "^16.6.3", - "react-test-renderer": "^16.6.3" - }, "publishConfig": { "access": "public" } diff --git a/packages/core-data/package.json b/packages/core-data/package.json index ad9917d785bad3..44310994619003 100644 --- a/packages/core-data/package.json +++ b/packages/core-data/package.json @@ -29,9 +29,6 @@ "lodash": "^4.17.10", "rememo": "^3.0.0" }, - "devDependencies": { - "deep-freeze": "^0.0.1" - }, "publishConfig": { "access": "public" } diff --git a/packages/custom-templated-path-webpack-plugin/package.json b/packages/custom-templated-path-webpack-plugin/package.json index 8cf29e724b1874..f1d922ffd59498 100644 --- a/packages/custom-templated-path-webpack-plugin/package.json +++ b/packages/custom-templated-path-webpack-plugin/package.json @@ -27,9 +27,6 @@ "@babel/runtime": "^7.0.0", "escape-string-regexp": "^1.0.5" }, - "devDependencies": { - "webpack": "^4.8.3" - }, "peerDependencies": { "webpack": "^4.0.0" }, diff --git a/packages/data/package.json b/packages/data/package.json index 493c9af3bd7be5..a547a316872d2c 100644 --- a/packages/data/package.json +++ b/packages/data/package.json @@ -32,11 +32,6 @@ "redux": "^4.0.0", "turbo-combine-reducers": "^1.0.2" }, - "devDependencies": { - "deep-freeze": "^0.0.1", - "enzyme": "^3.7.0", - "react-test-renderer": "^16.6.3" - }, "publishConfig": { "access": "public" } diff --git a/packages/edit-post/package.json b/packages/edit-post/package.json index 6c98e2826302c5..d8d3f4802aa391 100644 --- a/packages/edit-post/package.json +++ b/packages/edit-post/package.json @@ -42,10 +42,6 @@ "lodash": "^4.17.10", "refx": "^3.0.0" }, - "devDependencies": { - "deep-freeze": "^0.0.1", - "enzyme": "^3.7.0" - }, "publishConfig": { "access": "public" } diff --git a/packages/editor/package.json b/packages/editor/package.json index dfe9ac51ed3fdf..2f2676ee36853a 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -59,12 +59,6 @@ "tinymce": "^4.7.2", "traverse": "^0.6.6" }, - "devDependencies": { - "deep-freeze": "^0.0.1", - "enzyme": "^3.7.0", - "react-dom": "^16.6.3", - "react-test-renderer": "^16.6.3" - }, "publishConfig": { "access": "public" } diff --git a/packages/element/package.json b/packages/element/package.json index 5e80ae9220c0d7..19707938c6a674 100644 --- a/packages/element/package.json +++ b/packages/element/package.json @@ -27,9 +27,6 @@ "react": "^16.6.3", "react-dom": "^16.6.3" }, - "devDependencies": { - "enzyme": "^3.7.0" - }, "publishConfig": { "access": "public" } diff --git a/packages/hooks/package.json b/packages/hooks/package.json index 8f1eb12b94ab1f..cef3b444b83d74 100644 --- a/packages/hooks/package.json +++ b/packages/hooks/package.json @@ -22,9 +22,6 @@ "dependencies": { "@babel/runtime": "^7.0.0" }, - "devDependencies": { - "benchmark": "^2.1.4" - }, "publishConfig": { "access": "public" } diff --git a/packages/i18n/package.json b/packages/i18n/package.json index 18158a6f0162e2..a1005eac73e6e7 100644 --- a/packages/i18n/package.json +++ b/packages/i18n/package.json @@ -30,9 +30,6 @@ "sprintf-js": "^1.1.1", "tannin": "^1.0.1" }, - "devDependencies": { - "benchmark": "^2.1.4" - }, "publishConfig": { "access": "public" } diff --git a/packages/is-shallow-equal/package.json b/packages/is-shallow-equal/package.json index 290b2810f45519..f5553ceb41a47e 100644 --- a/packages/is-shallow-equal/package.json +++ b/packages/is-shallow-equal/package.json @@ -26,14 +26,6 @@ "dependencies": { "@babel/runtime": "^7.0.0" }, - "devDependencies": { - "benchmark": "^2.1.4", - "fbjs": "^0.8.16", - "is-equal-shallow": "^0.1.3", - "shallow-equal": "^1.0.0", - "shallow-equals": "^1.0.0", - "shallowequal": "^1.0.2" - }, "publishConfig": { "access": "public" } diff --git a/packages/library-export-default-webpack-plugin/package.json b/packages/library-export-default-webpack-plugin/package.json index 00b91c6dbeb442..52d28f6e9e1847 100644 --- a/packages/library-export-default-webpack-plugin/package.json +++ b/packages/library-export-default-webpack-plugin/package.json @@ -27,10 +27,6 @@ "lodash": "^4.17.10", "webpack-sources": "^1.1.0" }, - "devDependencies": { - "rimraf": "^2.6.2", - "webpack": "^4.8.3" - }, "peerDependencies": { "webpack": "^4.0.0" }, diff --git a/packages/notices/package.json b/packages/notices/package.json index f7ae5027587cdb..5e642a6d7f273a 100644 --- a/packages/notices/package.json +++ b/packages/notices/package.json @@ -24,9 +24,6 @@ "@wordpress/data": "file:../data", "lodash": "^4.17.10" }, - "devDependencies": { - "deep-freeze": "^0.0.1" - }, "publishConfig": { "access": "public" } diff --git a/packages/npm-package-json-lint-config/package.json b/packages/npm-package-json-lint-config/package.json index e7912c2c2f2cd1..ddf65a0f15c8c1 100644 --- a/packages/npm-package-json-lint-config/package.json +++ b/packages/npm-package-json-lint-config/package.json @@ -18,9 +18,6 @@ "url": "https://github.com/WordPress/gutenberg/issues" }, "main": "index.js", - "devDependencies": { - "is-plain-obj": "^1.1.0" - }, "peerDependencies": { "npm-package-json-lint": ">= 3.3.1" }, diff --git a/packages/nux/package.json b/packages/nux/package.json index d754dc9f891f5a..28dd725e40fae7 100644 --- a/packages/nux/package.json +++ b/packages/nux/package.json @@ -29,9 +29,6 @@ "lodash": "^4.17.10", "rememo": "^3.0.0" }, - "devDependencies": { - "enzyme": "^3.7.0" - }, "publishConfig": { "access": "public" } diff --git a/packages/postcss-themes/package.json b/packages/postcss-themes/package.json index 947039a78a9460..88727a2f2fe094 100644 --- a/packages/postcss-themes/package.json +++ b/packages/postcss-themes/package.json @@ -27,7 +27,9 @@ "main": "build/index.js", "dependencies": { "@babel/runtime": "^7.0.0", - "postcss": "^6.0.16" + "autoprefixer": "^8.2.0", + "postcss": "^6.0.16", + "postcss-color-function": "^4.0.1" }, "publishConfig": { "access": "public" diff --git a/packages/redux-routine/package.json b/packages/redux-routine/package.json index 5eef2653d07824..bd28bcde065c75 100644 --- a/packages/redux-routine/package.json +++ b/packages/redux-routine/package.json @@ -26,9 +26,6 @@ "is-promise": "^2.1.0", "rungen": "^0.3.2" }, - "devDependencies": { - "redux": "^4.0.0" - }, "publishConfig": { "access": "public" } diff --git a/packages/rich-text/package.json b/packages/rich-text/package.json index aeb67dbfa3506a..4ae221a90812f3 100644 --- a/packages/rich-text/package.json +++ b/packages/rich-text/package.json @@ -27,10 +27,6 @@ "lodash": "^4.17.10", "rememo": "^3.0.0" }, - "devDependencies": { - "deep-freeze": "^0.0.1", - "jsdom": "^11.12.0" - }, "publishConfig": { "access": "public" } diff --git a/packages/viewport/package.json b/packages/viewport/package.json index 11da1c485c9d70..527fca472cd3dc 100644 --- a/packages/viewport/package.json +++ b/packages/viewport/package.json @@ -26,10 +26,6 @@ "@wordpress/element": "file:../element", "lodash": "^4.17.10" }, - "devDependencies": { - "deep-freeze": "^0.0.1", - "react-test-renderer": "^16.6.3" - }, "publishConfig": { "access": "public" } From 51583b83b66ee86201b36c70028640ffdbeece43 Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Wed, 12 Dec 2018 16:27:11 +0100 Subject: [PATCH 226/254] Fix php notice from the recent comments block (#12812) --- lib/load.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/load.php b/lib/load.php index 0c55db3a37f6ca..ee1c973f99568a 100644 --- a/lib/load.php +++ b/lib/load.php @@ -62,7 +62,10 @@ if ( ! function_exists( 'render_block_core_categories' ) ) { require dirname( __FILE__ ) . '/../packages/block-library/src/categories/index.php'; } -if ( ! function_exists( 'render_block_core_latest_comments' ) ) { +// Currently merged in core as `gutenberg_render_block_core_latest_comments`, +// expected to change soon. +if ( ! function_exists( 'render_block_core_latest_comments' ) + && ! function_exists( 'gutenberg_render_block_core_latest_comments' ) ) { require dirname( __FILE__ ) . '/../packages/block-library/src/latest-comments/index.php'; } if ( ! function_exists( 'render_block_core_latest_posts' ) ) { From 6c688159babfbea1adfa2eda536066cd19762813 Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Wed, 12 Dec 2018 17:57:26 +0100 Subject: [PATCH 227/254] RichText: Fix React warning shown when unmounting a currently selected RichText. (#12817) --- packages/editor/src/components/rich-text/index.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/editor/src/components/rich-text/index.js b/packages/editor/src/components/rich-text/index.js index 0731916d6a1a73..bc814d8ebff9c3 100644 --- a/packages/editor/src/components/rich-text/index.js +++ b/packages/editor/src/components/rich-text/index.js @@ -129,6 +129,10 @@ export class RichText extends Component { this.lastHistoryValue = value; } + componentWillUnmount() { + document.removeEventListener( 'selectionchange', this.onSelectionChange ); + } + setRef( node ) { this.editableRef = node; } From 4dae3bc56cca220b916d2e2bfeaecc9da79e3779 Mon Sep 17 00:00:00 2001 From: Andrew Duthie <andrew@andrewduthie.com> Date: Wed, 12 Dec 2018 13:16:52 -0500 Subject: [PATCH 228/254] Packages: Reimplement ESLint config as plugin (#12763) * Packages: Move eslint-config to eslint-plugin (Fails pre-commit, but in effort to ensure history preservation) * eslint-plugin: Add npmrc to avoid package-lock.json * Framework: Update path references for eslint-config to -plugin * eslint-plugin: Reimplement ESLint config as plugin * eslint-plugin: Unmark as private * eslint-plugin: Undocument custom ruleset --- .eslintrc.js | 2 +- docs/manifest.json | 6 +- package-lock.json | 13 ++- package.json | 2 +- packages/eslint-config/README.md | 24 ----- packages/eslint-config/configs/es5.js | 12 --- packages/eslint-config/configs/esnext.js | 8 -- packages/eslint-config/configs/rules/es5.js | 84 ------------------ .../eslint-config/configs/rules/esnext.js | 66 -------------- packages/eslint-plugin/.npmrc | 1 + packages/eslint-plugin/README.md | 48 ++++++++++ packages/eslint-plugin/configs/custom.js | 19 ++++ .../index.js => eslint-plugin/configs/es5.js} | 87 ++++--------------- packages/eslint-plugin/configs/esnext.js | 36 ++++++++ packages/eslint-plugin/configs/index.js | 1 + packages/eslint-plugin/configs/jsx-a11y.js | 17 ++++ packages/eslint-plugin/configs/react.js | 28 ++++++ packages/eslint-plugin/configs/recommended.js | 16 ++++ packages/eslint-plugin/index.js | 3 + .../package.json | 10 +-- 20 files changed, 206 insertions(+), 277 deletions(-) delete mode 100644 packages/eslint-config/README.md delete mode 100644 packages/eslint-config/configs/es5.js delete mode 100644 packages/eslint-config/configs/esnext.js delete mode 100644 packages/eslint-config/configs/rules/es5.js delete mode 100644 packages/eslint-config/configs/rules/esnext.js create mode 100644 packages/eslint-plugin/.npmrc create mode 100644 packages/eslint-plugin/README.md create mode 100644 packages/eslint-plugin/configs/custom.js rename packages/{eslint-config/index.js => eslint-plugin/configs/es5.js} (52%) create mode 100644 packages/eslint-plugin/configs/esnext.js create mode 100644 packages/eslint-plugin/configs/index.js create mode 100644 packages/eslint-plugin/configs/jsx-a11y.js create mode 100644 packages/eslint-plugin/configs/react.js create mode 100644 packages/eslint-plugin/configs/recommended.js create mode 100644 packages/eslint-plugin/index.js rename packages/{eslint-config => eslint-plugin}/package.json (72%) diff --git a/.eslintrc.js b/.eslintrc.js index feced45620657e..af4bda32427a94 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -19,7 +19,7 @@ const majorMinorRegExp = escapeRegExp( version.replace( /\.\d+$/, '' ) ) + '(\\. module.exports = { root: true, extends: [ - '@wordpress/eslint-config', + 'plugin:@wordpress/eslint-plugin/recommended', 'plugin:jest/recommended', ], rules: { diff --git a/docs/manifest.json b/docs/manifest.json index e8925eec9fb9ce..dbf955bd2d2478 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -498,9 +498,9 @@ "parent": "packages" }, { - "title": "@wordpress/eslint-config", - "slug": "packages-eslint-config", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/packages/eslint-config/README.md", + "title": "@wordpress/eslint-plugin", + "slug": "packages-eslint-plugin", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/packages/eslint-plugin/README.md", "parent": "packages" }, { diff --git a/package-lock.json b/package-lock.json index 086978a9ea5c95..ea80146bc3857e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2590,13 +2590,14 @@ "@babel/runtime": "^7.0.0" } }, - "@wordpress/eslint-config": { - "version": "file:packages/eslint-config", + "@wordpress/eslint-plugin": { + "version": "file:packages/eslint-plugin", "dev": true, "requires": { "babel-eslint": "^8.0.3", "eslint-plugin-jsx-a11y": "6.0.2", - "eslint-plugin-react": "7.7.0" + "eslint-plugin-react": "7.7.0", + "requireindex": "^1.2.0" } }, "@wordpress/format-library": { @@ -18668,6 +18669,12 @@ } } }, + "requireindex": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz", + "integrity": "sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==", + "dev": true + }, "resolve": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz", diff --git a/package.json b/package.json index c9a2723cb90084..6493f7d59e1d28 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "@wordpress/babel-preset-default": "file:packages/babel-preset-default", "@wordpress/browserslist-config": "file:packages/browserslist-config", "@wordpress/custom-templated-path-webpack-plugin": "file:packages/custom-templated-path-webpack-plugin", - "@wordpress/eslint-config": "file:packages/eslint-config", + "@wordpress/eslint-plugin": "file:packages/eslint-plugin", "@wordpress/jest-console": "file:packages/jest-console", "@wordpress/jest-preset-default": "file:packages/jest-preset-default", "@wordpress/library-export-default-webpack-plugin": "file:packages/library-export-default-webpack-plugin", diff --git a/packages/eslint-config/README.md b/packages/eslint-config/README.md deleted file mode 100644 index 5f2b003e765c5d..00000000000000 --- a/packages/eslint-config/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# ESLint Config - -[ESLint](https://eslint.org/) config for WordPress development. - -## Installation - -Install the module - -```bash -npm install @wordpress/eslint-config --save-dev -``` - -### Usage - -Next, extend the configuration from your project's `.eslintrc` file: - -```json -"extends": "@wordpress/eslint-config" -``` - -Refer to the [ESLint documentation on Shareable Configs](http://eslint.org/docs/developer-guide/shareable-configs) for more information. - - -<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/eslint-config/configs/es5.js b/packages/eslint-config/configs/es5.js deleted file mode 100644 index 2e56e7a2f5fcdb..00000000000000 --- a/packages/eslint-config/configs/es5.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * The original version of this file is based on WordPress ESLint rules and shared configs: - * https://github.com/WordPress-Coding-Standards/eslint-plugin-wordpress. - */ - -module.exports = { - env: { - es6: true, - }, - - rules: require( './rules/esnext' ), -}; diff --git a/packages/eslint-config/configs/esnext.js b/packages/eslint-config/configs/esnext.js deleted file mode 100644 index 3b3a80d7d4cd82..00000000000000 --- a/packages/eslint-config/configs/esnext.js +++ /dev/null @@ -1,8 +0,0 @@ -/** - * The original version of this file is based on WordPress ESLint rules and shared configs: - * https://github.com/WordPress-Coding-Standards/eslint-plugin-wordpress. - */ - -module.exports = { - rules: require( './rules/es5' ), -}; diff --git a/packages/eslint-config/configs/rules/es5.js b/packages/eslint-config/configs/rules/es5.js deleted file mode 100644 index 61e3e01c343f1d..00000000000000 --- a/packages/eslint-config/configs/rules/es5.js +++ /dev/null @@ -1,84 +0,0 @@ -module.exports = { - // Possible Errors - // Disallow assignment in conditional expressions - 'no-cond-assign': [ 'error', 'except-parens' ], - // Disallow irregular whitespace outside of strings and comments - 'no-irregular-whitespace': 'error', - // Best Practices - // Specify curly brace conventions for all control statements - curly: [ 'error', 'all' ], - // Encourages use of dot notation whenever possible - 'dot-notation': [ 'error', { - allowKeywords: true, - allowPattern: '^[a-z]+(_[a-z]+)+$', - } ], - // Disallow use of multiline strings - 'no-multi-str': 'error', - // Disallow use of the with statement - 'no-with': 'error', - // Requires to declare all vars on top of their containing scope - 'vars-on-top': 'error', - // Require immediate function invocation to be wrapped in parentheses - 'wrap-iife': 'error', - // Require or disallow Yoda conditions - yoda: [ 'error', 'always' ], - // Strict Mode - // Variables - // Stylistic Issues - // Enforce spacing inside array brackets - 'array-bracket-spacing': [ 'error', 'always' ], - // Enforce one true brace style - 'brace-style': 'error', - // Require camel case names - camelcase: [ 'error', { - properties: 'always', - } ], - // Disallow or enforce trailing commas - 'comma-dangle': [ 'error', 'never' ], - // Enforce spacing before and after comma - 'comma-spacing': 'error', - // Enforce one true comma style - 'comma-style': [ 'error', 'last' ], - // Enforce newline at the end of file, with no multiple empty lines - 'eol-last': 'error', - // Enforces spacing between keys and values in object literal properties - 'key-spacing': [ 'error', { - beforeColon: false, - afterColon: true, - } ], - // Enforce spacing before and after keywords - 'keyword-spacing': 'error', - // Disallow mixed "LF" and "CRLF" as linebreaks - 'linebreak-style': [ 'error', 'unix' ], - // Enforces empty lines around comments - 'lines-around-comment': [ 'error', { - beforeLineComment: true, - } ], - // Disallow mixed spaces and tabs for indentation - 'no-mixed-spaces-and-tabs': 'error', - // Disallow multiple empty lines - 'no-multiple-empty-lines': 'error', - // Disallow trailing whitespace at the end of lines - 'no-trailing-spaces': 'error', - // Require or disallow an newline around variable declarations - 'one-var-declaration-per-line': [ 'error', 'initializations' ], - // Enforce operators to be placed before or after line breaks - 'operator-linebreak': [ 'error', 'after' ], - // Specify whether backticks, double or single quotes should be used - quotes: [ 'error', 'single' ], - // Require or disallow use of semicolons instead of ASI - semi: [ 'error', 'always' ], - // Require or disallow space before blocks - 'space-before-blocks': [ 'error', 'always' ], - // Require or disallow space before function opening parenthesis - 'space-before-function-paren': [ 'error', 'never' ], - // Require or disallow space before blocks - 'space-in-parens': [ 'error', 'always', { exceptions: [ '{}', '[]' ] } ], - // Require spaces around operators - 'space-infix-ops': 'error', - // Require or disallow spaces before/after unary operators (words on by default, nonwords) - 'space-unary-ops': [ 'error', { - overrides: { '!': true }, - } ], - // Legacy -}; diff --git a/packages/eslint-config/configs/rules/esnext.js b/packages/eslint-config/configs/rules/esnext.js deleted file mode 100644 index dcfc27f06554f3..00000000000000 --- a/packages/eslint-config/configs/rules/esnext.js +++ /dev/null @@ -1,66 +0,0 @@ -// see https://eslint.org/docs/rules/#ecmascript-6 -// -module.exports = { - // require braces around arrow function bodies - 'arrow-body-style': 'off', - // require parentheses around arrow function arguments - 'arrow-parens': 'off', - // enforce consistent spacing before and after the arrow in arrow functions - 'arrow-spacing': 'off', - // require super() calls in constructors - 'constructor-super': 'error', - // enforce consistent spacing around * operators in generator functions - 'generator-star-spacing': 'off', - // disallow reassigning class members - 'no-class-assign': 'off', - // disallow arrow functions where they could be confused with comparisons - 'no-confusing-arrow': 'off', - // disallow reassigning `const` variables - 'no-const-assign': 'error', - // disallow duplicate class members - 'no-dupe-class-members': 'error', - // disallow duplicate module imports - 'no-duplicate-imports': 'error', - // disallow `new` operators with the `Symbol` object - 'no-new-symbol': 'off', - // disallow specified modules when loaded by `import` - 'no-restricted-imports': 'off', - // disallow `this`/`super` before calling `super()` in constructors - 'no-this-before-super': 'off', - // disallow unnecessary computed property keys in object literals - 'no-useless-computed-key': 'error', - // disallow unnecessary constructors - 'no-useless-constructor': 'error', - // disallow renaming import, export, and destructured assignments to the same name - 'no-useless-rename': 'off', - // require `let` or `const` instead of `var` - 'no-var': 'error', - // require or disallow method and property shorthand syntax for object literals - 'object-shorthand': 'off', - // require arrow functions as callbacks - 'prefer-arrow-callback': 'off', - // require `const` declarations for variables that are never reassigned after declared - 'prefer-const': 'error', - // require destructuring from arrays and/or objects - 'prefer-destructuring': 'off', - // disallow `parseInt()` and `Number.parseInt()` in favor of binary, octal, and hexadecimal literals - 'prefer-numeric-literals': 'off', - // require rest parameters instead of `arguments` - 'prefer-rest-params': 'off', - // require spread operators instead of `.apply()` - 'prefer-spread': 'off', - // require template literals instead of string concatenation - 'prefer-template': 'off', - // require generator functions to contain `yield` - 'require-yield': 'off', - // enforce spacing between rest and spread operators and their expressions - 'rest-spread-spacing': 'off', - // enforce sorted import declarations within modules - 'sort-imports': 'off', - // require symbol descriptions - 'symbol-description': 'off', - // require or disallow spacing around embedded expressions of template strings - 'template-curly-spacing': [ 'error', 'always' ], - // require or disallow spacing around the `*` in `yield*` expressions - 'yield-star-spacing': 'off', -}; diff --git a/packages/eslint-plugin/.npmrc b/packages/eslint-plugin/.npmrc new file mode 100644 index 00000000000000..43c97e719a5a82 --- /dev/null +++ b/packages/eslint-plugin/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md new file mode 100644 index 00000000000000..fd516bbf622b4d --- /dev/null +++ b/packages/eslint-plugin/README.md @@ -0,0 +1,48 @@ +# ESLint Plugin + +[ESLint](https://eslint.org/) plugin including configurations and custom rules for WordPress development. + +## Installation + +Install the module + +```bash +npm install @wordpress/eslint-plugin --save-dev +``` + +### Usage + +To opt-in to the default configuration, extend your own project's `.eslintrc` file: + +```json +{ + "extends": [ "plugin:@wordpress/eslint-plugin/recommended" ] +} +``` + +Refer to the [ESLint documentation on Shareable Configs](http://eslint.org/docs/developer-guide/shareable-configs) for more information. + +The `recommended` preset will include rules governing an ES2015+ environment, and includes rules from the [`eslint-plugin-jsx-a11y`](https://github.com/evcohen/eslint-plugin-jsx-a11y) and [`eslint-plugin-react`](https://github.com/yannickcr/eslint-plugin-react) projects. + +#### Rulesets + +Alternatively, you can opt-in to only the more granular rulesets offered by the plugin. These include: + +- `es5` +- `esnext` +- `jsx-a11y` +- `react` + +For example, if your project does not use React, you could consider extending including only the ESNext rules in your project using the following `extends` definition: + +```json +{ + "extends": [ "plugin:@wordpress/eslint-plugin/esnext" ] +} +``` + +These rules can be used additively, so you could extend both `esnext` and `custom` rulesets, but omit the `react` and `jsx-a11y` configurations. + +The granular rulesets will not define any environment globals. As such, if they are required for your project, you will need to define them yourself. + +<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/eslint-plugin/configs/custom.js b/packages/eslint-plugin/configs/custom.js new file mode 100644 index 00000000000000..0318c85c324c9e --- /dev/null +++ b/packages/eslint-plugin/configs/custom.js @@ -0,0 +1,19 @@ +module.exports = { + rules: { + 'no-restricted-syntax': [ + 'error', + { + selector: 'CallExpression[callee.name=/^__|_n|_x$/]:not([arguments.0.type=/^Literal|BinaryExpression$/])', + message: 'Translate function arguments must be string literals.', + }, + { + selector: 'CallExpression[callee.name=/^_n|_x$/]:not([arguments.1.type=/^Literal|BinaryExpression$/])', + message: 'Translate function arguments must be string literals.', + }, + { + selector: 'CallExpression[callee.name=_nx]:not([arguments.2.type=/^Literal|BinaryExpression$/])', + message: 'Translate function arguments must be string literals.', + }, + ], + }, +}; diff --git a/packages/eslint-config/index.js b/packages/eslint-plugin/configs/es5.js similarity index 52% rename from packages/eslint-config/index.js rename to packages/eslint-plugin/configs/es5.js index aa02fdc33ff9d5..167e542ef69a68 100644 --- a/packages/eslint-config/index.js +++ b/packages/eslint-plugin/configs/es5.js @@ -1,56 +1,26 @@ module.exports = { - parser: 'babel-eslint', - extends: [ - './configs/es5.js', - './configs/esnext.js', - 'plugin:react/recommended', - 'plugin:jsx-a11y/recommended', - ], - env: { - node: true, - }, - parserOptions: { - sourceType: 'module', - ecmaFeatures: { - jsx: true, - }, - }, - globals: { - window: true, - document: true, - }, - plugins: [ - 'react', - 'jsx-a11y', - ], rules: { 'array-bracket-spacing': [ 'error', 'always' ], - 'arrow-parens': [ 'error', 'always' ], - 'arrow-spacing': 'error', 'brace-style': [ 'error', '1tbs' ], - camelcase: [ 'error', { properties: 'never' } ], + camelcase: [ 'error', { + properties: 'never', + } ], 'comma-dangle': [ 'error', 'always-multiline' ], 'comma-spacing': 'error', - 'comma-style': 'error', - 'computed-property-spacing': [ 'error', 'always' ], + 'comma-style': [ 'error', 'last' ], + curly: [ 'error', 'all' ], 'dot-notation': 'error', 'eol-last': 'error', eqeqeq: 'error', 'func-call-spacing': 'error', indent: [ 'error', 'tab', { SwitchCase: 1 } ], - 'jsx-a11y/label-has-for': [ 'error', { - required: 'id', - } ], - 'jsx-a11y/media-has-caption': 'off', - 'jsx-a11y/no-noninteractive-tabindex': 'off', - 'jsx-a11y/role-has-required-aria-props': 'off', - 'jsx-quotes': 'error', 'key-spacing': 'error', 'keyword-spacing': 'error', - 'lines-around-comment': 'off', + 'linebreak-style': [ 'error', 'unix' ], 'no-alert': 'error', 'no-bitwise': 'error', 'no-caller': 'error', + 'no-cond-assign': [ 'error', 'except-parens' ], 'no-console': 'error', 'no-debugger': 'error', 'no-dupe-args': 'error', @@ -60,31 +30,18 @@ module.exports = { 'no-eval': 'error', 'no-extra-semi': 'error', 'no-fallthrough': 'error', + 'no-irregular-whitespace': 'error', 'no-lonely-if': 'error', + 'no-multi-str': 'error', 'no-mixed-operators': 'error', 'no-mixed-spaces-and-tabs': 'error', 'no-multiple-empty-lines': [ 'error', { max: 1 } ], 'no-multi-spaces': 'error', - 'no-multi-str': 'off', 'no-negated-in-lhs': 'error', 'no-nested-ternary': 'error', 'no-redeclare': 'error', - 'no-restricted-syntax': [ - 'error', - { - selector: 'CallExpression[callee.name=/^__|_n|_x$/]:not([arguments.0.type=/^Literal|BinaryExpression$/])', - message: 'Translate function arguments must be string literals.', - }, - { - selector: 'CallExpression[callee.name=/^_n|_x$/]:not([arguments.1.type=/^Literal|BinaryExpression$/])', - message: 'Translate function arguments must be string literals.', - }, - { - selector: 'CallExpression[callee.name=_nx]:not([arguments.2.type=/^Literal|BinaryExpression$/])', - message: 'Translate function arguments must be string literals.', - }, - ], 'no-shadow': 'error', + 'no-trailing-spaces': 'error', 'no-undef': 'error', 'no-undef-init': 'error', 'no-unreachable': 'error', @@ -93,23 +50,13 @@ module.exports = { 'no-unused-vars': 'error', 'no-useless-return': 'error', 'no-whitespace-before-property': 'error', + 'no-with': 'error', 'object-curly-spacing': [ 'error', 'always' ], + 'one-var-declaration-per-line': [ 'error', 'initializations' ], + 'operator-linebreak': [ 'error', 'after' ], 'padded-blocks': [ 'error', 'never' ], - quotes: [ 'error', 'single', { allowTemplateLiterals: true, avoidEscape: true } ], 'quote-props': [ 'error', 'as-needed' ], - 'react/display-name': 'off', - 'react/jsx-curly-spacing': [ 'error', { - when: 'always', - children: true, - } ], - 'react/jsx-equals-spacing': 'error', - 'react/jsx-indent': [ 'error', 'tab' ], - 'react/jsx-indent-props': [ 'error', 'tab' ], - 'react/jsx-key': 'error', - 'react/jsx-tag-spacing': 'error', - 'react/no-children-prop': 'off', - 'react/prop-types': 'off', - 'react/react-in-jsx-scope': 'off', + quotes: [ 'error', 'single', { avoidEscape: true } ], semi: 'error', 'semi-spacing': 'error', 'space-before-blocks': [ 'error', 'always' ], @@ -119,11 +66,10 @@ module.exports = { asyncArrow: 'always', } ], 'space-in-parens': [ 'error', 'always' ], - 'space-infix-ops': [ 'error', { int32Hint: false } ], + 'space-infix-ops': 'error', 'space-unary-ops': [ 'error', { overrides: { '!': true, - yield: true, }, } ], 'valid-jsdoc': [ 'error', { @@ -151,6 +97,7 @@ module.exports = { requireReturn: false, } ], 'valid-typeof': 'error', - yoda: 'off', + 'vars-on-top': 'error', + 'wrap-iife': 'error', }, }; diff --git a/packages/eslint-plugin/configs/esnext.js b/packages/eslint-plugin/configs/esnext.js new file mode 100644 index 00000000000000..7e01f959fde72d --- /dev/null +++ b/packages/eslint-plugin/configs/esnext.js @@ -0,0 +1,36 @@ +module.exports = { + env: { + es6: true, + }, + extends: [ + require.resolve( './es5.js' ), + ], + parserOptions: { + sourceType: 'module', + }, + rules: { + // Disable ES5-specific (extended from ES5) + 'vars-on-top': 'off', + + // Enable ESNext-specific + 'arrow-parens': [ 'error', 'always' ], + 'arrow-spacing': 'error', + 'computed-property-spacing': [ 'error', 'always' ], + 'constructor-super': 'error', + 'no-const-assign': 'error', + 'no-dupe-class-members': 'error', + 'no-duplicate-imports': 'error', + 'no-useless-computed-key': 'error', + 'no-useless-constructor': 'error', + 'no-var': 'error', + 'prefer-const': 'error', + quotes: [ 'error', 'single', { allowTemplateLiterals: true, avoidEscape: true } ], + 'space-unary-ops': [ 'error', { + overrides: { + '!': true, + yield: true, + }, + } ], + 'template-curly-spacing': [ 'error', 'always' ], + }, +}; diff --git a/packages/eslint-plugin/configs/index.js b/packages/eslint-plugin/configs/index.js new file mode 100644 index 00000000000000..035c09a8fa767a --- /dev/null +++ b/packages/eslint-plugin/configs/index.js @@ -0,0 +1 @@ +module.exports = require( 'requireindex' )( __dirname ); diff --git a/packages/eslint-plugin/configs/jsx-a11y.js b/packages/eslint-plugin/configs/jsx-a11y.js new file mode 100644 index 00000000000000..38dd0ed2a3bf00 --- /dev/null +++ b/packages/eslint-plugin/configs/jsx-a11y.js @@ -0,0 +1,17 @@ +module.exports = { + extends: [ + 'plugin:jsx-a11y/recommended', + ], + plugins: [ + 'jsx-a11y', + ], + rules: { + 'jsx-a11y/label-has-for': [ 'error', { + required: 'id', + } ], + 'jsx-a11y/media-has-caption': 'off', + 'jsx-a11y/no-noninteractive-tabindex': 'off', + 'jsx-a11y/role-has-required-aria-props': 'off', + 'jsx-quotes': 'error', + }, +}; diff --git a/packages/eslint-plugin/configs/react.js b/packages/eslint-plugin/configs/react.js new file mode 100644 index 00000000000000..05c09b7e16809a --- /dev/null +++ b/packages/eslint-plugin/configs/react.js @@ -0,0 +1,28 @@ +module.exports = { + extends: [ + 'plugin:react/recommended', + ], + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + plugins: [ + 'react', + ], + rules: { + 'react/display-name': 'off', + 'react/jsx-curly-spacing': [ 'error', { + when: 'always', + children: true, + } ], + 'react/jsx-equals-spacing': 'error', + 'react/jsx-indent': [ 'error', 'tab' ], + 'react/jsx-indent-props': [ 'error', 'tab' ], + 'react/jsx-key': 'error', + 'react/jsx-tag-spacing': 'error', + 'react/no-children-prop': 'off', + 'react/prop-types': 'off', + 'react/react-in-jsx-scope': 'off', + }, +}; diff --git a/packages/eslint-plugin/configs/recommended.js b/packages/eslint-plugin/configs/recommended.js new file mode 100644 index 00000000000000..370355bd1d6ad3 --- /dev/null +++ b/packages/eslint-plugin/configs/recommended.js @@ -0,0 +1,16 @@ +module.exports = { + parser: 'babel-eslint', + extends: [ + require.resolve( './jsx-a11y.js' ), + require.resolve( './react.js' ), + require.resolve( './custom.js' ), + require.resolve( './esnext.js' ), + ], + env: { + node: true, + }, + globals: { + window: true, + document: true, + }, +}; diff --git a/packages/eslint-plugin/index.js b/packages/eslint-plugin/index.js new file mode 100644 index 00000000000000..0933aba1cc826b --- /dev/null +++ b/packages/eslint-plugin/index.js @@ -0,0 +1,3 @@ +module.exports = { + configs: require( './configs' ), +}; diff --git a/packages/eslint-config/package.json b/packages/eslint-plugin/package.json similarity index 72% rename from packages/eslint-config/package.json rename to packages/eslint-plugin/package.json index 8d5c6c93c35eee..7bce174d05838b 100644 --- a/packages/eslint-config/package.json +++ b/packages/eslint-plugin/package.json @@ -1,15 +1,14 @@ { - "name": "@wordpress/eslint-config", - "private": true, + "name": "@wordpress/eslint-plugin", "version": "1.0.0-alpha.0", - "description": "ESLint config for WordPress development.", + "description": "ESLint plugin for WordPress development.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", "keywords": [ "wordpress", "eslint" ], - "homepage": "https://github.com/WordPress/gutenberg/tree/master/packages/eslint-config/README.md", + "homepage": "https://github.com/WordPress/gutenberg/tree/master/packages/eslint-plugin/README.md", "repository": { "type": "git", "url": "https://github.com/WordPress/gutenberg.git" @@ -20,7 +19,8 @@ "dependencies": { "babel-eslint": "^8.0.3", "eslint-plugin-jsx-a11y": "6.0.2", - "eslint-plugin-react": "7.7.0" + "eslint-plugin-react": "7.7.0", + "requireindex": "^1.2.0" }, "publishConfig": { "access": "public" From 64045784bac1b29a243ba8beae33c31e887c2fe3 Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Wed, 12 Dec 2018 19:30:32 +0100 Subject: [PATCH 229/254] 4.7 (#12819) * Bump plugin version to 4.7.0 * chore(release): publish - @wordpress/annotations@1.0.4 - @wordpress/api-fetch@2.2.6 - @wordpress/block-library@2.2.10 - @wordpress/block-serialization-default-parser@2.0.2 - @wordpress/block-serialization-spec-parser@2.0.2 - @wordpress/blocks@6.0.4 - @wordpress/components@7.0.4 - @wordpress/core-data@2.0.15 - @wordpress/data@4.1.0 - @wordpress/date@3.0.1 - @wordpress/edit-post@3.1.5 - @wordpress/editor@9.0.5 - @wordpress/eslint-plugin@1.0.0 - @wordpress/format-library@1.2.8 - @wordpress/html-entities@2.0.4 - @wordpress/list-reusable-blocks@1.1.17 - @wordpress/notices@1.1.1 - @wordpress/nux@3.0.5 - @wordpress/rich-text@3.0.3 - @wordpress/url@2.3.2 - @wordpress/viewport@2.0.13 * Update changelogs after 4.7 package releases --- gutenberg.php | 2 +- package-lock.json | 2 +- package.json | 2 +- packages/annotations/CHANGELOG.md | 2 ++ packages/annotations/package.json | 2 +- packages/api-fetch/CHANGELOG.md | 2 ++ packages/api-fetch/package.json | 2 +- packages/block-library/CHANGELOG.md | 2 ++ packages/block-library/package.json | 2 +- packages/block-serialization-default-parser/CHANGELOG.md | 2 ++ packages/block-serialization-default-parser/package.json | 2 +- packages/block-serialization-spec-parser/CHANGELOG.md | 2 ++ packages/block-serialization-spec-parser/package.json | 2 +- packages/blocks/CHANGELOG.md | 2 ++ packages/blocks/package.json | 2 +- packages/components/CHANGELOG.md | 2 ++ packages/components/package.json | 2 +- packages/core-data/CHANGELOG.md | 2 ++ packages/core-data/package.json | 2 +- packages/data/CHANGELOG.md | 2 +- packages/data/package.json | 2 +- packages/date/CHANGELOG.md | 2 ++ packages/date/package.json | 2 +- packages/edit-post/CHANGELOG.md | 2 +- packages/edit-post/package.json | 2 +- packages/editor/CHANGELOG.md | 2 +- packages/editor/package.json | 2 +- packages/eslint-plugin/CHANGELOG.md | 5 +++++ packages/eslint-plugin/package.json | 2 +- packages/format-library/CHANGELOG.md | 2 ++ packages/format-library/package.json | 2 +- packages/html-entities/CHANGELOG.md | 2 ++ packages/html-entities/package.json | 3 +-- packages/list-reusable-blocks/CHANGELOG.md | 2 ++ packages/list-reusable-blocks/package.json | 2 +- packages/notices/CHANGELOG.md | 2 ++ packages/notices/package.json | 2 +- packages/nux/CHANGELOG.md | 2 ++ packages/nux/package.json | 2 +- packages/rich-text/CHANGELOG.md | 2 +- packages/rich-text/package.json | 2 +- packages/url/CHANGELOG.md | 2 ++ packages/url/package.json | 2 +- packages/viewport/CHANGELOG.md | 2 ++ packages/viewport/package.json | 2 +- 45 files changed, 65 insertions(+), 29 deletions(-) create mode 100644 packages/eslint-plugin/CHANGELOG.md diff --git a/gutenberg.php b/gutenberg.php index 9f9d7b183aeb55..62215b96d48d18 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.7.0-rc.1 + * Version: 4.7.0 * Author: Gutenberg Team * * @package gutenberg diff --git a/package-lock.json b/package-lock.json index ea80146bc3857e..1867b41831f791 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "4.7.0-rc.1", + "version": "4.7.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 6493f7d59e1d28..03a6bdd8f76163 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "4.7.0-rc.1", + "version": "4.7.0", "private": true, "description": "A new WordPress editor experience", "repository": "git+https://github.com/WordPress/gutenberg.git", diff --git a/packages/annotations/CHANGELOG.md b/packages/annotations/CHANGELOG.md index edceba997fe675..9d8994d0b48610 100644 --- a/packages/annotations/CHANGELOG.md +++ b/packages/annotations/CHANGELOG.md @@ -1,3 +1,5 @@ +## 1.0.4 (2018-12-12) + ## 1.0.3 (2018-11-21) ## 1.0.2 (2018-11-20) diff --git a/packages/annotations/package.json b/packages/annotations/package.json index 5a73fb5e31ae49..3e909c7a0852c0 100644 --- a/packages/annotations/package.json +++ b/packages/annotations/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/annotations", - "version": "1.0.3", + "version": "1.0.4", "description": "Annotate content in the Gutenberg editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/api-fetch/CHANGELOG.md b/packages/api-fetch/CHANGELOG.md index 912d833f3b909e..b4c58ed5d9e1a9 100644 --- a/packages/api-fetch/CHANGELOG.md +++ b/packages/api-fetch/CHANGELOG.md @@ -1,3 +1,5 @@ +## 2.2.6 (2018-12-12) + ## 2.2.5 (2018-11-20) ## 2.2.4 (2018-11-15) diff --git a/packages/api-fetch/package.json b/packages/api-fetch/package.json index 3cb6fb3c22979d..0c3ab7e8e9bbc6 100644 --- a/packages/api-fetch/package.json +++ b/packages/api-fetch/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/api-fetch", - "version": "2.2.5", + "version": "2.2.6", "description": "Utility to make WordPress REST API requests.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/block-library/CHANGELOG.md b/packages/block-library/CHANGELOG.md index e27ca871be3848..f784c887fb110c 100644 --- a/packages/block-library/CHANGELOG.md +++ b/packages/block-library/CHANGELOG.md @@ -1,3 +1,5 @@ +## 2.2.10 (2018-12-12) + ## 2.2.9 (2018-11-30) ## 2.2.8 (2018-11-30) diff --git a/packages/block-library/package.json b/packages/block-library/package.json index 5d3e510bbfe93c..3f81dcf5dfd3df 100644 --- a/packages/block-library/package.json +++ b/packages/block-library/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-library", - "version": "2.2.9", + "version": "2.2.10", "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/CHANGELOG.md b/packages/block-serialization-default-parser/CHANGELOG.md index 6c4f4c53b06453..aaa75a2df97c0d 100644 --- a/packages/block-serialization-default-parser/CHANGELOG.md +++ b/packages/block-serialization-default-parser/CHANGELOG.md @@ -1,3 +1,5 @@ +## 2.0.2 (2018-12-12) + ## 2.0.1 (2018-11-30) ## 2.0.0 (2018-11-12) diff --git a/packages/block-serialization-default-parser/package.json b/packages/block-serialization-default-parser/package.json index 8cf5725dfd2d2d..b5482f3d1efd42 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": "2.0.1", + "version": "2.0.2", "description": "Block serialization specification parser for WordPress posts.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/block-serialization-spec-parser/CHANGELOG.md b/packages/block-serialization-spec-parser/CHANGELOG.md index 12ffab6521d47e..c2db52c417908f 100644 --- a/packages/block-serialization-spec-parser/CHANGELOG.md +++ b/packages/block-serialization-spec-parser/CHANGELOG.md @@ -1,3 +1,5 @@ +## 2.0.2 (2018-12-12) + ## 2.0.1 (2018-11-30) ## 2.0.0 (2018-11-12) diff --git a/packages/block-serialization-spec-parser/package.json b/packages/block-serialization-spec-parser/package.json index 7739f0bf041774..d7bafbb41539ce 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": "2.0.1", + "version": "2.0.2", "description": "Block serialization specification parser for WordPress posts.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/blocks/CHANGELOG.md b/packages/blocks/CHANGELOG.md index 33ca7db882fbd1..e268a8260c14ff 100644 --- a/packages/blocks/CHANGELOG.md +++ b/packages/blocks/CHANGELOG.md @@ -1,3 +1,5 @@ +## 6.0.4 (2018-12-12) + ## 6.0.3 (2018-11-30) ## 6.0.2 (2018-11-21) diff --git a/packages/blocks/package.json b/packages/blocks/package.json index af7151b58d8065..afe971954c18ca 100644 --- a/packages/blocks/package.json +++ b/packages/blocks/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/blocks", - "version": "6.0.3", + "version": "6.0.4", "description": "Block API for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index fb93a60cd4712d..8dfd644e15ea0c 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -1,3 +1,5 @@ +## 7.0.4 (2018-12-12) + ## 7.0.3 (2018-11-30) ## 7.0.2 (2018-11-22) diff --git a/packages/components/package.json b/packages/components/package.json index 9ad1244df0dffa..2425f1bf79dcbc 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/components", - "version": "7.0.3", + "version": "7.0.4", "description": "UI components for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/core-data/CHANGELOG.md b/packages/core-data/CHANGELOG.md index 98ed42a5473f08..3594be36a67ec8 100644 --- a/packages/core-data/CHANGELOG.md +++ b/packages/core-data/CHANGELOG.md @@ -1,3 +1,5 @@ +## 2.0.15 (2018-12-12) + ## 2.0.14 (2018-11-20) ## 2.0.13 (2018-11-15) diff --git a/packages/core-data/package.json b/packages/core-data/package.json index 44310994619003..316ac6907f9abc 100644 --- a/packages/core-data/package.json +++ b/packages/core-data/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/core-data", - "version": "2.0.14", + "version": "2.0.15", "description": "Access to and manipulation of core WordPress entities.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/data/CHANGELOG.md b/packages/data/CHANGELOG.md index d496bb6162e239..8b90a5d7c526ae 100644 --- a/packages/data/CHANGELOG.md +++ b/packages/data/CHANGELOG.md @@ -1,4 +1,4 @@ -## 4.1.0 (Unreleased) +## 4.1.0 (2018-12-12) ### New Feature diff --git a/packages/data/package.json b/packages/data/package.json index a547a316872d2c..825ddb8f71295a 100644 --- a/packages/data/package.json +++ b/packages/data/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/data", - "version": "4.0.1", + "version": "4.1.0", "description": "Data module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/date/CHANGELOG.md b/packages/date/CHANGELOG.md index e6aa964c70518d..54bee67c917a9a 100644 --- a/packages/date/CHANGELOG.md +++ b/packages/date/CHANGELOG.md @@ -1,3 +1,5 @@ +## 3.0.1 (2018-12-12) + ## 3.0.0 (2018-11-15) ### Breaking Changes diff --git a/packages/date/package.json b/packages/date/package.json index 847cdf2762ad44..7378306fff28ce 100644 --- a/packages/date/package.json +++ b/packages/date/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/date", - "version": "3.0.0", + "version": "3.0.1", "description": "Date module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/edit-post/CHANGELOG.md b/packages/edit-post/CHANGELOG.md index e4b0c1ad32c680..092061f4501efd 100644 --- a/packages/edit-post/CHANGELOG.md +++ b/packages/edit-post/CHANGELOG.md @@ -1,4 +1,4 @@ -## 3.1.5 (Unreleased) +## 3.1.5 (2018-12-12) ### Bug Fixes - Fix saving WYSIWYG Meta Boxes diff --git a/packages/edit-post/package.json b/packages/edit-post/package.json index d8d3f4802aa391..0e0e4d008fd956 100644 --- a/packages/edit-post/package.json +++ b/packages/edit-post/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/edit-post", - "version": "3.1.4", + "version": "3.1.5", "description": "Edit Post module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/editor/CHANGELOG.md b/packages/editor/CHANGELOG.md index 69dbe834a4aeb1..e06655f6738b6e 100644 --- a/packages/editor/CHANGELOG.md +++ b/packages/editor/CHANGELOG.md @@ -1,4 +1,4 @@ -## 9.0.5 (Unreleased) +## 9.0.5 (2018-12-12) ### Bug Fixes diff --git a/packages/editor/package.json b/packages/editor/package.json index 2f2676ee36853a..6850b40372ae28 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/editor", - "version": "9.0.4", + "version": "9.0.5", "description": "Building blocks for WordPress editors.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/eslint-plugin/CHANGELOG.md b/packages/eslint-plugin/CHANGELOG.md new file mode 100644 index 00000000000000..3d6a36751ddd1d --- /dev/null +++ b/packages/eslint-plugin/CHANGELOG.md @@ -0,0 +1,5 @@ +## 1.0.0 (2018-12-12) + +### New Features + +- Initial release. diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index 7bce174d05838b..13fbae86bfd68d 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/eslint-plugin", - "version": "1.0.0-alpha.0", + "version": "1.0.0", "description": "ESLint plugin for WordPress development.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/format-library/CHANGELOG.md b/packages/format-library/CHANGELOG.md index cab02a21043113..730914fc9ebd85 100644 --- a/packages/format-library/CHANGELOG.md +++ b/packages/format-library/CHANGELOG.md @@ -1,3 +1,5 @@ +## 1.2.8 (2018-12-12) + ## 1.2.7 (2018-11-30) ## 1.2.6 (2018-11-30) diff --git a/packages/format-library/package.json b/packages/format-library/package.json index 9c78a6ba2eedc9..bd41d3742ddbca 100644 --- a/packages/format-library/package.json +++ b/packages/format-library/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/format-library", - "version": "1.2.7", + "version": "1.2.8", "description": "Format library for the WordPress editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/html-entities/CHANGELOG.md b/packages/html-entities/CHANGELOG.md index bc270815c38c88..f9355f9f968653 100644 --- a/packages/html-entities/CHANGELOG.md +++ b/packages/html-entities/CHANGELOG.md @@ -1,3 +1,5 @@ +## 2.0.2 (2018-12-12) + ## 2.0.1 (2018-11-21) ## 2.0.0 (2018-09-05) diff --git a/packages/html-entities/package.json b/packages/html-entities/package.json index db944aa08cffe5..94fccaa33295c4 100644 --- a/packages/html-entities/package.json +++ b/packages/html-entities/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/html-entities", - "version": "2.0.3", + "version": "2.0.4", "description": "HTML entity utilities for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", @@ -20,7 +20,6 @@ }, "main": "build/index.js", "module": "build-module/index.js", - "react-native": "src/index", "dependencies": { "@babel/runtime": "^7.0.0" }, diff --git a/packages/list-reusable-blocks/CHANGELOG.md b/packages/list-reusable-blocks/CHANGELOG.md index 8e8526a7f98e62..dd73de16abca40 100644 --- a/packages/list-reusable-blocks/CHANGELOG.md +++ b/packages/list-reusable-blocks/CHANGELOG.md @@ -1,3 +1,5 @@ +## 1.1.17 (2018-12-12) + ## 1.1.16 (2018-11-30) ## 1.1.15 (2018-11-22) diff --git a/packages/list-reusable-blocks/package.json b/packages/list-reusable-blocks/package.json index 8c455e8a458125..2ddb7b4afdf532 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.16", + "version": "1.1.17", "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/CHANGELOG.md b/packages/notices/CHANGELOG.md index 3699727ec708e7..76f6ad9cadaaa7 100644 --- a/packages/notices/CHANGELOG.md +++ b/packages/notices/CHANGELOG.md @@ -1,3 +1,5 @@ +## 1.1.1 (2018-12-12) + ## 1.1.0 (2018-11-20) ### New Feature diff --git a/packages/notices/package.json b/packages/notices/package.json index 5e642a6d7f273a..c7b0b8c623294b 100644 --- a/packages/notices/package.json +++ b/packages/notices/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/notices", - "version": "1.1.0", + "version": "1.1.1", "description": "State management for notices.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/nux/CHANGELOG.md b/packages/nux/CHANGELOG.md index ffe9ba7ca937ec..41aaa82bc4ced1 100644 --- a/packages/nux/CHANGELOG.md +++ b/packages/nux/CHANGELOG.md @@ -1,3 +1,5 @@ +## 3.0.5 (2018-12-12) + ## 3.0.4 (2018-11-30) ## 3.0.3 (2018-11-22) diff --git a/packages/nux/package.json b/packages/nux/package.json index 28dd725e40fae7..c9db7b58c0fe0f 100644 --- a/packages/nux/package.json +++ b/packages/nux/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/nux", - "version": "3.0.4", + "version": "3.0.5", "description": "NUX (New User eXperience) module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/rich-text/CHANGELOG.md b/packages/rich-text/CHANGELOG.md index aadcae6d607998..38b48d8af87661 100644 --- a/packages/rich-text/CHANGELOG.md +++ b/packages/rich-text/CHANGELOG.md @@ -1,4 +1,4 @@ -## 3.0.3 (Unreleased) +## 3.0.3 (2018-12-12) ### Internal diff --git a/packages/rich-text/package.json b/packages/rich-text/package.json index 4ae221a90812f3..18b5352a78a915 100644 --- a/packages/rich-text/package.json +++ b/packages/rich-text/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/rich-text", - "version": "3.0.2", + "version": "3.0.3", "description": "Rich text value and manipulation API.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/url/CHANGELOG.md b/packages/url/CHANGELOG.md index 984fb0428eb281..1b5f99789bbf80 100644 --- a/packages/url/CHANGELOG.md +++ b/packages/url/CHANGELOG.md @@ -1,3 +1,5 @@ +## 2.3.2 (2018-12-12) + ## 2.3.1 (2018-11-20) ### Bug fixes diff --git a/packages/url/package.json b/packages/url/package.json index 197f2726a64136..43b919a19eb785 100644 --- a/packages/url/package.json +++ b/packages/url/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/url", - "version": "2.3.1", + "version": "2.3.2", "description": "WordPress URL utilities.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/viewport/CHANGELOG.md b/packages/viewport/CHANGELOG.md index 0dd9a1369466c0..62418c83073504 100644 --- a/packages/viewport/CHANGELOG.md +++ b/packages/viewport/CHANGELOG.md @@ -1,3 +1,5 @@ +## 2.0.13 (2018-12-12) + ## 2.0.12 (2018-11-20) ## 2.0.11 (2018-11-15) diff --git a/packages/viewport/package.json b/packages/viewport/package.json index 527fca472cd3dc..e74015bd91349a 100644 --- a/packages/viewport/package.json +++ b/packages/viewport/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/viewport", - "version": "2.0.12", + "version": "2.0.13", "description": "Viewport module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", From ff0ca733a8ed862f19e6557d6702237a00a171bf Mon Sep 17 00:00:00 2001 From: Marcus Kazmierczak <marcus@mkaz.com> Date: Thu, 13 Dec 2018 10:36:26 -0800 Subject: [PATCH 230/254] Add a line to PR template for developer documentation (#12822) * Add a line to PR template for developer documentation Developer documentation is critical for making a platform extendable, developers need to know the proper way to extend a feature, or in a sense that does may not even exist. * Update .github/PULL_REQUEST_TEMPLATE.md Co-Authored-By: mkaz <marcus@mkaz.com> --- .github/PULL_REQUEST_TEMPLATE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index a968800c43fed1..cb60c1c2f0354d 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -19,3 +19,4 @@ - [ ] My code follows the WordPress code style. <!-- Check code: `npm run lint`, Guidelines: https://make.wordpress.org/core/handbook/best-practices/coding-standards/javascript/ --> - [ ] My code follows the accessibility standards. <!-- Guidelines: https://make.wordpress.org/core/handbook/best-practices/coding-standards/accessibility-coding-standards/ --> - [ ] My code has proper inline documentation. <!-- Guidelines: https://make.wordpress.org/core/handbook/best-practices/inline-documentation-standards/javascript/ --> +- [ ] I've included developer documentation if appropriate. <!-- Handbook: https://wordpress.org/gutenberg/handbook/designers-developers/ --> From c7afa348457e63a21ceaf7853b3f96ce71ee124f Mon Sep 17 00:00:00 2001 From: Faisal Alvi <faisalalvi33@gmail.com> Date: Fri, 14 Dec 2018 14:47:23 +0530 Subject: [PATCH 231/254] Grammatical mistakes & Missing spaces (#12643) * Grammatical mistakes & Missing spaces Fixing grammatical mistakes & adding the required space between 2 lines. * Update docs/designers-developers/developers/filters/block-filters.md Co-Authored-By: faisal-alvi <faisalalvi33@gmail.com> * Update docs/designers-developers/developers/filters/block-filters.md Co-Authored-By: faisal-alvi <faisalalvi33@gmail.com> --- .../developers/filters/block-filters.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/designers-developers/developers/filters/block-filters.md b/docs/designers-developers/developers/filters/block-filters.md index a8df36979dd1e0..bfbd33936df38c 100644 --- a/docs/designers-developers/developers/filters/block-filters.md +++ b/docs/designers-developers/developers/filters/block-filters.md @@ -115,7 +115,7 @@ wp.hooks.addFilter( #### `blocks.switchToBlockType.transformedBlock` -Used to filters an individual transform result from block transformation. All of the original blocks are passed, since transformations are many-to-many, not one-to-one. +Used to filter an individual transform result from block transformation. All of the original blocks are passed since transformations are many-to-many, not one-to-one. #### `blocks.getBlockAttributes` @@ -322,9 +322,9 @@ function my_plugin_block_categories( $categories, $post ) { add_filter( 'block_categories', 'my_plugin_block_categories', 10, 2 ); ``` -You can also display an icon with your block category by setting an `icon` attribute.The value can be the slug of a [WordPress Dashicon](https://developer.wordpress.org/resource/dashicons/). +You can also display an icon with your block category by setting an `icon` attribute. The value can be the slug of a [WordPress Dashicon](https://developer.wordpress.org/resource/dashicons/). -It is possible to set an SVG as the icon of the category if a custom icon is needed.To do so, the icon should be rendered and set on the frontend, so it can make use of WordPress SVG, allowing mobile compatibility and making the icon more accessible. +It is possible to set an SVG as the icon of the category if a custom icon is needed. To do so, the icon should be rendered and set on the frontend, so it can make use of WordPress SVG, allowing mobile compatibility and making the icon more accessible. To set an SVG icon for the category shown in the previous example, add the following example JavaScript code to the editor calling `wp.blocks.updateCategory` e.g: ```js From 1ac6ee74263cfa59fd45a74989b791697bf8cbe8 Mon Sep 17 00:00:00 2001 From: ramizmanked <ramiz.manked@gmail.com> Date: Fri, 14 Dec 2018 14:48:24 +0530 Subject: [PATCH 232/254] Some content format issues (#12636) I've noticed some content formatting issues while going through this page. Hence, I'm approaching some suggestion from my side. --- packages/editor/README.md | 28 ++++++---------------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/packages/editor/README.md b/packages/editor/README.md index 56040081b11e69..0b908871df924e 100644 --- a/packages/editor/README.md +++ b/packages/editor/README.md @@ -28,15 +28,11 @@ Individual blocks are handled by the `VisualBlock` component, which attaches eve ## Components -Because many blocks share the same complex behaviors, reusable components -are made available to simplify implementations of your block's `edit` function. +Because many blocks share the same complex behaviors, reusable components are made available to simplify implementations of your block's `edit` function. ### `BlockControls` -When returned by your block's `edit` implementation, renders a toolbar of icon -buttons. This is useful for block-level modifications to be made available when -a block is selected. For example, if your block supports alignment, you may -want to display alignment options in the selected block's toolbar. +When returned by your block's `edit` implementation, renders a toolbar of icon buttons. This is useful for block-level modifications to be made available when a block is selected. For example, if your block supports alignment, you may want to display alignment options in the selected block's toolbar. Example: @@ -70,32 +66,20 @@ Example: ); ``` -Note in this example that we render `AlignmentToolbar` as a child of the -`BlockControls` element. This is another pre-configured component you can use -to simplify block text alignment. +Note in this example that we render `AlignmentToolbar` as a child of the `BlockControls` element. This is another pre-configured component you can use to simplify block text alignment. -Alternatively, you can create your own toolbar controls by passing an array of -`controls` as a prop to the `BlockControls` component. Each control should be -an object with the following properties: +Alternatively, you can create your own toolbar controls by passing an array of `controls` as a prop to the `BlockControls` component. Each control should be an object with the following properties: - `icon: string` - Slug of the Dashicon to be shown in the control's toolbar button - `title: string` - A human-readable localized text to be shown as the tooltip label of the control's button - `subscript: ?string` - Optional text to be shown adjacent the button icon as subscript (for example, heading levels) - `isActive: ?boolean` - Whether the control should be considered active / selected. Defaults to `false`. -To create divisions between sets of controls within the same `BlockControls` -element, passing `controls` instead as a nested array (array of arrays of -objects). A divider will be shown between each set of controls. +To create divisions between sets of controls within the same `BlockControls` element, passing `controls` instead as a nested array (array of arrays of objects). A divider will be shown between each set of controls. ### `RichText` -Render a rich -[`contenteditable` input](https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Editable_content), -providing users the option to add emphasis to content or links to content. It -behaves similarly to a -[controlled component](https://facebook.github.io/react/docs/forms.html#controlled-components), -except that `onChange` is triggered less frequently than would be expected from -a traditional `input` field, usually when the user exits the field. +Render a rich [`contenteditable` input](https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Editable_content), providing users the option to add emphasis to content or links to content. It behaves similarly to a [controlled component](https://facebook.github.io/react/docs/forms.html#controlled-components), except that `onChange` is triggered less frequently than would be expected from a traditional `input` field, usually when the user exits the field. The following properties (non-exhaustive list) are made available: From a617aeefb8359a5a12ca20ca3ef00681d1f814e1 Mon Sep 17 00:00:00 2001 From: Marcus Kazmierczak <marcus@mkaz.com> Date: Fri, 14 Dec 2018 01:18:51 -0800 Subject: [PATCH 233/254] Fix incorrect value for __back_compat_meta_box (#12776) --- .../developers/backward-compatibility/meta-box.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/designers-developers/developers/backward-compatibility/meta-box.md b/docs/designers-developers/developers/backward-compatibility/meta-box.md index 8e722b4cb31565..5403ebf674ac31 100644 --- a/docs/designers-developers/developers/backward-compatibility/meta-box.md +++ b/docs/designers-developers/developers/backward-compatibility/meta-box.md @@ -27,7 +27,7 @@ After a meta box is converted to a block, it can be declared as existing for bac add_meta_box( 'my-meta-box', 'My Meta Box', 'my_meta_box_callback', null, 'normal', 'high', array( - '__back_compat_meta_box' => false, + '__back_compat_meta_box' => true, ) ); ``` From 068bb76f4004d8a18c7fae512add230d9c86916b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Wrede?= <soerenwrede@gmail.com> Date: Sun, 16 Dec 2018 20:58:51 +0100 Subject: [PATCH 234/254] Fix Typo (#12902) --- docs/designers-developers/developers/filters/block-filters.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/designers-developers/developers/filters/block-filters.md b/docs/designers-developers/developers/filters/block-filters.md index bfbd33936df38c..3214fa1ba2da51 100644 --- a/docs/designers-developers/developers/filters/block-filters.md +++ b/docs/designers-developers/developers/filters/block-filters.md @@ -200,7 +200,7 @@ var withClientIdClassName = wp.compose.createHigherOrderComponent( function( Blo {}, props, { - classsName: "block-" + props.clientId, + className: "block-" + props.clientId, } ); @@ -335,5 +335,5 @@ To set an SVG icon for the category shown in the previous example, add the follo var svgIcon = el( SVG, { width: 20, height: 20, viewBox: '0 0 20 20'}, circle); wp.blocks.updateCategory( 'my-category', { icon: svgIcon } ); } )(); -``` +``` From 797c5f330d7437d66497209da1e1996ef9011f7c Mon Sep 17 00:00:00 2001 From: Naoki Ohashi <n.globe.us@gmail.com> Date: Mon, 17 Dec 2018 04:59:29 +0900 Subject: [PATCH 235/254] fix typo (#12905) --- docs/designers-developers/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/designers-developers/readme.md b/docs/designers-developers/readme.md index 5e54798a3c7873..107bad052f309b 100644 --- a/docs/designers-developers/readme.md +++ b/docs/designers-developers/readme.md @@ -6,6 +6,6 @@ Gutenberg is a transformation of the WordPress editor for working with content. Using a system of Blocks to compose and format content, the new block-based editor is designed to create rich, flexible layouts for websites and digital products. Content is created in the unit of blocks instead of freeform text with inserted media, embeds and Shortcodes (there's a Shortcode block though). -Blocks treat Paragraphs, Headings, Media, Embeds all as components that strung together make up the content stored in the WordPress database, replacing the traditional concept of freeform text with embeded media and shortcodes. The new editor is designed with progressive enhancement, meaning it is back-compatible with all legacy content, offers a process to try to convert and split a Classic block into block equivalents using client-side parsing and finally the blocks offer enhanced editing and format controls. +Blocks treat Paragraphs, Headings, Media, Embeds all as components that strung together make up the content stored in the WordPress database, replacing the traditional concept of freeform text with embedded media and shortcodes. The new editor is designed with progressive enhancement, meaning it is back-compatible with all legacy content, offers a process to try to convert and split a Classic block into block equivalents using client-side parsing and finally the blocks offer enhanced editing and format controls. The Editor offers rich new value to users with visual, drag-and-drop creation tools and powerful developer enhancements with modern vendor packages, reusable components, rich APIs and hooks to modify and extend the editor through Custom Blocks, Custom Block Styles and Plugins. From b1c17a6a06b63aa42ec8f36da9a6b5f1923405d7 Mon Sep 17 00:00:00 2001 From: Naoki Ohashi <n.globe.us@gmail.com> Date: Mon, 17 Dec 2018 05:00:56 +0900 Subject: [PATCH 236/254] Fix Typo (#12918) --- docs/designers-developers/key-concepts.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/designers-developers/key-concepts.md b/docs/designers-developers/key-concepts.md index ce270c0d2ee816..6ea644009b145d 100644 --- a/docs/designers-developers/key-concepts.md +++ b/docs/designers-developers/key-concepts.md @@ -4,9 +4,9 @@ Blocks are an abstract unit for organizing and composing content, strung together to create content for a webpage. -Blocks are hiearchical, in that a block can be a child or parent to another block. One example is a two-column Columns block can be the parent block to multiple child blocks in each column. +Blocks are hierarchical, in that a block can be a child or parent to another block. One example is a two-column Columns block can be the parent block to multiple child blocks in each column. -If it helps, you can think of blocks as a more graceful shortcode, with rich formatting tools for users to compose content. To this point, there is a new Block Grammar. Distilled, the block grammar is an HTML comment, either a self-closing tag or with a begining tag and ending tag. In the main tag, depending on the block type and user customizations, there can be a JSON object. This raw form of the block is referred to as serialized. +If it helps, you can think of blocks as a more graceful shortcode, with rich formatting tools for users to compose content. To this point, there is a new Block Grammar. Distilled, the block grammar is an HTML comment, either a self-closing tag or with a beginning tag and ending tag. In the main tag, depending on the block type and user customizations, there can be a JSON object. This raw form of the block is referred to as serialized. ```html <!-- wp:paragraph {"key": "value"} --> @@ -16,7 +16,7 @@ If it helps, you can think of blocks as a more graceful shortcode, with rich for Blocks can be static or dynamic. Static blocks contain rendered content and an object of Attributes used to re-render based on changes. Dynamic blocks require server-side data and rendering while the post content is being generated (rendering). -Each block contains Attributes or configuration settings, which can be sourced from raw HTML in the content, via meta or other customizible origins. +Each block contains Attributes or configuration settings, which can be sourced from raw HTML in the content, via meta or other customizable origins. The Paragraph is the default Block. Instead of a new line upon typing return on a keyboard, try to think of it as an empty paragraph block (type / to trigger an autocompleting Slash Inserter -- /image will pull up Images as well as Instagram embeds). From 6b82b6c86591b280c9aa24a45a9386ae5059e569 Mon Sep 17 00:00:00 2001 From: Koji Kuno <koji.kuno@gmail.com> Date: Mon, 17 Dec 2018 14:27:47 +0900 Subject: [PATCH 237/254] fixed typo (#12936) Changed from 'moreso than' to 'more so than'. ## Description <!-- Please describe what you have changed or added --> ## How has this been tested? <!-- Please describe in detail how you tested your changes. --> <!-- Include details of your testing environment, tests ran to see how --> <!-- your change affects other areas of the code, etc. --> ## Screenshots <!-- if applicable --> ## Types of changes <!-- What types of changes does your code introduce? --> <!-- Bug fix (non-breaking change which fixes an issue) --> <!-- New feature (non-breaking change which adds functionality) --> <!-- Breaking change (fix or feature that would cause existing functionality to not work as expected) --> ## Checklist: - [ ] My code is tested. - [ ] My code follows the WordPress code style. <!-- Check code: `npm run lint`, Guidelines: https://make.wordpress.org/core/handbook/best-practices/coding-standards/javascript/ --> - [ ] My code follows the accessibility standards. <!-- Guidelines: https://make.wordpress.org/core/handbook/best-practices/coding-standards/accessibility-coding-standards/ --> - [ ] My code has proper inline documentation. <!-- Guidelines: https://make.wordpress.org/core/handbook/best-practices/inline-documentation-standards/javascript/ --> - [ ] I've included developer documentation if appropriate. <!-- Handbook: https://wordpress.org/gutenberg/handbook/designers-developers/ --> --- docs/contributors/coding-guidelines.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/contributors/coding-guidelines.md b/docs/contributors/coding-guidelines.md index 8264e765231048..c47fda1ab070a5 100644 --- a/docs/contributors/coding-guidelines.md +++ b/docs/contributors/coding-guidelines.md @@ -170,7 +170,7 @@ function MyComponent() {} An exception to camel case is made for constant values which are never intended to be reassigned or mutated. Such variables must use the [SCREAMING_SNAKE_CASE convention](https://en.wikipedia.org/wiki/Snake_case). -In almost all cases, a constant should be defined in the top-most scope of a file. It is important to note that [JavaScript's `const` assignment](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/const) is conceptually more limited than what is implied here, where a value assigned by `const` in JavaScript can in-fact be mutated, and is only protected against reassignment. A constant as defined in these coding guidelines applies only to values which are expected to never change, and is a strategy for developers to communicate intent moreso than it is a technical restriction. +In almost all cases, a constant should be defined in the top-most scope of a file. It is important to note that [JavaScript's `const` assignment](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/const) is conceptually more limited than what is implied here, where a value assigned by `const` in JavaScript can in-fact be mutated, and is only protected against reassignment. A constant as defined in these coding guidelines applies only to values which are expected to never change, and is a strategy for developers to communicate intent more so than it is a technical restriction. ### Strings From e1092c0d0b75fe53ab57bc6c4cc9e32cb2e74e40 Mon Sep 17 00:00:00 2001 From: Koji Kuno <koji.kuno@gmail.com> Date: Mon, 17 Dec 2018 14:28:09 +0900 Subject: [PATCH 238/254] Fix typo (#12937) Fixed typo from 'comma separated' to 'comma-separated'. --- docs/contributors/design.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/contributors/design.md b/docs/contributors/design.md index aefba0d0eb3d4f..7b037fd3a4c946 100644 --- a/docs/contributors/design.md +++ b/docs/contributors/design.md @@ -46,7 +46,7 @@ Gutenberg wants to make it easier to author rich content. This means ensuring go The initial phase of Gutenberg as described in the kickoff goal is primarily limited to the content area (specifically `post_content`) of posts and pages. Within those confines, we are embracing the web as a vertical river of content by appending blocks sequentially, then adding layout options to each block. -That said, there isn’t any fixed limit to the kind of layouts Gutenberg will be able to create. It’s very possible for Gutenberg to grow beyond the confines of post and page content, to include the whole page — one could think of a theme template as a comma separated list of blocks, like this: +That said, there isn’t any fixed limit to the kind of layouts Gutenberg will be able to create. It’s very possible for Gutenberg to grow beyond the confines of post and page content, to include the whole page — one could think of a theme template as a comma-separated list of blocks, like this: ```js { From 6390264a47dcddc62efadb25cb4ee6e6735b72f4 Mon Sep 17 00:00:00 2001 From: Marcus Kazmierczak <marcus@mkaz.com> Date: Mon, 17 Dec 2018 06:52:34 -0800 Subject: [PATCH 239/254] Add content to Contributors index page (#12910) * Add content to Contributors index page When visiting the Gutenberg Handbook the Contributors index page is blank. See: https://wordpress.org/gutenberg/handbook/contributors/ This PR adds some basic content and links to find more, including linking back to the repo's contributing document. * Remove newlines, update links --- docs/contributors/readme.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/contributors/readme.md b/docs/contributors/readme.md index e69de29bb2d1d6..1feaadfcbf5b9b 100644 --- a/docs/contributors/readme.md +++ b/docs/contributors/readme.md @@ -0,0 +1,11 @@ +# Contributors Guide + +Welcome to the Gutenberg Project Contributors Guide. + +The following guidelines are in place to create consistency across the project and the numerous contributors. See also the [Contributing Documentation](https://github.com/WordPress/gutenberg/blob/master/CONTRIBUTING.md) for technical details around setup, and submitting your contributions. + +* [Coding Guidelines](../../docs/contributors/coding-guidelines.md) outline additional patterns and conventions used in the Gutenberg project. +* [Copy Guidelines](../../docs/contributors/copy-guide.md) +* [Design Principles & Vision](../../docs/contributors/design.md) + +Please see the table of contents on the left side of the Gutenberg Handbook for the full list of contributor resources. From a15545474c798805bae3d98a8780163a1318654e Mon Sep 17 00:00:00 2001 From: Marcus Kazmierczak <marcus@mkaz.com> Date: Mon, 17 Dec 2018 06:53:14 -0800 Subject: [PATCH 240/254] Getting Started with JavaScript (#12689) * Getting Started with JavaScript * Reorder add_action/function * Update prefix to use myguten, instead of just my * Update docs/designers-developers/developers/tutorials/javascript/readme.md Co-Authored-By: mkaz <marcus@mkaz.com> * Update to block_editor_assets * Updates per danielbachhuber review. * Updates, and broken up into sections * Add Troubleshooting * Updates with suggestion. Props to @nosolosw * Update docs/designers-developers/developers/tutorials/javascript/versions-and-building.md Co-Authored-By: mkaz <marcus@mkaz.com> * Update docs/designers-developers/developers/tutorials/javascript/versions-and-building.md Co-Authored-By: mkaz <marcus@mkaz.com> * Add screenshot with style * Reorder TOC, move versions to end * Update link for browser support * Add check for dependency to troubleshooting section * General edits for spelling and flow * Update Table of Contents and generated manifest * Fix up header tags consistency and spaces * Edits per @chrisvanpatten review * Update docs/designers-developers/developers/tutorials/javascript/extending-the-block-editor.md Co-Authored-By: mkaz <marcus@mkaz.com> * Update docs/designers-developers/developers/tutorials/javascript/loading-javascript.md Co-Authored-By: mkaz <marcus@mkaz.com> * Update docs/designers-developers/developers/tutorials/javascript/loading-javascript.md Co-Authored-By: mkaz <marcus@mkaz.com> * Update docs/designers-developers/developers/tutorials/javascript/loading-javascript.md Co-Authored-By: mkaz <marcus@mkaz.com> * Update docs/designers-developers/developers/tutorials/javascript/plugins-background.md Co-Authored-By: mkaz <marcus@mkaz.com> * Update docs/designers-developers/developers/tutorials/javascript/plugins-background.md Co-Authored-By: mkaz <marcus@mkaz.com> * Move screenshots to assets directory, add additional screenshot for success * Move block style variant to assets * Formating * Update images to full URLs * Fix titles --- .../assets/fancy-quote-in-inspector.png | Bin 0 -> 44857 bytes .../assets/fancy-quote-with-style.png | Bin 0 -> 42284 bytes .../assets/js-tutorial-console-log-error.png | Bin 0 -> 33443 bytes .../js-tutorial-console-log-success.png | Bin 0 -> 24046 bytes .../js-tutorial-error-blocks-undefined.png | Bin 0 -> 6986 bytes .../javascript/extending-the-block-editor.md | 66 ++++++++++++++++++ .../javascript/loading-javascript.md | 49 +++++++++++++ .../javascript/plugins-background.md | 16 +++++ .../developers/tutorials/javascript/readme.md | 18 +++++ .../tutorials/javascript/troubleshooting.md | 34 +++++++++ .../javascript/versions-and-building.md | 11 +++ .../developers/tutorials/readme.md | 5 +- docs/manifest.json | 36 ++++++++++ docs/toc.json | 7 ++ 14 files changed, 241 insertions(+), 1 deletion(-) create mode 100644 docs/designers-developers/assets/fancy-quote-in-inspector.png create mode 100644 docs/designers-developers/assets/fancy-quote-with-style.png create mode 100644 docs/designers-developers/assets/js-tutorial-console-log-error.png create mode 100644 docs/designers-developers/assets/js-tutorial-console-log-success.png create mode 100644 docs/designers-developers/assets/js-tutorial-error-blocks-undefined.png create mode 100644 docs/designers-developers/developers/tutorials/javascript/extending-the-block-editor.md create mode 100644 docs/designers-developers/developers/tutorials/javascript/loading-javascript.md create mode 100644 docs/designers-developers/developers/tutorials/javascript/plugins-background.md create mode 100644 docs/designers-developers/developers/tutorials/javascript/readme.md create mode 100644 docs/designers-developers/developers/tutorials/javascript/troubleshooting.md create mode 100644 docs/designers-developers/developers/tutorials/javascript/versions-and-building.md diff --git a/docs/designers-developers/assets/fancy-quote-in-inspector.png b/docs/designers-developers/assets/fancy-quote-in-inspector.png new file mode 100644 index 0000000000000000000000000000000000000000..6bd8c06a9e3979801ec080c74af798ff7d93d031 GIT binary patch literal 44857 zcmYg%1z1#1)c1m-QlcUu9RgBIcXuNo4GStIEwFTpG}0;EB}+(N8U=(!x?$<qB^D$^ z`dj|*`+d*1&+M~v&&>IqGiT1s-gEBUNG(kzLOcpQ5C}x5qAafi0zHI+KscaB_b`-* zlcX)okFA!vuENO32*WE*Fu&Z|#<n{h;~R*zhmT)yWMXz<xt+6jWsJun@TCe?k?UZ5 za{Bk?YV-2XAgEvltL+fvT8(Xy2y}LW!aC39a)UY%Z%W3OD<GZ7vt}DYa+;SJGk@NA z6kz1s-ri21--^iN+kil@Xcc)GT`#P?#c-!e=_k_K)_Zevsb)8~Zy?h?vOK@YB}@-g z+^{CyntnmOI55m0Q1@*~G%={_+Nesj#ax~YBn^#(A?rFpAbB#fTog7a0zBpznCksf z3<N5Op6+LwV8>MOQ`4|O@@G9`DGCPZRu3^u`M()j9Etw7>Wa{t!vZ~lAcrpww-0xJ z=9c&4j^SYVb4dHGwwgIRj<i5Q$O@b@azse^3KbAzHh=><6duz%p7qr9bkS8sNu(be z`z|tqg6xlRnH7Y3tKRNQwTCvRSh8JJZewrc7vO{BXT%i^P;>4x8YRIwYQqxJAkfTK zjv8UUQ(zBVZK<L-b!vB)Ei7L4i>)ejKi*VhK*C4)l7*&l;AJ}XtmKP84P6jO=o!NF z`y5;!T%79E;N!n5TNgRjK)!!a0z<8ydM8StsLL-h>n$w3&YEuLH4ZTL;DbP#cD5ky zO>_R$^r6UAD3r5Lf3{<KHDk)LrGu=(x&-J_KU+VZ?KhBWpBQk&SS!<wAJVzESq1TE zP5kjUKR3$B9v0^H)7{CvO5qZm-ruKT)fgN<c(z?bME!D|iL}5MAJo;JnkDzU;-b!J ztg3i2*Xh!p<v+fDixS>yfb=YJQ8L<JWs3r+?d{)=I2iD4;q@<}JhK!5fzC*07WLB> zU`__W8SH9O2R?wTP@=R&=c`JxSkhC$J}xjF{tF)z7hQ21Iykg(QDWopB!vjH7=UWt z9hjzPDP)6bjE&_j1WhcM`MmoEF0Z9F)qb$Lu*UpRvF18=b3G1Ithw06(w$}>m2m%r zNG&_?b_EdVMR%vS2-JL^{9kg5!+n02ftv?zDcJ6`4BfsB_EXG+trc%P*?eK$uS3u^ z1PD_r=s#|v7biDqd1AWZh5Az83JMAsK3}r1TnSSFDT0tcEf6)}tfJH!(LSS^2|gdR zVlD-`6G7X4k^4h9?zAE3?bVOknJ4<x!#iek4|p3|Ms^=}R#aRV2j;+x-2tONhl0X9 z#X7Hp!PlZcrEY^)`%jGYwvDz7l=cU|0S;o{EAnD>wYA4?<DY~*QO}`9H?hd4i|=HP zR#CfLHrk-G4T)|4A{|@XC&ASxX4XW%o<3s0nIiW8MR@G7#Tx<;$u?b#=^T~`e3RE* zmO&hZ$PLaEYMzZGmyL3_ozF<Wka!4;g$76wKsL^@*1^ZiL{p6<Vybdee|8_snoqKq zm6g2;qyKyvhzsR$djVX+n!XCeS0+ybHaa>ZZA%G*O~^13g|&FvZz&OD6|D&^8uxZ? ze6zu!7gm|mf(x0qrN-efW9Nx4slul%!ENb<IrAlIu6UlvoQDvqyfk6cmes9EC(aE! zgO(;O17~3dGgSoz9N-qOv@&eMY&p!Sb9ttHx;=^cz}S6(y?AbhN2n=Z?OBQazeVLN zqho7Mi~8)A1mISuRP9Rwm*#?(^WHw)sPQK-s0D%2L>1Z1u|V%hZ9q3~&t8XsqmarU z^MkgWNqPOLxa&I(kM(qQb+gdG3UpUeE3IL=s2=_EH*%@IQq#aLtvr7K(K9*!!JTj? zO^A<d5<=H4OkQZU!qxfGd}e~ihT_j;_}7Vn*Pmg$^DGvbyh#Ke+-$~In}eHtj}yav zxwa{xJn^yy688YcNKOSI7OH)o%aix{i_@T8+PYHwa7)vJa-s~!y3X&iil-+DCCOu4 zlvgsneVpT~uSrNq+KVO><jo0G3=0TXbDp?7G0-8*p)Sfx=l0Uipv&xuY1SZO42fig zF8J@}MR&kC<n~)w5KUE2x0XJ`4ksU!e9dSto(1L%>5mV%g>*dga}Hyqefzw&J3HDl zzs;YQk2EAkI&EzTD@=hcZ@Hwti|%rc3e0`A^o=$hH!eT;1OqD1>KOJr^FMr)G3Pio zEj4M0iBb~deKW~PGU$laQ1<X&;+Om1-n{x1@*Ga7bsH<V*);p%8UBmt-sBIf*Wa!_ zObs;lWWD!@jxw7>^~KjusL`wJg~&(r^`^~M-^W*?Cm5&;e5955IfrzahKU*{ib`OG zw()bMIV;qp6d*$_MY}KW7dy?#c<u|vm(Yir!KJOfpn{ma*&PIVh4R6bx{qpVOVfW& zn7Kk2zO^wW)tntM5;J`w9!d#37ieo)=Jr?uAt(a5?cGs|$D5o;W`UumiZs4y;fsZC z)I;zKhE;k|vh@#VFTxi0Nb#%`4h%h@q2WLO{jpDteA9#?(j8Vx4^I$*+OXatujOM@ zw+f5!p_tK3VMYU;Jdb~A;iYGD-+HWxplG*$-jDmm7VfsmSLR$sXz7c(&F27`x()Wg zcVW1)Y5_0{huLa%QWePy%Fg_<5py#rix%bpWuF(mJ}~rs=C-crvcvbwP;CZsDBO`) zKjG(E*nQoc@gtma_aF@R+cZ^e&(iY!{)1PZr_&iuvS}xX8I)|0`<nBdx0(C$F*H-5 zT=&<8bI@y#`x$e*@be{u4lO5q+qI{0@;WrseDk)55gfXXw+m{TWUQOy@jOQRFoO^^ z*U6tw*8TdGm`yDW3Pc=87UX`Xx5WeJ6{&d+BN#xS1D|rj6otgkyfa_yGj<a`&Az(5 zr%y!8WqaznKZ_T>Tjb6)>WEOXu99xT9c}p7q;hN3*UtCV8Z*A~OP&S3uJJDekB$aM zde5{OBPqSYae7XOO!FCBuFa6HXj0um$AY1rULN?UUR)!pgGp0pM`V<GfVLL-B#zp< znt^v@PS%J7%tTwm+?jydBMv>;#@cl=>1K$W!08yVlUAj=S65IoZ&zA-Xn(g)O`<hv zaaVKvEFQ_3x5V>b$!g5`mMzO8vHT_|*`W#jwz6*(w+5k4B;T_Dk@aVF`<#69ZUXi@ zp}>)?`fgAgHiXE8SmkpuVT3I@%t^w=NfnY=nMxqp;5sM6YQ|J~?=x5PGaQj^HLO>8 z-jq$zyHo9>o1v6a(+Mp_&z=t2KLsM)AP4Q=Ua`3VPTVlu&JkDOCaCo@HI~)7b_Q!Q zNq2661e4Rx6m$L_!&Y|9a}7MyM5cj#=AeOo+wCcN{0Vi+5?n!nYX|yA_QzA%+sT|X zT$Ox!{PK~m_p!0INHt;x=x_Vof(e`+J`SO<Z2nq0gCQ8@YKLR*0fjf>O*_F6KZDr% zGgdovwOQj^r!c@{)gySVec1K&vpvT}31M@u?D9Yk()O1G64@TDI{~F|&386QK9))J zE|wSC)v{--ng$XJC|Ulgt3Il<!Sjh>)XU2>_GG>8=mNu<r-6T}(z0IV6Lb>w_i^H+ zE5$~!-J-`Wx^5SK(6;(RU=y5dJ_@DL(!oz?#!#ym-D^81Bj`nX%>gXVjP{l85jA2( zB*W&}5bFIqf|sLHL-Ny%Y3>J-s%PW*Eos1}2Q@&7C|#8PNjSi*z9R}{yy0Ejbfen; zdvS#Jc<L+RUL)htg<??BqQS*`UwGj^8oIOBD`Oqma_?8Q<aA!8U_FY>&#3|Re#KUk zOsIq*_e*VVFmd06tWnBaUYb8aPOcXhFQz>`qTOG~Jc_>XY?`db<BPu2dQ1?atKY|( z2cbv|3iUF3T#D!O^5UD83#1tL4bsUCkhiR=Ho0NYm~Z#(Ure17-I=aTBk<Ny98FAl zSAT-X*975((z+)Un>z(|P>ldWk-czDA=##*(S71A8n)6O%*c5HNCFN%=V;)b!ehfy z|JGUG7drOsY3p?phum#Uj=&DfJakMP;SZ_{w;MNrX-#i>M;B1qXnN}G3eB~Lj*BsK zdq$)GvXm;?i1NXcA;LVuo%I;i+1k_IAU!bdPV}@n?*pnQ@htLjgsKQ~+@o~Y@}V`S zbCrZgamC!9)O`wQXeq%ksDAOSrd$B&a-dtxgTc7cM1tfe94@mq6dptwp|N_$J65u% zyslu6*Aol#P)0LHC*Ep7398d$F&(Uir=8k6wlk=;T;G@20N=K-HJ+8mudL~m!5Ife z)ZD7B)P^<x)2&GFUgQ<|qq^VKM*TfxdfM8kcB*wT(X1kR*04vQdjzMaqZ#wN7P*Q{ zFa3U@>KcBe7}@{+WOC+GftYkhKJ(GhIEC-^38O5S{`reST`C{cuf5wIdq+Ts_%pdZ z+WYwD#FvNt?+mgZiPYtv@#=;E+LqroA|Q4?T?_55qZ$pX^nYu0ME3iyu6CjBi_}?@ z^qsRirupCS3Hp0xe_bZ0bHIswWLN4I0{(amD!+)svW{wJi@Ngy+fw%B<5=B{_~)0# zfYF<S-*1@tyK1niZF?ThLmhwkQGXiQJlt8xiB#E1+sB6HYx(cVOLJp&RW#Q4bFBDF zLRV=zvDI8!pS~C+PtSGLmB3q#8PtCJw~R^Yk^5O@p4yi*4GpIpCpl_Mh;4&FrEZ`= zaTBHcG9MxEc0bR$sU+$+cMs|^w}9m>1(}y?ku@R%iyT-}c5h03?|<U|{%Hn1K8GkI zH?hRx?NWD5zmW~*&d^>IG6{J<t-URq_z(*hiyLdTw>t9M=7~3lSBG)pAy%n9Y$h1? zqiZ90H`{5UQC;3N^CCy-viD-sm<J@z%eouB8<tB1S?u|+Gwas;v-ICyCPId{KhB&x zaAuEb#CL#4YKNVQt0N-zOHo;)2qJ;9^q*xX_n?{@JF;U5KI#jTIph4E$I#_L=V9)g zPP%%<4YhEy7gz55n7s(zSY|SRwM(&1OjMhol?*f0bd{mvrae$~*T>f+TMuW9#liw& zrgY|o2PF3Xnpc#SA(^sa>Ue)3SNjYsvA&)6-w%N3l;Sl7CiQ0co2zsa56>2$)w#L% zaNQ&8w_&CQ3)Av;FB!Fr0wW~g?|J*aslSXPa5G#xA61}|WVDeT4(t<MWZWCWabi_R zzh7qEIQrHgAnR$-$yFX=9vF5_0VUZK*r-U+6aCz1+V*M(rAc;)^`LKf*=F$hpudlj zkve7fgEcu<7g`|>Uv13ar%7zXKVHgeeEpEk=%^^O5fnoQ+=lh#m=Jfc$RnY&=LtCv z4f$6Dm@%&X_{Wr7n*7qP>#|8Bf9&GkSyO(riDpKm;08Uvk&M(xMQ$)SpN#11pZIsQ z^hzg-_n-cnXS6ab*?F#*1=Q@-U;z~!t)&9>9s}`%lCQ{qUAyJAom}0JA6cgi1=wXe zjnB)_uD;s)kgwG!rAHQuir$M$iuD|eAv5O`g%pYB1!hg08oc$S?W;g`-RXMBD)&eQ zl0)ngiH`ax)#*sDkKKO${lsFoQ0ME%F5RzN!`Wc*C8T2OB<k@?$6)oJMSc;}UzL~y z78)$j0XP7jP!6{73lE~}3d>L0BjHYF!lgA0?@~T+H`KZ$t3zfclpXD>0+iY^TGR@x z2+K#9NCMvwiW8u&wx!7^l3XlH^rYx^@gf7ntT=Q8kFlApPXRVIHikEu6y0{Vk>vD2 zaJLS&#HWZOTp@))B-DTEsEqvGSAp6Ksa{R%PBc_02QALT`+Z3vy*rQ*x*B#PsEru9 zFDw2S5+i%q;>MVz`n#kuc?h#_nVT14kl6;srzuNL`|l%tO4WYe6Z!H9qR$AA6WKq* z+~4Eud_B?r8%3CYDI|vIG%yvC=}Wxu|81Az?%G;T|EI+T7F!R|3wF}Rd=!5>*)38B zyjv;F-{t>h@<559(T*xIqgkX%j%-rT;ri{9ff{T#^J$dq&mnmt*m_vk;&Lk6C}wkJ zGe+xR7glbBB}frLR`=*6$)<+TX)0qW5d$!3vNaU8-o*VWmx~7i_&R5H!u#{L)}c&D za+2_e+y<-PXNxKs-X7TBZSrhvqwZRlpPU!=PnJQ5;0S8xchLR>m;uFkk`ja6bUr>n zadfXyuy2xs(Xwl?KmYPVR%o3=gHO@`k)FppahuyNre#YNc2C&By48t}#PDet=;Ye` zD5kZi*Q(aknZj*ZC~`p>gcW#|Rv(5}fWOkFq`pzZ5xvp-6&ZHJS#x=_Zni!8a9UAl zac~<twFuK%UNE|G0ufl*7gBok|H$+YJhu-Nfj|lDYuVEiqe_<DMvi)ju%+pNF&ORR zK=KR7+Xj<!g4UDwc}@#5Y#@pV^f=<|k{+B1d}}JnoMYHtRwo<7`9KGpS-uwXGh2p| zf|bNTbb$PMW0;N;pDgPPE+(6jgC3L-OjgTk?Ir;gs3qg>T#S3zS|3Z1VG=n7^{EIw ziB><=!WySrp*z+LEstW`Gh%w`p!H{W9UP2))O{Y>lz}jJ=eK5WV$wU$s;JHJWzX9& zRqJNWCaqb^8%Wv$91qiCb(|3h@ncUW(!rtwl3#0vLdGrv_a6o!>sl$3z(L4iP#255 zrvwJ~u1G_%FeNVllk>rp7|fR$0xC;GVh16y?<klu_3Xc%Z=XXz+$e16)c@B&h~X#C z4ZEo(cpb;7NEXx|?qgbNEuLE<nmxR3%K!NK!H-#EC>A;G4)1W}%$p&TlsH?_c2a!L zJt2|UFA8Kq!e+4z5pri)=_ROQ{R#fxIN<?>AxLcgv$-R~B%)mITnjgL7`o*!b6?2U zVJB7>)Hc#`Y#4s-Kdtc)F`+g!)xMBNNllRn#gaesblt|I8S6)OTuma;m%slO;Ex(Z zxw@;m7j>X<*VamyIRXP@ZaC(g3Qe~U<;n6|ki$GJ?&`pIWE5C>JqdmF`#ZWW9jZW< zcaH4ak_cS)F_JfcVo}7Qc`f?{E{+7Uy%aFl^HRzw`iIduT=pjp<0O!v+qdKXW`Fh4 z@RqK<`OTe~qn&!F%R%$v$|TqC4+x-Cy(t1iUla%sFOrPb%y<)?ksRE{&)`TwAd&;t z5#juqr$W%YLCshW>itsQw^ACd!tal_RL@A*A=_1m)Xiz*10;?ztpRh=Ks7-xao0v_ ziz0|&VWb-R9r~rp-hP@L;tc09e9a}h)6AQp4ufX?A;#i#`SSXvzx0WEUB7n@rlZEd zq;>H)r^AbP_&$$pY_Vb<=L*8qnUiMB`ef8zbM~izCogGztseO&7Td$IS~XGnfew`6 z1srJ?ot-sTcetsr=NUny{;@_q$_GTc0B7v3qa)REXV+=Ytqy8>Q@ULd1vrY-oQBf# zhUUH(>Kx)sOLTgtY#3mgq0X#Erku~tLVrz>)^sI#o06~Jykp87)s5j*MWybhfERUN zsASkRW@~MMSs;3Qk~VitT(M(6le+8PKDchN9|Nnr+UJ`#vwlM}|KsC*CvuUfb9UOr zTm6yE;@$Q{uq&5HdmFX5rSiMJy&+ytZIo*5<)n*|J>lG=VJqGYgEW|zGvM&#Cx27* z!Kw3#vF+#@4quV)69#+N-H{~rzWSfxKSpvWq8%2GUyPs1JIn$F!4-<=cPK#WjBcu= zv9aP?l~DP3%4N<p#NdNK<q$+4(Akr8H_i9#rhSTI#asOv20f1uP!>ieAn<jA?rmLD zFv`CVgBm4Dtdb2eeGqcGQK={5o&K}R#c=eiB6VAN#x0~N^n`6M{Ss{uCg^?|Dz<d( zXfU7I>&^3hXv(2VD6X^OiX=|zYH`tp#NF|vxem&gF}1q*rHwVl<%E0bvfhfWH@a!H zZ)><g)|39)WbvidX;wqp!jIR_uC#U=bH6N#D>o|8{GmfM(N8^A#uo*%L0${}S#A|p zG$Ne+-KT%gssdWo(f?wZxPkAvXZGNjpSlaN5`UnnyV10dske_vk`d+b!D7nElED<L zf^B)if`4`4IwS=Qon4XT1@ZLCK!5l98hQAQnU;Gz6jm%t8d675Y{xK?qVlc6QiD~B zN72t|fu!x^Jl`9kVt5Jl>}Pw^d5up{9-5~S6fvv6MpB)8w2@JiUdo<-o9Q4D7?=Fc zOa-6PR@V+a@ybH-c<AXAvzy(&q_r|ibN`17xW(OM_Cu|~+<G@Ya<8?dpl`aaHpAG< z?G%dXqD;2*;o}qVnLo=de|fXm$3xxU6tzwX37@{LcruravRYwnA37B#P8R~JbM3ld z+(KZrawm%VjK1Z#<WMg!(5JYPU6blkM_7fc$K`q+2SwwIKw1@Lk!~H7R+<jD_5D2@ zb5F}JyB79H`!RdvBAFfaMET6Za|1c*k#9xz40+CXa<b_1=V7_E@Y#zLa8_kWTKAR< zN)+-$aa@23`24Uurh0n@D0BOw<`u6RhyQbZ(2{qz+Z$}%UKsKItHIL4^PCxNR2_7G zdY-lh?W_mXpLKYm1FF=7H`IS$sd_&D@cN{0htoKWOED!Uzh0$EZf5IQL2L>0l-t8y zFf&BWpmv!RLJ9YX+Fu_(y|{kC1euJM+)tr59~Lm}POmoz;|6Pp!}JmcHd;3Awh;|% zrzhphPD6ht(_X-0Tyl>?ev%(ZTo6h4YhUH78i@ZImw2C(QN5-k#B1XxX83h7-@u2I zrV82s4M(l10J2Y2(prPE(dQn9`T!2r$mN6=cCV0XQ%p(9i=Uf6#vA4(Re_Ib8pc%j z2JE&r>RK7sTGp|BOe-0Z2F~2Hx9t>BJv9=!egfk}G?LMo{dUbdU#&G&Qov?gPaln2 z-GJeBp2OtZc%5Q|41dMUW0p7g!7-l~1<-XwHi_RwX*x<TUynOF`0kw-6~oQBPgl2D zH%7lV*Hf<|@;8f2`tfl^U+y^jT76(l#A*1M`5Z-B3vJ+b+~JMC$SCn`uJoB|Ilt4# zo<3>d!}d;U_JKP}?0VOhmO^BJEE_-9Ve$2@&k4TL_IQrjGzqI3*-ZYVM6FNj$L@ix zDk`Mx0+*yqll6|fqPO_RHSn+Nu5+?d0oF6<h`ffs25_k|5Soe(U+4O~jLv@H>$6B{ zW(cIH%k==ty337lE$U@>!!3);bs4lNSF%m?Wk`OhqgPOhg7tHtoRa3PDYoMMt=P)C z#<jSJDBg^r_2yQ0@UbV!Vc*kPH`GoUIr^}mOJM-6ZS$+f>$0|#=>67c>&6*$)0ho1 zhU$7M8|r@Bw?4>kDh>EyZ_^`JRv80ew3{#~YXcz7B9EnUM*!79&79UZZuJ}6$=1&F zb6;^$r9FRM(H~B@v_4N}b-7Zpudlc5l;k}dVrDmbNK#va`HN=Sk+MwAzz?ofSpzRu z+B`k8qZ!iYyhEBiu5pj{73T`B_UA@&o@9s1Hp%_-Dvq|`HJ7Uxzh*;3*+ea?#!_a5 zDJHkYqvPAiZT?MucjSuA6z(AkVa^{X`3f(Ey3jZI(d!Ac`xONJNxK(9$}cGc=pc6h zq?pHxdEKYQM~Bt|<5NS`?9Nh!S;d(HZI7N*c?#v2On=m7ue1=(utrC~4ddoEWk&W0 zJ|F#?lq?^|`90+ebZY(Rf#v7#B}ly&A(*U=2XBMR5=1tb(KN#OhZ>!w?CnFVdF%+! z1Sc_dX7gmIH|`czlbG_zYgD6}+L^}%s+C|W#kw+j*$0K%gyIaxgPCe?P@C$^Z}OVk zF^(Lw>_V$NnNe`KL?i%~H#uY>YnBXZy#81C%AFOJ1BSGe+Gp`Nrz>rx2U}6I&0f<P z^2-6vhkSJhSp?Xy^URA>C)MJGtk=Ee!sgAIq4t55<AmFf=d*np84w=xK4hhOYC`Tl zbNYC25fZLzx~YreOm5hA(!hqQ&<dhm)L^D{TYIJ}uE}rYmEC#L3!LBM`$exbVbDTf zfcU+h>}o6J$^&uh-Z{RrPv&sD{`;3S+6!noxQX{_U-Ues4A@$#ouH^I)L&`J72mu! z1^w%f`uI3+Joq?xB%wi*tMq`^@>C3<OhCss;dk=ZV}}0OMIu+7`QF^%WsB#ZANSp~ zYyI1o{g-~9if0ean4;FqBT}H(7UNSpKk|zfRK|89PVp=O^6zj42$REKj;51?Rp+ui zf#2(|xqf`%fGGc?l2CS!r7c>cy*`3zo1$dbICGzTJCmuHGzdG{m1T0p+AydQ4vW8` z)5FGH_Yurc4-gPqxz&#RN7S=19HxON^b3u8t?)0}z<1*byU4+<da;`M0m@7~1RM{e zENaDR?C0P!RsaNiCj0-eUssPtS3=NFIdg)e=Tcw}kAIF+)5vb|u|Xo+Q^B60&`i+^ zsNBesw?_j{YRFP}s(S}-kxCkxRD=_80C}KexRyfy8?P@#kvT~Q6{TXW(01a=#f5+B zSK6oqRSf52Ga4z!+U|f^>FKJZz&Ik+cO($FI^z<pxGy!YN{bf6g7#4ThD5YRU7B)7 zBCmm@$Ni!yzdTu5-oVM^7AK8?kofCbw-CQ|)w7@aKYcr>-p*X6fMc=7OVet5Wp++@ zYfzLlKRIfJ!>n=ERWWyl!hzMpy}MG)i8hH(>Az&QKjB}RPtnT^DNSO7MBzi2AOh`I zV6MZj<;ClEWHUBxlK1HjInh_sa<9kx%dSPn9amLros^`D@R!!C=tj&lp~Y}WO&7`j z)B1gTj(aGWvOzm%eJ#h5YRxic6Hve4jmLc%vx~CiodeJ}#(JSVKSFd_3Rd|QZ6<49 z**?4tgW=dW+dOnLJR~a81sE2+6xd$ULHLtxEN63@Ll^#R(m`%?xej6_z}KT)8D8tY zUt1c?#Z;RdnfS^lp@mTD{o5UpF++W}{#8E;=LHX^J<6|PIh^+xJ(F?<7(XneArA9k zp9$G#*-+DP$M?C?<6#dYkuE90MJ_MwIRk%c;bo{dqN%BSidJG&tXFt;W4?Os;M2<W zA4NG2N<cn6JoN=0(L;JP5QER1<Ez_(qgBZg_tpQ%ZHV2>2Mor~JAIfA(H(`&dvsi{ z{PPkwdrZ*8OPjUwVGo?U=t||oAaunbdoKBD1~FfKWsY)U%l9(`6ui}BNSOk$BTX)g z^ZI*b#+(0b|6>f(`DMu)+PaKm&*}TkkLSSy>)uL$@e7e7G7`pNdi$}s@~4t=%y<g3 zGFb{|^IK+$w0Q8wpfVQ6E>~e`guzzEwsKk|hxoW=qC`ypzdp|RzS`{*Ic+ja`i)cP z_qlBZMe1B>190iV@9(A|%VpIC=Nz?>60D@;Gd`FN(o+2?vU4fO?K!>UYA)W>VqIXc zJs!qCAgeRvJFvoP$srzU7SH}yel};Z>*NDTJ9`TI>2SJPnAQ6I2r3F#u~?vLVLi9S z>y6UtBc%%Hfm%=C3|Q`T1_GVR`HM?Q^NVrbL=KP7I&!?&WJJG2EzN1X`RK%zIce+? zC0K#HBKTOY!<_dFuSY-d5kF9(-;>LTu&;NaDt;negZoV$z-+9JY~87u8n#bgI_#Iy zjKN}x)R~AJOI4d(`6+G2i!+7q;*?@IziMLzNWpBP?7=Bhikvh+O-7jLM%HhLg*yRX zA|0=+G0VDKw4#$Sg0R>U`(D3K<|0U7ZEJg#@(NLPy0!YS`Au=A=p7V0B&sd)^Rrsx zdc0aYX;~xvXBF9z_;#PU*dQxPozo<Qx(yp$#GWs(8-d^y$*<mLQWk8+obWjIqybA5 z+f1TvT@ene@;%gp0d}(8<uUV^8zFN8pzE?)bgG4ApEvpOwO}YGqk{~W^wC}1)KQB6 zOW@6?Ck6DWO!4A3{5wrW_Q&o~85>@<C_I<kNz~@43Yk|i%PvYmnc^<UO9A5{l#@W5 z&%;HzeqW3Z;uKxr83DMh1XXH4m$$w5HK%=hA=%pPoCxuBV=#9Fp8U@JPoy=b+?!u- zZfWP>Q+x7eBo(Bon-Vc|?6jYEd5i-^j{~tlDnhc`7Bb$wl;5QvE<|aU(~7Xe@lZJ* zUhKR9K%n^X-%z{vI*nAOg4#ERzJ9m!;3;iAJpBQ6(>Q*Iafvw1AZY7-P&~}tXuv=- z&z<ydqxVfs^Q6?Tqo^tNG**fmO)|{s4e7Ro3KB&(rU@8M_RjC9F?;Cj{TS$NE;LQD z2y^Hn8|Lt)SU|63Drr8m6+UOm`{L8Y#QHKS5sHO@))xHjGt2z4vi@V(Tgt^;-GhJr zfHD~<s3@~rkMx~3BCOJ8$AKT;>Q?)nl{_=*F&6{`Dx&=L^(sj~@4C#W^P!dZw+pS> za=pfcIIptW;^;e6rw4|<9~WGFwHkE%hRSXxbR5ZYx#uOaXpHW#L8#$OVz{Ii^Bhi* z!>V58*>tAa7ZpS;e_%5D)UDGruIMrhN(=&}U#V93)wDTJ`d<_%@8eK1@y-HTF2QB| z{>kWh_fg2y#su6JT&Qw~sOe_a8&gPl;%C6Rr#SVpY0H)iaXRljERl%bh1;5iCxSts zp*PM$70uQDZU#x`oBYRy<2az8a-rwn{0r}ywc-P6EEqx;(3G2dU>;|rZUMDFST-%y z?pqhG%in;{^O?JMxgP1C)qSYPSQYfW%4Xfij@CDR!xg5W$F((7y44P#?W-5bsOwB` zvfUCe0y#uMsb`(X=!H%Ho4#S{Mn%H=E>xsj{?d4Av9j;bmx0RchQAYT2J}C9d5+wR z9QQtmGy1--4#is0{ht(21Q-;7Mqq=^pjcfc_@D@<2mhNJx@*7`7$#rj3xM8E;9yQR z-4%57|DQoZqaNUZsG6(qXo6iy6~@fnoza+76a&D;@&Po4Glug7B2Oj_^=4cC?<ZLU zbCEB9CLV-L?N7>mxCq5^0EEpRJb{$OA!9rJmdp$ABhY*)WSQU~4NUe8Lr*Hirvo-O zNz4lH-%j*87Xe~HNIR}El}z+MpF8ddbVN0G?h+<bL}uqP(LO}QbuYqcfBC@!b1^eB zUrZk72U1q+cns2dsR6}ug-V${aVQXr{kqVONe)GzC%;;})gsPBH?*$9#2W7eHt_sn zE@<_rfN7Aur9}Yet-&(;g_#HUAs1=xt&5MxaAp<BhG(Ijp0sEH0SaK$+Zvt9D~U^! z`X&N^1QEuKc9*5y6OS9WB#T5B-6^G^^l)>oQUxJ2koMpSLMlAsu`Zz#Gl?K#a<OLj z@oyY?p^sg5@v&+BE^ZUB%9bGHblts_g4R~T`i?I=;Oh2cpG8wprjx`eR;A3QhLCe} zON*QvmQ#UAfyI*zcDa_8XbCbp1)(c;4#fu~oo2(o+sy)CO_f{NRMcGI2r|hybhF2H zT&kNJmi$bjy;dvjPb9h`Gm{+v=}nYZ9TpnXxYQKMNNmuRwV}3l%*>a{!SRt!y(XDY z(5SGTCCz@8DV!RVEVZK0efJY*xLO|26<eLQ8~85{VH{B7Ndv^P@sK~riZ|2G5375I z$$_1%oUaqMxV42elbk2X^Y&s*N_zLCD@YX|E;=>+K43tn?KhfT-!^&2G8^ODaglw| zA+mUH^Bwk%oEehs`kh)OQBLwo=$LzaZ@{7PvRkET7r42gL}$X_`s}9h&P{7Z^~rB# zODaXr-T(?8VD{u;@5cQJQ?lcA=S`)Bhexr!9gN!J;`>2M$|zD_ycA`FG2-(gHA$@N zxQB0zygcT|g?1E|#m+>LaJV((2RHQF-#TR>l{i!aj((*xlg^{Kpuao4<=jQ=CkzmQ zXX6TI#sf`<dp7s94kaPNO|RY}zKZ<@!rqbRThZCFK|bs(^#FlF@o3q%BSrKD>#q<# z14l<uRbnpSP{$W+HyevHd_%wGewe*?CJ$3FP%45Cs!~L-O64A#kDW~aj9bH}Exe+7 zXRLWczLpzfS{c*&>5)afGC*X+IKn*Mo;$&hP~a634QM#yPjE`!_O2h_87we9FUYe5 z)=V2=Yl~}gcgiqBbc)qDbr6@u7Iw8fNrdTK!G1#vF5I?g&QDK5NO?_WhAAGu3ouQi z|NHz}7Wnt+m_FKb@>$q!*>uisnAi*p!Ym6u?bud=!;G<sIzaIfeV;|2%tVF%Qeh#u z=#L87%j&Wn3B`aXFGD_0+PS|PzZghDO*25UsMBI1^PZqmYlUY5w{CYq8s8{aOfLi{ zs#Z;VYmtH|im-P>MrL8XyrthZDi5AV5A=<={un$`7P9Y?Azf@wabIe7mC}~+v2gp* zDSwp<4wZ^3wgOIiS;HXoh>5Zp4k_7-;qKO~<FlpEKTns;+0TWBi$^y%*F>lN33RGd zX1EDLl;?h~C!njS-4=X@9<1jVy%1}~i&dkDgC;3%^(;_LSd9ORrsRKJ*Cl4+xsd|y z8lXp*;OD0DH1)?d_kY}fws9&(Hm!GTs#6p#1*QBYK#=sGJL>G=cz*Ioo_XWIUy;-v zS?-O7L+u}N%c+N%oh#5#aqsdZfPWuxu#Q*B1B<x?)gHZbUOa-%{U<@Igcn!%iziYv zk5a&>3@~Y0W!H1Ed;eovwh495<SAka@(}4@280g-VsGf92Zv%GS;n_$gNNf96BdI< z{t-~MZ_(?WW+$3Io&@0=Qf@%Zm?d6a_^o!k2YeNKb2sIGbJ4{14cu(ZL&<2$b})pS zarxBY#r&ano`F8+1qsUf$NUm^%+rGqT_5gqGq2GZtXR&ElSTh6nBsEzKi6-76tST+ zr^HVoUr}LFlEu?*;<YvkfJ}yy!8euW=QUQ!M4zD{uQ(<I2!4#R>y%$Y>DANmB}#GN z@B^OJ44BS4LrzY;4wbaaXd$w+Iw7RuMo(hB-1d&8nvV-XJHZCW3~6-nojj6q&BS|+ zT86yBp~aW025XnPMbVgp1Fw_;d^Q~(Og!F4=mg4e7NpBloc;czD21%(q7ML|(INv? zV2nEwB!Rt)d^?qUnV}NdmBwBTF)j5t_w}8gqf`kf7g!KOERWkY{0{dg+)3)$lxO_k zexLC2x$z??1Ml}(KIv3uAFn>@xyKYwhrsCwg%aNR_h4#%?&bB((2|ex{6D@Zu%;zg zd#G^z14lZzpte}K2`aT(@y@GAELJd3p#+~!OSvf-%hR1jD42+_XtyA`K|)pFaw_2B z$h9We5gz}J{6Tr9hyozXBo<C}h_uxqIf@Kph78FLl|p$g<UAuzbmF0W<;tNI<gO24 zv?rCYcJ-N+wAtNp!qWH<E6i-BLrFfcA@&2}^NDvkcE>olbOP4|$Y-o#ijL%fy6A9= zzv99k?@0q&s4f0|3^tYrT%P@4Wl>I>8D#NXDY<cY^4F_e_yzWNm5WY-An~&X<;c&_ z=)9ke|3ccurX6SX*I(sqbk(wW9#7LB+$QdiK47IgKQ8V3@CJ2`SY^;Js6c*{jw+&@ zfG1i)@j;bR*!w1vWu>nvJO4bCKC^v=_9HMA3U1o+UCbP7mrESmEbCnf&3J!-FA{~? ztoxy8keD|+By!Kk%4>(u<r}{LBBb5Vx2*|7r+c*YCP7iK|8$l@gxOG8uVd@_LMHLb zPO7oKf0$=K^ukSF(lioX^$6Xg6bog-J^D7<i4{#dfs133lUnS?{~;9F++F<d^c4P_ z5wXCSO94^&<QTcWYLQ?&bnndGSc-0QI-^)B@1~SX!M6fmpb=)~)a+ist^zaL3f5Cg z4xDnnN4d}ipK<-|l7IaRT0l8@rj{7vv_$=B4tamQkyty%HSUvy>QEq2f3u@_;2ctw z7f|mAhy-ei1TL<d6$=tzV<|5`yyoO9!{J!Uav_pC7QBkxgzLT;+{KvWjM`5{C$}=q zgs<+4dv{Fc-d4WK2Xv=Si5Zh1A{2Ww+NY2*>E&#5KqMNAB)N%7IKpv72tEDV+<_92 z^-{(x;TyDjR^;nHz1!J+p~B4gmk*qFOfwX%)rsa714KPHN2!Sne!qhfC*G&@aHzWt z(5Ip3(OC@Nhc^lzI=C`c-Z%05;-s9oGLUH})OlrfNaazfxp<ou3I2${RTO%u@+8Md zo}YOGCFY0gTyw*&jyZR!Xw5qNZD=Ac8us!9go4njUE}AvP2F^?IO(5256!T#l%hxC zEHC%YS@whr`uM)#kwaAET6{!uNlJKrG0pYD5n`irLuxtmA|3BOtTLRw7KnkpR9G4F z*856b86SY-5VWJS&XeK%j<1k3hZ3R^6)%u19wB4#%X#5vwU4)&MCe;6)0cP9fy@bN zV%`d`&qTlbAeuuBK^ce+V)8<E6_MFRx{KoxZwGtbfqbcNCI7CvkXO+dEuy+cGYoeM zUxbkMiA9mS`SEf2^;5P#%lkL_J&lPKySGwj?fVfv|Juwem5*uNjLM3p%B87=324+& zT+&#(QX(Cd%+mmRr{K$!(&Y~3iihuy1m0lL#Gp6Lw}}eb%dMeeWn6vQbX@L|N{c-N zZ{VA+(%yQK+K*kg`Cak@iGr@Dt{qf@8jeAb89}!pt_GV@1=mGNOeRn&D+X5dnGunv zp1jU^1p8}Z(v;?C-=3b=Wg%!@C4aW=XQZXc8>o^vhA<-v^Lr@aK4;y_9$Uc>$SUnd zKXqP*bbvD1uFt_Od+8%R5SRzPBq~&2=L4iE@w>`K%sRe#VZ?9D!8cL}@Xg39E0CHJ z<wNdDm1Ec9{(f1*m}QpugXdSB@Jjr~c_IA%ZY9O-molls7&PkodZhE#lNaz*c?LE4 z72s0*?_6ul>eF<c#8tA54NU3m>X^EEEJc?GSa?6!w_mX5S$A|b0XNP4RKMPRa@2oj zw{?I7w=D|*iTB;8V&oIHEY!J<*Kb)BQ|^I_E>p_`Zn^%Vg9Kb&5_ElRe3w|{R0w*% zu%>cx8@SOM@C8^hrTnmW+H=gIWuy2ujl@h+!;T*4+LI?!2afN(dMA2!%iRH=#4nd8 zqoYIwF#KCd%`cpc0C#bWg@TC@THAplVMy!<FyCu{@&85>Pa&YO(3o=beHOs{egs(k z`q`yO0VYal_VLCZyJnDvuJ+$W8&46|bO7e{i92?91R65cHt@|l=D+Bq+*=5C*QG@T zdhk{I+kaJU=&ls6NgzFj1wZ?aN9ygkqvm@i%r*Q=fr-FD#cs+Sxv3?72W=-{au;!o z5jyG$_Og9pzf;ZW2+mW{I%L;(*Q<c?FBxv^)YX4Fd)Coll(P5_74sB+j7jN^`9B>o zR|=R%(->|4t6|8CrI?|l|A$WUe`$9pm}l~su6IK3$i4m;y;@lx*ah0(AtLlxucnrt z+*zR#>WaL;6?7DnbP>H1kvpMnqEU?o--^qV8FGwi7}WU^<B59tP_rztjI|SDpb@8X zs;EDU%YWTxqoXS{i5_GwyH7Z@HvM)@8P9(OKb9F|#0b00Kx{%=qd;*<d#LV<n>E$S z?dOjX=PQTff%oq~kbC9nnSTvec9DJjR5V<LzTSBNdIBd^6Us%qeJ1&b3es*0Q4r!? zAl+nJ1LkP@7xo(?LgdapJ|a8u?#?eb(S*@fmo^nxh)EK~x_7$99hBbbf54bWtKdnd zB0-%y?CFA-T3LnP*+CtsjcHW0;wsGCDP&W8D^K>8cdUIx!IK1TSQ?FljVu3WRTlI1 zMZ!KJTfQVGW}w!ga#Zqf1V)r*%j>gq^5teaHe$qyDCxz=7l?n6NNja_U^o2H{vltR znJJxnU=-RyT!^hT`mdSky9O{kSI_(mCHSHM|1bp^yTktrtwG^FVKL)8o>}U0vA<Te zE$4|o@}E50zuJ`O*@u^REXN(hVT6&|#xG=EamwkXJmBeeq8V3FZwzU)TlX`SYL_b8 z-MRM0c>7VI30c2`^_k?1g8L7(zmy&ZfjQN*Ya17sAlfhh8qGhub;dj^kwtmgkrYeg zS?luEKXXRdY&lM3Df{sgNKmn&&~sE7?2W2p+H$7l8-l@qiTPS)5Q+xyOsz{5`wS!w ziOnGixQp72=be9WF1Y+RntJOF%?w6j578hPsP#}~LkBz$@8(gIy1`a!^USaw-FoLv z3Ze^JXXzm-n3>pxj(eO-{I{q~ioP><_EX)>&n{FS(*Xk%0lqUW1IAUNF()sAkZY26 zep-aV_<zf=026j%S|0!~j#e2W%qkH(mU?%T8*o18nEkiVT<;A>a~tHUDPRWkq7vEI zKKgG`almSn&+Y4dk8%X(SvUTFjxWUxICfeh4$LnDwbKg3e%y6-=E{W1C1{|_P=Vki zFh<6fx9@rQPQ`UUdhqri7da_40(=qy=HLNVo&OFutGJLvuf2SZ#b0o%HSefl^5-(} zmm})U<3Af7Nu@^pFNPQ9ZGLU`e5t<jR;EaqX1fTa0sv~_X@%kfYG3oyNnG%s$3HWM z9KRlJPZ{dvo9N}kFWmC+Tg)^1a;xYC1I3{3eEts&GTI~_j}ZSurUk<$-t}CHe#Y56 zmx|e*xnns6Dn<!8WrW0V|E329w^Ug_5zhq>&km&HnkY$L%aFOM$)?#|`Re_Lj_)qw z*>lp9dwZFo)=(++3CK6XR!*pRkCc5tw!LZhtzwi@hdig$SS1upNpUF@NJa6t#o^&9 zZf@H}=jRDWIV|YZC(SfFQ!_mhz@#|&vGA<p5K3HJ&feF{5;#x!+~9r|Yr9`tZ)zb) z8U)LmfdX0^Ba5Ai*F<L1Hl^1SF=$uKCd!w{@kr)2{Vy1hPaf<^_l@_y@bs_Z&Ij=F ziu0~64fW1n^UcELTwAtu^G!COr_1<=(HoH7^_5ckel<=uj5uG-pd7ba*FrB8=Fkii z1yOFbJz5mI<+<nWyR0(t>QRAe&SS{<V0u$Q*A7%J`fukVawj)uPw@5EwNIKtQKQ8I z{{;U>KCfH0&UFgV`ic_K>-F=OZ(5`g1%FfG{?08fqDluRM6Z;cGCQw9bzF1^VRu`Q zXcipqRc0)^Q>UK>rXypKTP8EB<h}<~JOZe1?!5|NY|-&Ae-nO0QOO2PW1x5Pb6U2A zoKNNBOAMgE)rUlK24|3C_Jc`<of4-|jaZFF!cZDZ94ifU*Zu*0{a0O4E`I!#fyvh+ zJ?fm&4{hVe$3k4a->;W=!~AKAU<F;;K<f9}Jp!b0K)iZCxRd)Si;?Jj>T+))1(}v! zD0=YaFi6C<-K|oJvN8S9<0Qwu!uL(2DR>czxozL0{3kz{P`ZsK4K$)Y;BV0TSq(ir zvRoCVWh5FLDKWr$Gg2B%tTH1nJTUtEVP6niXo=*GXGfI|E8b}P$)|pr&3<iNo0mUz zJu2eZbW1OP_sNa<v+>|AIs|HzO~mY&ver^GTwPcxY|Jm9=Y~mW9Qi>+(blA!V+>is zCF?TjCP&D5&7mcP`~zzLIrQ6Zf#;tb>coF*hB*b|f9U1oOG?&#;0$4|jJC(PA*2gQ z^F8DZ$XoEiert6fqR(_AZu0sK?Z8JfM;5acenl^#2<lgJ6x!!&=m)FYF8@f)33PVc zXg;@G@G*Pf-jh3%RLG8PA?}UdAAFfTd>`TPHcO58TD?t?Z`?8*<z*G0a1?%e8e#qM z5ecA<t3Rxfn3RuSI{Ai{c%#*iLmFtM1k^JGpC|9qU?HS`h5CHWqWck$J^6Fh#I`Q! z8T4so;Cv;3&mZ;_pR>&6sh%D(M^P+*h&#)^i;yemwKVadF-v~yu;}LK??ifvi!W`C zeG}s1)SwR97a94UML(l;Ri<y1M-}isKA~<FmzR~?{X||wOP%{*Fp!hv_(HlkNTgg8 z@t#UHCqGl=Da|8~lX6X7DQVAFA>g#}l%x!n(I?PNc)7{>Md&RE!mcovZ^{epy_Pqu zVnZyoRFC?{vO#57J8D~wS4MWQllGalEt*ts2fwVK9#txi_U^Aew;DDFAAl-s299c| zyjS@KTo|q-6xu0|Q*mUto8`V<zeq8?pq2f)Nhu?At*1@?H!Yn9YP$lzTI%u0{+Hg7 zQgmN<(Kr4ix`Q?5=s?GU?UXUI8)f*n$lV>VyfQ?KAw?w|<BKlKv@Bj-$!InaJC+E) zu=S@5DJxfreEGfspaM+q^ZDT-E6nSXwQMaQ%i3>4(X@qlkS@|ncqGA!7HMb4=p!R? z05|4*xi83b-SW{7dI{y;)yRB9CB-Mn@5f_6=F(&@5wN}Byw>l6)LYbvUMqxKk#=DM z=@}otCOzj{ze3$kBs6d%l-*Qp9G^njp}+UP$RTiB1#znO(y_|9)FRV=WUs9_^+1$9 z2((Xo8N8SLrU&fJeAavVb^WxeX=qya`wsH+FVjNVM`PUX@ZibKY+ve0<eCI2Ftn(N z<A1sJQS{zV52Fh_L}|G!+1o#4t2s^}x@W|X={`C!VZ8x4Y`*}~l7$zy;pIXDeeVb$ zG``^^%N!9q-mm>v=7dln3OO=Wl9+t>J&PQ2Y}`7PYB$(guW%rqB<2kUfX8xY8I`)v zJobJ6yA)8+C?|dvOcnT749|ej#KElFQJ2+nLxU!p+klBPQAw0O<^_vredn<fiP8rl zwl!J-5fR!lR!J%#|6S+2dg^ZV@z?Y!)-o3njJJNW%-~xzBRmkn>OUp3q7((CsZfT2 zNz0z5%Xo^ZriSi|%cS+l2(`UOGEdUK*_!*}$@XY;itVqHf@WIOaGo!X4B4BxP56v3 z)%NQKa!Pk{MO>W)t&$z+>1VRB$;=1=K?!<1>-h0=-04hg%f4sGvO4qVeWqKhmIvqe zEtw-{qwAx^8?QVL$sN7;p`0Y#9q|%Y_NQHRf;a-;vWgw3!SuY|-xJ=&wPBFrRShwA zg7q^XQuHk^o!r0?_a|_8;U;wW(+7X=+&z(J%$4kt?QM$IpTLA1(qwY}2{cv0euY=Q z&WU>&Hd?u6IR{Wiu7?wBRa-VGb684$EFiDD7POKcL8NN%sBGV*rvgf-`#W0$FUpf| zeCgcR1H>bI$)O^B)*PEYh{<{qlGg&@k_tSC@qNU`s7C4Z%w-wGW<m<#T-wSO490dt zZeer^1BWs`#_uFR@UJdI5<YhJB=XA$Jst5~;DI((I;>!qX5zh&OFJT3aJNaU+pU!| zG4pc=>gNBHio1I*%QVqP=+N>4_n)zRo7AjPC2=6aI_D+rZX9@QE-s@;mXV&cshRfl zm;PDpXv2xMs{YJq{qeM6w$oHnUZ%Evnh1oOd-WOqfW>Cj!qKLcs(wvd%;DbsN4=vO z(|)a3kNCozW+dGyWQ3Rk@srLl>o_;BRp>rsFt=#NnkuSF`s2%TQAn9h*5S@85ew0B z$_F$o(Qmvj%8fG1KdIYJQNcMrO-hVT@#l37$dVyHyq~0^#Y2pawklTMdJ}83vRBvI z(N9sAFY$hD@9yg1ulN~tMZeA6w}w^37li&ZSb9H;Q9@Z^;7Mv<YbZLQCxP(0;;4p9 z>4pa%I}LBddl7QmZtnn}jEAZD`23xGP}V?DuG>waV>W}~6l!o%z$j^t+~nQUS^O*_ zYhKBEs&7B9i=H632qCMfHC{cuQyA-jx$B{Xs2(U$sMG6Iedyu7rG(%@QiUj@f@Qz` ziuABHSK=5yxr8(ihbLTnnen6b@XCW{t6X-q`N<+6@njDC<NFAr$FJvN#d5K-@KtfO zal?K)os}eACa>~kgl97&V;`pGMel}3%ZyTQ_ctYv@frLZ4wh-$MQdfln7{B)y-&M} zWj&uHsQNgZf_PQTDi{?s94F1reM#g7<ZsIR;ld9**<j`YS^ol%N9{J}f7m7m)sFfs z&Ng?Tl{p{?$Mi+nsi@-(H_!m|@Pcc6M(uDOGv(`5`8T>xvf>o~$nLb%x3}nOc~0Fo z@GSUAstg%2ma0SbqcJ1PC#NPSKX}u*@^`$;pY6%}MJE(ZUM?GiGHFqy`MxUo)4#Ys zp@pSjK}%-ui5_XpNQEKTcqq;NZr&mBTqo&~uSxvu2H(aIFxwq?Md=%-Io!j!6w&jH z7K7CDMW#=Lf=3e$J{!|)(~lB9`_0%1-#^(<o<iv(C4+%cq~<H~gnSNMLJ&ShJCBVP z1-4cGI?4O3kVN`DQb26uMfkzf`}R?JnLb^dx^=_Mp^WnhiFt4Z_-Lr6O&D7E)<a=I z+B&4Icmp|7F>3YO1915_H)j0KbRm7XobEu0+;&vB)UK&{A|=<3nb;JGO?Z}&B!LZ7 zY$&#^iX2wc|N1|ay#-WMU-UmJN~b7DNGctJAl)gAbPotf!yqt7gGzUoboZzrbtwfA z=>Y@;98hwop}XG2@9(V_|NnYxy|q}dnS0JYd+)Q)x%=Mv?0s@?g00DpJNR#^;OrF0 zmObLOkCAU*SAU4VHTwDzr~Yt?S?i(qaFLAYbWlqCFtv~1hka4HR)xR99t_x61HP;r zcgJtxkrgaXiB*e4`A!p8puJs$8p`l0j~M*r=46j$Pa_)ezOCVrY4y5X_X`-HhhGC# zw=_BinfME(Y9SF~2D8hB+^F-iz5`p(4+j*Q&~assMFr-Xj8)^ce&X=L+_Rsjy_Goh z74Fpv&43JD{Buustq0OV!JG0{w7PY?2~g=L7-~HJ=smY?VG0u6!qzwA26f|?Zf)A* z&mFzmUbG!ZK~uFoEZ=`G_B>J^<ezOw9hABYq_c4v(fM*aP*#nn85Ew2--d7NP+RD| z!8}&4MY_&R#$G$-yy6YHO4aR`{;@VJ;%G;B|3{&n+|)H=4kDv~?T*+3S&=?K!;TsK z`tinl5{Jr#14v>GLaV7tp32W1EiLz#@gjwpgJVj_BwwI>)`48(r#U_<$Z2W;zx%LU z422wyfQ<|hb59B&ZR6{-`OAg{Zq^(jiV$jYcnmk0xq3t?ib`e=7%1d^XrpfvAv3(z znBWCmUXuXkx1zX7?atapL04)QhaU$Lcgs{l=NY--*PU8M_Ftdo!hCARDKP@!;obur z*Q|Hat&4d6z!xo7;drQY4OPUaD*2|WD)}j9m-z88hD|u9<`wqyzH;xE@6+o~Ey|rc zX?i-_#?#s$KMl&kM=LXfBH*~LU9KiZ&CzHDK8uqBxvu4kUbB}k;|)P{-T`97QIi_i z<2=johmPNb@hwODuYZ7s0LKuA3L6PuTvWE~><MhYpnRXq%jDajPl+jX?~Mn`4@}Li z!uewhK!!Mjtu&)X<>w(>@an3naU3MR-mX^p>>L&paHRWMYn?%?#MwWdYA1nO&bNS3 zMIg*&Pi?#fEnB{WHmd^fd;VoVQ*t5K($6D|dZ5hgdb#$d7&z611nNh&4bLW=WIh$5 zjUG+dH~LSbnw@PisDis^4(#bM!MrFHC9YjgG=~plI<=tUA?6j(BcXdLCZQ|m<cs#e z>KQ}GHOY%u-k8E57g5&g0rvCUp06X^d)!H7|Lfz6Z-A}0<xa8OZeDxF0F>!^u6qpW z<^4y8Cg^+1ziF+KH$YceVU?@<-=$KCL)xsINGszEnc!&aZs)aLs8Bt<HVS`dr2-*> z{+$hFqOaZFTNGxL{ZCk>Dhb^*?eEgaHF4Yrsw;j}9pYD1gT~CQ)I|@mfOzM{_d=@+ zdIIHZ=U>K<N{TH_uP+{m121%Mo)ZlWCf1H_3j)2HDm^czJTI%(R;Q(}A1@S3<}+Pt zwI<ay7JV}Otz?n3lJcX83Er}Rc=_R@u*y?IkDJ~fs<nxHa;nDT#;v|eIw*71C0xo1 z=!7es7Pk$GGsI!dq-FhibFbNfJ$}-Y>-F2e*<BAM65+*+M9@V2j{e-%mb%)q0af<q zn@OUYcx^kli{bHi9_jY{JexUw+EV~R(BwM;-OC3eb$<q|#_K0$Bx9MyrI*hKkm@>Q zI0(KfI2iHxF{+!-7=5%LnJO5R>G+d`mJtui8u6m>k|ghOmM2%Zleq>c>TH}CvA>(N zwB?658JYJc-!r?pufs>@sP}iRkzVg+I*;YJ)RC0Gs%*=-0}#CgxPY4u@)cjj1=%|_ z#=97_Um{jVOO8bwZ5L&UI^-$1l$A2@phT|%^s(5VEINnS;MR~kn`?suNx02HUY`2! z%r<fd-x|M`sDGV%eR8KD`@4nzLknym_x_UQ`8J$}y@@C)k#9k8ycz8&MaV$B|7aNX z>5F;YL&fLm4{8?eRCJ^<_wAdXKOSZfLj{K+lK7}5a4Xwtv&$StCOqQ|K9mwdOEme4 z06d@PVGu%<fyIZOew1i&onQgDlsh88lNfdPrGOF*&_&?uC2V|UY<I>_uj(;x{E-HG z;@0UK)%a4P^dtid_pevTpGlR~LoVT*Sb)GvTp7{yZ~3XvK95v$n~9B^?Mf3Z9}Iza zTVg@OBe?HSFck}|uqkEH)C!hm$V_EZziQwmW7;Nwa(I*(+TV4Gy3#1nh|?qQ3;l4b zE1VXfw0o`~N-vKb<vKG>TpDH2XSd|hF}$k#f+HI!>pB1CQ|jPzlF_t@=C&#>cmruS z<S2=)s7QAe&Y)9FP%h0C(6ALJRvPIlp8HOJb1^M<R()p~E_;V{dcA=kul5d`Fa~i( zEo8CbDNdS!;qtOCmAr6UYB88RGw6YNRw`dJoSWxj9#h7?(NuZqzzB76*vF20)asQs zbq~GfUaaw5Y@@UMZQ8CVm9Syy6|*&_C-^x_Wd`tvNQv6kT}eSSXq#y}M`+N?WK8AR zfK^MZ7s44W5&r&1i4<=+#n0vvP-;HBh_n_oyu&yCxTuLD7V@!p+R;<R)zVL4c;K2F z&X_#OrW!~5GLPj@^-R_~=Sx8*lZJ!Zanu|ofXB7}l$!qe-;ER39QN_K?2-*&4?<tG zLEB(}mVVaW@W%wvSfB6lZesZrY98JRCuwvRorDW(Bu{vM3bpOB|FrPNL&u<&UqtU% z?&@CGdM}RPoOslex!2ZSg4hxKUVU~{D$~#189W_HZRZ6u%h*PKa#KPl$EYSwe89&Q z+Hj0PIe%^;0eYo<j;)XgD|^}<7bU5R3&nH@C(5w%1g*98aI;wxiWje*I+t{7GT_%z z(;^Q$SAm5nBXoKd9j)eBKDh?JD#=+{D|6MDjaI<SGBM?HaN9&h#I){~>m|;dWlqK4 zg;-?yi-IdWrm&nDNuFv4e7}zYud#rnVsn!?G9&O6i||pe)l~03HNe>Ynq*z};b$#{ z<F3~6-5E}EV+Q#j1=(63Vt1(*;94ERHdL%8mXglYY^|MPR-LCs&izFp3`d-VWOVZm zxv|djY#o+#ZhNv*eYInq_uvs9Q2`90iF@X;f@RH#TySQ+Ukdy&ogpx@TPV5gtV!Oz z&-R=_A^5NT1>Wo|B^ElHXy<&Sc2Q*FuZzAorQ23Rmfp^6NraW=RDHbJr&!bEjw8Dh z2FZ)PG?6`jIsY7Rz64YL0ID}w)vKW$BHF?s5Y|4%5;b<@C4IQ}>Ua)f(EM@GkiR2~ zbGBc}A0mZrR|YiwS{GZXdeRBi6*gDCa~P)mJr<PX7pHdY!)-VqfZuj@x}hL5NHyQR ztXz#f8uBt-Ol3--QIG1``<7+n-fe=u3NV>m48DB(WL7nEcthSdZlD@mtH`fJ4sju& z^x+u7h(C`MsLID<ayzLF<)@B>3BEUP<wa)c9C1^7QpjeCm&T%St{wdrB~6&dwyY%+ z3bQR!D7zl$djS?*u_JQiWyJaIy=(Gvs0GxTTzdQU)Ne00{Ks4uJ|ySicQq%x1k;^+ zU)5Kg80~627YhG%$@RksHnoiRqvF;NWm1yS>xGIj+WZpm>Fy8d2Wo(cQY%B2$~)$( z6o<Mz631{9b)Z^J2N+W~xu4xeKnj9I%B3EpbCP}D&%CFIJPaHwlRwTPtZHhkhpd-= z__&_ifDuR~w1w5Seh@%i;nkNG*XQ=I#8B$PCrv9Eq3)sUOra$vqWw*&+Opoh-e)d` zg1nTdWzCD&XmN_E6PKGubAJDDNM<VkDIR)(<xDO$w9r3n*}zS#dg_^<pO}{vOk{<3 z%RlSh+|t`8Vl=?D&9;k=c|+QhO^U3MZ_gO<@4-X<Twnk2m+GrlEic)$3mL|sj>QZ@ zmUmw;ss=jQ#Cnv%`}+0dj)!rMt=U2^bl2h;(=aD;YwxpQHJZu6Nid7w*XGR7Lf1We zi5Ro(2w|bl6HYJhM&BBEgWfa1Avqxxq9f2scgEEcNTH5@%4Ke0@k0KvSaZ)DP|dnE zhQnXI#X>PX5@>3T30&rq8DtxY$jBIxOp312X>&f-Y}r#xx!V<|-yIUd$I&=Z22YZY zb`nkbrsh4dBNtGiG;Z`1;5xQfwfLbpv!ZjDa8mHcp>(#W+o3xP!Y|QPcNzm<sfe(| zWln`i`n2Q70Rm=Q+!RU%pVFUY@*&izKG64lP@7_r!>fT8I-Tg05<!JUeDNk8)t$?A zm23->IUF3a@#M&4UWQJ^YIQ6;<0?vZGb1C#+n-zG3%RRCRmYnv;rpT7s<|d<0rj%G z)-Ok=y++;0DKO1wWw%;$Or4*4jjgzR2_dHHe4^bIKs96f-bR;jsZ2CY4cBh(jc|__ zqB0S>?nHw)keyK>m0e5T+>ur_TxqEMCv*<0erD-y&c+JER<(VhHKzk6wL8M9sS6V- zMnPXHQnX!RD}4nWtqsaSDR{p&9ax6Eevj`hmYg%Hd5i3cNQZ1~x|@Yw<jb!a#z@!p z;6x5-MyLj?m&rv}7*h6}t#mx|5Ju)$Ecsf*YtueaotaS&uqnyry4Uj#wxOxFzq@O= z^69hmK!fkCvD5+02~k#};o)7=F_AQwfi&}~hPOz(_R{Xwym`O9{Ey=d446rP9m$sz zM!fQAndIFx*F_}@TD^>QmTv#ZXkT%h$n2rcDShd=(P`+N(pD;zzsN%gEiZyXI9AEI zc&6vV!fNpAD>GG*z$q;(r7J41$#QFx)bj5y7i{>2aQAPa19Wp0s1To-k=6NgDX8}C z;9IE}hL=|;Ws_ge566K}ZNPG2;p@l6h<ClGM#J#8;u&M8gg<*yo0(Z|V-Z<*_tZ;g z84?DjhH`BN#w`wPX*i(nkkVG7EKlg#vESMHrt&-SrrBUxaI|2oU)737X$YB3PAJTH zZj~<PknH1m)?N9v<ZT{k@Gl+U_oueipmJ^Pn_-ufRi_v^Q0=0at6xIZn<AG4pu?SZ zjD>k}q5|!Kh^{bSvby3`jf}f$85hKBXQ29w5WeUlo>tg=JHb~kTjr(S|9GF}L2}gB z?#bhqsvqUvG*rh2Sxs94Z^l?d!QS9-pbYoaGVaQAK=<s|0+*@&(I^>>nwX7?UdI?~ z4cNhIt?a7#3H)_H&f9<8j-QViUS0H;ABVSmxF7;xdgJVtGQdfk+mwh!1tNcw_oWs% z=8utgV@i+7m;Ev9E#E5jPK<otIZijLUoox3sQ!;kYr_aL;T(yUnxy%izuUM?lcT8+ zX3zksn4{i(JjHV>GhjYP7UUH~n+ltym!Nue-U<E2Q{rUb^||EUAkuRw67$PToOX&{ z$1ef?7zS2!2*XmrYwZyXb^Ju?ddH?wrBh*$b~_?<@IXp$v-z`-{Xd1JXVY1Tb-x5L zx7=-?us8_)n9X8S%NnIdQiY@Ut^Pt%T70Y8%rPJz&i6}zl`5I&a*{Mp>CLmx#*P+O z<UV_PDQW{fHgCo>sF$~uOf+&E%isjep<~eE<3GgtaxQyrR)k#H)zPN<aleSLxu`xS zRZx4s&3$nwRl@CsE>L59+f6Kwe@&d4oP+@j0iGHFWu&#7P<hZ#jH1}Hg1$e)uIEB| zn!aq0Oj`&<82ekQK8{4k62Q1he_RRsZY~=<Y#F$_VyvzjiE_`GwQ1UXgqrETsKnQr zj1boTEN07`y^1V>M5OAR&CQ)NgmNqjc+I^3H*?-$7Ch0VQ$lN`iQ|ttm1y9cr?(Fy zjZ<8hEj76G!gbcWwy5K*gg2Q{5A@a<qyis7A7+|63F|f-OE)<Z+ZE3IbaejDSz-pI zKAgpRDgk=3IqP+Df7$fX{ul(kF!^SYlKRc{h1;4i!%_E1p2`&NBl*(mpVap#TsM&a z2{C0Y3HOCSU8uRBvS>I3+>eJ%{(jaU2Arg<ngv4#*xRN&{y;-ur&Gtirk29>&;2z! zgam{01@y%kY=Z-|)AYNs+;?a82Eg5PZ78e$+saQJzx`!Y6qakw)5>=5*ejWo?Yx7R z_UH7tlsep*DxJ;OP+lT9bosAy=)eHk7=6Bqk%Ke&8p_eY;zxd4vpN-bUlD&2tSUoh z0byY&U|P=E?-jkl{S-yMe$d_D_TZN+H}q6<Nr~Bdf_=I%9<*j62?vUdmoEf8_6^3r zmu%dIL-Bmgly*4%m;T+U`(6Dqe;SsTagCbZ9dQXNwPb!VSnKa_m247nJAW_!c_m8! zX64Zq#9^`R$Mn}7KLg~BAELOz?&`lQVsNK=8GqLi5g(k^ScRAo$t}%`g>RErv`Vwc zs>)wj-vKjS2TodL6~0comw_w#;Z7Q}j#16-=AofvCEO})*`0hwAvi_NDA_URb1WzG z@9?SUvm#@Qjg8P{=%!@4B{y{Ih0XKcH(s+TmIQ)hX$M}kx1NP!5&YBZu9~;!c^zX8 z_@cn9+<H>qqBKSfwIpvCldWiBo36$MwD!F+DaA#IMa9p|R+5WyDFjImNvWI7x6Y`K zmKLQU;5(Zou#d_X4`^toL959$Gm$H->_Cf5zq1aSD&Oo355W5;%L@!46O;qZ%rjYS zuN)ovOq3{AAPGC>+vBZrB#-eBt|3qs^7=D6gFt_6{q_2W!u^ZwJH{IH|F$p;?x$U2 zN<&`F={~YYc?3WeW7nU~bw4)`;Z-xo5bKtSC0#Bml+p#@714Qk0hT-vDdN)%BSplt zrUuP?AtG)6atlHIL$Kejvf_R11HFjJ|Ms1cKKTcqDM~)mgpYYzgOKAhE=+J}Z;q9x zet4#Cft^1~^ANY!e@&Nq71p@^ZJr(F|2EaX8X5oZJ$29W2*VK;cbBl+;d6xr`T=n5 z`k-Gh+}#x4e16z>5LY^Al(<O!(f<Y;BXI*f82eu^GykDy{^#SK{~G;&Ky3as-@ZZI z@PPdOFX+wxgU$amGTR<iJQkIOe}bQ1Jib9ijNC&>^A+E~AQ)JHKnR$7pm@wN3iZ1g zPf7tkTMHCNffw_bfWW!x);Q=W&dDy0&t-Ls=LgIPdF`a3(y{X!bk~L$VB}(BaI9bq zINs_V5Ve49413Ij=zYf<i581LUe1}14&4Ef6lSZovw)7G2sslF33J@{-FT)N_YaOy zDg{Gv^xarVu_-p9_93g(KMcnA-;FSen@{&T%O&`nH=f-9kXQrPCi(fcYaiaIm7y0L z|LI#4SibcJ&g3~hc9V$_i4iwtsAV+!$7Awz%7;-<S@l0qmA~hQ^=Ezm(M`?+hi9y& zsyFzTg<P1VdG6T_kV<`F3`LfaaqtfzH9J34dxJ2^iNzQX-PmP00B+=YgM|{KKppac zB{>u`ibr`}V=Mkivp9MwbAwNr-~5f`R-{7x<}VgCR?L^uI$MLb05C_&H<nSOXJgCt zbNFcBJR4yJo6!CIC%54?l<0fpZy}$`AHozB3)BnL6|n(q;D*EoERQ~Y%5C#I{C-vQ za`G!_spa*?zpHj!?(vzGDZj@Hw--~|d|ErU6Q-`4Hp=gkqsJJdVfR!N<^M6cVV6^W z&T>aw_2l*+qTStX#aRAFNF3Dp9jJ9II4b%cT35v;79TEmd?lMfZKGH~1W*tH->9qO zPOsU6jGZNzkvLFfyA+c3`!BPZ)}ppEAs~n2`scr{V@>d>9~ESm`fVA7Yz@FIfKSKd z%?;1+do~(Fafe5O3w{BV@XF2e+i>t(zncLx*LiAj^-%K19%yTfHWqLrAyzDZ=|P0$ znYvTG=LxOeM44+`^KV^`C`_+kL%%6a^?dk50H0NOivkY%9po*H<1IqhG{d$f<E{DD zFGcZr$*-IF(<K=)SW{<&P!h=C1R<!P>}}|6)Cot&bNKQfSh=gRl945UL#NMMLVEMe zhe}lt^g9^-TgYxU1}Q8u5&5c-W^zkh!q;}=fVc{=K00h>Gwi(>0v&6f(CQ-cjMMRw zqk?CU!K3(Bj`bHWN;cO?5q^NwDGmBGm*J>QMrJLd+h6Zjv9`}jw9mh`r0}SHxOe-u zsDhD+LHe=9tQGIf^O22V|9<|Na2JFT6<p5y^u{2EGk#w7Xs&vgw69s5Gnczdgl?dI zLRDQ0IpI(95C0=4|M&H0A8<~k6`HhA>1<xj!^ny6YbzB~MOqvhp6viQX>YN?Nt<26 z`oRE2QRJ7ME}iBfCUd=0^*Zx4tlpnzufKz!_rfw?DkZU!oxhCI3-7O^+<Fy<fB@6B zyJ)U&V9mM>mE)A|$o{OBf7U@8xZ1fUH4p_cM0rsXprtvGQiE(M_UQFBxyd2O$mo_t z+GM8UI*aPT&*DU6sfQh+YT!Ts9S(KwC9MConl$Z6bJGc>N!9B>O4Yg6VU(Y+zFDW- z6ctX=P2@D;U2dkJkfHY2C@T$g8C-LijC01BWOPy>GSI=C?s4?O^ymOk(ZyB90F_Ga zhyC6<Vu@=_sNZ*DAE7hol%;}ibtY$@U3rh@<%j>auzNr&^8n6BgZ8s&&{|EE8J$>$ zg(G0C9mFLV1{p7GprG}0OZ8Z46~p+zK@nQ59@Eg7TX^+qX>l{rE&?XLdtT<>d)f>o zUm>I+!U2w|=1WQ7wQgw*O^<bd#q6~X^4eDPZ)y*`)jxQLcJi{(aIU&O>pQ-zZmQ+p zHR#)6AE5zPgwyp$Z#;?kVr`0W&f-NP+HX$A!wVhP>ZA}C-9bSeLiVWKoM0V~$EB$l zrQqAMOga&D$Hot|POw}RwJZ)8t-*AuO|NSAUA~T>^G0&0v`QG5tI=N5L;s2$N_0z7 zr1u451k6O_N`lM(8Z6))hF00XvMfaK8<BV@W(2YTlGz<`XsvPbQCEb7O>it4xchE1 ztIW`+mbB@;^Sfn@j6eava=ERO_ZghsomBBS7byfRtsm~J@6>enSR=UAfA_p%1~yZW zwgJzZY+$f6MYucb*1@~^C(*FqVKDjOwO4jen<P8wU_pK+hU=dMBPzr;Zk;H?M;JzJ zbJiYs(htWY3fL5zT7rhw>uOiJYC_L_7Kx|R-S{5$Tw)JuF|jLWkVBurV_Mvvj5ky$ z6`5VDhNEIzU)O*9?jSS;onrtBv5nY%$(~9mHf$(wwLGV>R?HwrS!2w8kCPY^0gCXg z*c{1VuW9imQ%|>R*QeHB88~EW#hX3d;=e{h9s@gZEvw2RldfB*OcOjUf#>uCI}vw| zGM$;%UkJnRzjXhV<M&YI8JuP`H)mko&~yG4R47|Ov1uhv+TUw>i=WC!VwF=N7A;U@ z*4!gL-cu;jdv{6P#&s`$jd=7>74K<JA6_Q7o9&u{ENm60T3P2iiqFX|;F53>kK9kf zvn}quu1t7a8plsXe#ZuKx*o#=I}TPYEbJ6BcBP<3@`hA#-$ZEv>sy2EpBisrGx#9q z#t+>u2EO@cZPd(V*33hN;dkDNV@s=eEkr^VHM~8XuY&96iwm6-)xjR<_Is$Uj@QIW zU;a+mw)8&LcZ!|Jj@bd0tcZZo?uxe}t5UX+Hx~Zeh|J8+CVdNoI`G_Y{6+8l^qIP1 zQ+wZK&ct$amUC6h$FuBvIS{cDSI<~6NN{K<)8Eu=n<qsBkqB?0dD6Zq9V8`B%Qj=~ zrwwwH;Dzf2vXQnDb80*kHaagj6(|)@Px5jnZ2u|P0krx8q>$fRoDjfOf12(RT*F8v z$#L}8H-i4h5vSRZ(oYH7=Rtq49s{xH^Z*;o4QJn_mAPMsk%-t1mxteBxRwlhjbAA| z+ohWqi~li15Geh%#>gRsnp4GXrXB+X>U(Y7q=`>aaV$~bc(LfoW3QN&2stC|{ZBHJ z2@${i12S-pGTJejYleHg;LO}udrE(9ig$$p8ycdHcsd!ix;FW2lj6eR>e4>&!Vxxx z2Vd6<r@nFA^cIX{AUz)RE9)#xD{GN6-rQMbX;Mm*bxXj-!X6E{?rxm$4xRONK_tTX z5^9(5YdvZIYv?H$04hvrvf_Vv!edH}(bQJgQGE<%7!s5p+l`Q$1m{;xDAOK!vlq5p z;nvZEn9uJ#K&_H@v{hiO=u++4#>-W~rmR+CW-uW7Ia!=Ng0&;o*@6*Jygx-(^spYy z(2s>2YLX#4IubyLDSF37=#P5cqQmTeKHS=`g9RiJg3Zofc$SY{hk%gl-?sQubgfsp z7`oBHrVJ&j;P|h3-ozi^>c9Epywnlsl_zm%>)yMlSvU3=*n{y%$ZO)!+%pTfXVx?L zC!0u^<zwqjC6Qc5MJkDKbP!*|K~eU1!J-&?1vnWKKnljI@o&-57>k>J4@N0b-j9G^ z{CA<FL+t-Q|FVJYcO;;rtTMohr#os!Ah3L{>jtwM;m8Tx<ntC0&D|v2Cf|gg9w59J zj0?d~YZRw*93<zyaQ};Sm8kv$oG+8y1heE|c={h+z3(5qcYe*(K^ESm*Cf>aBH#lY z>xc?f=sf-+08EZGfj8~@nwZr$z@K!WZ*Ot|3RAq!DUF-dmSD5{sM2Y)OEC>dzotMH zKAe2!T7UD;5e3TDbz+R<I~DZJrDD^@<ZG|o-J3uVtjl&I322Ymb?R|_C8{560Ei*B z$;kpq)*Dg?qCbkG3m(1@4KpW4X}m#4BhZu0x{CjLK}TH)nQmHnHH}_+S%JahF*|S* zau#iD*@7w=!M(rS%;jB_`(uEe7R3)4EIJ40;<AApbw3UkMWh6Os}FWBnjLX30}IcU z+`ADaW_`FFeR~@0t<=QVkZu8g8*tO6l^n{H@^5(G_x%$gncum9FS|E_VUER7CzaYX zHcu%>apE;^lyWw&YGRCpy!yx7Y;i{Pd&48(3;#bgqLgo%f$()B_^QuO9-!Rn)Hw`F zj{3kjeDMI)SY)<9P~=Z|_0cU{y=wglUDK_1N{%CuX4vbb9oGsLCq_SB72^%ZMzoq| zlX3t2tU8v+*yZwITI-va)`+4zyIgybbJd#WGuAjB_uX?mFHSX6%8MvB+Wm!Vo_mMZ z_4T41m(T&q+nr;htMW~CiL-ceQwk!#c;Mb4gHT2bT4z@mi7bU|lTMrpe74GMhzI|P z9I~{;U}+Zj(dg(Ok%y4IPDfF<7bhCZj2Av8LTx8fa1k<;=w;kDvoJ(s2t{p13kKpO zIDT4fCQ?1`UG2mVXJ$Jy0eK|34XoBO_6^!qQ;t;&6g*JQCxQO1L`hQsE21jxVLMXr z2`xgVW6^w?YlUMf>2ygE;d&6IdEB@Z)^z7(qv<moUUi1tfZ&kg|5y<~)3Nt&1FJ$# zdm73N0KlHmuN6^}H<!D)s9-8{%ged`vZ7%5Z{nt@uopK{sgz`IK*9U*Hk?E%cVA#c zkRGzqJ6P&rMi0DvE%uAcOmgkehzzQBd){U0+jh15lmb<->mMe4EkvvpJ|RhaH2;IW zo-!CZ1JS|6l!U>|zClD__WV7_P_y~FHqWdA(aF=xFPt~C%=VB<_f~}cZP9+{1r&l~ zpO{jP1X5w<_3qh+7Xz=T{%R1J{XTDD%?3!&W2JoSTawUY<OBntuGmfqRQX!wPb&MG zU_-(8_%7j;cy@kh!y_rAL6*k0+dD)A<W@A9AHmDtiP4)`N18<E%RhHCzm~xW09=_# z5qu~av^_%?<9wXYzgxPToLked@TWsSyR!jcjIDI9;3>jlO^HbUDEtSdG(i0wZT`Yz zYg6n#_4X7eD!MphVV2k7Y(gn#MS=VXf^)G<T(;eclMKV5R<WX~_AWkNO|hcTQR7r! zEg!&cFr|GFm>KatyG(-Tu31Al55qch$SbR<j^7U)TE5x)7z)c9sZ&`-8FSWDPGh;? zI;V~+IZAX_$!r+)l_%kT{F1|JWu+6{Tx*1^b@)ImKTVcWY5Z=96kT`Q`c+KNi`etp zlKqUmzhjz5KLXRSPik2WPHaZ{=b^X0lbHKGT@C9|otoqZEnGx_jv^)bdq29N^-?k+ z!JYP)L=!~;Cr{zMPC$|@C?rd#E!~Z&RmhlgOQn>4@|`lbKs0x5g4pJ;v*DIHR%t9- zos;|by|tEVX1~*g8j8he+h<x_6-i|9puh)P-&+5QheV!N_YHHD{0Z8}B1J{vhMf-d zoRsn|_kK|#Uj5Ld6lbc3rro>$=@HM5Z#=_<VjMF1u?70NzdqwG6F$hSXW^+)y`5RR zigb1OQ1#V4Z}bnotA#AflCWUQyb9&S+LXiD*=PN7)hAL?I6YncX_mM2?TkHi+_gT` zE+Y?-ynBDEIp3W_RT6q_KD(oZ6QZ9etIToH^t!hMq6xVbktrgo1WojAcgB8+!VQc~ zR6Hbi8*!v`!mWf4Na?(EO9Z-pRd^B92Vr}sX1&o3NJZOiw*09zNGr8hIP%Zfey`j# zaQNzHmq>FI<>C(pt%MHTTyqja!d2!6f4x>~rUl9+6p5`j4`0$B#iEPm!$R`^VwP?% z+@S-;wKZp$+$w9#CYr8`KJ^fY!SzDaD_9qNKk-JI{!YX&xwvR1WF$S+Sy>EDzRTCQ zsgiW(2<LvU{>t<V(<94hmNZ9cyGLQgs7D#;V(Q7yyRA8`8%5!NW$X$m%)hXN_Y=;v zy<zK#xfJS(^X%N`@i={@N_u6r9ostoJxw$^`{R~+T_zp=t=H)3fNQeTfD<YALaMeE z+iMP!4R{kGQSfQ9q*0^t>nW}HqhQBS6hRKI`;O4~1iP#wEW2=KJJT^RMQ?SI2_28D zo#i3plg&^X43wuLD)u{<F`LZtsP+#>j5Wn~po055&+P$Mlj;wP(ki=qy|x$J)z>6i zOC>+s)T${6ncnZS4N$sH9*HichO*ehY^gMU_L)3BL!(4Al*$G3?v)Om@v~Ld<*$8j zG+0A?rwK=BNL>aF9lEh6ljLC?^jP(chKHlM0BO4yIHhYnfm~%jzphgvTNehMzklmz z>*MiGvMr?d<sFjl9!2+Smwsly>lpE|_7I{(olW<N>Ejt(p39{;nomCPcFEEDuYMQg z(m@MYa{3mW07nqs8V?;xz5*=Kdympjt)v>!JQWO!0vJX>%MwI-{UH$lLzdATlVbCm z6Q`IHL=}qaskP##=(icSoHx4+H<)}g;kEZ0W8VAbi**c9Por#)y(B>x)4W4y2*I<v zpZf<z3B)*e->cShP|w%-;r=}wc3k|WzBDJN#qM3IW}OM<(SQKlL}%1f^>cU8*ZYy7 zAG-|JG=o3WfnQ7~+~N)U{Ol*en*g@MH+^fK&Y_s=T@8_3T8gI4h*nGV3Rh&eqxaa; zDa<Y4F}!8{ht9o_NE;@y;A3+SeSBC_f6s7$z`5<V?jLV@ppU1)Fv$o1NtRJjA_)YR z&wXgT(H{=+`)<SCjZCY2hY?tLg6(rQ(B0KgAY_rh@Odo+7R2H$?CS`7i%8Q<zLE4E zJV|z^a`myT<XaACv<Bk!9_iYc!a5P~5O$jy$hj2`aXNagm237GjwhRukDu`<y=9<( zq4cU*9W5U36qqBlbt&r|3L_<fUNDi`morh%bF*wbg3o8f<%p#r6uh@hffE6^`&d-g z3`NN7@l;+vM}nextAK+_nVLFvC3B)1CA?6~35R&O*TLq~<)Q!uU?{9=*Ak;5g~V|< z3gcYpE6Q&Xt6N)p>4fZCGPzv(abO57j(-4`!PU~5p(&KW2L{`f>KadKK6>Y3@?|>a zc;UWFUsFWv1_BklLm?gD3oCqd$F(GOmXy=M*6&r{o$Hm{kC(`>!^(!!!`mCBdQ3VB z0vXP2hyD{KmA}7iRMv2#0<IP~k6Y5Lu&t`dd-!1>20PH7=Fa_FW**@R-qODQ{{FP2 z(3D;p%u{f#6DTu)R$>G?HReA4sjuS*o*!8RV&a+yzDg-i)y-BuTuTm=oLU(N1<+kt zV4j9%k5}&PrJmm7(PVrszg~_<R#fGE3);DoE&Jx^s!&3{2}j<`pAy)TSaT~JcpVuh z_md0?FE%_)3hhq@hKNX44Gh}~Vp-5#xVlC<G-GOI77?(xuaO5I72WZ4M^!AawS`3F zn&v){JKloVZc_e_L(1OOSc)+a@45fOtf%qmn$#`8@Y)k8MN;BNFDw=ai$hM7U+x_* z9H1IE$!62z6e8M3Wn|QleoPxx$G4!j*-gwa?pT&spj)Q+6`FZT%mXs0>mWfXq-ItA zuZxorxnqBJ$>2Fe*e^a(k)7zCt@El6b%m_~s!78?vII)fG}^JD-`Ysuz}{6e<f;ED zf*}kwEp9!b1H{k}YBsV~D`Z0*_>yjfX8>7>N`sxDaG{MD*}<v5$cD505Qsz~ejURT zF)vV1`kZ_D=9tW#=Rv^02jWi}A)Nz@qen!t-fis~an4AC7m<PS$3uIs@gkqx0yyeN zsZLUB#sKb&?y?|_`v@1bX^{XnH0$hicA(9|iXgYbS9E!W(uw3PL}zRx`0S{z72iY( zxh!p>j=Mj~Eoi1(RZr>de!gW}ea(qxds;a|mX{;l^b9?h`UWwMk~d;TbfgK(9}{8g zPJnMc?eXDOqaKrtDy)3quhN%CoAEjo*$v*&M+#I?!><&CCy?#@iCkIZYX|3wrWCqE zJA4WS0~#j*OlbnY?bB`13o7fq<LmGJFW>5yta4?f=i`^03Bvnww61-!N%C`q9YZnr zP~k-Kt*%jg)Ok2WA^R}3iCAcfI0Vy%`bNdDm}IC#?%R4s2zUPJjjXv}9e#Au7;Rvl z!~GEWc9fXPjNSJ$2%RTAyaXxo(cX%C`}DMh&-WqR(k&E`*1+RUUguXh@a2P}B!O@0 z5w>+?UXXRwAtnN{9?Sq_0xh;49*uFxOO1Z<6(Tt!cYU!MPeJ+PphEQ}(hJ*JnUI|C zhNaK;MUX?w>ZHJ<RF*F#cFG#8d}U#C0Upw*_3l^aPa#pNR{nOMG}3UO7lL7_%!&Q_ zrh3y}GuGby6ZXJB49~YjN2U7IwFrcT&BBi90=i}sgnGu@I7XpUbc3n+UoXkG)UW=0 zZkoFUode#rg*-f*`~q&L8@J&btJ`k%<Uqzfdf-A03EgKfFwCuSR7TGc!8J(?b@cxX z`Lcg_yHqLPkUFBu%(DL|pEZa=dC+-x58u-fU1n%9hl3hyyEyNUc<?}pprW?l^bu9m zKwZIaiEb3TUSMGfw>Qw;o!l01vTpkby(0h1POl6PNV2|2((dg~)}9D_t&?6Da~Jab zhwWO1U!E)lOi$REVFN`3t$xn_n-m&qd+_duT*~-rdg1KCJ7u)^L`t8+@oKd!)zrXC zyRu6JlD;rzMD0Se!TtiiT<5YU9~fYh(ea$^d5_4_mX}Ox7Cm3$UwgLHkBqRAp=7d8 z`%Cu|ynLQsE+q-c@GituF_NW}2c)z5V5%g0rhBBjR}TiT28`GHS(VB;5_CtVVU@9h z_02FR#4xR*-JLscYwI^XBMBMOf0wUtOX)mOHm+!ZSg*Yscki2GV7O;D<mG_Ph3jNz zo8poVsMMha6P~2ACYiNsvhmInj$z9$8)r@hn7X}uk20$co+I#jcv?gL)Q%H<zaWQg zGlNzXT(MH@_hKGfC`!qDI{?9GJHoYG+wA%A3vxinQj{o{aAs0eVY{KGc?7sfF~#{^ zs^8t&f9{k3!Ri#_ows9tM8*DWvMD2AoR9YLPR(TFFGX@W5rbEYBceVAhOi!6k=MEQ zF9fGJguRT8$QtxZ9CQJ-Zh3p%;r6Z;75UBg4BVQ$K*(b}t<G>JftuP}^UD?!%+tQ2 z@w_?u3=(<XB0uhZwfkuE<+aa9Gry<L6OLS4+MN&ALL=bXEl7<R<L&ze9BNLun&4d! z-%)4F5*I}dNSg0T3<P=ZE2AMh3bPA(ue<_UB&PT_7qUucJ~P{e>AvyP3H^JoKcma{ z4l#|tU@S79v9cXUc-Au8q|DHw^-J&g6yf{$6?5ZnTP*@vehJ-CdHu$ECq<Ryci?Nx z9bbblB<FjqR51q$5Y2iDoewqbUs<e4Q2G|KVc}_N!~^_|vellps=^y>BP=mm{`bC{ z@y)J{+3IO~yWeKMNZj+W1XaO7(fR(!kC63t-^CXFd3lsC;8!gNhJX*`Mc28mvkN5T zz31x2h6Cwf>RMM9B)2n_(LokY8FrnxZ0rdov3dG3gTn*E48VapxyY&=5U!7RMVud? z&j3k^eOsm$Ere#A&ploRuv@z0sxP@s7_7J@+D3kVy#Li+q2Um_?(d|3^BE>|cKp5i zw4cwf-QelgJIjJ^d1DvrrYW(z#&oa^NlDwT73QSAsM-v%%$dPNzHZM~Zla@~mag$- z_DG);VH}Y-KzzyKkeUR8hphJ<^c)L1==`K8#?{uB98=!bKUT=AzyG|vDYdn|d82vH zGVZmHF#Ij%^yqd<p~c)?g1caU+ur#d*J|CTmHCC)qjnwRAKzEZZ#HRg7n?bY$H{sR zprsSr2q+#E5W$^i@>&*<mok5KTbL{8>OWU5bv+uc^`qS`8-EC14#9!Hg*jGP`Fpt; zu}n{qk;>D%;&O2tRk*U9o(R)ClQgf~?3cI$$WozU_fS;I9peaU?Kk`0x~c}tA4H7U z++Sxn--`_Pu{~7lcpc)Vuf3UIQ8m?>cMp9VTF|NkcYaS;_XB6*eaXhjS9h^Ev#(JG zGp4r6$t}bnN3hActSxPM)OGHW!W-?ZRmU9t)&)#6i|?U;OA*h?^OoP%wmr&x8O3;@ z0dVhtbwjb?Zy})m_{^Z}B6pEWAn_0a(&X~^H|%bR8&Mw@#N53hz>i|?-UdK3V?jef zq7bC1m_y&(@BoSbJGcLnuUXl7pa+fU#-LSxY>@t{@7#IsMHyGwP)}Keg6@t@W&UTg zJZI7YPu6VDc1Hb)&yVx-t?Ik2EGhpD7mB=Jyb-CP`147k;A+q2#fsp3@v}UIDa^4# z5@YV_s>e{=-}eE%Yb{iHTh6BzT5*M-B<pv3RbAdd<T=b`n_jB8D%UbTgVRRTWjpW) zb@wGyB#F3GFSZPmt+ZxMq({V73yy5rCz+*7?mew#^ZI+b`UF&ylHI)KhJ(JW(XcYT zP{939d55vKz39DMCknaRj`+Nvdi=yRoHUc<Bf&=B(RKCv-4_m05`+2?pxEyf2SJk- z7GR-@BR*!TNb=fO^(G*<mQjo8BCYb_hWmqJSsr<>+kEEcH(751vPP))H-?tG4QR0G z1Y~`3#ltW_r!|VAvnBO?^U6m_01x@uO8R0K2^l-r4?#{;xX^-!F)g6MDFg}z;AaRN zvY@~?7%yy3TYRo|2rv#@(pxQx4bse0#{$jq?4LA=SWtxBy%Dm!waKLBzHU2?=JTeS z?Dy{pERT=6e_L~Xk>@ICeJ;AMWd%+#;XPzS+b`An%E8SHwCRxG^R)3D={(QZ>jIi+ zSiUqis3e=`3dvt;^rO(O&@lZi_yBq@t|lbe)&M01l)uB{>x@XxZ+Pf7XKC?=O`OkL zaC|W11!z}8Bdvh9pj{PZr!>XITQ&{TsRzD~@&c0Y?Ujm&pUQl)SczJCTGzHkRW;G$ z28?$qQ=zLA1?vAP%LAqj*N&Nt#P@zsw0}G9u$Ox#u&gN!2}%}Mbg9|$sfdCE3D2t; z4ye{^=j_fAOVd4G%7WP2dD`5lL?MkkMkTZ_n!LNf(olNz8#v}p7%*aZ$TnH_wBI}9 z&)k(yX7%Cc*4t=YS&^V}OrG1ZEEO+h2UcP5ePr4P;GDmA@k1W7LbZ$~7wglvuv>7l zKE_IWrKZX6-t-ZSd0Vm1MS|NIsgj-_cF%})H&}Lbh+Ry4eYO7gj^Fia`59IiJ{pmv zS=kf3MfCvHI}}%3u9P8qxIa4at$}-!O<<{&c*79>jh<1Iq4SCmb-ne*RFe)#avuq0 zuCAo%U<YJHw5~LyI05rokvE7DfD*YCskNARX?gN#It0xSLZ`j)2G;xrLzvdH!#fJD zf;sd3pZO}wlgdpR*<LwnDGR$)6X5>VvuLyGPvVw2ceufz&_v=Xt4;#%#=H43dS=8L z{Gb{XvwdJ(+$V*0N^WXhS=MULPfQfdBwR1ydIPx)fJbejN@pDE1D2=6jCSw2)@oeC zOzl#2cWv{!DMK{l_=k?4O|iIbEv`)}(jlSs@$M$fO|*cJR3xJ91qWQLAm$BbzaV=w z86fV6Zyx{SP}Pz>IY1{gN$$S79gF#!+Y&HS{fGdKFuJr{&|Psn<iF!w=-lcS2|+4> zekXvanrT1N{I;H+b<{mcPQx4Xh$?0_!*Q>9P;62$=|OELC$M|@ZkaI-p}BaPAp<`3 zo{e6~Z02Hl7D?Xkl^aek!jZ84R515>{#v1<`Rjb>^sLT^x8%Weu6>?|Rmw%ZLl6ra zQpv1$KmX6qeDaL#6`uo-69q;Tou}eB#l*WuEcyC+E(&Rd-{)8|2E7&2`{-wMG&6aV zlE<2;tA8>Zgp%wqFH@zFUi)m|B;eO^trWS~$Y-?GPwn_}E>p5Kz{JPozxMc7+!eV! ziAModLpKqgs&%Cue^rZmjt%98j~<%z=^Ue`6wJ(82t7Jvc&8MaZlh^2rhWzo1Z&#} zfE|va14yBnt&oV25ufLM%x&?nh&;)xPMD|}7$y%X_vp(AQ6_`Gr=QAtr(t@=@?YtM zZ|rq2@<?G*_YoYW1XRXYP<_omRL~TEzw7Y4yi(vY%2NBZR0xOBX*FJROSdrc*YOka zK<a9xY64VvTmCY~2;Q8jY5f#T;S23Mxnbk?guXkLi;eUOQ~2}~I$gpn#M7_Yy&P%{ zxZfcDB1%0gJtHvfLxmei*OL`P7YT<WPN{9A(yLqWHIvu(!!d)uSPp)S4^*eGzE2za z@T#!t2^sk_K|C=}@v6FG8TqEBCR=iT*=UY|akh*q0#EAF*wiJuLzcgG#MRL@a%;{O zOSb66ao2QSK`(FlisEjb*{#;E-YHR-tNfS?pPklDV;LLW@`&V1IYJ;HUSzt8dP$@u z7QWwx-kBV$;&Dz0vp10sOUp?c>~XA8CJ<7XJ6{#>d%#yvEgxaPEyheq))oj=!<yAw zI_`X0cE3^li|o|vgi`f)KMn$cW044iLK7hrw;iUO5fJJ9v<CY}qDjD?PbC`n0M7<h zG(o|D5dKu`-_0X90%DRD(;1G)S?<E+2#8166Sw51s!3`1VBdixHjF<HMDk&M6^Tuo z`ibZR=yRO!0N+BRsJOnS3hs-b3^SF9c?!jsc18WA;s?5j>ErIYwL~QsttWG`(RIce z-WA?Ll#W+S7W%t+qQ&7^dS$H(uA!T=tIpA#C&*fj2Y}oAmXTX12l-CICpBNlE26UM z$B|*>yf9alM~?-Qt{%QnA^b6l3}b#@aM4N_y%R><jHc9Cr#mH&J2sV@O1G0cPNDOv z45MW(=jos*dKNJp4v|?cNT}DetRIb@d_6U`_>eaOgBk=U+F%|Sckhw>xyIclUizev zRT@6DQCVv-={dl+wlC|g^dnp9aWCH;1zdPbS94ll-DBIO9_c&-TD2lt66g~x9a?~6 zK}@@O;htF<Pw=oT)iazAS_Uw+JHSkWc!T!SB}Vt%Z%PrvZy?t1sbdoZTpNlSP_Anf z%dh!vbZ-&C0~D^cGx*_VCP_L%5@p5TR<q|`CL;sI5V`N$KfXHsTNu+&e%fQW8zyE< z0@ajhcMOd+zbs_G{;4;K3+FW357Tqvnhl4%CIvP=1(85`hu%Pbmr?NkByD-{nhk!k z5XbZ72R!6P?WR&sA7#&*Vb^M!hbFzdYF*-fF>W1`(KOM~vk{;^zIOWFwOsxdVpK6R zT1>?9b8|fn31!V5DvPvtqWLQjY|6Jk_cAS+00q$Tf6cRib=OtSz07`3fa2On<Mn<3 zcPt`-vJQnq9&u<A2+70bj3O|EV0EXzq_)hQ040YoD(wKC7<A3603(G)KyoLzEFMpc z=H7xHQ42hXOkl%D85PW42Qr{Ng1>F<Me1+O%Qrbqgy*LJWtUO5egLOA#xl})^HIrc zr^f}2AT%dN3;2~N5dvy621o;WuOF`HbtIG+3TeE!d}22G9@7Z7_Tu}euZebdMaC|P z>fx6;*R-^%sb2qNq^+NCp7ecqmcyT<s3rvvGNSrGg@{E4+tD(>+#+K@@}fx$4+^ah zNPM|ZsP3u9S=%JJtzAajn$MRqNVnEMpHN+J9!LUDLvj~ZD|myqBAlc(+<MB|Wkkuf zGa3+s(*um|5kLAlv^Hv+zeX(QraqYt)89X+1XtPm^>t-q`^EBsKt_J-Z=KxquLAs0 zS>pq=>=e{BS6`cc_UoIC$9pzu@ulo@p}D}dBn;m|=7vC4r>ualE=ta=&TFJ7&P;_V zWnd5@Qj#PXj@UbW&jueCBg)7p1b9lFo(<?~-qv3Qi}+}M_xbrb1UuShMV_m{4!`L% z;OaefB&$;3<kdV;WmR=y%+z$^EKw!+h19s3h6vg)(XN$y?fM=7&eFAJMiMD~t=hYx z0mcRrD+xLD-rXNlzGp9y%WuiMHwCD`V+#Sp0KRvah_0xTy=X~kh<(98CPI$tunf<} zEuI7#0GSi`EYe8K>6qb4v;G-e5tiT6nTMeSP(o+Lh>7S)cvI=)eT$V9q!(HseVIl) zAb6rX(;oVRY4yM`p-+x#R^~B0On}(VSVldezlc>8xC`XFvug@9@Pw2y#HYANaTHkZ z+^t4*@;lMT_tf(jsBay0^NX`gvGR8FID%x6c$zk)M+e(-vyc0DVJkHdYi}lI(@-RY zN`tcrEpXO2F+bmFi)aypUq<O~<nJ`u+5Y@Q#uP=F=$qcQ?R#7Ib4rueoIUDgVYc)` zc$7p=ls)@`vg1@+&>sP`?v9oDP(+zd^Z6L-qqlSPMCDyM*8*a!ubdMn*V(k<R*2Dc zci);34_l=RzN*bn?xRb%SEZo!u^F%8D>@5l0OAX=r0@ky<xbLCleKQYb6lk#8Y(p7 zom+Kl`9$db$s9uV93N`fShDuT!a`NR|5WK7Y++_pVR3Gj9^Ac3xoZbb+y~1uNCK5e zqj)IeKaf$m4p?1wS3j(ON5!J|3cFN|5cN`(+<Wg}W;D<^-?m3otsS6I+6yY0S51#S zs^?<cYVzpz)*Y%eB(xTB;^cd{fUx=P!W;J>Fr5%+Z)b~Y>y;y#U@nJ8i6PT*%Ojo^ z7L#jo&Ha{0O>z-fGkj%JifVuxbF29+u1}GEZp~E47-mUpebyT6FMzg=ksWG^ciZe+ ziTK64JWZ|f>MviPw`+$&208sE@%OO5f&M~|^IyaTHOv7rFA`u6{hL10-~N8cffZ44 z8^6d_K8&rWDMV9{gdmxDR#mvxHiil(<z8@QnxIoYNdde4B?BcQv?}o_tNFLD%Z7*J z#L%#j#<=GqOEyP+E;ux$<TFrJw@ODUUEfq7Tj5xDq@;}ma6S1`yOl33=H<qnvU#>* zX<N-R(7*dASuOeVY<`_l^IYjEDO5Vek&5h0engyI6IWiq!ifj{?w!!xEH4d2JiB*g zME2)TLd=(6W>JUjnW>V&YpxyTzpB1aS$`ppCMA6U?`a`OE1SJnNekFGhRSzlT0t9i zByYShD1yY=eoXWYFj+Vr=O89OZTjKXc<q})!Q0;I;*S(2Lq1dhb9BITEpkh_Zs<GR zhp&t0*|6*_kJf2ElJ~~@^uStBEheZ=xAYgQdGLA327kT%j4Ezoupie`gxzVG04q;@ zn(0_<gd{hV#h4Pm#pTn<Jj{^WrF-_oK|jaK6Kx@iXEKKkEf5z&r@tPD@4JZ@+B#+H zgPeIEBbE6rMtfth%N=Ep)z0}ysK9W|B>DF(RF42^WN-Ny!~WS3o7u70l@St0e428o z6SWxu)u=k{@DmFoO0d?5lz^jtGtdAp*w3BOm=mRv>H46zi^2G)WIGh{B4SH4c0~g2 z+yB#s=EG;JpBqRoPk%-9n2b!R?OZ?v;!ryUubw^|S>=83j}Ix(%syf#ejB_7tMi$c zF+C<twL7(|53oP(r{Hy|ul)dJCx5LtBK*?R(?0+WfD71&b0pn|#ipFF)rif><?&hb zprva}ydLT|9RM3~Tn@$s43bu}H_!Q98_XYkIA!Bx=lSHrySEU$iNcDhQ@Iu4?nup^ z>q^Y@Sw!UktTLy*>ZW<V*Z<n=<=zD;tk;F=`yGbwY$IvQE&hrI>9e~c*)SaN;6_7$ z4;MVC5v;f$iP^Y0vJot%4<e`|A+qpbGVmX;IR9N}8{sGpSgjI-(8IC7B7o@ICsIX3 zV4X-Nz=sm7?+-$D!1EqK=+7w5jz3sm695l|yg%nE(RObX=MAOO)U5SOREz)b;l@HU zBDa{l?KSW`NX28Xt6$Yef0y*DqcM!m{)T`e)faVg)TEfvgUPFeH}{}LH-n*i)fa0U zRx$hrs_-ZX$e#l&svpl^Iz2V@6`pcwB`E;jI68$?41x2_owObqeHzcx`w<G)e@8Xh zN>43FpLd@KwEQ3KeRo(B+txRTfJ#R?(nO_6kS-miDOIJ35Rj_0ATac%qO?d61P+8E zL8S*3F)#>tKtQC1sz5-g2_-b??VG?k=X>t`?sxxw|9E*Go|)ORXU$%F@3oSw@>`{e zQt<K{x<D+c3#w>YiMAdKYp_mT7L|yq2&vVJ)W_i$=LuZncB*W56e1n~Dc75jJ@hRt zb8{_sO#8gO&yt508H2&z`t4ov;>%CAI1G&=5KX1jTz`4<H(}JIxGrI1eY#ZE>yJw~ zI3>l1EoNc}Gm1YoWW$KeATWBl8MUv8?dw0M?hW$?dasL>bv)1tQk<?_`Q`!rTt7A6 z_KTK0=i!eD7FZ{R-mB#TDLx6&vbeR*wIQTmLglPRgmC}ftJy%m@t1>|B16A!Sh{f> zt`bHP2GdA3ab@Q{ud}$rkFWQa8Z-sEB!+5jO3E^;a%teD9z|SpeB|}{Ll2lLOuSdG z`x;%6+$Sr6w{P_I-yE8ri^E(<)m6;S7SPoS#`dl$!=_@*c(HpTSRv4h=3%ts_eso3 z|IIel>`i`n8M1}Cyi^oS#jdD<!KP(|(GT`Q5^!wxuNV@F@~13ZBb2#>FkQ`i(AtuU z$R~7`0xi=V)z<y`irIZ(fJp$(2s7w_kn+06f)6;R2~;u_#!7qEpak%?qb*H8LG@lE zku^!MV{Kt(8C=8jGUL0bp*jqf2%hK6Z`9i3R&E*%3>OeCpq^Q<;cKoFZ<J%NnAhcc zo<Jfo`1>!Vaqwdl+Lq?=EuP)WV*Kmdef}||&EiqDPo;#9v$$e($@TNN{`O(!@+ZdM zZM_j`9PoI#3t$^m1+^GfQ1#JMcB0_pccdydGGCqO@l49&eNT3{Gc|9~h=LnQ<TW87 z)JLZ?*vZoI{jKF}J!nT#pjCQqVLzgyudprue7obX@S}Eq?c0oAKu^>T^r~#*58;>d z&M*FCpo61#swtM{r!J4A`~UpyGqpP68M2Ud%Q5Rmd_1mEI#gO$gru}G7ej&;`c=`6 zOc!3LFZ15(IloyBUO4u8eLW-&Bf{XKdB$DklMxG!jDHfEwArs>c~(<;+=+3cgyf6z zAd2`UiU4DvK%+#>)3)^JO0?(FJr+mZms}v+%-gi+5^IdLV<_gJUjSAn$=JLZp!O(k zxQ${Umf&VDGQ&uWlo_k<b{1xT*e?i<BZuq6@8|z`c@i%R<!lIooT){IJY0V)?H=eJ zstbCkuq@p}!eeMP3SvkSkZ1{lt%FaXx*AYtO+l~=n$=??wos*yozT0&?2sNgdf=FL zfDritAK$O(h~DvlluZnYo=l1rYV4wdNG=k=1$sRomj+TP68H{7KfH??uIznXNQrpO zr2};uO<*vQ(7BVCEqm9SnHIft)qdP8mUI<&R<!lt;Q3&yD)!x5=kfpw|1sLa>*LxK zn}PD{g+1|tp7iJywBJO|@3Z-Fh0|dn@v-FGPAx5BLNU77Ct!%s5JK74z;fPBAH2uj zv9q2@&cjqvc!3zBW>1|?R-iEtvI2Q{piHPRtCXD&@d{9y(`y4p`qy;Z6|)~?N1ctO z^aDElFBW~+Gs#pfxpkN(%9jT?Uq7HNvsGKf;8a~(IEh_`hd1yqf#>3jJquFsLAZmy zfWNU8=T%wGML$J#R+{%+o``$_^PX(hWW;j>i+7dLyWsGN<Yw!ZrZ_xDrq}uTP_Z>$ z)K(I(OflBO_bu7?EIB1LEGA-pX3BX(!Cc<DoG?^W$$VGGIbQu`5K3GfeDrE3O!B*# zyolNF?AAP>t=}!MnuGVPMZiKDU>)(0f|%f(Oh`Ut8A7iA*|**K>wDbRyXxa>xw*6+ zLLVtRHm$b3%Vm=h&P#K|zM|P@amDd|2`)?3W~Q~%XUD`CfBBcDkP5M`>BjSuYY4^4 zOP8R8%kz5Ci$KWDR_5H`tNreL_AFYoV)!!o@N3`&GmSRClB(}dTB8QsHd2Xf4VW?F zsIlc&-bUow(d04Q?a@&gEI#8M#U*W@LgJc5Esj5tm{~X*(j@;T4`q;^_Kw3g?WDmM zJnJ9FiNVZ>#w;=ec>vRaF?L941TBE;e~mnOX?*)`y_+)P>oBWtYA8#(NLRmk{2r_5 z!%UK|r_bTk)!ywXdo3~N*rS7B4{NXf!`9CJWW8e%K5OBL*q+{?oIUpVsk^RUt)D89 z6MD~Z>03e^tXw3%84Wm7zE$vwuKcW_C6AuGM8v>JS^6Ir=L}wc4tHfA4$^@#MqA(6 z_bf~-reLJA@SsUDqBKoYr+ZsOD_O`2J{}$$LiwB5QPki9IRiL>p}yC%t!Xs?96nyK zXyzQ843seReC=HOG&0Nn+AoN{*ZNVhz#GmN<P_Y=m`lWQw=JEXg|Mq0R3kxQ??aR( zB+hl1H`T;;-9A>6<}H;&Nln_@3n~_PcDUCY76I={)rbO#AHy;eRMw$N0B!Hh@}Bib zp!WR0qRyo}ebD?-#fC6v;tNuZrYOEDG^|d;&cN{D4e(g>pRrU!0~-C`=DVgG*d(cj z^(qoIq_BS^sN5^RY|j3t=>3|mf<A0qh@Prei9c*lW`c5=l2xcBx+i|oYb0KZbRZ{+ z7mA5<OL|aq!z#+B=PfW=ANZbq)BN~B_?hWHqfieSnqMwn<!0)x>iEEp8CZ%l!kqos zxzXnydF6F*R3DTx+#=ULrGjUiI-#Weg0!-ge*4zh7nq>-CJW=$!cmVAOw5PC{-LzG zCgr{Z|E=XQ@u;$);~AiA8GF}l;lYee>(2`Q@nv}BVvWAZJ+u%i*ZG|fm*A5#wqWnf zn1lY4p1nb2b8&y}cCVDe<6K+OJtMn~{Oc!zsEDu|xHG*Wr)MI**PLGa5HCuRFDT4x zd))7n=28{8CUYvV%3b#jZy?Vx<7CeZh04Pm#%ryhkmrN$cy4^7xu0l`{TZJ5UTR%I zFcJPVp(8bBhAWZb0{rKJ2-xBtQtPMKnL-amfNKKzS32aqEi8dY&tpgv{pat`tU2TO zXa&K+4Tm`)mk{j$dZ-MN<!cToT-rwve0R^uEqql1iZ-Tmx&q`^_4*2f>%?PD9C(<? z+VO}-`5@Z8hx+R|kfi|3cvT2R3d1yExH6HUk<B6~68M-C8y>|)TObmSNP#Ymp)YcN zvgVcKUdFiWj!a@^2RU#X`j^r=JS_B*$Ed@J<2y!<O+LEkOU5&YqLU>+C^Q<vTRskj zfI}hD$DqUX)qj1AdPN9q>jd&4!1OI<yqz){Mb5W_BA&_7-{d&*!+7jyazMC_qR3$w zwR<i(pj-lsfhB1`k<q1O0BDGM91LO_B13+$3iP!Ty%Rj95=VMU4ml4usSUu@ULt4m zxvZx3jJCB|zBR$xNVc)$(=B-!3WL&fbtW3%1^j!rRBrOSunV#H#%Yp$t+L!bg@866 zIA3_yZkQJ-O@Qtrc5cmWfS$2K8btGU>`lxnP;jGe0X5~O@2+LTSM6)AX^`Tmy^1Ql z?vxj8$f%kDzDR_e;tMq0C4XRIgcNEyR!ONA-d>F~JCoU=Rdbf(6~4oRC1qJvjjzTr zpixUl5lhhZ)e-|K^7Ryt4cP+O40t&R5Ih`7z4NAP!QkSYKoAeVIB2D_(Km7fS{WLg zWJ1{eiD@FHYMztd;T+=hNJKxJt>sr98f6OPSI^n|>2L~`6V&@>aBAnPB)y7<6mLjs zD{erqL2ea+1~}@kQcx^mZe~}04g8fL_+zm*e!6o80+|1L8C>RBnMom?t?kQW!9Nqh zoB9s+zd!Yqa4htZFWB@;xJmjrEw1A-STLQokba-MgShck`y4p62&q-;%nj<%GdhZ6 zgq%$4b+6nzLo&ycJOQky-2^7=;zdT>=zd*+BX(Q_*C7WsPB%kd6D|pGc)0K52j%s2 zlf^J4B;G11m*kaSz7TIweXS_Q+S_rEo{1fAh*t5y9MG@|MlvmMrjLO>y5@~@XDiqJ zE=uOqpfolT2Lh06&-tKP3ew(KRf4u<a~vtRcy4=l;Z^}^xjTvA!i=}~HEpj((<ONW zCTBq{G>WK>@0zwb+<vv6U)4No^}jp=P46@=o@o_(8ee`IuTW9qaM-qw+sjclLGPHO zt<AE3WkCqzTY3SqDEx<9osAN0Y^idilHvih;Ymq^=`;8bn5)u><Ux?-MvjOnej5i} zJu-k&9N6)*hDw}J`U5-OV4+vCIK=^WC_9`epe=C{hO8zWOBW_f(p(*OP9{zQ{5(Y# zCd6-r4js;_b7VPDBPY>9M?UsLIUq|JIR!WlR(U9KnSk$cNHWME=#XTP^AO0z3I6Ky z&w<=BL@ok=MqUp%aVT_@+5sUoHWtx#KNL~fiuQtW%`u@2C*Q|z(()dMR9yDlb$9cN zJ++HgKU>=?v3eI;DMx(D!}$x$9N~kgb|<2zjm1W3adgwXSl;lx#oD-#(-;~QTbKY7 z>$zXH=4-vy`sD2Eq-U>do;*+#GGunT@#PQOG-H*fSi|pOJlsVzc)B+cXKc~^&9xFe z4W;lb9U}BKfH}Dg;En0qx@6#bHGn@pn#aD?qM0%G?dLMO_ei#(D2sb9lH8BQ8^x<O zPU<FO=$qnpHObE<dE^0W>fn=;Ocidl?|&!3GLtYoeW^RdP{O?Tr>lAAOL<mfIbYFr zK{+2c)#%MlK`3eboTDh-Li>I`s|t`cyM7kG&^0{4-PYlDvgT}tW<(5aL}(q{BoBaq zAg+bq0B^XQsJBR62z`hIdRgPa^VJ-FaE<s2r}^#eMg*N_UPpNb#0Kl9AM_q_P3L#2 zMrU~+hL;X9)u(Pf-eB+Nj4Y|}Y**Q~$peDOokCyJ>56)9cA-f3`%rg`qp1fi2YN+i zzXojWIAi)nUy~s@u6^wBv$$y0P}DtdNmTwMYr!KaB`ABbiHAzv(^3>pMt)e?6Y769 z10m3B&KU8<Hpq!aX`Z?!PWPxr``4bp%46RzH<kRp>=m)z>9AT5aOnCTS+q&IiwY!k zMyMJbv^bj|hyoF8t5@0^eb(ceb91DqModq|KOV4drh!M2ewjJDmcI_lOi0wwbI$p_ z!}GQ0oKbROmaqC+mWxJ{ohMUt9?GyK8ZkrsE;)>N_k~NK*X}AO<J)Zo^#^~LL@~@? z9v&X|r6@MpQ^oEyzI5+qD10_MSDAp^*ceeNP@Mm!G^n0%F*ac2BwVrNKzk`01#nx; zG(K35M#%8pZ|kn@d@CQIzOOD)Kr#J8>ixE~*^gaszCXJTa$#dG&1nbi(Y+g^))Tp^ zT*z14qMb7Xo$EgIk%-GTeY<zAr%yylLYdI}XXIgG>$Z{erzfETNOk;-o<=R{wKP6G z*!7@YhL)d`U&h_STb^RA{Aa^+6T4~;(n)#kU%2iVF?G4A)_Xm@GWe4Blk0%@3l=|T zKF^JUg*roQamdwlw#~gEzRH~A%|^OjHR8#0rw=Hi_<nT{I4O$)MQG7UE<4-6CFWL1 zpCJZ37vPzTND4WAmnzQVCUt?7cwF=O1b<iAp+{d7wI#dHR$LCIfu|6?c3q(|Zlm)B zdl!50-fc~i6~}-K(DSBN=z`wh@R!1fgBOT<-SQdxx&$9q?B4qdXlw?3X&E(n_jwh5 z9^crLMK*GBJZ!qNjVV02wBx4907kqsOe0z}!2%*7dv`3)K9MsX1(cPqRqW4RI4}P> zv`nPq<#NE#`)_Z$K48k}8diciPN9__rw<P8xVAOfq*!bC-QF>&GGf>=PR@A|6I$*~ z;!>G3DiHs<6vr_m!^2_FM~yDCS7q;?XJfb$pLkaMHoB|H`8QWmVW7h8#B-JzJFNaf zBrdV4yw7?=y-(Mebr)vU^6j*+zqVLk4LOPeHj*}hDa6%oH4HsSga36U6@lCQB&4NQ zIukb%#E?<*)}Mn<RsOk-rKojKI-!|YbEBog|Mp1mA2qzfQQxIi^knx3a)T6sM)u}s zV1R0WXOMG`SY{fdRWGfs$uV>w2c~K8l`duSk{zgs9DXaV`ATW-wT6P!X?ng$p=*_T zC$m4MCM32xb;}dtJd8OX{uCa**JGtuHSzJO2}6o^>m<f>Z(c!ee?s{OnwqOBFYt4X zUY>?{#7g)JcCH%(U1!QMAx;)lBcEo!=D*VW%>OcV#+8O=*=*Z^bR4%`C7V7eN~z1` zmmzy6YqH|&y@tC#dH^f$w}8H-9yGk&jFvi+K`B84bSCm^6W=wX3G|()!j&$H%RSi| z*<2l0OGTYjIKbsZZcU5FW+8pu_DKJsaeEnVfB~AVx5_Ke&MRK9*f5`_3TZ#C@c4A; z<7Sq;{$fRZ`Z4~-Gc-?hs7S6|d~Qqr3}=GsM<RJ1OKm&IZXJU;9G9HFe3uKnZKY>I zg=X)fvr=Q!zZ|Q=L|D|I4PBJJOm()NlP8Uy*Mr;e)>5#0VqW)7|9Q3yLr5=RuB6+5 z=Wd<=6@8M<QR`0e!xTMaDmfR8x}BS;+p>i6^hfU7{JP>5sbv2M=Bo`;A!(jIThP8M zLM$-_m4kVbn&T=2J6dJ@W<(@Zp7d^>@=xgPdFqzcn0}A%_qe==g&NJ(#n!OY!!;RF zhi5O8&^;yPKij6DrM6=}+5h0eTknA7(GzevJ89f4f(*3<Y1QVHwVc)vLy56Kp$D6R z%<Gcw)Q6%LQ?7<oIeSug7Kxf=fl#dSquLKwn{b8C9~=)P-hZt|qb1UBbOGXLPzLY0 z_-LLTq-3PYqeJeb+@{KOP`(Cn^z?v+aOyg35JuunJ3Qm!ds?3D#H+-j@*gh)1O*v1 z3rGGm8~Jg{eb2+M)Bu-(G7UcFFw1Qqu^`PA+^K8$h6$2T_inXpsZMxjfumOXcfJ^g zfhO-WI>t(czwdIVol7iqQ9iNA*WD4>twU{n<IDZNNuO!FR=1z0*`VJGZ^6$VIZU;U zl-En`32`bW877(UC?s4YViSFfU6?*4<IMh4ko#!-QbNs*;Hz{{@x^JI{%~WY7?j_b zHP(xQVKaZbU$V%ERJfYXq=ah4{EjxH4Y;cKAxS@zq;Ms@HQ<*~h{CI_bNzLhJ+HAe z+EctQB}j7Pfe#888(tGS!kTS|^Gek82_p+nAa+bN57|*N874-URQC$y8KizMJv--> zqMT6D(LJU0t)1bmi*gkm_`z`ez0lseX1W4HoYR`Z`qBW4St3)h)6Q^STXA;xl<BEX ztW4+aG*Lb8k@-l3Ab>DZ$WW&w@2!npZ4Bqv5Oq1=U59r1vC6_>8n}8)X%_Fzu7b9j zJfxdTRtUdT?`E@+(W92OjU|JuvHie|=icseg=fwRD6a2w65J9Wb;E}r?p)}lg=Sx# zV5>R8_GK*sruPJqF)_+uf$@=-@s<U=q2H<u31K>@vEz6-qdDnoJqz7aX8>v~|8GKh zwJ}P&_c(4{(ag!hU~YuhCKd0DUoC#bTQ&+yIb77*qz|^l)q`|lP0KCWl_lHKybvbe zBs27Vd^P2C859N`i6~H;Wz0_RcJ7-mw}%bCOB;52e2Mold1t!b$de#FZV8d*WH@t; zsQc&k*hhL-Xw-U$*l_Hjj4kx43AZ*02nO_QJ?Nft5>Ed*c?$nI3uYx6kIq8**=cRO zV*n?!ik&omrV8q!FGR|Iei{;sb25Mhr2><)5~uPxPsD_KT!OKI_5L%+N=+?#nh3>M zDRe_8{1KuvLE+OGtq}U?{S4anza*gdn3YTQ&qd<x?(^$`Dfd`>>tyJ((&h3p`>m0# ztWRJPY~cKf%E91UZ0sH}xF~@)`hB}#918<W{p5qnIDubnVy>Fo;!fY0j^P!4+Tz-V z+q$HEf~+#6-<#8&f|DAqCZ5m>p?}Kg1?=l)K)N1UY-_;~A^%)GQcQQ#=;_k@@oe5! z0~>AbaH8(wQ2(tfCtp$0ti9oT*PL`eL2KGA2}8Jk>ch9x+6KzlOzCl6i|t^Z<-y3I zs%O4DvoBzJQq0|PtHeG*7k8E5N0sBoKYuk`+H<;q>QMJ)DHqt;Sw{w`erGZDU1YO- zd7<$+zg?7V*xj+D*26uLb2)ow;RS2;{vz>O_mrCA^{kYsKlgk9Vi-wTP~?@^iSrvn z2^<XWmZJ>yFu&{1mG0k96W(0ea!CT2eP!~VX?KJsr1~{?4;Up%6#z=XJj_igEwhLm z#@r61inx87OOMCe-7CRPrjDK9)XJE|M{tPh9HfC}%o=!|We7Zw*d|m<4oVkLl&kOn zTl>g!wyDqBE_CnTqy&8oqMMd{b`p|GI0@s!d-8x+*OzaJ&L7VTMtXZ*xhjT|cWXX@ zrKBNNAwYgDjX9Bg5z3(Jk>}38={p(Xr#JWMHbb2r6UQZ=;`VNj$e%A68#9HtsttQ4 zkliKj4&7lDhpU13ruR*Kx#^a8X^Ap3P~_0lMLl)vvpBn?|8*4xy2Z`Zlhz8X70=6V zT29J{B)h)MKW7RCpDm|KrZFEkR%D~TBWA_?%q^sH{j0H2L?%7xyuTNEy`@De)vkQ- zS0Y)mwVQ1ZoEME>oHlQ`97iQ-Y}0dl3KsfC8T!Sc*t1W4FRMB>DL!iq9wq5%<XNDs z@UqBj?#94*Q1s2L4HVLVlBW^_%YC|^eh$zCkl%Wq|Bx9m%lI)ntdmZdE^9KKeVe>< zc99i+@xxFFmN3hRRza7P1CN-l0Pyh|osGM$+{L`Ocn;z0?Lc*J?Txw}KxEZrxvz}6 zLmQi`?lv%-jkpO4gWI7L0<7>sd9Z}}<J`;xRPZkM8C-3^8<hb=EFZiLm1?Gq4H2xY zUUdCh66j__VJ88qUk@!Hh!@*Qf!if4_z?84s=re0*YN&mP?vLfRqaFT?gthyZsJ+n zm?e987{!t~S^{=mSvx?%L^9-$@;RNYl-Pk?_nj1U$utNRC_|Da2Mhzy<v}Xp1;{y+ zWsUea{!-Z^myVwlbvFQWct8LkQR3WX?_OyXn73f^3^QnlB#dH{W`C_vlbkb4COd2N zIAPZZ=DuuoM`q+)5JRs0@TFLKiY`%QNTJilHcD#McK{;3e6x#bLYovnA+()zrt&J- zar7|p7gy$h0#i%(;3q!*1WF3+fVVM-=oU@!nhr-n`QGf`9ApoHcKQa<rDLLC9Ku5& zuJ1Nb9kyBSxcaN$qQrM47G%k;!C08oyTMs!Bj3u^q_soO0_2|3AN_R2a@;s)o%@Io zVb+Q(bK31*@;dKd4aFd9zy&d~Bhml}L1HGi?<ns34z-$+9gyjSbuM^o^Wf@;cN*ke zghqlL+W}DX%|A%U=-6b)b@Fl$@-Dc5$x|uIm%GeIHsZ*BO^6a?)qvorAP*npg&@1s z$iqAId6M0a4+-m-2c740phqE^mW-}2R5!9Izdz?&R|0G|{g6h@?WgMP^9~!J?#y|d zGgRAL;~bbm1h(g<>fwZIEM5FrsQ7jdf&H<8`D<i{(e5*XOPA0}-s$swX=@Lg^6_8A zj^6wF+Q3|;ikFJrVvQg&Jta1A#^yc-ENoXOTX>E6RQGo4Swly=|5JAclS48w1P;>@ zwd@2*-H#gITZ=y_*CgslYQ<QVg}M)AHZ?9kOD!|$Zuak^4}ME1tbS(|e2&ayxF#T> zV!g%HA!=)Cv!1>dTvV%U?REcNFaK~tgsx?Fyh!B_{&2<lBaSl*n}_(jaQLPSB8)>x zCCVW!$6xC7$hV#q<qFo2P(J+j%Z0GpX3|r2`^JMUzbu_a8Q)Mwb3kq(3hhDufz|EY znYVu=81zH%$^NEofPc}LR4(jJ6jpoVWrjDq;w+*0(YPOYHxM@km2eXTedupJ^C<Un zXCJVjPx0eM@}R0i3;^VFe@>SYUtleNdx`yMh6O>GRo6^!-@02Fx<p)`CB>#FulyN8 zRu(31ZL1f+T`*E4Nh?4e#>H2euQ~8>2;bMotaFS#8+h}1XWC}{PUCggRVR;_JY|=Y zC~&}vmx)O(c2a1$BM=ej2&^vu=>433xCJ;;e~iwl;3IZ}tt`tOE)V14ccN9tvUT6B z=mM75h}uy3-Iqr_6;kn|e-ITmesc5@NxS*i;rDRKldBCgTA%P6Hwr9ehsS-8xPeT< ztgNuc@1vLBv9dA4qOG&M1{o@Dw}GXniam^wneH_MlA5x;l3E4=;}7(j6cW;k>rnQ3 zkQ0{z7vEKI?f2uCQ1!#$q0#fEvMRGH!7vHsv@$rok4Hs@T6I&O1HhTz7F`<Z;Q<<i zo(bnHS7Foup>mHDs?YTi+KMhWMf|oPSGbO_<sj`xVm}eB9`=kEcY$63Y$xyeZ|m<6 z#U&ngmY1f>x%zp7&HtSJzU>Ea4*@q{@LNOJK(fy3(rLqUlIxknowK>^(OV5v<Q2Dk zwX{E0fSlePmr8th`<&G7pD;D4EZy4qng_^L@PX#hHm}(kgsb0a9brGZ<ebx!%nvpp zJE$SRx-V;oXP;|?!7Tz&V}0Sff&&*#<NfEh6y{dUcOQ;Me6{&>g{SxA*zo86XuE(B zu|xA#7YjV#(AM>rU3dVJZOFP7{EzwT$P)I~U9#N^glyPk3z`Ah$V|4<1vrW1!w!k5 zsQi$J?oi;eS-hP@^t9V2m&sN=H|);1<M~8A=z*@y%N2>mOpwaMb!>XCZR;qq27sEB zIJNxV-x9lb$TWSM7rqN0VBO2BI<$dDtY{S%Ag>y|b50X54@iJoPN74raV`4VoTBHz zpbmmP32M8ArX$<xs>t{#7a?0X1egL*B3t^X|L?`$XWnGP-Q7dTJm>`Chzy=bK7-7F zkN>`?I^^;Yp^!}K3dASpAptVSM`uv8J^2|2GO2P%0UnwP|E<E}As7o8$M9$%8&VXT z+tBb1!3tHSR>gDDA#w+{5ZcRIfU<75`t+3cJ5h?pyXb5#-i5dtThr-10iQ{q8*et) zgSYB_d^V7&Jn*|_j653ge;)hyqp_3AI{ph!|Hi5sG>XI8kN?*G_Y@p8uao&D*Z%xx zYW_oasMCHG;W4=aC3(`b>-iF~Gj@&|{EMA2u%$&7{Fb+B-NDOp<j#xmo7^Ri`d#<5 zIX5?ANEhH2OVrW4kfrxg#0vty>4G2_@!ZNg+(eoMZ%>Kl^($M@-dL12a9+0cZTl?> zMp9`9q`aS+TZ<(Awm0wa1vpFk#Rd8-b@omL@5^Ny^2hml+V&No+Gs1L^Dc**+VrZS zY;wYDM~w@-EXvV4Z738P%$|M&k)oo~R^)t;drMhIKCwta+y}^or9*Km(W{}EJo#FN zjZ!V^b?bl%W?{lVfHaEG*%;JQEDrI+5EvPm@qzK5L`{PSJrTp-f=Bskn;pBVyX}8x zYRr%7nOCfI|5h!{ug)7J6eEUhaD1$S;LV1{-S1|y4OLl2!eBM3An;_Zn$#`*DXnE7 zmq{3usyH*aFjZGtADEh$JJGcE@oVp*7($P!;{DiPG`Jww5+dCku^@(6ZTo5T!^UI0 zu4rX^VPcc50P+zi^>2j>$a?lJ?JjB^S9vq(+}?s7w(}dFj)Q&f#=*7Kh0~Giem;O) zy`Am!0ic}WB@A|D+xa0RLPk}MN6nBs(;>R=d;ydxZs8s12|+yGT_TWtPdX~pGJM3A zgz@!+VmqoOU3!U5BWHBohmnYk{25o9HjS0|P`fX_VK?v}A>Y6y0PywbEhIV&>AKZ4 zdQAEYP%W9B5<rs1IeNePf5pZ3hHn7oikXm3Y1G|_yre#c=H-jOSiV}1NK9O;R|Y4R zzQ>YmfSnSA93*E^i6r%>w=td(jKi&q>S_Ka@2EbMIneJ5g3Ae@g}HeM52Ao&rmpa) zb3DSfQEjB($HbSZSBRDSilkK6r0p~Tmj6{C7BSDMQ@DD+#zWkiO{bqt#aIW+S#ckd z9(q1}%uLPj)O+GI;fA6jnm3&Yw@Hg0`JSkw7?HSg3tzCZJ@>7ve`-Q`VeQT>3@J2M z8!L76C_kRd$dS--1^+4mKEwW-7;KerYda8_+1UEzk=VA4NY)r>c4fxrw4;xvW`Zf` z6LnyEO=c?Iu4r~wPC-J0AHS}O9unJB9!p#9N2^?fJUW6PN}=@mm#hXOzD^&N!~|R4 zEXmO~$n-(S{c|})Te%K?&zi)7WidqJR!PakA}3rN!X=f5>R$($hpoTe-aYsLmMAB- zc*aOIlNCTc6dm&aR(JhN<J~oZ1XlV`6Oz?D+0${TSIGXrql?4le-y>Pwf{@^qfY;g z47vNEHu+mO|6Nu2VI4k9o&b69&~#4;;Sv<(>C3^~|6lb@RxnTX|JF(}a(}D!|DdAh z$*djKX(oRp{EO=Tr~5pS?Ei5fW6}N>P5)=kKb`(FnIq~U<e4L@!GFmAhqoSe`fp^& nJpJ$Bj~3*A*3ADRku;32Jz+KEP6?!d{;um7Yh$k7ef)m_)mmCM literal 0 HcmV?d00001 diff --git a/docs/designers-developers/assets/fancy-quote-with-style.png b/docs/designers-developers/assets/fancy-quote-with-style.png new file mode 100644 index 0000000000000000000000000000000000000000..31f38063a1f1dd489a0b9776865c01bbb2402c41 GIT binary patch literal 42284 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 Wx(Bl7^uhW=Xsa8j6`#2r`u_maF0s=9 literal 0 HcmV?d00001 diff --git a/docs/designers-developers/assets/js-tutorial-console-log-error.png b/docs/designers-developers/assets/js-tutorial-console-log-error.png new file mode 100644 index 0000000000000000000000000000000000000000..836a663484192f4f204f002a3702a66063a7dc35 GIT binary patch literal 33443 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 literal 0 HcmV?d00001 diff --git a/docs/designers-developers/assets/js-tutorial-console-log-success.png b/docs/designers-developers/assets/js-tutorial-console-log-success.png new file mode 100644 index 0000000000000000000000000000000000000000..7b42853fb406427256db4e103b58779e19413a73 GIT binary patch literal 24046 zcmZsDWmr^Q)aalhf+95{DIq9_7AfhDVTKw&x>348q(NzwaEKuVWC&^LPJsbwX;2WP zJEXY>-uM0Pcc16}IkV5%d+pV0ueHzF6Q-&xPe?#T0D(XV6%}OEA&^@b2;>I&t?S^E zr^JIVz#nl{xP~kv3+MRE?D6sO4VT@Ei;KmjWnmFXO@kNq9)Z3g(Vw#N^7^J2nAu0O zJ#u}#wnmEHDLr{*+^wDc1IMkHKqBA2$QTBJ7+5RHNNRZCtc?W(uW>{DXUi&|Wp_>g z^_$_i{U$8f^yTE{b!8@!CN5ma3yyBj@kVirh3EGm3CzeS0MZFXx1>Y-;n4sOE(FbE zmGrYD#ePc5oB+audv!rygAjE>FPpMU^hih)&<TauOJJ^=ZaUS<7@3WN{h5hWwFkJ6 zMquxqz|MudIjrKG^){q(=usys8|%R)PLDkB9ebLA3Oz+*2_f{z!kpIJyCaVrzPx<7 zVodQMZxyv2VP-tnptPch@wQm4mwiogQZgrx`zxvbsj4kXbAtOgo>D?XVEepi|CN8; z6FGsoZ@yMUDvfzLiG_>)k*x~8X_&I!gb^Jxp>#XF(dW80FYo9rzAI-IuqZN4c3d41 zHea~G!^_;-{$V{U)i@f;l>K=aTH08!n61hJB^Ytf01C@{r2C<x2N{5y#U1KJk4OwJ zoZ7KmJaO>!=Oe_KSAWM};YV;!w~9q%Lg?u5LO)%8<T=|Ydr-Steyv&YIdRX582}{+ z$^eGA2Kot`k;KX-U!Nwnl;LlVqWaF_OwYnzxSanySx88-Lp-C99Z1+TZ&z2q+-}qV z;6c!~*dfsa|2%V7@9#SXO^~FE63kfdvy6ZcCRSw0=1Ij@q{(x8f&uqR$q$%e>f|gy z_kw@-8J#6o*wMcBjuuCI)nj(redxJl5kWIE`$tc<kpk%z@l24rd^fvMol%kjV!kXl zyUB=b!E~QS%+f2%@bia~aS??V0V}EwG24F{wj%m}76BhRd%NQ-+mTxALHS<6%f=!> ze!Xnp*Zw5zDvQdeKsIaUgQU`p?fRofNC_X|B9FuJUKN~{Z3t%qleT|K^jCf_i691? z{?xx@KOGr^wce-_QbYbpHMI=EXm{|u4tA()eE14(Hc08HcdoGJ0$Dcj_+9ZS16WjJ zfhHKrA=?ycq7q3(+sjQ^8RXlmHy-jazg15ZH#tBgUK3o#Cm$JTeAxySD;Vl%L8^MB z1K-9^rYCe)Z4;C|(l+M*_-D1b?1)=q%kzX{+qE?dCT@?rJ@X<u@W*W3xQ948gA!O4 z&-fW<x}7izw3qjmX8_WpgtPKd65+N-ol_{*w`zk@E;X2WFYL9m-z}_rO;(9K&&l4j zH(J4#c1SXcnGQMc&rgvkg^hEbIwU$v+D(QvT2C)_2<Mj+(F9Tv1{=KW8-?;{al3xN z9CokGy}K7jgk*sxdcFCjVq45}ch8K^?sKDcxRafLBqJa~OcGJ#cs~<JIcu9jiEl*^ z%tEJm-#5JcwvyM}0q^c^kwO-4RAndkZ4N;_2*?es2!x+fhs3?;X3_3Aifc)p1Acx1 ztb6kI4`v2_cU5(cv?^gRQ_>m<^*jvJ%j_u`fb%ma(E;o|f*=7)Y!{){w*W)F(t(RJ zL|O&)Baeg^GKM4w+>CUW-twaqu@CQt7Uht<Q%n>Qa&mUMap&}RDgzPsVswj{zq92h zWx9yUUa%r^n+*}URP6jeY@I@~jSpbR{D*s5b9Z51z134;{6E!`f3pauQ6tiGNMDTO znUD0Y$|Ca=(y{>;$LP^wvf%fjSe-=mOhwuVjQ3;ESr+AyO-oas3}ukXv~zl5*8%Y- zt|@=56Wz}Ol<7!~S$jb5;SzXGkkZ4Ho>{}hii%6(e1*s@-jHuY?rn?q4MV>x_!m&T zt|TNW@m&XsQFaK)$h$R`j%i7vd(?nhLspf}i^7XGOM+)?WWK$c>FZjvJyXz%k-OLv zL2<T3^}o!C>MW<Fmav<hh~*k%;01*xy^8dtr^>L~fdX&4>J3ax7QpkP=mv)LLi^yF zf%+5^YMx1`;N5CsuD?DxT2lZq@Yn5_BQ-;u?Lx-PGY>;S$_tOR$YpG92+uzCDUwYi zs%Byj&>0MlwNSzeiWvi6J;FV|xzyc1aT0rsE_QjJ8KS${#gF(MjK20MR9$OfA9@Wi z46vZEYEC#yhI*k6Y{E-MjAz*qx#jkBTqptOeIe%0M3|#GRTguba6~x)M~akEJXXs+ zbU%t%!r+UuuWO+qV(xc<dU1Y!#P_t}dF9!sB3;8+EFZ$@k#+d#4*l1ee%@Hb(EwB) zf{FQ<bWLxpAL_d2pF=Um=o>g#|1#SuuNVWv)3;AbhL>=5AZf@ygb3_y80?=kOv#%a zh#}Jb-z2HJU5HE9w<6b4)hyqe9ksWLd-@cwY3o1wss1N*ZqxyNW;zqBm}w(A@ZIK2 zQ(kvIOrc-puk={xpdq4JJbu*S;G83Xc6DQHG*RkS#=8;smsa8WIVOd`a9gZsyBmu0 zuPP&FPzw%1@;2W95j95&Rg&s@*~fCTJjXFBbe&P=zLy}kmX6gEn`w3HXL#Ph$@wkV zAj}O@Zi^`90aV2*Q`DUPOJ_b=GPQ*}aAvN=z?^Tl5xLJ>*-HC654!b{=F>Y06n93N z?Ujd7WG$>hD&lcTz5S?1&yhX$h?2VwhsC!O${r+|x<AhDq_j=o5D5|xQezou_6?1# zzuiS9mBPA!`_$m~3s%ji14Qb14q5Mn*^aA5T7a^2;Q6RV=xp5yf=)#3^{sCF#6j#n z#`O$I9ZDXXH<rq0xwaz0#|90@{3^-2TW(#DMHY!J*L!<Ew8Sx6^5Yo5C(dm}FwrCA zGp~=_de#3CGsePdMXZZ1_Yoe@c?pt-^Jpr6@4JS6Dn_QLzsMQ+KxKCDE^*{dPDW&U zZt;;0QcU=nte;s;gs*%yQQE-rbwo+;fOHYj#vHp%!Rd={>JYP26!UVNAg1F)EgX0q zxa{mlZ1pkDwe9^~SppYmQgL*P?JM5)e9G%8+B#MV=v0U8Sn`$ByLV~-zCu*bwAP>S z1x=IJr322*dNRmBii3AHuWOsi?FS(D34+cN1Ibx?j`!mV@d;^#OClafLf>ji=>Pay zsySxiWGrH%RH~T>S5~3uhmLnT!)xK`>zK5JMAKmWmEgiyOPIrZKIsOGAR`%hS@#Ik z17>KCO>49w5HZ(7HcfKKUL<d;?@_B;CS3>09z6^isdf}z7#|Yv;+cNKpml&PN6~Jh zZnhXzF3%dmcy`tA=na)^gt&H=Ilh;gd-Lp0UM}nS(t@`aUl8r5ALV_f<PX2>q-}i7 z0Nh{l25lyq=4XB7m3{Oojhanop`K`zLRY(6PF$<`-tC|k{t}f(48D=dNWbWyvB9H| zIQ#tm6xuI|YgH4Azl${8o+emp)3Df>y?*ytMVU7ObLYzwDjPh05r#y@w`wyJT8}to zbW1^tj81K|Nffom-bGYTsbE%XUiU-KJ^@lcLL>Ay^co)U$v(#^=xze46H5!zegIrd z(#RL}Vh2y$TCw2<n=1);N;t_CB2KdGGJnf*o)0Lf+huBh-_9rD$DdEsO1`-``30zZ zBGRB!34Lqml?i8FZ`l^`S^LSG^#+rCAE8(p&pE7#dC<(EqX;<i8k?o7j(rY`<Z??$ z5B{Fo{`moiZjG#TynCB%@L$8%!~F{Zw;EM(9|m9~Jc}Xu3*0%A+$J}5{?N1T*a)-D zTM>BrI4-{S$|gFH7-VmdF4m2}(098F3*bVPo)rQiQc{o1JOo^i1!I=lP7l6u<}+8; z4s|3wRtm^Y`0@PZO6q)os0RBmXC2pVge2jfQ<0A5L1kmPRi1Sv#v|3yre4^cZ3gFg z`1k7;e=uWfme^;BfyRn{jZHPVm^2D@jB9|`W3P=aDo!YjH|yGxaLSJ=wN(qHkl^0z zESs1Q<%i7U{Y_mJ9oL=S%FnwrV&KGRlV1$yq$HIwS*GvSd61R^t8Y$}D=<szP~Bg| z4puEO$`%}hrTr+FR@SxPBD*aNk?GT0<)@W#8RBZlUOINV2w`>0#mq_Jn#CAhopSM@ z5r-*0oeI0XW!Nnl->kTSe4*Lu2Z@0pA1%JMdEBlUsm%mt9*1#T2TbpU4shRq5j}pF z6ljU9t_GYilTWN*XT*gKpD#GfZ4?pXis|#1!zNJ~5thSdm{Q1N2(NbMcdrjdh8xQz zt#cAGNN-=>ps-E~a{2TCg-4{y%L(J!Nne1a{gUoBs4A@w1LcySWq#p#r$|<shWed@ z?Hr&qcjT^^%Yd%?s2Z}kkbvOp^^fy6ZtE@iRT7<b^R=7%?tgO1=S~%E$0f6$ck<R+ zbT(3$ap{oRr=GuG*{<WZ6FGpgP6wuUU#)-crFh2q+d)(K=U#v%gW2zV0iPRQruHxb zm{o*jc;TQ*_He<I*iZslx1LX`EI^s|o2U*KCA>)m!nnU|D#l|B{+0xHPb)16p6te= zL+Dv#a`AG$29M8MgGR(Y5SP8=M=?W~dQFC-K}LkJbYBu#3?z@v7B8@rwQwT+la13| zk7-=F@SX%@SB<vT&K8mnzcrYcQ+UJ)nppf6&VqpyhotDoS_UCXejy(@8RV@LvB>hO zthy=Gdh}FX{-5{LfEBiv{*9Ay->c(yM87dn6JaX9g66d~l*luMZh5pqI^)h`Qdg7L zQdH7ad9&){M|FjUh1m_<G$?eR)o(=kl!|P5@LIGK&jz$a62Aey6+goOA2zC<HZZ(C zI9BH4l2*~0*{fbUob_|0FgzU)^dS0{)Peiy$<gz@Bk2V{Dc*LDmd6LN7*%Y#+f8dz zBo|`B^e<5y5r+;JS2Ro5=j?|TE)RpW_)4D>lzdKvZQHo#rK(;}5<yfqx)63IT9rP# zRq%jbwKfB&>nCH^E5FI3eyW(+FNLhD!3e~?xS-%ko{1-rM1ART%jN;V73LEn1BPYe zn07-;eaX@B;@<LLg*km}56Edk(ITEGiv)$;JaskbkZF4{=Z~=@&3~9KB#&%x<=gHr z%GfuJ-$zhoEz}rAoU0;^ob6{h>^q(lmAX&}$@;!0g}?hc@4f#?>Coc!SKpuV(52YO z>0(iD-12q)Qj(a-)dO<1EN(@4b1MQ4VfoHrQlN1~W4X;td+Q=5S<3oE*oRo_nF?}? z)FRT4eBwLru+*4pccRE;5#^yo@taAk+|?}T#g|~%lr;@y>lVsfD15G(9B*#hs^=XB zu8+RMtb+a&P+OQ8_HGJANz?JS6v%6#Oe{s5O)vF{RiFPIS|;j5934+^UN2wTSNm&7 zmw8BjMi1-Osz@P5Y%9VBEfq?H@O$}6gFYwQqjmrGBG}#(@|I`^QLj+k{w@;4-;HGN z7>qcnJGIh$sc-G*-aH~&lHxAq9;j%WmFdFAY%n+}uEa_AEncdd)#ggtoq0lKs#e?| zv*UWd+@`A0{?4sS68Mdz%)RDAwud0`=FKB8g6%t3v+|5azui)H9(509@tsCbW&-zy zXyv#sZdc>gIwKS9l&pMB6&QcNI~Eo<bRgg5ll5z5)}Q*4`9T|aB7TR;#v(cynJt6N zFky)d`7jyrm@k!$`(V%KhEJNDV*A!%uIi?8I_}LC?viv3|36&N^$xGd$R`SI(|f2J zfFA~9?8<Vdo$x5*5r@Gx(f15hmPDS6lBMOD!K`lt`|O0}flsuyisKSxhc&oGXgx$l z?6Lc>%$iblrnjN64%<#IcVRwRF6^zD=eGO3mIN#*TrYhxRlao$U3Z(_GCLA}T@^t^ z0*xrVCAIUGy~9op6nC>WlfqwxABusGa@U5w0KRR1Np5=Q=amu$otIV);dy-hTfpS~ zp>UUspPhZ-o;hxt`b-{Bl^mu+ru93}Of;i2sKpR8=4N%v^tyb*3Rg>ISBF2>)0c1x zU0P3UvVsburQ`81(KJxLua>Z5;nM=nlX6RfArI#E@C18|lp&=wcC8=f_NU||0}#ip zF92ld(g7^ns$e@}<CSoYEOiTkn-njuhlc#r!}ldL-OHn1D&%afLN@Q31BQ5}587Rc z>VB$g5Cpqp>4u=0?!+;hu|m>F((~8juHX87ZgcwF43c8t)5<O3{t~t|CV9YJwv)2G z^&t6Ufe8(3wy*6b_wHY_t?a`p9O`tyyzu!>c-XvtGqUclh{-M=E)>kh^DU@x&V3O$ zsY8}u?cG%@m_HMK?hV8ETOjt21Qa)aFmzT?2KQQ4ulXve844ni72?mq((W%LY;4+k z*2ZURu=mcz#)|d2An6dr@zK3#4{X^y8f6Ld3&t$%KZWki#4aMAOy{m}b=;^r+T_Je zHQnaxQqU-2ve0@dht%R-@CwM$mR^B^3V?qCf~01%1DFoRB3(?m$*O^Hg_2W&Zyp5H z3!+cRYQOD+5PJ>+ch92#xcsUr8WMLp5wZS`geK{5-RiQAwE)-#KAH`n96}P%3Mp?Z zSL4)qvs77(T~Sc@wo@|JC5+TrQ-rmY#>?j&@8NUCN9P}u>CR4&X8RZV^a>7G-E&-> z+V&FlGKIf;R%gCbHx1Y4Cp1!y-A}6x9vqk};TVFPr-fu&$F&)D4K61#(uzHA_~2Tv zz9d_kFJd%CHZY#3F2#8KW1?jxvns3Dc<rr8i{nd5kILq-hOHw34~JJeuyj?HLw_O4 z+BcC{#DJTag?<&CkETG)ojc#Z)^r-re?2=4-5DCZ`1%U3#+uOt>L0LI<+k!dV6x%j zt33?<)G&lP_e1EJSU&xoHxs;~M=1ldd)-^|!Z1MM=Q!ld4C~c>w=k-lu=ICiX5t0! zpAtf&#V1tajhwH2_d;2(`w{A0w~R0H<Lst-!mNkuKQ&-f8S!#RC7@JOYsIgZ&}d!k zxjMwS)f(n5ZXh_sy8QF^h;wzcJ6wnRVo3v7Yz81(dQX^bsIwLw=FLr?XBrtgwA_N^ z0oAo8BWbL!^5bUiQ`_T0sJ&Meh5p9;RU~`e>cIzT3QyQ6)C}IMRq#DAmFnI~gb*W* ztL{PGaWPf$E3P()aZeFIzCGFH!OZ0CN}Rpy`C-?i`O(wphnv|~!`?4_$<$?uKWg&~ zS6@26{(Dp~lQ$DKyxOgocQD$KWWd!}``vJ;VXHmthe6@rY@eL7=RZ7R_Ro(keR8HV zJm*?%t~G^-aYOtQ0u!$JCtUktMgR!~LILnXkAcu*=rP_nzalSx!5gr@$;B-E)0>2A z68A}v#7KxLVtK2(Nyql>C2&lM8GqODSMc$YEFW{<dwOshilmrS=BWL8%-^iR*#!%g zbPiDvLmsf;s)dm?d$32RJ(8TbEIBx)?;C}8BhA$VM`e>O4}jRxB9$B0JTWT0uOfC( z{!w}dStU-9ycI*be3Kl188DDyA9(Hvn<>^xGEGWiq;lP}-4;pQr44(gr9?@mL)*2k zS;#Aukx=>oQZ0-zN5IG5z|Y)5#ejuLk6aY|OqOpU7ovf@n~mFA@B_8bKNN(IeHMaB zjvucied+_5d7r?OOip9Si{XxYg63p0qil!z3p~W|O0_$8QVa=I@@5kve6m`H<LeXf zGQ6uF6sn6R#81xuZ1sC_Y)j}rMTevk^|p&OxSMTOxhB!Iw~%QC%MI+vClRMQe@4^b zcTjQRa|p80KRw2bF&CuWZQkaEuTnTB^}WK}Ls}OkI3Gsy{Xnr+ef|LaY)3^x4%6zN z8p4x10uEuT3`{SS^gJ$Jiq7?n-@+4v2VgYB>BGz?*QUjPy0Rxd?neD!^-~wYGe6g_ zYIg2y!=pVJxw|1w@N@0_PF^}=R>T%4zD$+D8y48WDQJfiO@{?+AN_mEFx{S_Lb@l- zeyBXP*~EX6-6$4Nlb^{Epl7rVWW7!z*^6##{K0Cfg-NL1As*)7MACFPCChEmqe%sl z819B~e22dFj|j}d$DdlL{Z#Pwi#dT^bi&(j3b6Vj_suVAuWW}}R38KP_gQfCMoldU zX1euhc8<9A7Y+{%c-lcSoCI)%<ORk7J%U*KA+9=la`6*kDhsTdBuCmSvu^8Y=H`st zSBk&R>fnlYw0f0apr2X5jD*+#BlzLBppx#MTCotbG6pNX`qD}Bt`aGAmBl0PX@nww z_!58jEhcoSwC>%B%+3T$(%+hYY>?+pg~PL=xRcxb6^`@S-Jl>g!t+c?(xO!Zsx*G_ zFb_G%$+1~@9)4q{E)&Qo>Go;LoJKKG9Z-zER*&M}t3$ffArxmbA{CRl(q$9hShw9U z<9a80<5DWs4?Q8?eiyV<%Arbfeg<~faY`0sFcVUU%cp*~vZ5CFvhSG^uJFzH@k11l z2Gjc-p{j|6eoTl3Y(sn2OBZAQ&}7;Z|6Kkp*qfrpf=|_OBv#@WYw9Sla0`oyy$I~P z-&8*ZC5?Q;h;%+6LjjMRBr&d?&km^G^v(CZ%Wt;`|1J3U&9wlo-L3fqBGnX~?QJ)@ zc!1MlVhYtK*z#%hWdAT61%%lR-lJtf!sg`AE!&1ttAf2}-%+YF>2HtD%iQxYQu;y- zLyG}Cxa(&|$l+gqQpENdzCLQArj4^Ci@)zvRfG;Wx^}8p;C{t2b)FpO{Ma9gppLRh zu8^DYK;*Tba~KJSj^_LB$5F2ypc`pFccP%&@OmXi^doiJxW2GgS=Mo67fQ|Up~Jja zbI^r<+P7h{#DD!7Uh4zsW7rRiPfs|Ge@WdOC2Ftiyg7Ad^d{K%`58u~Oq?Us9OXC= z!X~LcFUK3^%9q`bZDUf5I-q}}S-EZ3t{5}+`jtStQ*v!Bu=`D3P}yLpx7ep{eTm1S zSzI(!cogF=K+T22UM(bNu8dL0w;&LpQ&}5c+f!WANiropwvT-EQ1SbGhlo=prc6-; z!w(whE978pjh?1cuEEii&`d-89Ht7c<BQMX`$Iia@^c09+gcqwy?-f(okiTEUsRoQ zLdG*S7O>!qn{{86+%-9B7y|vp>&?d)6bvy;@Y$YACEdh(-&tT`z}U$iJIGA9Aqs5l z_Jyl2GR?Kz`ej<EtpDWxjSh?I0K$L7<Az`pFHdFJteJMNej(EMt)a1qzkM3o?K}k) zktzQ5_?VwtNcNNN3~?SRo0vBmtK$I|+vP-VMEn+H$!p6YG=vX|jk*6u>+_`s_|zut zi1IMs@#Bx1B}bPwNMU|adM+Hf{O+o`Ns1>8U2>T%x_6NOk^zJV&B^*l5TD#(0(U`0 zN<7S0pQ=y)tmRcF^c6C2gf}h8`PV0Rt%zc#V)o1%L{WmxpMK(e(Hc>W2R{53xhcC4 zz2$rKiEPqPZgRxh+rZuCv75j`E>Bj9($h~rX_n@DsebYGd9*4)aq(h1+cEN3&=<Ys zow=37kDMh765KK0ZPTgJ^tcL7n=G(>AAuV_k9}Dhb_;l9&3+cu+E4#gYh}YB33G-} z(JbB%IATuImFX#a{vh10#`L*L%|t%eaan+G{l+#g`kK`PCkKOoO(eH=&9O6@^kgGc ztRYG+jM0gD2tiD<_$oE2C4x`Wd9Z<tb2mY;XX?nVp^v``Uj4uopS>0Fq)IgKvC2G; z?D581!xlu&TReKt%tcXF)OLGPsHJ1_&j6|c$7a0Q?+1y`pbK-s4|cVu{bG{_nOowQ zYh@a5q36EvsV-7mVEeywzol+6bM?PvPbEjqOtASI5%~L5NMJ!<Q_;SZ_D|NW%{zrl ze#r<NeGG5kP=P}XV0qzC)KoDA?g2NMHQ*9nkK>Zp$1K$-<>N&Y%LHF^P*EucHP4<e z@z2ovxspECBl+=(Ak_BZOXp-G$nZ!9xrNEKB;j?xiH-*N8}v}m$sy9x03*?GoTd|f z|A51oZrbihjVFlJLZZ{SN--NQL@(lR9%A_g7R@rrA|$rRM?VVmZZ>xYn_7=e1r6#_ zk9(bKkWKPs{B$K)=hjBIOzqzHG29Yw=o=+>v@4}S$7o=610^xGUte!?3s_KRAq-Vx zEV+%b!K;c!Z1<?*KKyPv58b{ePE7IVT5!B_azV^j*y4bFxXh6pI-|8r<3l=|-_eu5 zpGha$-&BaWI(^Ezz87uW%wti$abmJ`vcJN~W1-+|(274HFp@1X$1gUF7?+cbsH0#S zv6j+v3zv~pFzdlHSr+_2{@S@q8rZy;Xwuj1@vhS%d(NgrXdk}C7a2P2^dpT!B?gdw zWgOMey~neVTF!7(iwkLx=lqq9_hmMF8K3u6d%XqLQ_%Fx0-d3b4*%wedbG)?_-1cT z@6+rfXv|`|oRwZtfXp}+Zczm5=9T>T@Zq*hPF2#}X))6B{oU$MvWJ}C7k$yiq<ru{ z{2y)HkW7r&Ht!Q#cIWh$!UcxdXTe){=v9vUm>c*@92O+aKH}UyObd_T$)YAbv;IL8 zw(-RtX7uAzLSk$~Gb}yD`IPj_Evjx<kBBrZ?32mIJ|`yKM|4!@nv8+;_ilaIZB<S$ z4{QmK)h$XW@xC6YO1u9;K8c^YtO^KqHldZDced5F$4tY~*+xx3*&(9#^|p)hz+;c9 zgD0c6X{jeJdWtE_6;0i=k7vyqk7M9rBRo&`mwDv2VdxkdBFb+`Te0d|+PtBu)~T+P zg~^2MsT{&lZ50gq5tweE*(vS=Yj3JS8WeA^U6<M2pAa?O(Q#Xx#N2DDYDk{5a6$pa z+Td3YtBOqFVP7fso*KsMJM81!-~hJR?$Av6SkEi%W)NwHM+5iWu(A7JrMY?9&tLZo za2-<+F-<$YfjvtMiM~S^!9j{JA&@bqdKSHVT$RLQ#Kh)*>l+(!@A~dD+KB5Fdex}V zN>+O(B=JH<Z&TKo|Kqs+-ocRePAJ~rq>g8+S*nQ%zueIo`47|L0Sak5;1Q+F{GkdP zQ#HNib0AQpk0n7SI4+A^RFgNaqmMMR8@jmPJYF-lhLNBbBzaO%NqILRrL!WlK*uD0 zYGw)3M8(EvK@!3$M=JM&y9ZIRm}wF2`c=W>F0os~{hv`&_g_m<{H^RpS-zsBMoSej z^FvWgtJ0C`l8*<{3qnd2xus=dR0nS6KD_Vg@?){`*CF`IVvy}xapVAic~yS+k=qB} zQ7tN__2t(wsqT|NS=jB5E%Dz!gjz{$Ou~1cJT{LaHuq>qniK4sSCM^r-;4BJZIT7d zVbq%ixt6E0x{=IkMU>;9TgWisX&tU_Xid8PbT?%*dxpU_S-F_(Mk31#d<g9ROj#5D zSQgX0JJF^mrq}H3!?#u7Ozq-Nyu3ZiuQx9>^WZ){2y?xmG7)vB;qcGn#_sNeS!Vgb zK}-3h8^R+F6=T(3{3wd{#nm|dlpEc|e${V8><9|m#J<d<x-n$a;MdjT^Zxy6;R4RS zE9T_%;^Cv7_-==%%xjgSDXT3gMy1Z0HlrB8*$|`f5k3nPeT@+G+^5N8Mk<Dmn36_U zwJ&gy5PsO5V9C2~vilXneiKhdQx<{~#jeT}_`%NX5%V}3qLuTvN9Wj6H-sHL=^lnA zhrbq3P$?z^^zg|@W`9g@{+W$l#4yckRNm<mn&A-aCCg<+j%4!zXKAEGljh+DKS&6m zN-)^2Vp)`6w!sa;Zx$$Bty@w<EdyE^iQ6yK%c=p!K*-OJYp1-9&Yu<WHF3CA&@rnT z=AH?z@S2-qH)s+9_gD?2Y8X`R=aKC?xFVUzYu~>5Vk*OwUEs<P<0YnwaSbwQz#LG9 zEZ{BbIijV4ky`S(4LTT&4aHZ1Rt^#tzAjs%-cees59OXnYtxyL2PM`~;;W~~topwD z>Nu<wNzug!q5H_6F8560I`ee+5{0x@*=G`rk~$BKkuBA3A`gc>0e1Vt2;xc18~k8n zBmT+MP?c!FbZx2UsV~DbkZH8u?h{wJ>U{EtNcQhDxIR%urJ~Uc12q5@^;<F(gIUAX zZN@s>4jtZ{FM6-hZHh3uWfGwb1tJ9^Ud<fYZ^}Jcg?Q>QiL$6*CAV9CUy}m6<RxX4 ziM0iod#i!pW2h3*G)AQDaTpISDa~S-zR1J0Zjndc=NT$`k~T?1JDSR)=y`Lz7+#;- zGE5Szfxp?Q5ERmH7~Jr_ll4EwU-&dmJa%2G=tiz)R1Hh*J0G0SOwO6QvZzhF?0za^ zI@^M9H)JU`SDsS?bC}V0`xlSJ5K*vi&$tH;b6K4GX3H^sWvdwy&R$|iiTyMr%K=8E z_bp3gNIAA^4bBC&)>4w6#@R?dzrazK_^|REqaVZfCm%&qmMX;-?@GW~1MG^+vy_g> z(s8Bx;7lQw*U`Wi^}^BzyJw{QOryMB_xB_>W-&6u!d@$8nly_+xk@rvLV4C8VLx7O zqGCQbXwIJ<ZF)CgllR7{!b*!n&Tu$?N|LE0>a{@o8h@~cOK8Ftnz_tmiJ<Q=X#SV? z!wt;X_fA?Dyad1FZF|B{TUlLi;<sEG6pgzqA4s!~V~hfRgN(~Fhg?&8XL$?w(@S^j zUS}vKcrChkbU4V8O6ti&D342on2{@zqI-{?`R0&(%Ufw1frV=7n0A7A`Rn+-0(16Y zOttwVm+({Mr1<nE6Tj_6edK^iWa~@2aD(fETS|_nP~8IBSSFEB)-NVUQPk>w5~%km zz|34rngK!$K~KJ$I=$UhY%;)L>;FDimOiWKmWw+~!G3;yqoa1#zN41p71EcgVQFZ{ z#AGj4Hm$$%V$YbZMJhVjXs_8I9ND%)%BIUC*c*}ggNS0$n3RU8op>xbpW_Ce%$xD$ zrxjnj$KDV|InyYu=a$t1{0xDD4J<13VZS$!QJ$KMO(mK;??>+ov5O1-4C{t|z5n`0 zWTSvb2<ZgzdqpcS*#6btwK`LyqdZxqZQvv5*}S1M5V&;$GBrV?8poZHL^{TO*X)t# zEnycFfsA~U2)#c2oUz$7x2+3O=0zpT;?157zMGq<r@Qt94s|nX7zKfdQ)6_-sK=z` z#9@wv;Pp?lhJpkl)%+Hs&=||*%@PK*A0(fz=hLb*^1?i}#rb)&!5TDW(&jTy1z+D| zQjJTqzE9B{CHCyoEfei2s4k<9=i4Za@SDevZejV2dlo&(^C-HI>RDx*dpdX0@o8&x zg120(kvDnhnJSVHcP;|XCuzoPs{EIIFkh_Cd1!+v8iK!0Z#w-@o}%a*RsPhDShhx& z9#3rB%k<X7oQ+-(_bYyuDZ5P_;5)7in0|)V^%qK>xMm1_C`<b#*e#ojU@a$Q5e%P< zwn0k$B2OJOd1u*d3@a6N0~Xgqc$Cyw-52n<*pa~-cg`XmkvTAhIz!Ccbx)sqIKCW) zVlTyrym;LE=G@Zc`K}rd?)D}QAs?%mlC2Z+Tq8nEtGdVe%m`Aib#y+`cpbxdx-&sA z5up=vlZ9;BT+2R!e}LrA$6MCVMJJh%n1MH{ISP)+3)c#KUAUFVaP-FS;80(ey>+cN zQqnj1cyYRuM&!?O+h@32I)$f!K#(D<Hm&qCVb^hmVG)1QzUMR}l6R?5xC+2Q0n1$$ zuvt0u&USz!<$FIe-AYSq%NXDf#Y6+^^W_PP@M*WH0%i(xY%q0{T^ZMubB!;hpIJdT zMp1@spEPJ(?#IJC_4H;Pp$&|-{F|a^nkVV<M88uw@DVt4;X9)uNoWm~J;gA?5rkC~ z5FtAA-ae${`CROqjbE5~E@V8=;jN09qCGo2`TMlD2B6@BWhRyC5C%2q71a2$<TubG zHAbg9{iB+r)NKgYSwx|Rvi8vwG-=rc>p587ki^$ItV7CTht0$^FRSUlKp#Rs;D)FH z0VG|&VNqTre3`M&4oZlMRxuQ2y2qSD-NqVNGotR0w?)#GnK8hmiHDxsPLZC^dB{Qk zx>u#HyjY`~L44q_P5BCA?M>01@|B>UE(vw<_g^8o+e{iGCL_<3m5vDM_WE5dmHL(w zkF$MSeH-cSE)AcVRbD_pw7I|I)v(3;@JnFRosKS=!F1j%H0_qUdHA>M0X)n%&n#V$ z{L`vxNkE(<?JzLyNsKRPZah_}u6?Z=`@TpcfraUNYqkApd?)>oxRmtHm!qP=U#G06 z_hYU*$UfDsY;`XcwO0Bp(wD-k=Ob;eQs<7p-qg4wcXK}3WVSI%zYF@}Y1)0fWv>lQ zx%Isqi<KG+VrhKCDa823)7npTC!$|7p9d0EoTjJ?O%to$oVENwAtiw>m@|4{a+CR6 z23eQrd-#zTy}7PMxWRnMo*$@vi>geuTr!6Gy}Nim*{g^gruSwm<{6=0^u1g-)*7-| zYCYXdF!3)c`D?|c7H#i}@{{A;kP6~7-M2rUbnyh$zMh-im7fu){YrjEcmw>D%9LP4 ztzz?|C8eIZ5;{<=>$3tyXZqyrS^&zFX&TzfB3*$Z1o|uAj76EGF-6$_tz}1Qn2L;X zIXO#kA4KuuXCcfTubI-YAY&NA8KqagO^gl4NQ)ZYu#N}fzRT3yKlj$bSn{wE*1x?E zVSLl6x?(;@-t6RypC>29o%b$!hcnU}J+@bJ-@T-IFeNvBW?4EPUc!EHKW24;<omdW zuxZk&<R^kz#Pa*K*wNw?UAgh6%h3jRXN-O#0vgsYUL?&sDCqunu%57Z$xPFYisJ2g z`9yJ!*D}7)EadYmR#Yo;)2eRe_cY4A<%eacT+ZH;x|`v-Z9$lDdcoczNj00$3F=(e zuzO7W4O8EHI{H_b;(^Neez8XW_LZnIvX@>J)ni=1Hl7Y^_Q<d)Z?{7DV2~M^hXs1) z*~-m+x%5tj=L7`5F$pPRLdT3kJMK%(Gj%`aXU8V*y4Vw>CzVQ|rFaj{YF0;l==kc0 z2`DwgLnYq)K7H_qS!UnNUrSFT#o1VH+*LEv)IMu^8ntjCwet*JdfPJ=esmps@9n3_ zACDx_F}0>2kDgk`8o1fRyq(ZeRxvl^3*{6GMi5;($BdNk)XaBf(HXyU`dFIPYXS2C z9_Bjs29Azxq-6nPZdr)p_Z(<>7x!p8#yWPl#U6Vt3a=XI@zq7E6U|A-0nxHwzx=qc zASi`X>YeAy@q4k<lc{1<i|Z&f=qA~buKLyEiPq5iH86H2!|~(I3DQV~gUJp2;ouIZ zA44_i9nFF+bU787)-=VqQ&!1{oFjU$mj~RK`!WT6h`bw5I|fS&3IN0u1MWR0_5sPK zmE_po6cUg{GTg52MJVRn$!r?u7({^6^<j(ZB5Ro1-AyB7^5ql$j4Kt`O(9;h9C8W- zt$Z#-JcGxUZdwe41SI-=uiy_lB|z_>txeuMc(5>x7@!nr??e^2Azh2Td<E<FUg-BC za$g-Qyqn`$uoC=`nZp-KxN<Ndm&+CF+ojepY;<U5jhsT@zqcn)Qajx0k{V}1@-DC- z-Q}6fhWV1cA1=q;_mSB;2Dg+>nAmf)cizv0H&_TTpU}e*Ic~_k^$fU)t&$~W@WSSv z6U`#Cq=BfqW)g*(+VOBprhPuwfhMKE28LZ9lM`BJcc3PdWPvsGU%peBpsaF~%o`EB ze)8!3YdRM!J>f0M$|ONtiXs#Mq!)1_wV!5a(444<w&@gD^2igjC}-q3%N#eZKMc;} zUr2^G%eT#AEZywGL*L2IMJ#ny19^<jp)zfkd!&(4HUZH;53=F~SSM3s{dj8MSSo)9 z_n0y)VB`K|N#@ubNjYxTJAoQxN;z<6Ze@iGLf=Sux*)#oLlLDQ|M$-*4w!=d&)n<@ z4(5k!6)-+hrgx<`WcMg9x3hmPll9gz^dx+^?=^B}XGFbS;UUm&gWU4Xge#sa>UJpp z!B={Dbb93Qb*RbmBJPoDmbH%AofzfiNDJ)7f(3zHjAL2Y<45h12Bn$3#t+}r*(`br z$cE(G<|oChk&zZ8#dv}#7fr`&Xrck(u9%Um-eZra-`#_c8Y=M8u9Kf9i5VtkFqLHn zKmvE}5K`hu;Xetc>GpLmeJ&&Y$WC#Dly-ey4YIgkIqm*BJCb$Hd%X%Y(M1xL{JeyH zk+l|LvPE;HoTqC>`-1Byrzv8b$YkbB`Hm?zXXf>K3Kk^$<({O@-LD;~J><D#h8U|E z0iX~*UGVw)<f~(-K1Pt@CV9WJBntuAD`Vkzcv)+KmwW8wgvuix`C8R=D`pc%s+-6P zs`fwK-=|S8A^76BA+nDKWZ>SkDb|j&pa_#KGQ*GI0u6<y<G(!xR7J}Yzn%*PbMxRj zjW<n07fqJ9Wl*ZY=@+3?E%j5VX5~L1Ox%a~{+$<eNDZSE^?UW+4`7tYFBCV!j6=I0 zU9xwzBY9VoMSMT=X!!mgvZ~YEZ=c#aDBqIlXMz17)gN{zg9ru}*38^W?7SNsi@u=+ zU-D`+J@-EEXrMKgxFf9w-8Y>%uN2|9Mi5PkeP(i#g{3gfs22PZ-x$a2T;%;x<I%58 z9s~~hnm=3;d~b%7z(9-zaM6rNh(aPY@8eFC1!R73*r~S;pT~k0vP`6Z3^5KO|M&BL zfA}RJQOs)<5GwK7<uQOjI8IX*_|-oI4jvKs_kjck9N1(}0NxHkuMQ4WfCJ14z{?tV z8epQ#&wb7RO#nskT$QQu&x?}}nTOj&ke-pR6<tb(yPoR3sa8e3HWvrlax{PL$9buX z@Gr66d0HMzT;(`o84ovv%h1zDkx0?hSif(zK@Y_qzb9GATA4M0Ip}sQR?yUxw&Mx! zUx$$1WdL9=ywa;^+XDm^W-c0QE*AZeBPUsIX(!&@FR??lePyFTc9CX(Ja6)Eq>}dv zYF11u*id~|Y+aLxSG~hjHVeIl%6H6u{-YK@xU2UEvFA5jH$5Y-l(;|rXq?m^_x0CO zKM~3cm36W>iRrR2U&wD>)L*=QyZaIwQh(B<GaWi7C{$tF3gsKS&_#wel9+qc_q=yj zrqbfy8cMS<1T0>k-)Pti_-QvibFC66N0o~ET|-}!cn8;6t<Km}5x+Zl_IT{P4`L20 z`gnFk2Fg#WZu#rP<zb9n9poC&i8=)PPDODq)mf4a9<;dSmc+9G*leQQu7eZ#^DVsl zSkvGOPU_F);{j?7O!+{y_*=PiF+D_@UNhFYyoHFz5%DZ^;B-P`L3M^=?XWd_u;nz+ zG;SCpbi9)GROpc}u0;XU>VId4HvQq%T~L2z3~?AdS1%TTq)bBRq5wC&N>(JK81wca zaz)%Bz6;bhjF-pa=Yu>rjs)h$L!|v=tF8rs1g||veKt7Mv=lUj5wKwnWA8*224gQR zMA@>oJQ#$X0{d1D@_juWahejZHF6=pUDL4%g@H5CgUwtd+m+>2|7g4@w|!&rA`9}1 z9~Z4&sgm;)?6D!>$+FLMGq3}|gDH<*V(s)o(3=OH@zt@Ql$sMu{f->nVOJoPz=<j_ z^z;hEVhwdR^L_P_3LdcSB~Wm6f(5m)d@s$bQyHhpj$j1kNi+mNv_THKw_3lN2whSk z;4|XmrM)S6bpzMw6?!JTK>i5{aZCz?FYCcB%BC94uY*j88ypYE4Mtd0hg|srPgcWJ z=32L?%%x7EfXZ!aEeyy7^EGtofl){NB^JhLK&cT8O1Kk>fwevtTV3b8Bx$x2Rj+dC zYL!5GC(5(78w$w>+C<W?2L5*#=Tgex6rc*}kqK8<@Ny}nYyNy!hjoIK|MmXMhoJWt zgOUX&zq)_~PF=ZhV#_Aj6$M18TN_yC){BUqzdZJ^JzHiFJv~b!grF~(qU8U_v;XKE z5Q5l8{&9G)<tuQBXaqX<@sfl8O&CZ!s3TB#{}Vy%E@MMJ0B8C~=1R{DF2^$yqX$3e zi(Qc;V~VJb+xW-C7?2ZLBGs2-gx%m-=v;X3ec8sE^DxI(mV%MT4s2$5Uq+@xo_M&G z%FkVz<<3idr0@9_c8Y$gSY6!&@-b&4E3TvtwZTR!%NrqKkC7l5J!S|>9_A|(zBPgx z*104qy%TCf0I^55L<7I^*NZXW>l$N3fK?^Xdb?}nk`i#__TscItRQQ|oWLJ$!fJzM zNda4v&=;kccgLdPlTeWBCpt*)<?EnM+7n~C^~U4HAf2c#b8PfaEvyORl5$?2EE}Tb zBct3CyUI%q()+xT01@+m=6!C8z$du$fPA1CQiYJj(2VZ%_h#$o10+{A8ShM$c8NM5 z4DE~}vh#?!wEsUAux_qKNiU8O6g{X_V?-C%iDC&x2FDoDoTx4~A8&a{KDxzx1ZDHg z!0g2(s>fv=hoB~=FcSXvW4L~QgyR%rxKq+zb&r~PUrma6fz8QM83V<_&4-ME<2WW= zYSM<l<x*ysssw%L(mp|Jy0oh+-Cu^N%hhA@(iASOMEvg6>haHFVRs2&^AG4{oV%Lk zG75kbfN&C+{{N<)!f;1lYW6?d`42wje?5%;*W&E2mN!sR|15N%*b)DDf%**S9wX3% zG+<pEbvZa60DC&2#h4qH6y^ilINHc2d=5|;GkD>A%tDu>cAfnkLEXBFO^JM$)PAk^ z>*%n<3gAQmpcjEd__AW3k0+8|Dt!uLZUfT?ae$3Vzz8IuDG5|I)I3~#XmJt@wcv-s z;Ov#ac;YpQH31*Tq?iK0uk=+&t=&UmNRXtd_FHqupBGLJA4l|5m|%&WaSMmu(}dwv zWWtTEO_YGjH!y&g)f0g0ZWE?&kQX;`<H4C<MJW?TscaoH>t`{E4JRUkJnjW0-LP#- zFq~~(K#19U6zfc4_mEBO)C%Wv?cah<;D}j7cA`#|ZM3&%as0mmzp8=9rWZ>{Wn@bc z)6nI*-sGTg9_=MCASctd#B1>)Y{os0%#NBv>{JJwqX`q)_={K8o$pQil7!U4H*|*D zOYE-@Jdih$paTulpRu}T9`xz*FJT<1CW+TB5!hl+LVoH=+^6&N3W7grBo}TBm~!&Z z^m`XF(xPqwj<F(0HBRKgh=T7*jK3A_9T&jEc4&u-EYEzaONS@$uPBGt!7*jqwFU=9 z%j?wWjN6;^Y(F`$aI0qCtGKd|-{V!n<vMSVHdWgEM0u{KU?51%(@q6(<j3uc0yc|c zfMugG#cZH@=WgBv!CapR=zYAnA_5DV*}#g?!n16kCX*QqAzNkWYZzeD@7&dp8QIoI zU}v@$1@GFdd@FdRP7wpl+E(j>#F*+uQ=c|W<hmvPD+~Y;1RY|*>aREaG)-idSN2tz zjNp|hB`_kJ(nyVv8p&ECWDfOB(g_Y=rtmX}s>OjDq4xCXJDeJn96j>x^e*N2N%3Ap zG2%LY>{dkC&TCt~Z8qTkL?ZU>h>|p?t$Cs;<@AZobug}*W7UWdCFj+}PaNk*wD3q1 zXYi@Jpqfft3O0YpoXa&@78m|lg=17c^FzRwww)-m2Z<SaDiM|*FWpFyZ`%&w_(cWg z;Z-M-Gw|r00vztkF||7?)%&xLsl@KtE%P%ZHFkDz@U*{Ztan?q59Vpds$pe#K_`{K z(3CFjE?AX48NS2lHk;EIBWRo$(~4$vi$lD<To@!UQ!4rfjuvioazmrz-&RlG^JJkF zr#uqX>#7rA+$uPrq@sWjF<xc?v9mVJV-w~$BD?wd`CP0vCc&F!bMMH!okaJD_&Qkp zt<Jl8#3ZZWBwTXL@HvSgUzxy^^QN;^V#Zf4*=PVaMQl;);=DR)T;<cpSKT9=5d!An z2(~P%dhmleILOoF=F#xzv1bD5I^%r|z;gk+V*wLg9^dn<iWCp3@NLg?!24$$-KX#G zIihy0(%mr7>r~|f@a1gadJ)l!a8xVGy;%_Vm+d9z^@~p_oy)>jJh>Jyk(y_ul}Q+A zL5Vmrd@}!8%C##gUbk#q{93VU*8U}Lfmb$<B3e%cImbT){W7SqDDL%-1gGBktP|{K zvDKg~NMvP(L!9?dhfqB7!$tpgODoqf#?7VMejwMBL5*iF2;aNghw^lhk6^jpG!6x` zG0OFb%$faMuD|=G<&>Sqn5FCdK9`FhFRnTE4N*VL?UYG?h&SJ(Yh^LF6Et~?_o(fE z1rkw?d{i^VxcaQLpV7J33O231FL2|^qHw&#5EkC%>WyW*JY6oz$o#`}M&wRw$8(;4 zx&PabTA8Th=c(@BwhCXb9$JjD3isIje&$DK<TVbt;$Cm)&_Oxs`Nav}bOK=KooMQn zn&jNZE8Bki(l9_Jrl5;HzBfs4&&a2H`y$)Vp5UKFVuxjU)zn7YLo|xwJE~<{-V$7a zKvtuG$n=JwjU-Ix5CWN>h-q7B?7QId!;ilSMhwu<5{}5#3yQa`unk`UdD?VZ@6rhC zf867}0qv*tAfqrLq+B0nt!`-X)5Eg;5WFiIn!Uv6{MOHh8|C}M$#Wk{b<k~Ud2V4M z`Tp^hHwr#KlpAA&A8fi;f?x4urb^wu!bU^C@}=k~w%^md)KhlU^QHX4ULOJ-NAi#N z8Y}q_%eoH#odshs(@xtEYHkttAF;ZYE+X#=IiEW}*e95T-goM?3LmB;fxJXQcov8w z^eA;jgqXW0BQmkkHMXbyR@f%>-u`QswbnwumzbN|aJ=0<BV!pm^=^mA49Cs532b5H z04KQgm_-4NGp5*|zGpq~uAM~D^=NGsTo*c81<)Ywq`q07m%xl2yaxy?0znEp=#i$W z7sp}fjPA;!9CAXn_skicsAGz}<5YMUfBHUeR#k3}q3)vY<0_n6ps|0D#MH^ou0MVe z&ORzxzlywYam(sEPU!A$j`S>5`>VQLf*1F#1^eqJJt&*IQWy^nUt>%Tkwy6GPWAKY zQ=8b#Th#Fta#<dUN@rsCb=MT{DRno!{^(1B2AyB<7Me8cFZimvL<>*XEu^O{=<vBZ zxr(-5vAc0H<IEFHzOLc%;+QNt`8^jq4W(7X+e3}BWFq^7^hif~o&`4yK=s9qc;<GB zXxSVI<vGw^^ts|fyIiK!AtXT0=g~vj+nZS<;(b9qoIb1KVSXfNN}?(RB`9$TjK{@3 zo-&fjRGo1AyrlD1TKKK?%uE5og#Ipy&t!;4I74yf(Exe-<6g#CPTOvELL=h!jZ4!; z@0_y^p|q<<660MNgz_(X&jw=ixLnsViZn4E3|ilSQmfqHc&W=!F4#T}<;hxuE??9) z6lDAHb-W<kIfD<(!mAlIur6Riz>Q!)ViqM@UMNZ2=iJ*BRv3sIx5Ti&3b*Tl{y4V1 z4e7-|I#iISJNrK{KuruVq>2x`cr;O{P}%n9FET?j{4A5$j#@9XYiuwT=D}5fW1JY+ z%8gi_F!xT}(H&-b$fNGMOB($y6gqTZaQyfE!yW3Mi+zLdBC{s;sKuS-UTlwmRDkR9 zV)}2ky`B95*Pq~5%;5Vz%Ij)SIj&A~#uFol+sI>;YwfG8&t8zEtx`sVxz}mZHHni+ zFkyligZWj@YDd{+lC@NUfnQ~-7gu?fZ`TV$l|O&M0$>Eng8F_m{<s?e3qArU_RnGG zM~eqRm;N3gBDh$NUW{%5o!pLKe%xnKS1zvRDw})l;t;EjzRb@$zZ9qoSzIQkHrUXs zfwZG2(0u&)f<9b50W(A=1m#uYCsYVLyh=Y^^^tOy$tLQ9`6UpT?nLffl|5kcS9P@q z!gp0UU+spZzq^_i%sulj|F_`ZG}aHyK>y2b!8!k{SU^0N2{_36|K3SKK|lun&0~#^ zOgEKd)^C}x5Lmyx(R^OHI`e1$soxKJmwS)x!&t8V8{rDa-ODYMt0eZ*^3_I4{{IwV z|0cnXS2SH!W_wo*g5>|-g39pt|JVP&shhnTf5mfp<ds}6Ir$GONOH)(5B(diGW7p9 zO$Slk{U<D6X2$2#mooqF?;idqqsQi8!8BSMS*4E|K4!kuQ;>}XtU&zF6>3ftQ2qhz zuR_cwqDx_yVw<Hxm+qz`N2;VQ%_a&cPZhEN(a<A5iGp7kFDmPNb$&;~g9{Sp9Z(s& z(+|jlRaidI9}5VNdQJlOc))NfF`1~oYL{(pdj-dV{N_0dEW9NLVO(Fp=984WK;$D( z%kQYO8YJsgVKf5|6E$*$eT%!Sj1r$RJBzx(+8M4Yo#&YR{V{+NM45PP8eQ6<ADN;K z_HfEzeo5{uqrj=w!J;N><RkddF-gh`lT0M`bn$f<`(=gFpA49t@jY};g@O7pf?=9E zU`Im$rQVfG^rDU&Ka|S97rY_?1J7OT5EZ^GiGpr!iXVYY`X9uLb)tk~ud49=)qM*r z&GVj13*@o*VA*@ws5|Cz{<d+|Y^zUxDo<l@<v1R)pqFJ4{cCZT0RLKOC1}Cuos@c) z0jF5JnBu97=MA^=x6X-b#d#Gupl^7dzAKf_Q;BzkSJuwabn$&twr{tw8@8<1qCTg! zWnX?p3ojp5_dbM}<z^|@5;Zd{{vHhKw+<&R1>&p=IL7fFbtONIU4OQCWVECE?1ak> zW~e|{0=!PA?VxiO0}O7BLmAAGwmsC}#kduBpo<p?h~?@r+BC`Isg9qV3*Hhn;7n4T zm|vT-af3HpjLYlOW|GdZ7!=2}O(3GV?zOn857|TbJ-N-Ky}s}J{3cb3y^phQr`KNs zY5$)>t~?&f?f-{l>xQltMMLFkaxINaa%D+cCX+IXkRlV=qrxB&E%q{&F-2mgvXvzz zq+}gyy2diH3uPA~%kP|LMs>UQ_xk=bugBv&=lPt^d4JxY^I2w8dCgLPubsOmyJ;%u z)8iSD;260*G8>0)9B=o-9}RsMfuE7zYonqIgm}BP*Mi{g(GeE!K5FGO<=eOOxdUFD zNbnEGi|)ntZX(4*#H61KqEE}3#5=~0mZ8-9$J$muU&yuH@P==8wymFQF&}TSJ3d;$ z-ySZ$xf_%ABqzJa#4by9xuHu9t=YA`#N{pNWXp=rgyIu#ExJ^aW?MY$OqnhHWrHm_ z7!3v8s0TV1!=th%88@Es*UV|8(BpOuN#Rqz%426y`3esF{<b43JuS*F6cmF<VH^6@ z40q<W##-(my(3y_i(CyfkUYE^8>J(Vk4lOxST*8F+oX2yb3?FRCi-2_grI=Y%7J4% z8P|mu*811AL8ZGFCP&T{TMDvlR8u~6GqVeWc#>uM#Jtx8rAF#pCU0NM^@c~*>N#_* zqH&`0wqmQmu6H2?t5`Lmf9&H;>lyb-p(ekenClM3kZa;kjP++oU3K&Il^<&E=^K&l z3ctQ$+p{s}_|I#ssdx4dt6b2H3T+A%Hs?>H^Gj1*`m|AHn$I;~vV80pEG|!=2a9u< z2D@(5&(0cb!9ER5tga+_s$l=J5@sI$V&`xQe?E=&#;cVkFMhsw=lKK~GG6%FMN`jb zDs~RS9|#JUCVt~Gr0SME>A1m4u}L95_C1D+QZiOlBWZ+g5@^Z0o|)F16fM3jP2XAK zu48TO*}1~0KXg4w_Y_LKl^#6tSf-@&EZ}03SbBNA=*Iaf4ItrWe|v`aYp&%v*;O&u zP*bsiqkW{n1H3!%8{BMakLpca$f(>uo5Iz&ZAwgOW1N9*GArY0caYI$)p4l^Y)?nc z&Bs@A{Q`6v&26Ga5`?ykyz%Sbnsc^yo5En^zJV7vy}}w-3{iJvAD_8q)tMH77uX?_ z`Lx|%F7B}Y*Cr0;_UB9F{3&72@NyRI+!GaLtNz*IH{$2quQ^KLM>0bk1lQTcG$uF> zxqObOuN!jxhO6bxc!3ff-Ip~KSgIGCZXtO!#9r&8t9Gi2!fN~Vwk~STDQg?lHwOlk z`BLooPP*@Vi<PlZ5-C}8NX=4yJ3rfGT5*aVG;tRB4MFX~s#}dRSMgupO1^J<yX_lE zh4<s~+ee?*-k%qHwDBF^{SCH!nrfTHbQ)jy89&lu<Vv-@us@4el661*a3Em~ab-i* z+1))&LZdyWS>)y#JJ%A|0RfGJ9015&pO3I2q~GOH{5^m3rku;HxKkk>9|waP#G+gr z2qJIZ%lo~sH?t0F>WT?-^SwE(cYN&1L5#rWv>sEXcGUZKLc^t>$gYa^gy1mY2&~Z9 zQ?%Kb8R^bpQG_k#a*v&t#g?ek{;sUqy}tIjdM~Ml8P9ip+h6YO^sf~Bp&OYKZ`M3b zInxk|msSp}|NE|;?c$CMJf)d_S7>wE&c?TNsgEsmck^umBSiB{mZo)M5&2KiA|poF z0NT3sg;c-^kToQd55=U6I!LdxlULtk(KX_+DYclmT>9I?6tt3d=hbr5>&q(3c??2I zP!AvQODJEhdF1c2`J0&t?<tnpkun^AX(Fn2#WhMHUFJm&I^5M{%#b83Fulcp3L1%D z_mc^W*ihG^ioaOoi3TzM;^mv(olP2@o8zHk^ma!V%i4*0=30=CPkTAioa@op20GK! z_OS};YNQ?2k7-BkvL~n(Sevk}?rNS2(Xw{(RP@sy_of!DsipB@`4bu-2`tn-R1e1b z*6gq8v{c~@iObRq`%VA07x;2mYiY69g8XFuZ)=`@I)5npR;n2HuIHB5D5n-xMKY!P z9}0VK`|yVe>-Da74ewwlwW0fh1+N?kk1TT(llrbF;&uTG3KW?Q=&{VBj6B`6Y~9$D zEv_nRPi^hK165&01TSmgysL9<I}6Z_Dd_Oe{7D7qo3f?+1w<i-**9xjUKec*=j(iL z)QBx<V^NFhS$6{KSRzpE;9C<#X46vC@td9_>2xVlabi$t@WvB_(q3+rck8ASRMRA# z9}V@E-aa3rtj8+-aJBn_)xmSxd|U%9^rLE3rNgSn-;VBU)wgHl=j{_aUM%0XL-NVA z6i4ZYM1|47bF{uR$0r<F7e^^CFvg3wXQ5S9_8EK7E*Tb@<9!vipSWvfUfzv5XE)9D zHN!KwD_5!~LFcb_YSBPl0lMf+Q+8{{*Vhk93M~kkg_Dzx6WS!di|W$(Lekmb_ZsX2 zEjEq&?fztucZru;;~-c|vI%)~u=+|ttl!2yEN?)Uqk~Wd-?C?o5#uMb`V#j`TnSZG zjW++7l7=>pu+%=&XyKS2JFfe3i`;USztg>Y-Mrnah<Tfu=x6v04E-gN-K8ZL4-M-; z*ZQSSRx@g#?BR#{iqE!}!?IFY)tS`3;6?v)ri9Jh%@XS(P+Qk%`hPq8cMMPR;5)zg zl3Q|j%92H|b5~1NpJ1*%Jc@6M<yInz^oV4?-=IS+B5T>KjG6fMW?R6zA|~ouR*>7B zs?Nm~Thps=3;fxF8`mh=Q{EY9R6mv&;l@V5z#9V`J#pv2rASjfZB;LmyRX(OXvxjn ztjf$PyC;>SuKw*F#?~?0Xf$|dobv@fJ1L3M<OI=5@!#8I=Z<$on-W$g9`LJqroj~J zmq;8__ii=K9$l}()H!-jWA(~{u>MsjpY7rWBoAKK61f4%O95=>Hr#oBB~C3nDW<kl zze}dcPxQhiRaJ{CI@pDiO0t@-GSSC8LP?jn^dp+CG&!Fq+~2Ul+nSJb^`)lXwRD~X z8(#O{zOR@a*F?5Foiu$@USKgaTu1AYNrzx#!w&l=wiASl%Cj+#x&kgf(Z%W_CkQe% zBS0sJH5RB{ya%IhXoeYHOxAC`rBjoaT+rij+OI9j;q-pX#`f4E@6-H6PnGY6{+ZqJ zK5sDU_>TTHM|G2>+2;nIuz4Xh=Lfu3Hh*}g{ZP_DkSENqe5U(t&C3^^BbsPnvCuoH z23&lIALAa;!S-_B4OzR_#tEJ|id+;{L?4|C)#xr~0Cb-?&9U+2N7_nzy$I1IQ|Y~| zc|pz`d#()ZqLC`2<4DQhcwVlRy-XEu*I|3U!2W<j@t!i{Hz!$V_Vw-M9~&9O17cm= zLLFb^o*<7NpDBDho3X`7rGdE^UEryG;q0Lzr41{?!(v(&NoAQ03}&{eyh3#9vKp>y zM5m5!Bg28hZz1r6&9KH%DW~XvdsSCzPNTwD+?asbwnyaCEdwyHd2JebXZ|7*4z?x3 z8VXaa#xupB6`qZ^MW72bjRN7Yqgx0qX?SM37&zIo!~(4tbEjk-hT(bIm;j<ByB@sH z6>G+)f>Ts>x}FktWMHh5sW8ugPfHO@+9sPwZDt@?grU!uxzTJ|Yqk)lwAZ8Kv+&TP z7o?U!(Yva3)|CcADhf9*hUkWCk94s>q@&!?pFQxr>cviW(c;9F%=QVa4y+11%AyY* zp@4%w4uoywnO5``kiYs3lw`(}qG$0aaPW2{<bc}TqnRXOj7**$*ZulfKRwD!-4o=< zjv@A3J`R`L!NO5yn(Z%uH-MPZ$pJm_N!A;wkev8-XItWPet4?y{G`$whu$*G3>-2; z9-<o6mHq)HzW@u0o`-=(%wEv7yc$f+;-D&Su7j}zjAL0KLxvQBz};~40VJ;acbsbh zqH!4fy6}UYKpy?q^S^|LKjI52J5t@f4Agdh1f}d~nXH49Jb$gsbbxS-GZf~7e~wT< zJck6e<zx+)8gqGe>F;)7hzYV5D_m`!Ia2SFB8(>(KvjgJ<xwUH?EQub0#2|!m!&s= zhzu%hAXZvTstv}mr(l%l9pg-4()Mfu+|=`RPZ;58dC$Hi4nlxQyu$msKb~xRV9riv zq&|DVd7VQQaIy@EG?2Q3Z+95xyKS`5Lzz{9ygc6vzc3iDVO6+<m|BP6)|N;(wKhF_ zj)`P9wCSn0+dm<ZEZDXlp+*oK1HI!a*R>GYBfreqemqsE#9scXKU`&TxybZPatjy$ zG87mhV+AO*#2Tzzinnhn&c^uo)LDlLogP^Mhh#DXZM>@}!mgohj*D&}CJjf6w4cm4 zkxgy#bz%QG0xEakvjUl*hJu9V;N$yEmsM;W{VV>r0v3vthG8az2#VQDxdUi;Khh&0 z8vQXVlAt(Z19BwJHtZw`7bTOc<1h-K?MICm`Qb=Mg6bi5DrJ1b|0e-%qwB{(3~epU zTbx&RIv0VaA%bVO!mv3N?OewK7dRkhIXPEOm;N`hz?2cNzmZ_zn^?>^y-X(>EdvbX zDcwrLjS+*PEeEry$c(gf8xSkVpFvH|ieWpG;N!(>g@4Qq(8VO@P4zvFM-y<sNIS5^ zk2<KC%5XFh*q8diL{bG=5HbB%hK!XIgJr<}tQb{`jaH0Fm_i~?Ju@JclPlr0xJrk` z%rL#&^$;1Ual&UmDkg$o`??y@N`GA&_BHtCs89tEOm(=dyIC9<SvcCcM!zg8tAe~8 z+6=8%R^AFH3aZcH==od&D~1Yq!RSjkeGrhveI}%qpJD(~`heW3np?`L;uzX$XrkoH z?S?>AM1&RO-00b$PXj!VitZkx=?hZ~L_t4<fCF;D5>pRX-%B?W1o;mipW_RI(u8zS zhmu;bOfx=AHxmNs7Ow+rp8T%_5I_j0Pzx~n4_I(G1Gx$I{^!Po9|-IeL7=|GMC>#a zWGcvH;A7R&P<JOtA)G=nADemD?IKHsVrN7E@UcV>$RG>=Co&gAdp4y#7Ed=@0f9`J zDI%kz5!`qkqML!Vk`y6I54{T6e&)EvqIU&31r&py()ab~_^Z%XLd+0#HBv$5QwrP# zO%(yt#)SPr)CRf=4VL(s<h5?qu?C<J4|+|MOE^~;R)GN+AOW_;y<S#*fCreZ{w$c{ zwI^Xg2EaZQS|Y%yNvb#24v&Ubra~(@l|?6mtk_ffpk4;u5L|_$^+#~Iw}^0rgJ9ej z0sUDqKA3_9oUTRhD1h2(lOm8RZ?vU?jfcxs2Xnx3zo||9NrD0&oUO_W8xL|$m0++H zjyjXpj5I2!Ze6gQUmd1t>JNAGwe;AlHPVFbAVptJ2KwjND&Ru+Iv_d1icQF#a&EF! z{ZkTZiQ*-YHWGv^0C#YT_eH>~acIR4{HW{?*9E15NI_;rpuq+Ncxwli-Bf~w+zFf* zH=8j2>H6UF0FeI#z;~(a*#H9m>PHgx=T3uP`W1jtE$cXh0tzrDz{MhJ0R9%N2)6hv zErvshGtpc?#D9Ysa04zrs}bqdDJRDjUZilpZr1aCzP*=@sz%(_F|^yf6|%s#$K*V7 zpc~jaaiUg#7Ptd&jH)$dl;60FaQSF=RQ-d#8z(s;3`}zoKYmaJNljxyN11%@3Ay*6 zuSARi=qR1M>8ufhf=sZ{6c@Gz`~|xw90c>TXoLD?h$`|kz-1rwpxh%)=mX%)1R8tV zX|0-|<)WsI{yNVT=L^^Arzh@Fc(B<GW?vx>S5!5}<&a@-8DzDnT|V1sL*Ofr5XZiS z@$}X~c^b&>La?N&jECjO!M_mj&7B>_Jw`0LmEI#OpNwpFIC|)@0RvpZmWeO<)I?p- z(V+zzR>Yt0r4&4G5=3-QsV^JZdoslkQ55K{?ghs`0H-lYwiIks{&NR_mN<!YND6G< zr8NZS{+T4m%KsA%9FR3FG`30Fl&<2vAhAl-+9hdr!O2t3%DOK{FyVKvc*{Kf(TVm7 zM{rvF(M`oECO{GbP(s#-ET2>Fghw~d%`~>G9D#wsa9A}QSv~lG{CT!CdN&+Tmc5PM z!aTeZuHA#5f|)u#2+phOzR(X$R+G{5NUw_JJ&h-Y1YnwWf)}lEsa@M65o0DP6VFi$ z36K{1tH{T&<gm$J(hh_NY^uoKr@E3O?M$1Af`F%H!3p+&M&D=4xnpn#I~u<=&nZ5! z0sqtUP1p0Nydz;WE>GbGYLCSbQ@_0JuIP3`kTo~Eb6mI=gAAnW-HIHqRzCs^9m7k) z^tu;P(onphlTZULiKvREWXAV`y-G!2%%msPD>xFC%~purtuyz;9YI6D6-A&9E_k)@ zR6w3=%xiF6gS22p>D{W|nh^-s6maAUY1H5$SzmJxKd2;ZA=EEyk1$?r-i62pR*~uw zIzBc<LUgh79Ak&k?ROaPPcT-Iz}QmW*pALY4*Z~IFdi~9iAEt0s$pcRC#ze5YE*bz zlx2MEMl-{P1lWL+D7!hiS{%p+{G%y7X~!1J8ha4JbYN-gFfx*+pK6hE5}DoC2JIZY z`)9Ioj{g;ia1aCva+La4m8w$%y!RKj%Ra}R^#{Dcl@=XN4GbN;@aKXSg2Df0sA1r5 zIW2<-I>85YAHPoUy|L@N?Emoh-(~;qZkMWLi0t2)9PuB}8X!~u_la_Pspkuc8~#T- z8Ne}Iz%OsrvSYW8{cP$`!{4X=c{6@6|1TOY7XwXhP?zKrONUoPLx?>hUP^jN)LD+3 c>_FA<c7LfmnK%W0z<t>v-6Q+b_nz_lKi`SY&;S4c literal 0 HcmV?d00001 diff --git a/docs/designers-developers/assets/js-tutorial-error-blocks-undefined.png b/docs/designers-developers/assets/js-tutorial-error-blocks-undefined.png new file mode 100644 index 0000000000000000000000000000000000000000..1f27c36ce759560bda4cb689ab39c5bdaf7b9879 GIT binary patch literal 6986 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 literal 0 HcmV?d00001 diff --git a/docs/designers-developers/developers/tutorials/javascript/extending-the-block-editor.md b/docs/designers-developers/developers/tutorials/javascript/extending-the-block-editor.md new file mode 100644 index 00000000000000..67d703cb37310f --- /dev/null +++ b/docs/designers-developers/developers/tutorials/javascript/extending-the-block-editor.md @@ -0,0 +1,66 @@ +# Extending the Block Editor + +Let's look at using the [Block Style Variation example](../../../../../docs/designers-developers/developers/filters/block-filters.md#block-style-variations) to extend the editor. This example allows you to add your own custom CSS class name to any core block type. + +Replace the existing `console.log()` code in your `myguten.js` file with: + +```js +wp.blocks.registerBlockStyle( 'core/quote', { + name: 'fancy-quote', + label: 'Fancy Quote' +} ); +``` + +**Important:** Notice that you are using a function from `wp.blocks` package. This means you must specify it as a dependency when you enqueue the script. Update the `myguten-plugin.php` file to: + +```php +<?php +/* +Plugin Name: Fancy Quote +*/ + +function myguten_enqueue() { + wp_enqueue_script( 'myguten-script', + plugins_url( 'myguten.js', __FILE__ ), + array( 'wp-blocks') + ); +} +add_action( 'enqueue_block_editor_assets', 'myguten_enqueue' ); +``` + +The last argument in the `wp_enqueue_script()` function is an array of dependencies. WordPress makes packages available under the `wp` namespace. In the example, you use `wp.blocks` to access the items that the blocks package exports (in this case the `registerBlockStyle()` function). + +See [Packages](../../../../../docs/designers-developers/developers/packages.md) for list of available packages and what objects they export. + +After you have updated both JavaScript and PHP files, go to the Block Editor and create a new post. + +Add a quote block, and in the right sidebar under Styles, you will see your new Fancy Quote style listed. Click the Fancy Quote to select and apply that style to your quote block. + + +![Fancy Quote Style in Inspector](https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/assets/fancy-quote-in-inspector.png) + + +You will not see a visible change, even if you Preview or Publish the post. However, if you look at the source, you will see the `is-style-fancy-quote` class name is now attached to your quote block. + +Let's add some style. Go ahead and create a `style.css` file with: + +```css +.is-style-fancy-quote { + color: tomato; +} + +``` + +You enqueue the CSS file by adding the following to your `myguten-plugin.php`: + +```php +function myguten_stylesheet() { + wp_enqueue_style( 'myguten-style', plugins_url( 'style.css', __FILE__ ) ); +} +add_action( 'enqueue_block_assets', 'myguten_stylesheet' ); +``` + +Now when you view in the editor and published, you will see your Fancy Quote style, a delicious tomato color text. + +![Fancy Quote with Style](https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/assets/fancy-quote-with-style.png) + diff --git a/docs/designers-developers/developers/tutorials/javascript/loading-javascript.md b/docs/designers-developers/developers/tutorials/javascript/loading-javascript.md new file mode 100644 index 00000000000000..813b8fa3e2bdbf --- /dev/null +++ b/docs/designers-developers/developers/tutorials/javascript/loading-javascript.md @@ -0,0 +1,49 @@ +# Loading JavaScript + +With the plugin in place, you can add the code that loads the JavaScript. This methodology follows the standard WordPress procedure of enqueuing scripts, see [enqueuing section of the Plugin Handbook](https://developer.wordpress.org/plugins/javascript/enqueuing/). + +Add the following code to your `myguten-plugin.php` file: + +```php +function myguten_enqueue() { + wp_enqueue_script( + 'myguten-script', + plugins_url( 'myguten.js', __FILE__ ) + ); +} +add_action( 'enqueue_block_editor_assets', 'myguten_enqueue' ); +``` + +The `enqueue_block_editor_assets` hook is used, which is called when the block editor loads, and will enqueue the JavaScript file `myguten.js`. + +Create a file called `myguten.js` and add: + +```js +console.log( "I'm loaded!" ); +``` + +Next, create a new post in the block editor. + +We'll check the JavaScript console in your browser's Developer Tools, to see if the message is displayed. If you're not sure what developer tools are, Mozilla's ["What are browser developer tools?"](https://developer.mozilla.org/en-US/docs/Learn/Common_questions/What_are_browser_developer_tools) documentation provides more information, including more background on the [JavaScript console](https://developer.mozilla.org/en-US/docs/Learn/Common_questions/What_are_browser_developer_tools#The_JavaScript_console). + +If your code is registered and enqueued correctly, you should see a message in your console: + +![Console Log Message Success](https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/assets/js-tutorial-console-log-success.png) + +**Note for Theme Developers:** The above method of enqueing is used for plugins. If you are extending the Block Editor for your theme there is a minor difference, you will use the `get_template_directory_uri()` function instead of `plugins_url()`. So for a theme, the enqueue example is: + +```php +function myguten_enqueue() { + wp_enqueue_script( + 'myguten-script', + get_template_directory_uri() . '/myguten.js' + ); +} +add_action( 'enqueue_block_editor_assets', 'myguten_enqueue' ); +``` + +### Recap + +At this point, you have a plugin in the directory `wp-content/plugins/myguten-plugin` with two files: the PHP server-side code in `myguten-plugin.php`, and the JavaScript which runs in the browser in `myguten.js`. + +This puts all the initial pieces in place for you to start extending the Block Editor. diff --git a/docs/designers-developers/developers/tutorials/javascript/plugins-background.md b/docs/designers-developers/developers/tutorials/javascript/plugins-background.md new file mode 100644 index 00000000000000..91a6ee302fe758 --- /dev/null +++ b/docs/designers-developers/developers/tutorials/javascript/plugins-background.md @@ -0,0 +1,16 @@ +# Plugins Background + +The primary means of extending WordPress is the plugin. WordPress' [Plugin Basics](https://developer.wordpress.org/plugins/the-basics/) documentation provides for more details on building a plugin. The quickest way to start is to create a new directory in `wp-content/plugins/` to hold your plugin code, for this example you can call it `myguten-plugin`. + +Inside of this new directory, create a file called `myguten-plugin.php` which is the server-side code that runs when your plugin is active. For now place the following in that file: + +```php +<?php +/* +Plugin Name: Fancy Quote +*/ +``` + +To summarize, you should have a directory `wp-content/plugins/myguten-plugin/` which has the single file `myguten-plugin.php`. Once that is in place, go to your plugins list in `wp-admin` and you should see your plugin listed. + +Click **Activate** and your plugin will load with WordPress. diff --git a/docs/designers-developers/developers/tutorials/javascript/readme.md b/docs/designers-developers/developers/tutorials/javascript/readme.md new file mode 100644 index 00000000000000..8d60b35b6faa14 --- /dev/null +++ b/docs/designers-developers/developers/tutorials/javascript/readme.md @@ -0,0 +1,18 @@ +# Getting Started with JavaScript + +The purpose of this tutorial is to step through getting started with JavaScript and WordPress, specifically around the new Block Editor. The Gutenberg Handbook documentation contains information on the APIs available for working with the block editor. The goal of this tutorial is get you comfortable on how to use the API reference and snippets of code found within. + +### What is JavaScript + +JavaScript is a programming language which is loaded and executed in your web browser; compared to PHP which is run by a web server with the results sent to the browser, typically as HTML. + +The Block Editor introduced in WordPress 5.0 is written entirely in JavaScript, with the code run in the browser, and not on the server, this allows for a richer and more dynamic user experience. It also requires to learn how to use JavaScript to extend and enhance the Block Editor. + + +### Table of Contents + +1. [Plugins Background](../../../../../docs/designers-developers/developers/tutorials/javascript/plugins-background.md) +2. [Loading JavaScript](../../../../../docs/designers-developers/developers/tutorials/javascript/loading-javascript.md) +3. [Extending the Block Editor](../../../../../docs/designers-developers/developers/tutorials/javascript/extending-the-block-editor.md) +4. [Troubleshooting](../../../../../docs/designers-developers/developers/tutorials/javascript/troubleshooting.md) +5. [JavaScript Versions and Building](../../../../../docs/designers-developers/developers/tutorials/javascript/versions-and-building.md) diff --git a/docs/designers-developers/developers/tutorials/javascript/troubleshooting.md b/docs/designers-developers/developers/tutorials/javascript/troubleshooting.md new file mode 100644 index 00000000000000..056fa803cbd613 --- /dev/null +++ b/docs/designers-developers/developers/tutorials/javascript/troubleshooting.md @@ -0,0 +1,34 @@ +# Troubleshooting + +If you're having trouble getting your code to work, here are a few ways to troubleshoot. + +## Console Log + +The console log is a JavaScript developer's best friend. It is a good practice to work with it open, as it collects errors and notices into one place. See Mozilla's [JavaScript console](https://developer.mozilla.org/en-US/docs/Learn/Common_questions/What_are_browser_developer_tools#The_JavaScript_console) documentation for more. + +Your first step in debugging should be to check the JavaScript console for any errors. Here is an example, which shows a syntax error on line 6. + +![console error](https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/assets/js-tutorial-console-log-error.png) + +## Confirm JavaScript is Loading + +If you are not seeing your changes, check that your JavaScript file is being enqueued. Open the page source in your browser's web inspector (some browsers may allow you to view the page source by right clicking on the page and selecting "View Page Source"), and look for the `<script>` tag that loads your file. In our example, you would search for `myguten.js` and confirm it is being loaded. + +If you do not see the file being loaded, doublecheck the enqueue function is correct. You can also check your server logs to see if there is an error messages. + +## Confirm all dependencies are loaded + +The console log will show an error if a dependency your JavaScript code uses has not been declared and loaded in the browser. In the example, if `myguten.js` script is enqueued without declaring the `wp-blocks` dependency, the console log will show: + +<img src="https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/assets/js-tutorial-error-blocks-undefined.png" width=448 title="error wp.blocks is undefined"/> + +You can correct by checking your `wp_enqueue_script` function includes all packages listed that are used: + +```js +wp_enqueue_script( + 'myguten-script', + plugins_url( 'myguten.js', __FILE__ ), + array( 'wp-blocks' ) +); +``` + diff --git a/docs/designers-developers/developers/tutorials/javascript/versions-and-building.md b/docs/designers-developers/developers/tutorials/javascript/versions-and-building.md new file mode 100644 index 00000000000000..c7de41efd4d0a8 --- /dev/null +++ b/docs/designers-developers/developers/tutorials/javascript/versions-and-building.md @@ -0,0 +1,11 @@ +# JavaScript versions and build step + +The Gutenberg Handbook shows JavaScript examples in two syntaxes: ES5 and ESNext. These are version names for the JavaScript language standard definitions. You may also see elsewhere the names ES6, or ECMAScript 2015 mentioned. See the [ECMAScript](https://en.wikipedia.org/wiki/ECMAScript) Wikipedia article for all the details. + +ES5 code is compatible with WordPress's minimum [target for browser support](https://make.wordpress.org/core/handbook/best-practices/browser-support/). + +"ESNext" doesn't refer to a specific version of JavaScript, but is "dynamic" and refers to the next language definitions, whatever they might be. Because some browsers won't support these features yet (because they're new or proposed), an extra build step is required to transform the code to a syntax that works in all browsers. Webpack and babel are the tools that perform this transformation step. + +Additionally, the ESNext code examples in the Gutenberg handbook include [JSX syntax](https://reactjs.org/docs/introducing-jsx.html), a syntax that blends HTML and JavaScript. It makes it easier to read and write markup code, but likewise requires the build step using webpack and babel to transform into compatible code. + +For simplicity, this tutorial uses the ES5 definition of JavaScript, without JSX. This code can run straight in your browser and does not require an additional build step. diff --git a/docs/designers-developers/developers/tutorials/readme.md b/docs/designers-developers/developers/tutorials/readme.md index d12dcd1c4982d5..543f560c8d81c1 100644 --- a/docs/designers-developers/developers/tutorials/readme.md +++ b/docs/designers-developers/developers/tutorials/readme.md @@ -1,3 +1,6 @@ # Tutorials -If you want to learn more about block creation, the [Blocks Tutorial](../../../../docs/designers-developers/developers/tutorials/block-tutorial/readme.md) is the best place to start. +* If you want to learn more about block creation, the [Blocks Tutorial](../../../../docs/designers-developers/developers/tutorials/block-tutorial/readme.md) is the best place to start. + +* See the [Getting Started with JavaScript Tutorial](../../../../docs/designers-developers/developers/tutorials/javascript/readme.md) to learn about how to use JavaScript within WordPress. + diff --git a/docs/manifest.json b/docs/manifest.json index dbf955bd2d2478..79512ef3b96ca5 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -197,6 +197,42 @@ "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/tutorials/block-tutorial/generate-blocks-with-wp-cli.md", "parent": "block-tutorial" }, + { + "title": "Getting Started with JavaScript", + "slug": "javascript", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/tutorials/javascript/readme.md", + "parent": "tutorials" + }, + { + "title": "Plugins Background", + "slug": "plugins-background", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/tutorials/javascript/plugins-background.md", + "parent": "javascript" + }, + { + "title": "Loading JavaScript", + "slug": "loading-javascript", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/tutorials/javascript/loading-javascript.md", + "parent": "javascript" + }, + { + "title": "Extending the Block Editor", + "slug": "extending-the-block-editor", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/tutorials/javascript/extending-the-block-editor.md", + "parent": "javascript" + }, + { + "title": "Troubleshooting", + "slug": "troubleshooting", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/tutorials/javascript/troubleshooting.md", + "parent": "javascript" + }, + { + "title": "JavaScript versions and build step", + "slug": "versions-and-building", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/tutorials/javascript/versions-and-building.md", + "parent": "javascript" + }, { "title": "Designer Documentation", "slug": "designers", diff --git a/docs/toc.json b/docs/toc.json index 6dd5e876cbc00c..c106d9a499ca69 100644 --- a/docs/toc.json +++ b/docs/toc.json @@ -36,6 +36,13 @@ {"docs/designers-developers/developers/tutorials/block-tutorial/block-controls-toolbars-and-inspector.md" :[]}, {"docs/designers-developers/developers/tutorials/block-tutorial/creating-dynamic-blocks.md" :[]}, {"docs/designers-developers/developers/tutorials/block-tutorial/generate-blocks-with-wp-cli.md" :[]} + ]}, + { "docs/designers-developers/developers/tutorials/javascript/readme.md": [ + {"docs/designers-developers/developers/tutorials/javascript/plugins-background.md": []}, + { "docs/designers-developers/developers/tutorials/javascript/loading-javascript.md": []}, + { "docs/designers-developers/developers/tutorials/javascript/extending-the-block-editor.md": []}, + { "docs/designers-developers/developers/tutorials/javascript/troubleshooting.md": []}, + { "docs/designers-developers/developers/tutorials/javascript/versions-and-building.md": []} ]} ]} ]}, From d9257b11544c8840925180a45273ed18a2c61ddb Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Mon, 17 Dec 2018 16:38:10 +0100 Subject: [PATCH 241/254] Restore block prop for BlockListBlock filter (#12943) * Restore block prop for BlockListBlock filter * Restore hook order * getBlock with memory leak * Add clientId and isValid to the block prop --- packages/editor/CHANGELOG.md | 6 ++++++ .../editor/src/components/block-list/block.js | 19 +++++++++++-------- packages/editor/src/hooks/align.js | 2 +- packages/editor/src/store/selectors.js | 18 ++++++++++++++++++ 4 files changed, 36 insertions(+), 9 deletions(-) diff --git a/packages/editor/CHANGELOG.md b/packages/editor/CHANGELOG.md index e06655f6738b6e..a5e35f2c95108a 100644 --- a/packages/editor/CHANGELOG.md +++ b/packages/editor/CHANGELOG.md @@ -1,3 +1,9 @@ +## 9.0.6 (Unreleased) + +### Bug Fixes + +- Restore the `block` prop in the `BlockListBlock` filter. + ## 9.0.5 (2018-12-12) ### Bug Fixes diff --git a/packages/editor/src/components/block-list/block.js b/packages/editor/src/components/block-list/block.js index 2523029c010ed2..923e22362dd1d7 100644 --- a/packages/editor/src/components/block-list/block.js +++ b/packages/editor/src/components/block-list/block.js @@ -657,9 +657,6 @@ const applyWithSelect = withSelect( ( select, { clientId, rootClientId, isLargeViewport } ) => { const { isBlockSelected, - getBlockName, - isBlockValid, - getBlockAttributes, isAncestorMultiSelected, isBlockMultiSelected, isFirstMultiSelectedBlock, @@ -675,13 +672,14 @@ const applyWithSelect = withSelect( getTemplateLock, getPreviousBlockClientId, getNextBlockClientId, + __unstableGetBlockWithoutInnerBlocks, } = select( 'core/editor' ); + const block = __unstableGetBlockWithoutInnerBlocks( clientId ); const isSelected = isBlockSelected( clientId ); const { hasFixedToolbar, focusMode } = getEditorSettings(); const templateLock = getTemplateLock( rootClientId ); const isParentOfSelectedBlock = hasSelectedInnerBlock( clientId, true ); - const name = getBlockName( clientId ); - const attributes = getBlockAttributes( clientId ); + const { name, attributes, isValid } = block; return { isPartOfMultiSelection: @@ -699,13 +697,19 @@ const applyWithSelect = withSelect( initialPosition: getSelectedBlocksInitialCaretPosition(), isEmptyDefaultBlock: name && isUnmodifiedDefaultBlock( { name, attributes } ), - isValid: isBlockValid( clientId ), isMovable: 'all' !== templateLock, isLocked: !! templateLock, isFocusMode: focusMode && isLargeViewport, hasFixedToolbar: hasFixedToolbar && isLargeViewport, + + // Users of the editor.BlockListBlock filter used to be able to access the block prop + // Ideally these blocks would rely on the clientId prop only. + // This is kept for backward compatibility reasons. + block, + name, attributes, + isValid, isSelected, isParentOfSelectedBlock, @@ -777,9 +781,8 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps, { select } ) => { } ); export default compose( - withFilters( 'editor.BlockListBlock' ), withViewportMatch( { isLargeViewport: 'medium' } ), applyWithSelect, applyWithDispatch, - withFilters( 'editor.__experimentalBlockListBlock' ) + withFilters( 'editor.BlockListBlock' ) )( BlockListBlock ); diff --git a/packages/editor/src/hooks/align.js b/packages/editor/src/hooks/align.js index 46adcc0ba3ede8..e22922c198b2cc 100644 --- a/packages/editor/src/hooks/align.js +++ b/packages/editor/src/hooks/align.js @@ -206,7 +206,7 @@ export function addAssignedAlign( props, blockType, attributes ) { } addFilter( 'blocks.registerBlockType', 'core/align/addAttribute', addAttribute ); -addFilter( 'editor.__experimentalBlockListBlock', 'core/editor/align/with-data-align', withDataAlign ); +addFilter( 'editor.BlockListBlock', 'core/editor/align/with-data-align', withDataAlign ); addFilter( 'editor.BlockEdit', 'core/editor/align/with-toolbar-controls', withToolbarControls ); addFilter( 'blocks.getSaveContent.extraProps', 'core/align/addAssignedAlign', addAssignedAlign ); diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index d8b019930d1a07..86b127e2404033 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -704,6 +704,24 @@ export const getBlock = createSelector( ] ); +export const __unstableGetBlockWithoutInnerBlocks = createSelector( + ( state, clientId ) => { + const block = state.editor.present.blocks.byClientId[ clientId ]; + if ( ! block ) { + return null; + } + + return { + ...block, + attributes: getBlockAttributes( state, clientId ), + }; + }, + ( state, clientId ) => [ + state.editor.present.blocks.byClientId[ clientId ], + ...getBlockAttributes.getDependants( state, clientId ), + ] +); + function getPostMeta( state, key ) { return has( state, [ 'editor', 'present', 'edits', 'meta', key ] ) ? get( state, [ 'editor', 'present', 'edits', 'meta', key ] ) : From ddac4f3cf8fd311169c7e125411343a437bdbb5a Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Mon, 17 Dec 2018 18:10:11 +0100 Subject: [PATCH 242/254] Fix JS error when removing a block (#12949) --- packages/editor/src/components/block-list/block.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/editor/src/components/block-list/block.js b/packages/editor/src/components/block-list/block.js index 923e22362dd1d7..f29de8106fab56 100644 --- a/packages/editor/src/components/block-list/block.js +++ b/packages/editor/src/components/block-list/block.js @@ -679,7 +679,11 @@ const applyWithSelect = withSelect( const { hasFixedToolbar, focusMode } = getEditorSettings(); const templateLock = getTemplateLock( rootClientId ); const isParentOfSelectedBlock = hasSelectedInnerBlock( clientId, true ); - const { name, attributes, isValid } = block; + + // The fallback to `{}` is a temporary fix. + // This function should never be called when a block is not present in the state. + // It happens now because the order in withSelect rendering is not correct. + const { name, attributes, isValid } = block || {}; return { isPartOfMultiSelection: From e13bedbb213a45143a5f8aea12e4c381e3bac4b7 Mon Sep 17 00:00:00 2001 From: Jarred Kennedy <JarredKennedy@users.noreply.github.com> Date: Wed, 19 Dec 2018 03:05:02 +1100 Subject: [PATCH 243/254] Fixed incorrect example code (#12906) --- packages/data/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/data/README.md b/packages/data/README.md index b5745e7719ed7e..2c911ccf4cf8f5 100644 --- a/packages/data/README.md +++ b/packages/data/README.md @@ -91,7 +91,7 @@ registerStore( 'my-shop', { }, resolvers: { - * getPrice( state, item ) { + * getPrice( item ) { const path = '/wp/v2/prices/' + item; const price = yield actions.fetchFromAPI( path ); return actions.setPrice( item, price ); @@ -127,7 +127,7 @@ The **`selectors`** object includes a set of functions for accessing and derivin A **resolver** is a side-effect for a selector. If your selector result may need to be fulfilled from an external source, you can define a resolver such that the first time the selector is called, the fulfillment behavior is effected. -The `resolvers` option should be passed as an object where each key is the name of the selector to act upon, the value a function which receives the same arguments passed to the selector. It can then dispatch as necessary to fulfill the requirements of the selector, taking advantage of the fact that most data consumers will subscribe to subsequent state changes (by `subscribe` or `withSelect`). +The `resolvers` option should be passed as an object where each key is the name of the selector to act upon, the value a function which receives the same arguments passed to the selector, excluding the state argument. It can then dispatch as necessary to fulfill the requirements of the selector, taking advantage of the fact that most data consumers will subscribe to subsequent state changes (by `subscribe` or `withSelect`). ### `controls` From 92e3942d6f433ab15f21103de0cf45bf131f8bf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20=28Greg=29=20Zi=C3=B3=C5=82kowski?= <grzegorz@gziolo.pl> Date: Tue, 18 Dec 2018 18:20:35 +0100 Subject: [PATCH 244/254] Build tooling: Fix Travis failures with Docker instances (#12983) * Build tooling: Fix Travis failures with Docker instances * Docs: Fix manifest file * chore: Fix typo --- bin/install-wordpress.sh | 37 ++++++++++++++++++++++++------------- docs/manifest.json | 2 +- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/bin/install-wordpress.sh b/bin/install-wordpress.sh index a346152a579bda..affe7cc51d34e3 100755 --- a/bin/install-wordpress.sh +++ b/bin/install-wordpress.sh @@ -45,41 +45,52 @@ echo '' # dirty up the tests. if [ "$1" == '--e2e_tests' ]; then echo -e $(status_message "Resetting test database...") - docker-compose $DOCKER_COMPOSE_FILE_OPTIONS run --rm $CLI db reset --yes >/dev/null + docker-compose $DOCKER_COMPOSE_FILE_OPTIONS run --rm -u 33 $CLI db reset --yes --quiet fi # Install WordPress. echo -e $(status_message "Installing WordPress...") # The `-u 33` flag tells Docker to run the command as a particular user and # 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 +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 --quiet 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 + echo -e $(status_message "Creating an additional author user for testing...") + # Create an additional author user for testing. + docker-compose $DOCKER_COMPOSE_FILE_OPTIONS run --rm -u 33 $CLI user create author author@example.com --role=author --user_pass=authpass --quiet # 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 + docker-compose $DOCKER_COMPOSE_FILE_OPTIONS run --rm -u 33 $CLI post update 1 --post_author=2 --quiet fi +# Make sure the uploads and upgrade folders exist and we have permissions to add files. +echo -e $(status_message "Ensuring that files can be uploaded...") +docker-compose $DOCKER_COMPOSE_FILE_OPTIONS run --rm $CONTAINER chmod 767 /var/www/html/wp-content/plugins +docker-compose $DOCKER_COMPOSE_FILE_OPTIONS run --rm $CONTAINER mkdir -p /var/www/html/wp-content/uploads +docker-compose $DOCKER_COMPOSE_FILE_OPTIONS run --rm $CONTAINER chmod -v 767 /var/www/html/wp-content/uploads +docker-compose $DOCKER_COMPOSE_FILE_OPTIONS run --rm $CONTAINER mkdir -p /var/www/html/wp-content/upgrade +docker-compose $DOCKER_COMPOSE_FILE_OPTIONS run --rm $CONTAINER chmod 767 /var/www/html/wp-content/upgrade + +CURRENT_WP_VERSION=$(docker-compose $DOCKER_COMPOSE_FILE_OPTIONS run -T --rm $CLI core version) +echo -e $(status_message "Current WordPress version: $CURRENT_WP_VERSION...") + 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 + echo -e $(status_message "Updating WordPress to the latest version...") + docker-compose $DOCKER_COMPOSE_FILE_OPTIONS run --rm -u 33 $CLI core update --quiet fi # If the 'wordpress' volume wasn't during the down/up earlier, but the post port has changed, we need to update it. +echo -e $(status_message "Checking the site's url...") CURRENT_URL=$(docker-compose $DOCKER_COMPOSE_FILE_OPTIONS run -T --rm $CLI option get siteurl) if [ "$CURRENT_URL" != "http://localhost:$HOST_PORT" ]; then - docker-compose $DOCKER_COMPOSE_FILE_OPTIONS run --rm $CLI option update home "http://localhost:$HOST_PORT" >/dev/null - docker-compose $DOCKER_COMPOSE_FILE_OPTIONS run --rm $CLI option update siteurl "http://localhost:$HOST_PORT" >/dev/null + docker-compose $DOCKER_COMPOSE_FILE_OPTIONS run --rm -u 33 $CLI option update home "http://localhost:$HOST_PORT" --quiet + docker-compose $DOCKER_COMPOSE_FILE_OPTIONS run --rm -u 33 $CLI option update siteurl "http://localhost:$HOST_PORT" --quiet fi # Activate Gutenberg. echo -e $(status_message "Activating Gutenberg...") -docker-compose $DOCKER_COMPOSE_FILE_OPTIONS run --rm $CLI plugin activate gutenberg >/dev/null - -# Make sure the uploads folder exist and we have permissions to add files there. -docker-compose $DOCKER_COMPOSE_FILE_OPTIONS run --rm $CONTAINER mkdir -p /var/www/html/wp-content/uploads -docker-compose $DOCKER_COMPOSE_FILE_OPTIONS run --rm $CONTAINER chmod -v 767 /var/www/html/wp-content/uploads +docker-compose $DOCKER_COMPOSE_FILE_OPTIONS run --rm -u 33 $CLI plugin activate gutenberg --quiet # Install a dummy favicon to avoid 404 errors. +echo -e $(status_message "Installing a dummy favicon...") docker-compose $DOCKER_COMPOSE_FILE_OPTIONS run --rm $CONTAINER touch /var/www/html/favicon.ico diff --git a/docs/manifest.json b/docs/manifest.json index 79512ef3b96ca5..4dc762893f571b 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -270,7 +270,7 @@ "parent": "designers-developers" }, { - "title": "Contributors", + "title": "Contributors Guide", "slug": "contributors", "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/contributors/readme.md", "parent": null From 5dba988cf740653b9ab41672c462c785f1ff1a38 Mon Sep 17 00:00:00 2001 From: Daniel Richards <daniel.p.richards@gmail.com> Date: Wed, 19 Dec 2018 13:59:15 +0800 Subject: [PATCH 245/254] Add section to setAttributes documentation about immutability of attributes (#12811) * Add section to setAttributes documentation about immutability of attributes * Simplify explanation of immutable attributes * Fix typo and add link to redux's immutability docs Co-Authored-By: talldan <daniel.richards@automattic.com> --- .../developers/block-api/block-edit-save.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/designers-developers/developers/block-api/block-edit-save.md b/docs/designers-developers/developers/block-api/block-edit-save.md index c6f4d603cc3085..f72baac90858d8 100644 --- a/docs/designers-developers/developers/block-api/block-edit-save.md +++ b/docs/designers-developers/developers/block-api/block-edit-save.md @@ -80,6 +80,24 @@ edit( { attributes, setAttributes, className, isSelected } ) { } ``` +When using attributes that are objects or arrays it's a good idea to copy or clone the attribute prior to updating it: + +```js +// Good - here a new array is created from the old list attribute and a new list item: +const { list } = attributes; +const addListItem = ( newListItem ) => setAttributes( { list: [ ...list, newListItem ] } ); + +// Bad - here the list from the existing attribute is modified directly to add the new list item: +const { list } = attributes; +const addListItem = ( newListItem ) => { + list.push( newListItem ); + setAttributes( { list } ); +}; + +``` + +Why do this? In JavaScript, arrays and objects are passed by reference, so this practice ensures changes won't affect other code that might hold references to the same data. Furthermore, Gutenberg follows the philosophy of the Redux library that [state should be immutable](https://redux.js.org/faq/immutable-data#what-are-the-benefits-of-immutability)—data should not be changed directly, but instead a new version of the data created containing the changes. + ## Save The `save` function defines the way in which the different attributes should be combined into the final markup, which is then serialized by Gutenberg into `post_content`. From 5f683de5dd4396d4e8d965835da40a228c583841 Mon Sep 17 00:00:00 2001 From: Jon Desrosiers <desrosj@users.noreply.github.com> Date: Wed, 19 Dec 2018 03:09:44 -0500 Subject: [PATCH 246/254] Fix alignment of property assignments in the `__construct()` function of the block parser. (#12995) --- packages/block-serialization-default-parser/parser.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/block-serialization-default-parser/parser.php b/packages/block-serialization-default-parser/parser.php index 216cb9a5f78e87..0d5887381a1b83 100644 --- a/packages/block-serialization-default-parser/parser.php +++ b/packages/block-serialization-default-parser/parser.php @@ -63,10 +63,10 @@ class WP_Block_Parser_Block { public $innerContent; function __construct( $name, $attrs, $innerBlocks, $innerHTML, $innerContent ) { - $this->blockName = $name; - $this->attrs = $attrs; - $this->innerBlocks = $innerBlocks; - $this->innerHTML = $innerHTML; + $this->blockName = $name; + $this->attrs = $attrs; + $this->innerBlocks = $innerBlocks; + $this->innerHTML = $innerHTML; $this->innerContent = $innerContent; } } From 2934d6c742333897774f62fa302d94559beffc3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20=28Greg=29=20Zi=C3=B3=C5=82kowski?= <grzegorz@gziolo.pl> Date: Wed, 19 Dec 2018 09:18:54 +0100 Subject: [PATCH 247/254] Update recommended config provided for lint-js script (#12845) * Update default config provided for lint-js script * Update packages/scripts/CHANGELOG.md * Clarify usage of recommended configs in docs --- package-lock.json | 2 +- packages/scripts/CHANGELOG.md | 3 ++- packages/scripts/README.md | 8 ++++---- packages/scripts/config/.eslintrc.js | 2 +- packages/scripts/package.json | 2 +- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1867b41831f791..8038ebf538c07f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2762,9 +2762,9 @@ "dev": true, "requires": { "@wordpress/babel-preset-default": "file:packages/babel-preset-default", + "@wordpress/eslint-plugin": "file:packages/eslint-plugin", "@wordpress/jest-preset-default": "file:packages/jest-preset-default", "@wordpress/npm-package-json-lint-config": "file:packages/npm-package-json-lint-config", - "babel-eslint": "8.0.3", "chalk": "^2.4.1", "check-node-version": "^3.1.1", "cross-spawn": "^5.1.0", diff --git a/packages/scripts/CHANGELOG.md b/packages/scripts/CHANGELOG.md index 6f2977dc07f901..1af69f533f2fa9 100644 --- a/packages/scripts/CHANGELOG.md +++ b/packages/scripts/CHANGELOG.md @@ -1,8 +1,9 @@ ## 2.5.0 (Unreleased) -### New Feature +### New Features - Added support for `check-engines` script ([#12721](https://github.com/WordPress/gutenberg/pull/12721)) +- Update default config provided for `lint-js` script ([#12845](https://github.com/WordPress/gutenberg/pull/12845)). ## 2.4.4 (2018-11-20) diff --git a/packages/scripts/README.md b/packages/scripts/README.md index 04ddf958782e02..b240177fbc631a 100644 --- a/packages/scripts/README.md +++ b/packages/scripts/README.md @@ -30,7 +30,7 @@ _Example:_ ### `check-engines` -Check if the current `node`, `npm` (or `yarn`) versions match the given [semantic version](https://semver.org/) ranges. If the given version is not satisfied, information about installing the needed version is printed and the program exits with an error code. It uses [check-node-version](https://www.npmjs.com/package/check-node-version) behind the scenes with the default configuration provided. You can specify your own ranges as described in [check-node-version docs](https://www.npmjs.com/package/check-node-version). +Check if the current `node`, `npm` (or `yarn`) versions match the given [semantic version](https://semver.org/) ranges. If the given version is not satisfied, information about installing the needed version is printed and the program exits with an error code. It uses [check-node-version](https://www.npmjs.com/package/check-node-version) behind the scenes with the recommended configuration provided. You can specify your own ranges as described in [check-node-version docs](https://www.npmjs.com/package/check-node-version). _Example:_ @@ -48,7 +48,7 @@ This is how you execute the script with presented setup: ### `wp-scripts lint-js` -Helps enforce coding style guidelines for your JavaScript files. It uses [eslint](https://eslint.org/) with no rules provided (we plan to add zero config support in the near future). You can specify your own rules as described in [eslint docs](https://eslint.org/docs/rules/). +Helps enforce coding style guidelines for your JavaScript files. It uses [eslint](https://eslint.org/) with the set of recommended rules defined in [@wordpress/eslint-plugin](https://www.npmjs.com/package/@wordpress/eslint-plugin) npm package. You can override default rules with your own as described in [eslint docs](https://eslint.org/docs/rules/). _Example:_ @@ -65,7 +65,7 @@ This is how you execute the script with presented setup: ### `wp-scripts lint-pkg-json` -Helps enforce standards for your package.json files. It uses [npm-package-json-lint](https://www.npmjs.com/package/npm-package-json-lint) with the set of default rules provided. You can override them with your own rules as described in [npm-package-json-lint wiki](https://github.com/tclindner/npm-package-json-lint/wiki). +Helps enforce standards for your package.json files. It uses [npm-package-json-lint](https://www.npmjs.com/package/npm-package-json-lint) with the set of recommended rules defined in [@wordpress/npm-package-json-lint-config](https://www.npmjs.com/package/@wordpress/npm-package-json-lint-config) npm package. You can override default rules with your own as described in [npm-package-json-lint wiki](https://github.com/tclindner/npm-package-json-lint/wiki). _Example:_ @@ -84,7 +84,7 @@ This is how you execute those scripts using the presented setup: _Alias_: `wp-scripts test-unit-jest` -Launches the test runner. It uses [Jest](https://facebook.github.io/jest/) behind the scenes and you are able to utilize all of its [CLI options](https://facebook.github.io/jest/docs/en/cli.html). You can also run `./node_modules/.bin/wp-scripts test-unit-js --help` or `npm run test:help` (as presented below) to view all of the available options. +Launches the test runner. It uses [Jest](https://facebook.github.io/jest/) behind the scenes and you are able to utilize all of its [CLI options](https://facebook.github.io/jest/docs/en/cli.html). You can also run `./node_modules/.bin/wp-scripts test-unit-js --help` or `npm run test:help` (as presented below) to view all of the available options. By default, it uses the set of recommended options defined in [@wordpress/jest-preset-default](https://www.npmjs.com/package/@wordpress/jest-preset-default) npm package. You can override them with your own options as described in [Jest documentation](https://jestjs.io/docs/en/configuration). _Example:_ diff --git a/packages/scripts/config/.eslintrc.js b/packages/scripts/config/.eslintrc.js index 601a62eb177b85..5b59ed86f4d2e5 100644 --- a/packages/scripts/config/.eslintrc.js +++ b/packages/scripts/config/.eslintrc.js @@ -1,3 +1,3 @@ module.exports = { - parser: 'babel-eslint', + extends: [ 'plugin:@wordpress/eslint-plugin/recommended' ], }; diff --git a/packages/scripts/package.json b/packages/scripts/package.json index 6a0bc5e1986a92..c6232c62d586cd 100644 --- a/packages/scripts/package.json +++ b/packages/scripts/package.json @@ -32,9 +32,9 @@ }, "dependencies": { "@wordpress/babel-preset-default": "file:../babel-preset-default", + "@wordpress/eslint-plugin": "file:../eslint-plugin", "@wordpress/jest-preset-default": "file:../jest-preset-default", "@wordpress/npm-package-json-lint-config": "file:../npm-package-json-lint-config", - "babel-eslint": "8.0.3", "chalk": "^2.4.1", "check-node-version": "^3.1.1", "cross-spawn": "^5.1.0", From 0484bc368bd0e014bbfcba6e17e08ae44cad00d4 Mon Sep 17 00:00:00 2001 From: Stephen Edgar <stephen@netweb.com.au> Date: Wed, 19 Dec 2018 19:25:58 +1100 Subject: [PATCH 248/254] Add JSHint ESLint config (#12840) * Add ESLint JSHint config * Update packages/eslint-plugin/README.md Co-Authored-By: ntwb <stephen@netweb.com.au> --- packages/eslint-plugin/README.md | 10 ++++++++++ packages/eslint-plugin/configs/jshint.js | 17 +++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 packages/eslint-plugin/configs/jshint.js diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index fd516bbf622b4d..3463e97d84024b 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -45,4 +45,14 @@ These rules can be used additively, so you could extend both `esnext` and `custo The granular rulesets will not define any environment globals. As such, if they are required for your project, you will need to define them yourself. +#### Legacy + +If you are using WordPress' `.jshintrc` JSHint configuration and you would like to take the first step to migrate to an ESLint equivalent it is also possible to define your own project's `.eslintrc` file as: + +```json +{ + "extends": [ "plugin:@wordpress/eslint-plugin/jshint" ] +} +``` + <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/eslint-plugin/configs/jshint.js b/packages/eslint-plugin/configs/jshint.js new file mode 100644 index 00000000000000..d153eeb1a536a8 --- /dev/null +++ b/packages/eslint-plugin/configs/jshint.js @@ -0,0 +1,17 @@ +module.exports = { + rules: { + curly: 'error', + eqeqeq: 'error', + 'no-caller': 'error', + 'no-cond-assign': [ 'error', 'except-parens' ], + 'no-eq-null': 'error', + 'no-irregular-whitespace': 'error', + 'no-trailing-spaces': 'error', + 'no-undef': 'error', + 'no-unused-expressions': 'error', + 'no-unused-vars': 'error', + 'one-var': [ 'error', 'always' ], + quotes: [ 'error', 'single' ], + 'wrap-iife': [ 'error', 'any' ], + }, +}; From 76123a44d288500400a9b3d90da4d0d12bc57be1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20=28Greg=29=20Zi=C3=B3=C5=82kowski?= <grzegorz@gziolo.pl> Date: Wed, 19 Dec 2018 10:16:20 +0100 Subject: [PATCH 249/254] Scripts: Add test-e2e script to wp-scripts (#12437) * Scripts: Add test-e2e script * Docs: Add docs for test-e2e script added to scripts packages * Update package-lock.json * Refactor scripts based on feedback shared in the review * More fixes to wp-scripts test-e2e * Address feedback shared in the review * Fix runInBand flag usage --- package-lock.json | 563 +++++++++--------- package.json | 5 +- packages/babel-preset-default/package.json | 1 + packages/scripts/CHANGELOG.md | 1 + packages/scripts/README.md | 74 ++- packages/scripts/config/jest-e2e.config.js | 26 + packages/scripts/config/jest-unit.config.js | 21 + packages/scripts/config/jest.config.js | 28 - .../scripts/config}/puppeteer.config.js | 0 packages/scripts/package.json | 2 + packages/scripts/scripts/test-e2e.js | 45 ++ packages/scripts/scripts/test-unit-jest.js | 19 +- packages/scripts/utils/cli.js | 85 +++ packages/scripts/utils/config.js | 23 + packages/scripts/utils/file.js | 32 + packages/scripts/utils/index.js | 109 +--- 16 files changed, 611 insertions(+), 423 deletions(-) create mode 100644 packages/scripts/config/jest-e2e.config.js create mode 100644 packages/scripts/config/jest-unit.config.js delete mode 100644 packages/scripts/config/jest.config.js rename {test/e2e => packages/scripts/config}/puppeteer.config.js (100%) create mode 100644 packages/scripts/scripts/test-e2e.js create mode 100644 packages/scripts/utils/cli.js create mode 100644 packages/scripts/utils/config.js create mode 100644 packages/scripts/utils/file.js diff --git a/package-lock.json b/package-lock.json index 8038ebf538c07f..4a190ed1cb0d28 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2323,6 +2323,7 @@ "version": "file:packages/babel-preset-default", "dev": true, "requires": { + "@babel/core": "^7.0.0", "@babel/plugin-proposal-async-generator-functions": "^7.0.0", "@babel/plugin-proposal-object-rest-spread": "^7.0.0", "@babel/plugin-transform-react-jsx": "^7.0.0", @@ -2770,7 +2771,9 @@ "cross-spawn": "^5.1.0", "eslint": "^4.19.1", "jest": "^23.6.0", + "jest-puppeteer": "3.2.1", "npm-package-json-lint": "^3.3.1", + "puppeteer": "1.6.1", "read-pkg-up": "^1.0.1", "resolve-bin": "^0.4.0" } @@ -3020,12 +3023,6 @@ "normalize-path": "^2.1.1" } }, - "app-root-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-2.1.0.tgz", - "integrity": "sha1-mL9lmTJ+zqGZMJhm6BQDaP0uZGo=", - "dev": true - }, "append-transform": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-0.4.0.tgz", @@ -7357,16 +7354,6 @@ } } }, - "enzyme-matchers": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/enzyme-matchers/-/enzyme-matchers-6.0.2.tgz", - "integrity": "sha1-eU1OQyVNqqD//TpZHlhp/IYd7rI=", - "dev": true, - "requires": { - "circular-json-es6": "^2.0.1", - "deep-equal-ident": "^1.1.1" - } - }, "enzyme-to-json": { "version": "3.3.4", "resolved": "https://registry.npmjs.org/enzyme-to-json/-/enzyme-to-json-3.3.4.tgz", @@ -8015,9 +8002,9 @@ } }, "expect-puppeteer": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/expect-puppeteer/-/expect-puppeteer-3.2.0.tgz", - "integrity": "sha1-tMMi4ouG6edPXG1jb7Hg3eXEpVk=", + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/expect-puppeteer/-/expect-puppeteer-3.5.1.tgz", + "integrity": "sha512-SB5JeJCXWSRcUK39fBJlCA6qnVt3BG1/M9vYZ+XYq8gY9jab9Jm4BztsrAwDTWca1L+O/7dRYrG2BPziRtjh+Q==", "dev": true }, "express": { @@ -8513,14 +8500,15 @@ } }, "find-process": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/find-process/-/find-process-1.1.1.tgz", - "integrity": "sha1-V/sa28f0MEeG23IKSf69cIoxYtQ=", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/find-process/-/find-process-1.2.1.tgz", + "integrity": "sha512-z4RXYStNAcoi4+smpKbzJXbMT8DdvwqTE7wL7DWZMD0SkTRfQ49z9S7YaK24kuRseKr23YSZlnyL/TaJZtgM1g==", "dev": true, "requires": { "chalk": "^2.0.1", "commander": "^2.11.0", - "debug": "^2.6.8" + "debug": "^2.6.8", + "lodash": "^4.17.11" }, "dependencies": { "debug": { @@ -8531,6 +8519,12 @@ "requires": { "ms": "2.0.0" } + }, + "lodash": { + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", + "dev": true } } }, @@ -11309,18 +11303,6 @@ "source-map": "^0.6.0" } }, - "jest-validate": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-23.6.0.tgz", - "integrity": "sha512-OFKapYxe72yz7agrDAWi8v2WL8GIfVqcbKRCLbRG9PAxtzF9b1SEDdTpytNDN12z2fJynoBwpMpvj2R39plI2A==", - "dev": true, - "requires": { - "chalk": "^2.0.1", - "jest-get-type": "^22.1.0", - "leven": "^2.1.0", - "pretty-format": "^23.6.0" - } - }, "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", @@ -11461,16 +11443,6 @@ "source-map": "^0.5.7" } }, - "babel-jest": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-23.6.0.tgz", - "integrity": "sha512-lqKGG6LYXYu+DQh/slrQ8nxXQkEkhugdXsU6St7GmhVS7Ilc/22ArwqXNJrf0QaOBjZB0360qZMwXqDYQHXaew==", - "dev": true, - "requires": { - "babel-plugin-istanbul": "^4.1.6", - "babel-preset-jest": "^23.2.0" - } - }, "braces": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", @@ -11563,18 +11535,6 @@ } } }, - "jest-validate": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-23.6.0.tgz", - "integrity": "sha512-OFKapYxe72yz7agrDAWi8v2WL8GIfVqcbKRCLbRG9PAxtzF9b1SEDdTpytNDN12z2fJynoBwpMpvj2R39plI2A==", - "dev": true, - "requires": { - "chalk": "^2.0.1", - "jest-get-type": "^22.1.0", - "leven": "^2.1.0", - "pretty-format": "^23.6.0" - } - }, "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", @@ -11604,55 +11564,60 @@ "parse-glob": "^3.0.4", "regex-cache": "^0.4.2" } - }, - "pretty-format": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.6.0.tgz", - "integrity": "sha512-zf9NV1NSlDLDjycnwm6hpFATCGl/K1lt0R/GdkAK2O5LN/rwJoB+Mh93gGJjut4YbmecbfgLWVGSTCr0Ewvvbw==", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0", - "ansi-styles": "^3.2.0" - } } } }, "jest-dev-server": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/jest-dev-server/-/jest-dev-server-3.2.0.tgz", - "integrity": "sha1-Gf5g7hVgyv/E8W9In680R52c28Y=", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/jest-dev-server/-/jest-dev-server-3.6.0.tgz", + "integrity": "sha512-UbDPDBjpD3t9hjZ6z4j1NW8+jYE1rP5jJ6qVLbWsnpPgfJDBziOhhUSspSvyCG3DW+txK8/Xtw1lwwiEponWpg==", "dev": true, "requires": { "chalk": "^2.4.1", "cwd": "^0.10.0", - "find-process": "^1.1.1", - "inquirer": "^6.0.0", - "spawnd": "^2.0.0", - "terminate": "^2.1.0", + "find-process": "^1.2.1", + "inquirer": "^6.2.0", + "spawnd": "^3.5.2", + "terminate": "^2.1.2", "wait-port": "^0.2.2" }, "dependencies": { + "ansi-regex": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.0.0.tgz", + "integrity": "sha512-iB5Dda8t/UqpPI/IjsejXu5jOGDrzn41wJyljwPH65VCIbk6+1BzFIMJGFwTNrYXT1CrD+B4l19U7awiQ8rk7w==", + "dev": true + }, "chardet": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.5.0.tgz", - "integrity": "sha512-9ZTaoBaePSCFvNlNGrsyI8ZVACP2svUtq0DkM7t4K2ClAa96sqOIRjAzDTc8zXzFt1cZR46rRzLTiHFSJ+Qw0g==", + "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.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.0.0.tgz", - "integrity": "sha512-mpkfj0FEdxrIhOC04zk85X7StNtr0yXnG7zCb+8ikO8OJi2jsHh5YGoknNTyXgsbHOf1WOOcVU3kPFWT2WgCkQ==", + "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.5.0", - "iconv-lite": "^0.4.22", + "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": "6.0.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.0.0.tgz", - "integrity": "sha512-tISQWRwtcAgrz+SHPhTH7d3e73k31gsOy6i1csonLc0u1dVK/wYvuOnFeiWqC5OXFIYbmrIFInef31wbT8MEJg==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.2.1.tgz", + "integrity": "sha512-088kl3DRT2dLU5riVMKKr1DlImd6X7smDhpXUCkJDCKvTEJeRiXh0G132HG9u5a+6Ylw9plFRY7RuTnwohYSpg==", "dev": true, "requires": { "ansi-escapes": "^3.0.0", @@ -11661,23 +11626,32 @@ "cli-width": "^2.0.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": "^6.1.0", "string-width": "^2.1.0", - "strip-ansi": "^4.0.0", + "strip-ansi": "^5.0.0", "through": "^2.3.6" } }, "rxjs": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.2.2.tgz", - "integrity": "sha512-0MI8+mkKAXZUF9vMrEoPnaoHkfzBPP4IGwUYRJhIRJF6/w3uByO1e91bEHn8zd43RdkTMKiooYKmwz7RH6zfOQ==", + "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" } + }, + "strip-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.0.0.tgz", + "integrity": "sha512-Uu7gQyZI7J7gn5qLn1Np3G9vcYGTVqB+lFTytnDJv83dd8T22aGH451P3jueT2/QemInJDfxHB5Tde5OzgG1Ow==", + "dev": true, + "requires": { + "ansi-regex": "^4.0.0" + } } } }, @@ -11691,18 +11665,6 @@ "diff": "^3.2.0", "jest-get-type": "^22.1.0", "pretty-format": "^23.6.0" - }, - "dependencies": { - "pretty-format": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.6.0.tgz", - "integrity": "sha512-zf9NV1NSlDLDjycnwm6hpFATCGl/K1lt0R/GdkAK2O5LN/rwJoB+Mh93gGJjut4YbmecbfgLWVGSTCr0Ewvvbw==", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0", - "ansi-styles": "^3.2.0" - } - } } }, "jest-docblock": { @@ -11722,24 +11684,12 @@ "requires": { "chalk": "^2.0.1", "pretty-format": "^23.6.0" - }, - "dependencies": { - "pretty-format": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.6.0.tgz", - "integrity": "sha512-zf9NV1NSlDLDjycnwm6hpFATCGl/K1lt0R/GdkAK2O5LN/rwJoB+Mh93gGJjut4YbmecbfgLWVGSTCr0Ewvvbw==", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0", - "ansi-styles": "^3.2.0" - } - } } }, "jest-environment-enzyme": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/jest-environment-enzyme/-/jest-environment-enzyme-6.0.2.tgz", - "integrity": "sha1-iQUmYi1d6KggoYSdJoTwO8AG4O8=", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/jest-environment-enzyme/-/jest-environment-enzyme-6.1.2.tgz", + "integrity": "sha512-WHeBKgBYOdryuOTEoK55lJwjg7Raery1OgXHLwukI3mSYgOkm2UrCDDT+vneqVgy7F8KuRHyStfD+TC/m2b7Kg==", "dev": true, "requires": { "jest-environment-jsdom": "^22.4.1" @@ -11884,28 +11834,38 @@ } }, "jest-environment-puppeteer": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/jest-environment-puppeteer/-/jest-environment-puppeteer-3.2.1.tgz", - "integrity": "sha1-xS6aqY8HtS83hyHV8f8PAAxODp8=", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/jest-environment-puppeteer/-/jest-environment-puppeteer-3.6.0.tgz", + "integrity": "sha512-3ULqgH6f+HRu53wxP0NDFb8uZFxn2+97tK4eZicktgst/zWpSJucEpbsVVNWk4cIHrPo79rYoUfomxnui/ndAg==", "dev": true, "requires": { "chalk": "^2.4.1", "cwd": "^0.10.0", - "jest-dev-server": "^3.2.0", - "lodash": "^4.17.10", - "mkdirp": "^0.5.1", - "rimraf": "^2.6.2" + "jest-dev-server": "^3.6.0", + "merge-deep": "^3.0.2" } }, "jest-enzyme": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/jest-enzyme/-/jest-enzyme-6.0.2.tgz", - "integrity": "sha1-vEZBad5sLVBgLgK7yUZEYFB/spU=", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/jest-enzyme/-/jest-enzyme-6.1.2.tgz", + "integrity": "sha512-+ds7r2ru3QkNJxelQ2tnC6d33pjUSsZHPD3v4TlnHlNMuGX3UKdxm5C46yZBvJICYBvIF+RFKBhLMM4evNM95Q==", "dev": true, "requires": { - "enzyme-matchers": "^6.0.2", + "enzyme-matchers": "^6.1.2", "enzyme-to-json": "^3.3.0", - "jest-environment-enzyme": "^6.0.2" + "jest-environment-enzyme": "^6.1.2" + }, + "dependencies": { + "enzyme-matchers": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/enzyme-matchers/-/enzyme-matchers-6.1.2.tgz", + "integrity": "sha512-cP9p+HMOZ1ZXQ+k2H4dCkxmTZzIvpEy5zv0ZjgoBl6D0U43v+bJGH5IeWHdIovCzgJ0dVcMCKJ6lNu83lYUCAA==", + "dev": true, + "requires": { + "circular-json-es6": "^2.0.1", + "deep-equal-ident": "^1.1.1" + } + } } }, "jest-get-type": { @@ -12070,17 +12030,6 @@ "is-extglob": "^1.0.0" } }, - "jest-matcher-utils": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-23.6.0.tgz", - "integrity": "sha512-rosyCHQfBcol4NsckTn01cdelzWLU9Cq7aaigDf8VwwpIRvWE/9zLgX2bON+FkEW69/0UuYslUe22SOdEf2nog==", - "dev": true, - "requires": { - "chalk": "^2.0.1", - "jest-get-type": "^22.1.0", - "pretty-format": "^23.6.0" - } - }, "jest-message-util": { "version": "23.4.0", "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-23.4.0.tgz", @@ -12140,16 +12089,6 @@ "regex-cache": "^0.4.2" } }, - "pretty-format": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.6.0.tgz", - "integrity": "sha512-zf9NV1NSlDLDjycnwm6hpFATCGl/K1lt0R/GdkAK2O5LN/rwJoB+Mh93gGJjut4YbmecbfgLWVGSTCr0Ewvvbw==", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0", - "ansi-styles": "^3.2.0" - } - }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -12165,18 +12104,6 @@ "dev": true, "requires": { "pretty-format": "^23.6.0" - }, - "dependencies": { - "pretty-format": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.6.0.tgz", - "integrity": "sha512-zf9NV1NSlDLDjycnwm6hpFATCGl/K1lt0R/GdkAK2O5LN/rwJoB+Mh93gGJjut4YbmecbfgLWVGSTCr0Ewvvbw==", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0", - "ansi-styles": "^3.2.0" - } - } } }, "jest-matcher-utils": { @@ -12610,18 +12537,6 @@ } } }, - "jest-validate": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-23.6.0.tgz", - "integrity": "sha512-OFKapYxe72yz7agrDAWi8v2WL8GIfVqcbKRCLbRG9PAxtzF9b1SEDdTpytNDN12z2fJynoBwpMpvj2R39plI2A==", - "dev": true, - "requires": { - "chalk": "^2.0.1", - "jest-get-type": "^22.1.0", - "leven": "^2.1.0", - "pretty-format": "^23.6.0" - } - }, "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", @@ -12652,16 +12567,6 @@ "regex-cache": "^0.4.2" } }, - "pretty-format": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.6.0.tgz", - "integrity": "sha512-zf9NV1NSlDLDjycnwm6hpFATCGl/K1lt0R/GdkAK2O5LN/rwJoB+Mh93gGJjut4YbmecbfgLWVGSTCr0Ewvvbw==", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0", - "ansi-styles": "^3.2.0" - } - }, "strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -12767,17 +12672,6 @@ "is-extglob": "^1.0.0" } }, - "jest-matcher-utils": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-23.6.0.tgz", - "integrity": "sha512-rosyCHQfBcol4NsckTn01cdelzWLU9Cq7aaigDf8VwwpIRvWE/9zLgX2bON+FkEW69/0UuYslUe22SOdEf2nog==", - "dev": true, - "requires": { - "chalk": "^2.0.1", - "jest-get-type": "^22.1.0", - "pretty-format": "^23.6.0" - } - }, "jest-message-util": { "version": "23.4.0", "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-23.4.0.tgz", @@ -12820,16 +12714,6 @@ "parse-glob": "^3.0.4", "regex-cache": "^0.4.2" } - }, - "pretty-format": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.6.0.tgz", - "integrity": "sha512-zf9NV1NSlDLDjycnwm6hpFATCGl/K1lt0R/GdkAK2O5LN/rwJoB+Mh93gGJjut4YbmecbfgLWVGSTCr0Ewvvbw==", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0", - "ansi-styles": "^3.2.0" - } } } }, @@ -12857,27 +12741,15 @@ } }, "jest-validate": { - "version": "23.4.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-23.4.0.tgz", - "integrity": "sha1-2W7t4B7wOskJwAnpyORVGX1IwgE=", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-23.6.0.tgz", + "integrity": "sha512-OFKapYxe72yz7agrDAWi8v2WL8GIfVqcbKRCLbRG9PAxtzF9b1SEDdTpytNDN12z2fJynoBwpMpvj2R39plI2A==", "dev": true, "requires": { "chalk": "^2.0.1", "jest-get-type": "^22.1.0", "leven": "^2.1.0", - "pretty-format": "^23.2.0" - }, - "dependencies": { - "pretty-format": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.2.0.tgz", - "integrity": "sha1-OwqqY8AYpTWDNzwcs6XZbMXoMBc=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0", - "ansi-styles": "^3.2.0" - } - } + "pretty-format": "^23.6.0" } }, "jest-watcher": { @@ -13295,8 +13167,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", - "dev": true, - "optional": true + "dev": true }, "lcid": { "version": "1.0.0", @@ -13407,12 +13278,11 @@ } }, "lint-staged": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-7.2.0.tgz", - "integrity": "sha512-jPoIMbmgtWMUrz/l0rhBVa1j6H71zr0rEoxDWBA333PZcaqBvELdg0Sf4tdGHlwrBM0GXaXMVgTRkLTm2vA7Jg==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-7.3.0.tgz", + "integrity": "sha512-AXk40M9DAiPi7f4tdJggwuKIViUplYtVj1os1MVEteW7qOkU50EOehayCfO9TsoGK24o/EsWb41yrEgfJDDjCw==", "dev": true, "requires": { - "app-root-path": "^2.0.1", "chalk": "^2.3.1", "commander": "^2.14.1", "cosmiconfig": "^5.0.2", @@ -13422,7 +13292,7 @@ "find-parent-dir": "^0.3.0", "is-glob": "^4.0.0", "is-windows": "^1.0.2", - "jest-validate": "^23.0.0", + "jest-validate": "^23.5.0", "listr": "^0.14.1", "lodash": "^4.17.5", "log-symbols": "^2.2.0", @@ -13474,15 +13344,6 @@ "object-assign": "^4.1.0" } }, - "indent-string": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", - "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", - "dev": true, - "requires": { - "repeating": "^2.0.0" - } - }, "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -13508,32 +13369,49 @@ } }, "listr": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/listr/-/listr-0.14.1.tgz", - "integrity": "sha512-MSMUUVN1f8aRnPi4034RkOqdiUlpYW+FqwFE3aL0uYNPRavkt2S2SsSpDDofn8BDpqv2RNnsdOcCHWsChcq77A==", + "version": "0.14.3", + "resolved": "https://registry.npmjs.org/listr/-/listr-0.14.3.tgz", + "integrity": "sha512-RmAl7su35BFd/xoMamRjpIE4j3v+L28o8CT5YhAXQJm1fD+1l9ngXY8JAQRJ+tFK2i5njvi0iRUKV09vPwA0iA==", "dev": true, "requires": { "@samverschueren/stream-to-observable": "^0.3.0", - "cli-truncate": "^0.2.1", - "figures": "^1.7.0", - "indent-string": "^2.1.0", "is-observable": "^1.1.0", "is-promise": "^2.1.0", "is-stream": "^1.1.0", "listr-silent-renderer": "^1.1.1", - "listr-update-renderer": "^0.4.0", - "listr-verbose-renderer": "^0.4.0", + "listr-update-renderer": "^0.5.0", + "listr-verbose-renderer": "^0.5.0", + "p-map": "^2.0.0", + "rxjs": "^6.3.3" + }, + "dependencies": { + "p-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.0.0.tgz", + "integrity": "sha512-GO107XdrSUmtHxVoi60qc9tUl/KkNKm+X2CF4P9amalpGxv5YqVPJNfSb0wcA+syCopkZvYYIzW8OVTQW59x/w==", + "dev": true + } + } + }, + "listr-update-renderer": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/listr-update-renderer/-/listr-update-renderer-0.5.0.tgz", + "integrity": "sha512-tKRsZpKz8GSGqoI/+caPmfrypiaq+OQCbd+CovEC24uk1h952lVj5sC7SqyFUm+OaJ5HN/a1YLt5cit2FMNsFA==", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "cli-truncate": "^0.2.1", + "elegant-spinner": "^1.0.1", + "figures": "^1.7.0", + "indent-string": "^3.0.0", "log-symbols": "^1.0.2", - "log-update": "^1.0.2", - "ora": "^0.2.3", - "p-map": "^1.1.1", - "rxjs": "^6.1.0", + "log-update": "^2.3.0", "strip-ansi": "^3.0.1" }, "dependencies": { "chalk": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "dev": true, "requires": { @@ -13555,6 +13433,40 @@ } } }, + "listr-verbose-renderer": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/listr-verbose-renderer/-/listr-verbose-renderer-0.5.0.tgz", + "integrity": "sha512-04PDPqSlsqIOaaaGZ+41vq5FejI9auqTInicFRndCBgE3bXG8D6W1I+mWhk+1nqbHmyhla/6BUrd5OSiHwKRXw==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "cli-cursor": "^2.1.0", + "date-fns": "^1.27.2", + "figures": "^2.0.0" + }, + "dependencies": { + "figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + } + } + }, + "log-update": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-2.3.0.tgz", + "integrity": "sha1-iDKP19HOeTiykoN0bwsbwSayRwg=", + "dev": true, + "requires": { + "ansi-escapes": "^3.0.0", + "cli-cursor": "^2.0.0", + "wrap-ansi": "^3.0.1" + } + }, "pify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", @@ -13562,9 +13474,9 @@ "dev": true }, "rxjs": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.2.2.tgz", - "integrity": "sha512-0MI8+mkKAXZUF9vMrEoPnaoHkfzBPP4IGwUYRJhIRJF6/w3uByO1e91bEHn8zd43RdkTMKiooYKmwz7RH6zfOQ==", + "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" @@ -13584,6 +13496,33 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", "dev": true + }, + "wrap-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-3.0.1.tgz", + "integrity": "sha1-KIoE2H7aXChuBg3+jxNc6NAH+Lo=", + "dev": true, + "requires": { + "string-width": "^2.1.1", + "strip-ansi": "^4.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } } } }, @@ -13679,6 +13618,30 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", "dev": true + }, + "wrap-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-3.0.1.tgz", + "integrity": "sha1-KIoE2H7aXChuBg3+jxNc6NAH+Lo=", + "requires": { + "string-width": "^2.1.1", + "strip-ansi": "^4.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "^3.0.0" + } + } + } } } }, @@ -14573,6 +14536,70 @@ "integrity": "sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ==", "dev": true }, + "merge-deep": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/merge-deep/-/merge-deep-3.0.2.tgz", + "integrity": "sha512-T7qC8kg4Zoti1cFd8Cr0M+qaZfOwjlPDEdZIIPPB2JZctjaPM4fX+i7HOId69tAti2fvO6X5ldfYUONDODsrkA==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "clone-deep": "^0.2.4", + "kind-of": "^3.0.2" + }, + "dependencies": { + "clone-deep": { + "version": "0.2.4", + "resolved": "http://registry.npmjs.org/clone-deep/-/clone-deep-0.2.4.tgz", + "integrity": "sha1-TnPdCen7lxzDhnDF3O2cGJZIHMY=", + "dev": true, + "requires": { + "for-own": "^0.1.3", + "is-plain-object": "^2.0.1", + "kind-of": "^3.0.2", + "lazy-cache": "^1.0.3", + "shallow-clone": "^0.1.2" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + }, + "shallow-clone": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-0.1.2.tgz", + "integrity": "sha1-WQnodLp3EG1zrEFM/sH/yofZcGA=", + "dev": true, + "requires": { + "is-extendable": "^0.1.1", + "kind-of": "^2.0.1", + "lazy-cache": "^0.2.3", + "mixin-object": "^2.0.1" + }, + "dependencies": { + "kind-of": { + "version": "2.0.1", + "resolved": "http://registry.npmjs.org/kind-of/-/kind-of-2.0.1.tgz", + "integrity": "sha1-AY7HpM5+OobLkUG+UZ0kyPqpgbU=", + "dev": true, + "requires": { + "is-buffer": "^1.0.2" + } + }, + "lazy-cache": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-0.2.7.tgz", + "integrity": "sha1-f+3fLctu23fRHvHRF6tf/fCrG2U=", + "dev": true + } + } + } + } + }, "merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -14632,9 +14659,9 @@ } }, "mime": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.3.1.tgz", - "integrity": "sha512-OEUllcVoydBHGN1z84yfQDimn58pZNNNXgZlHXSboxMlFvgI6MXSWpWKpFRra7H1HxpVhHTkrghfRW49k6yjeg==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.0.tgz", + "integrity": "sha512-ikBcWwyqXQSHKtciCcctu9YfPbFYZ4+gbHEmE0Q8jzcTYQg5dHCr3g2wwAZjPoJfQVXZq6KXAjpXOTf5/cjT7w==", "dev": true }, "mime-db": { @@ -17670,12 +17697,12 @@ "dev": true }, "ps-tree": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/ps-tree/-/ps-tree-1.1.0.tgz", - "integrity": "sha1-tCGyQUDWID8e08dplrRCewjowBQ=", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ps-tree/-/ps-tree-1.2.0.tgz", + "integrity": "sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA==", "dev": true, "requires": { - "event-stream": "~3.3.0" + "event-stream": "=3.3.4" } }, "pseudomap": { @@ -19644,14 +19671,14 @@ "dev": true }, "spawnd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/spawnd/-/spawnd-2.0.0.tgz", - "integrity": "sha1-0gIEA9xe9y7Kw/+rm0L6Fq6RdfU=", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/spawnd/-/spawnd-3.5.2.tgz", + "integrity": "sha512-taf6nYLIl8b3b1RNt0YuxnIUUgHqfx+nix8Rdr2FkNG8259+Jt8YJahrPDShOPa9vCMnDPfPsefRAY/oJy+QBg==", "dev": true, "requires": { "exit": "^0.1.2", "signal-exit": "^3.0.2", - "terminate": "^2.1.0", + "terminate": "^2.1.2", "wait-port": "^0.2.2" } }, @@ -20591,12 +20618,12 @@ } }, "terminate": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/terminate/-/terminate-2.1.0.tgz", - "integrity": "sha1-qH7kJL4BodKPLzAQRQQ6W81nmgU=", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/terminate/-/terminate-2.1.2.tgz", + "integrity": "sha512-ltKc9MkgcRe7gzD7XSttHCF1feKM1pTkCdb58jFVWk1efPN9JIk/BHSlOaYF+hCcWoubeJQ8C8Phb0++fa6iNQ==", "dev": true, "requires": { - "ps-tree": "^1.1.0" + "ps-tree": "^1.1.1" } }, "test-exclude": { diff --git a/package.json b/package.json index 03a6bdd8f76163..a214f2987eca35 100644 --- a/package.json +++ b/package.json @@ -90,13 +90,12 @@ "jest-puppeteer": "3.2.1", "jsdom": "11.12.0", "lerna": "3.4.3", - "lint-staged": "7.2.0", + "lint-staged": "7.3.0", "lodash": "4.17.10", "mkdirp": "0.5.1", "node-sass": "4.11.0", "pegjs": "0.10.0", "phpegjs": "1.0.0-beta7", - "puppeteer": "1.6.1", "react-dom": "16.6.3", "react-test-renderer": "16.6.3", "redux": "4.0.0", @@ -179,7 +178,7 @@ "publish:prod": "npm run build:packages && lerna publish", "test": "npm run lint && npm run test-unit", "pretest-e2e": "concurrently \"./bin/reset-e2e-tests.sh\" \"npm run build\"", - "test-e2e": "cross-env JEST_PUPPETEER_CONFIG=test/e2e/puppeteer.config.js wp-scripts test-unit-js --config test/e2e/jest.config.json --runInBand", + "test-e2e": "wp-scripts test-e2e --config test/e2e/jest.config.json", "test-e2e:watch": "npm run test-e2e -- --watch", "test-php": "npm run lint-php && npm run test-unit-php", "test-unit": "wp-scripts test-unit-js --config test/unit/jest.config.json", diff --git a/packages/babel-preset-default/package.json b/packages/babel-preset-default/package.json index 08149a01f9b6b2..95bc302227f147 100644 --- a/packages/babel-preset-default/package.json +++ b/packages/babel-preset-default/package.json @@ -23,6 +23,7 @@ }, "main": "index.js", "dependencies": { + "@babel/core": "^7.0.0", "@babel/plugin-proposal-async-generator-functions": "^7.0.0", "@babel/plugin-proposal-object-rest-spread": "^7.0.0", "@babel/plugin-transform-react-jsx": "^7.0.0", diff --git a/packages/scripts/CHANGELOG.md b/packages/scripts/CHANGELOG.md index 1af69f533f2fa9..119e734319d31c 100644 --- a/packages/scripts/CHANGELOG.md +++ b/packages/scripts/CHANGELOG.md @@ -3,6 +3,7 @@ ### New Features - Added support for `check-engines` script ([#12721](https://github.com/WordPress/gutenberg/pull/12721)) +- Added support for `test-e2e` script ([#12437](https://github.com/WordPress/gutenberg/pull/12437)). - Update default config provided for `lint-js` script ([#12845](https://github.com/WordPress/gutenberg/pull/12845)). ## 2.4.4 (2018-11-20) diff --git a/packages/scripts/README.md b/packages/scripts/README.md index b240177fbc631a..2d578ef1b870bf 100644 --- a/packages/scripts/README.md +++ b/packages/scripts/README.md @@ -20,8 +20,11 @@ _Example:_ { "scripts": { "check-engines": "wp-scripts check-engines", + "check-licenses": "wp-scripts check-licenses --production", + "lint:js": "wp-scripts lint-js .", "lint:pkg-json": "wp-scripts lint-pkg-json .", - "test": "wp-scripts test-unit-js" + "test:e2e": "wp-scripts test-e2e", + "test:unit": "wp-scripts test-unit-js" } } ``` @@ -45,8 +48,28 @@ _Example:_ This is how you execute the script with presented setup: * `npm run check-engines` - checks installed version of `node` and `npm`. +### `check-licenses` -### `wp-scripts lint-js` +Validates that all dependencies of a project are compatible with the project's own license. + +_Example:_ + +```json +{ + "scripts": { + "check-licenses": "wp-scripts check-licenses --prod --gpl2 --ignore=abab" + } +} +``` + +_Flags_: + +- `--prod` (or `--production`): When present, validates only `dependencies` and not `devDependencies` +- `--dev` (or `--development`): When present, validates both `dependencies` and `devDependencies` +- `--gpl2`: Validates against [GPLv2 license compatibility](https://www.gnu.org/licenses/license-list.en.html) +- `--ignore=a,b,c`: A comma-separated set of package names to ignore for validation. This is intended to be used primarily in cases where a dependency's `license` field is malformed. It's assumed that any `ignored` package argument would be manually vetted for compatibility by the project owner. + +### `lint-js` Helps enforce coding style guidelines for your JavaScript files. It uses [eslint](https://eslint.org/) with the set of recommended rules defined in [@wordpress/eslint-plugin](https://www.npmjs.com/package/@wordpress/eslint-plugin) npm package. You can override default rules with your own as described in [eslint docs](https://eslint.org/docs/rules/). @@ -63,7 +86,7 @@ _Example:_ This is how you execute the script with presented setup: * `npm run lint:js` - lints JavaScripts files in the whole project's. -### `wp-scripts lint-pkg-json` +### `lint-pkg-json` Helps enforce standards for your package.json files. It uses [npm-package-json-lint](https://www.npmjs.com/package/npm-package-json-lint) with the set of recommended rules defined in [@wordpress/npm-package-json-lint-config](https://www.npmjs.com/package/@wordpress/npm-package-json-lint-config) npm package. You can override default rules with your own as described in [npm-package-json-lint wiki](https://github.com/tclindner/npm-package-json-lint/wiki). @@ -80,52 +103,59 @@ _Example:_ This is how you execute those scripts using the presented setup: * `npm run lint:pkg-json` - lints `package.json` file in the project's root folder. -### `wp-scripts test-unit-js` +### `test-e2e` -_Alias_: `wp-scripts test-unit-jest` +Launches the End-To-End (E2E) test runner. It uses [Jest](https://facebook.github.io/jest/) behind the scenes and you are able to utilize all of its [CLI options](https://facebook.github.io/jest/docs/en/cli.html). You can also run `./node_modules/.bin/wp-scripts test:e2e --help` or `npm run test:e2e:help` (as presented below) to view all of the available options. -Launches the test runner. It uses [Jest](https://facebook.github.io/jest/) behind the scenes and you are able to utilize all of its [CLI options](https://facebook.github.io/jest/docs/en/cli.html). You can also run `./node_modules/.bin/wp-scripts test-unit-js --help` or `npm run test:help` (as presented below) to view all of the available options. By default, it uses the set of recommended options defined in [@wordpress/jest-preset-default](https://www.npmjs.com/package/@wordpress/jest-preset-default) npm package. You can override them with your own options as described in [Jest documentation](https://jestjs.io/docs/en/configuration). +Writing tests can be done using Puppeteer API: + +> [Puppeteer](https://pptr.dev/) is a Node library which provides a high-level API to control Chrome or Chromium over the [DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/). Puppeteer runs [headless](https://developers.google.com/web/updates/2017/04/headless-chrome) by default, but can be configured to run full (non-headless) Chrome or Chromium. _Example:_ ```json { "scripts": { - "test": "wp-scripts test-unit-js", - "test:help": "wp-scripts test-unit-js --help", - "test:watch": "wp-scripts test-unit-js --watch" + "test:e2e": "wp-scripts test-e2e", + "test:e2e:help": "wp-scripts test-e2e --help" } } ``` This is how you execute those scripts using the presented setup: -* `npm run test` or `npm test` - runs all unit tests. -* `npm run test:help` - prints all available options to configure unit tests runner. -* `npm run test:watch` - runs all unit tests in the watch mode. +* `npm run test:e2e` - runs all unit tests. +* `npm run test:e2e:help` - prints all available options to configure unit tests runner. -### `wp-scripts check-licenses` +This script automatically detects the best config to start Puppeteer but sometimes you may need to specify custom options: + - You can add a `jest-puppeteer.config.js` at the root of the project or define a custom path using `JEST_PUPPETEER_CONFIG` environment variable. Check [jest-puppeteer](https://github.com/smooth-code/jest-puppeteer#jest-puppeteerconfigjs) for more details. -Validates that all dependencies of a project are compatible with the project's own license. +We enforce that all tests run serially in the current process using [--runInBand](https://jestjs.io/docs/en/cli#runinband) Jest CLI option to avoid conflicts between tests caused by the fact that they share the same WordPress instance. + +### `test-unit-js` + +_Alias_: `test-unit-jest` + +Launches the unit test runner. It uses [Jest](https://facebook.github.io/jest/) behind the scenes and you are able to utilize all of its [CLI options](https://facebook.github.io/jest/docs/en/cli.html). You can also run `./node_modules/.bin/wp-scripts test-unit-js --help` or `npm run test:unit:help` (as presented below) to view all of the available options. By default, it uses the set of recommended options defined in [@wordpress/jest-preset-default](https://www.npmjs.com/package/@wordpress/jest-preset-default) npm package. You can override them with your own options as described in [Jest documentation](https://jestjs.io/docs/en/configuration). _Example:_ ```json { "scripts": { - "check-licenses": "wp-scripts check-licenses --prod --gpl2 --ignore=abab", + "test:unit": "wp-scripts test-unit-js", + "test:unit:help": "wp-scripts test-unit-js --help", + "test:unit:watch": "wp-scripts test-unit-js --watch" } } ``` -_Flags_: - -- `--prod` (or `--production`): When present, validates only `dependencies` and not `devDependencies` -- `--dev` (or `--development`): When present, validates both `dependencies` and `devDependencies` -- `--gpl2`: Validates against [GPLv2 license compatibility](https://www.gnu.org/licenses/license-list.en.html) -- `--ignore=a,b,c`: A comma-separated set of package names to ignore for validation. This is intended to be used primarily in cases where a dependency's `license` field is malformed. It's assumed that any `ignored` package argument would be manually vetted for compatibility by the project owner. +This is how you execute those scripts using the presented setup: +* `npm run test:unit` - runs all unit tests. +* `npm run test:unit:help` - prints all available options to configure unit tests runner. +* `npm run test:unit:watch` - runs all unit tests in the watch mode. ## Inspiration -This is inspired by [react-scripts](https://www.npmjs.com/package/react-scripts) and [kcd-scripts](https://www.npmjs.com/package/kcd-scripts). +This package is inspired by [react-scripts](https://www.npmjs.com/package/react-scripts) and [kcd-scripts](https://www.npmjs.com/package/kcd-scripts). <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/scripts/config/jest-e2e.config.js b/packages/scripts/config/jest-e2e.config.js new file mode 100644 index 00000000000000..fd8d722b8c3fc1 --- /dev/null +++ b/packages/scripts/config/jest-e2e.config.js @@ -0,0 +1,26 @@ +/** + * External dependencies + */ +const path = require( 'path' ); + +/** + * Internal dependencies + */ +const { hasBabelConfig } = require( '../utils' ); + +const jestE2EConfig = { + preset: 'jest-puppeteer', + testMatch: [ + '**/__tests__/**/*.js', + '**/?(*.)(spec|test).js', + '**/test/*.js', + ], +}; + +if ( ! hasBabelConfig() ) { + jestE2EConfig.transform = { + '^.+\\.jsx?$': path.join( __dirname, 'babel-transform' ), + }; +} + +module.exports = jestE2EConfig; diff --git a/packages/scripts/config/jest-unit.config.js b/packages/scripts/config/jest-unit.config.js new file mode 100644 index 00000000000000..34f48ea0e33761 --- /dev/null +++ b/packages/scripts/config/jest-unit.config.js @@ -0,0 +1,21 @@ +/** + * External dependencies + */ +const path = require( 'path' ); + +/** + * Internal dependencies + */ +const { hasBabelConfig } = require( '../utils' ); + +const jestUnitConfig = { + preset: '@wordpress/jest-preset-default', +}; + +if ( ! hasBabelConfig() ) { + jestUnitConfig.transform = { + '^.+\\.jsx?$': path.join( __dirname, 'babel-transform' ), + }; +} + +module.exports = jestUnitConfig; diff --git a/packages/scripts/config/jest.config.js b/packages/scripts/config/jest.config.js deleted file mode 100644 index b884ac499eddbc..00000000000000 --- a/packages/scripts/config/jest.config.js +++ /dev/null @@ -1,28 +0,0 @@ -/** - * External dependencies - */ -const path = require( 'path' ); - -/** - * Internal dependencies - */ -const { - hasProjectFile, - hasPackageProp, -} = require( '../utils' ); - -const jestConfig = { - preset: '@wordpress/jest-preset-default', -}; - -const hasBabelConfig = hasProjectFile( '.babelrc' ) || - hasProjectFile( 'babel.config.js' ) || - hasPackageProp( 'babel' ); - -if ( ! hasBabelConfig ) { - jestConfig.transform = { - '^.+\\.jsx?$': path.join( __dirname, 'babel-transform' ), - }; -} - -module.exports = jestConfig; diff --git a/test/e2e/puppeteer.config.js b/packages/scripts/config/puppeteer.config.js similarity index 100% rename from test/e2e/puppeteer.config.js rename to packages/scripts/config/puppeteer.config.js diff --git a/packages/scripts/package.json b/packages/scripts/package.json index c6232c62d586cd..4ee9bdb00850cd 100644 --- a/packages/scripts/package.json +++ b/packages/scripts/package.json @@ -40,7 +40,9 @@ "cross-spawn": "^5.1.0", "eslint": "^4.19.1", "jest": "^23.6.0", + "jest-puppeteer": "3.2.1", "npm-package-json-lint": "^3.3.1", + "puppeteer": "1.6.1", "read-pkg-up": "^1.0.1", "resolve-bin": "^0.4.0" }, diff --git a/packages/scripts/scripts/test-e2e.js b/packages/scripts/scripts/test-e2e.js new file mode 100644 index 00000000000000..fc2e8b42f345d6 --- /dev/null +++ b/packages/scripts/scripts/test-e2e.js @@ -0,0 +1,45 @@ +// Do this as the first thing so that any code reading it knows the right env. +process.env.BABEL_ENV = 'test'; +process.env.NODE_ENV = 'test'; + +// Makes the script crash on unhandled rejections instead of silently +// ignoring them. In the future, promise rejections that are not handled will +// terminate the Node.js process with a non-zero exit code. +process.on( 'unhandledRejection', ( err ) => { + throw err; +} ); + +/** + * External dependencies + */ +const jest = require( 'jest' ); + +/** + * Internal dependencies + */ +const { + fromConfigRoot, + getCliArgs, + hasCliArg, + hasProjectFile, + hasJestConfig, +} = require( '../utils' ); + +// Provides a default config path for Puppeteer when jest-puppeteer.config.js +// wasn't found at the root of the project or a custom path wasn't defined +// using JEST_PUPPETEER_CONFIG environment variable. +if ( ! hasProjectFile( 'jest-puppeteer.config.js' ) && ! process.env.JEST_PUPPETEER_CONFIG ) { + process.env.JEST_PUPPETEER_CONFIG = fromConfigRoot( 'puppeteer.config.js' ); +} + +const config = ! hasJestConfig() ? + [ '--config', JSON.stringify( require( fromConfigRoot( 'jest-e2e.config.js' ) ) ) ] : + []; + +const hasRunInBand = hasCliArg( '--runInBand' ) || + hasCliArg( '-i' ); +const runInBand = ! hasRunInBand ? + [ '--runInBand' ] : + []; + +jest.run( [ ...config, ...runInBand, ...getCliArgs() ] ); diff --git a/packages/scripts/scripts/test-unit-jest.js b/packages/scripts/scripts/test-unit-jest.js index 2d511c07956351..1fb5743ac861c0 100644 --- a/packages/scripts/scripts/test-unit-jest.js +++ b/packages/scripts/scripts/test-unit-jest.js @@ -18,22 +18,13 @@ const jest = require( 'jest' ); * Internal dependencies */ const { + fromConfigRoot, getCliArgs, - hasCliArg, - hasProjectFile, - hasPackageProp, + hasJestConfig, } = require( '../utils' ); -const args = getCliArgs(); - -const hasJestConfig = hasCliArg( '-c' ) || - hasCliArg( '--config' ) || - hasProjectFile( 'jest.config.js' ) || - hasProjectFile( 'jest.config.json' ) || - hasPackageProp( 'jest' ); - -const config = ! hasJestConfig ? - [ '--config', JSON.stringify( require( '../config/jest.config' ) ) ] : +const config = ! hasJestConfig() ? + [ '--config', JSON.stringify( require( fromConfigRoot( 'jest-unit.config.js' ) ) ) ] : []; -jest.run( [ ...config, ...args ] ); +jest.run( [ ...config, ...getCliArgs() ] ); diff --git a/packages/scripts/utils/cli.js b/packages/scripts/utils/cli.js new file mode 100644 index 00000000000000..3778379ced1920 --- /dev/null +++ b/packages/scripts/utils/cli.js @@ -0,0 +1,85 @@ +/** + * External dependencies + */ +const spawn = require( 'cross-spawn' ); + +/** + * Internal dependencies + */ +const { + fromScriptsRoot, + hasScriptFile, +} = require( './file' ); +const { + exit, + getCliArgs, +} = require( './process' ); + +const getCliArg = ( arg ) => { + for ( const cliArg of getCliArgs() ) { + const [ name, value ] = cliArg.split( '=' ); + if ( name === arg ) { + return value || null; + } + } +}; + +const hasCliArg = ( arg ) => getCliArg( arg ) !== undefined; + +const handleSignal = ( signal ) => { + if ( signal === 'SIGKILL' ) { + // eslint-disable-next-line no-console + console.log( + 'The script failed because the process exited too early. ' + + 'This probably means the system ran out of memory or someone called ' + + '`kill -9` on the process.' + ); + } else if ( signal === 'SIGTERM' ) { + // eslint-disable-next-line no-console + console.log( + 'The script failed because the process exited too early. ' + + 'Someone might have called `kill` or `killall`, or the system could ' + + 'be shutting down.' + ); + } + exit( 1 ); +}; + +const spawnScript = ( scriptName, args = [] ) => { + if ( ! scriptName ) { + // eslint-disable-next-line no-console + console.log( 'Script name is missing.' ); + exit( 1 ); + } + + if ( ! hasScriptFile( scriptName ) ) { + // eslint-disable-next-line no-console + console.log( + 'Unknown script "' + scriptName + '". ' + + 'Perhaps you need to update @wordpress/scripts?' + ); + exit( 1 ); + } + + const { signal, status } = spawn.sync( + 'node', + [ + fromScriptsRoot( scriptName ), + ...args, + ], + { stdio: 'inherit' }, + ); + + if ( signal ) { + handleSignal( signal ); + } + + exit( status ); +}; + +module.exports = { + getCliArg, + getCliArgs, + hasCliArg, + spawnScript, +}; diff --git a/packages/scripts/utils/config.js b/packages/scripts/utils/config.js new file mode 100644 index 00000000000000..6be8ff91f0a4dd --- /dev/null +++ b/packages/scripts/utils/config.js @@ -0,0 +1,23 @@ +/** + * Internal dependencies + */ +const { hasCliArg } = require( './cli' ); +const { hasProjectFile } = require( './file' ); +const { hasPackageProp } = require( './package' ); + +const hasBabelConfig = () => + hasProjectFile( '.babelrc' ) || + hasProjectFile( 'babel.config.js' ) || + hasPackageProp( 'babel' ); + +const hasJestConfig = () => + hasCliArg( '-c' ) || + hasCliArg( '--config' ) || + hasProjectFile( 'jest.config.js' ) || + hasProjectFile( 'jest.config.json' ) || + hasPackageProp( 'jest' ); + +module.exports = { + hasBabelConfig, + hasJestConfig, +}; diff --git a/packages/scripts/utils/file.js b/packages/scripts/utils/file.js new file mode 100644 index 00000000000000..7f66c5e0ade789 --- /dev/null +++ b/packages/scripts/utils/file.js @@ -0,0 +1,32 @@ +/** + * External dependencies + */ +const { existsSync } = require( 'fs' ); +const path = require( 'path' ); + +/** + * Internal dependencies + */ +const { getPackagePath } = require( './package' ); + +const fromProjectRoot = ( fileName ) => + path.join( path.dirname( getPackagePath() ), fileName ); + +const hasProjectFile = ( fileName ) => + existsSync( fromProjectRoot( fileName ) ); + +const fromConfigRoot = ( fileName ) => + path.join( path.dirname( __dirname ), 'config', fileName ); + +const fromScriptsRoot = ( scriptName ) => + path.join( path.dirname( __dirname ), 'scripts', `${ scriptName }.js` ); + +const hasScriptFile = ( scriptName ) => + existsSync( fromScriptsRoot( scriptName ) ); + +module.exports = { + fromConfigRoot, + fromScriptsRoot, + hasProjectFile, + hasScriptFile, +}; diff --git a/packages/scripts/utils/index.js b/packages/scripts/utils/index.js index cda8dc66ddaf63..473952fcdfc174 100644 --- a/packages/scripts/utils/index.js +++ b/packages/scripts/utils/index.js @@ -1,99 +1,32 @@ -/** - * External dependencies - */ -const spawn = require( 'cross-spawn' ); -const { existsSync } = require( 'fs' ); -const path = require( 'path' ); - /** * Internal dependencies */ -const { getPackagePath, hasPackageProp } = require( './package' ); -const { exit, getCliArgs } = require( './process' ); - -const getCliArg = ( arg ) => { - for ( const cliArg of getCliArgs() ) { - const [ name, value ] = cliArg.split( '=' ); - if ( name === arg ) { - return value || null; - } - } -}; - -const hasCliArg = ( arg ) => getCliArg( arg ) !== undefined; - -const fromProjectRoot = ( fileName ) => - path.join( path.dirname( getPackagePath() ), fileName ); - -const hasProjectFile = ( fileName ) => - existsSync( fromProjectRoot( fileName ) ); - -const fromConfigRoot = ( fileName ) => - path.join( path.dirname( __dirname ), 'config', fileName ); - -const fromScriptsRoot = ( scriptName ) => - path.join( path.dirname( __dirname ), 'scripts', `${ scriptName }.js` ); - -const hasScriptFile = ( scriptName ) => - existsSync( fromScriptsRoot( scriptName ) ); - -const handleSignal = ( signal ) => { - if ( signal === 'SIGKILL' ) { - // eslint-disable-next-line no-console - console.log( - 'The script failed because the process exited too early. ' + - 'This probably means the system ran out of memory or someone called ' + - '`kill -9` on the process.' - ); - } else if ( signal === 'SIGTERM' ) { - // eslint-disable-next-line no-console - console.log( - 'The script failed because the process exited too early. ' + - 'Someone might have called `kill` or `killall`, or the system could ' + - 'be shutting down.' - ); - } - exit( 1 ); -}; - -const spawnScript = ( scriptName, args = [] ) => { - if ( ! scriptName ) { - // eslint-disable-next-line no-console - console.log( 'Script name is missing.' ); - exit( 1 ); - } - - if ( ! hasScriptFile( scriptName ) ) { - // eslint-disable-next-line no-console - console.log( - 'Unknown script "' + scriptName + '". ' + - 'Perhaps you need to update @wordpress/scripts?' - ); - exit( 1 ); - } - - const { signal, status } = spawn.sync( - 'node', - [ - fromScriptsRoot( scriptName ), - ...args, - ], - { stdio: 'inherit' }, - ); - - if ( signal ) { - handleSignal( signal ); - } - - exit( status ); -}; +const { + getCliArg, + getCliArgs, + hasCliArg, + spawnScript, +} = require( './cli' ); +const { + hasBabelConfig, + hasJestConfig, +} = require( './config' ); +const { + fromConfigRoot, + hasProjectFile, +} = require( './file' ); +const { + hasPackageProp, +} = require( './package' ); module.exports = { fromConfigRoot, + getCliArg, getCliArgs, + hasBabelConfig, hasCliArg, - getCliArg, - hasProjectFile, + hasJestConfig, hasPackageProp, + hasProjectFile, spawnScript, }; From 3a39da5b7de8045f7e7cef6f0b5311ee31b3817d Mon Sep 17 00:00:00 2001 From: Hiroshi Urabe <mail@torounit.com> Date: Wed, 19 Dec 2018 18:29:32 +0900 Subject: [PATCH 250/254] [Documentation] fix link to edit-post documentation (#12835) * fix link to edit-post documentation * Update README.md * Update README.md --- packages/plugins/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugins/README.md b/packages/plugins/README.md index 49f348e01706bf..4d4afd5896b190 100644 --- a/packages/plugins/README.md +++ b/packages/plugins/README.md @@ -29,7 +29,7 @@ This method takes two arguments: or an element (or function returning an element) if you choose to render your own SVG. - `render`: A component containing the UI elements to be rendered. -See [the edit-post module documentation](../edit-post/) for available components. +See [the edit-post module documentation](/packages/edit-post/) for available components. _Example:_ {% codetabs %} From 170f2a8b2b3375134b1c880cc3592f7e1895c610 Mon Sep 17 00:00:00 2001 From: Tugdual de Kerviler <dekervit@gmail.com> Date: Wed, 19 Dec 2018 11:30:11 +0100 Subject: [PATCH 251/254] Get the last mobile changes back in master (#12582) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Make a simple version of DefaultBlockAppender for mobile (#12434) * Make a simple version of DefaultBlockAppender for mobile * Use the same padding used for other blocks in DefaultBlockAppender * Update copy, auto focus and bind keypress * Do not bind key events * Change style of placeholder * Stop using classname-to-style autotransform in react native (#12552) * [RNMobile] Fix crash editing More blocks (#12620) * Use onChange instead of onChangeText in PlainText updates. * Convert the More block to Component, and re-use the same logic of the web when the field is empty. * Remove unsed variable, and format code * Move `);` to a new line * Fix SVG styles for mobile (#12608) * Fix SVG styles for mobile * Simplify condition since className is always a string * Check if Enter.key is the last inserted character, and add a new default block after the current More block. (#12639) Note: This is detected after the fact, and the newline could be visible on the block for a very small time. This is OK for the alpha, we will revisit the logic later. See https://github.com/wordpress-mobile/gutenberg-mobile/issues/324 * Call blur when deselecting RichText or PlainText component. (#12765) This is required to hide the keyboard when the focus moves to a non textual block. * Merge master into mobile (#12796) * RichText: fix onSetup doc (#12607) * Update broken links (#12660) * Update broken links There was 2 broken links. I changed same way with Data Module Reference page. * Update docs/designers-developers/developers/block-api/README.md Co-Authored-By: cagdasdag <cagdasdag81@gmail.com> * Update docs/designers-developers/developers/block-api/README.md Co-Authored-By: cagdasdag <cagdasdag81@gmail.com> * Docs: Update Glossary (#12479) * Update glossar with missing terms * Convert glossary to use dl/dt/dd dtags. Fixes #9976 * Fix TinyMCE link * remove spacing around tags * Add template definition, with link * Updates per review * Update docs/designers-developers/glossary.md Co-Authored-By: mkaz <marcus@mkaz.com> * Add documentation for `safeDecodeURI()` and `filterURLForDisplay()` (#12570) * Add documentation for `safeDecodeURI()` and `filterURLForDisplay()` * Whitespace * Consistant Capit… i mean capitalization * Oxford comma Co-Authored-By: georgeh <george@hotelling.net> * Update theme-support.md (#12661) * Fix: Undoing Image Selection in Image Block results in Broken Image (#12567) * Optimize isViewportMatch (#12542) * Cache createBlock call in isUnmodifiedDefaultBlock (#12521) * Cache createBlock call in isUnmodifiedDefaultBlock * Invalidate cache when default block name changes * Merge ifs * Font Size Picker: Use a menuitemradio role and better labels. (#12372) * Use a menuitemradio role and better labels. * Restore Button and remove MenuItem import. * Use template literals. * Set document title for preview interstitial (#12466) * Fix e2e tests after the WordPress 5.0 upgrade (#12715) * Fix e2e tests after the WordPress 5.0 upgrade * Remove the php unit tests testing the WP5.0 core instead of the plugin * Meta Boxes: Don't hide disabled meta boxes by modifying DOM (#12628) Hiding disabled meta boxes by setting `element.style.display = 'none'` interferes with plugins like ACF which rely on being able to show and hide meta boxes using `$.hide()` and `$.show()`. Hiding the meta box using a new `.edit-post-meta-boxes-area .is-hidden` class ensures that we don't interfere with third party code. * Add a word-wrap style to the facebook embed preview screen (#11890) * Add a word-break style to the facebook embed preview screen to prevent the long embed url from breaking the block boundary * Fix typo and missing space in scss comment * Adding @aldavigdis to the contributors list (#12686) * Make media & text block placeholder translatable (#12706) * Fix: Problems on Media & Text block resizing; Load wp-block-library styles before wp-edit-blocks. (#12619) * Get wordcount type from translation (#12586) * get wordcount type from translation * Add description to explain the options for wordcount type * Added mylsef into contributors.md. :) * Only render InserterWithShortcuts on hover (#12510) * Fix issue where default appender has icons overlaying the text (#12536) * Fix issue where default appender has icons overlaying the text This fixes #11425. It adds padding to the right of the default block appender to fit 3 icons. * chore: Tweak spelling * Classic Block: set correct focus back after blur (#12415) * Classic Block: set correct focus back after blur * Add e2e test * reset bookmark on mousedown and touchstart * e2e: Look for aria-label="Add Media" rather than "Insert Media" * RichText: only replace range and nodes if different (#12547) * RichText: only set range if different * Check rangeCount * Also compare nodes * Add e2e test * Simplify * RichText: Document isRangeEqual * Testing: RichText: Assure subscriber removal * Unsubscribe in page.evaluate * Mark temporary eslint-config package as private (#12734) * When a post is saved, check for tinymce and save any editors. (#12568) * When a post is saved, check for tinymce and save any editors. * Importing tinymce and using tinyMCE vs the object stored in window.tinymce. * Updated version number and changelog. * no longer importing tinymce since we use the tinyMCE global. tinyMCE.triggerSave works now. checking if tinyMCE exists before making the call just in case. * Using typeof to check for tinyMCE and fixed issues brought up in travis run. * using window.tinyMCE again to avoid warning RE undefined var * Restore the package.json version. * Add e2e tests for the custom wp_editor metaboxes * Rename functions, removing gutenberg_ prefix (#12326) * Rename functions, removing gutenberg_ and prefixing with wp_ * Remove wp_ prefix to match core * Remove function check per review * Annotations: Apply annotation className as string (#12741) * RichText: Ensure instance is selected before setting back selection (#12737) * Fix for #11663 (#12728) * Fixed Deleting an HTML Anchor attribute leaves an empty HTML id attribute * Fixed Deleting an HTML Anchor attribute leaves an empty * Update plugin version to 4.7.0-rc.1 (#12752) * Add an error state to the image block to allow upload errors to display (#10224) * Try: JS Console warning for when in Quirks Mode (#12575) * Try: JS Console warning for when in Quirks Mode This PR detects whether the browser is in Quirks Mode. Quirks Mode is a rendering method used when the doctype definition is missing or incorrectly placed in the HTML source, causing the browser to have difficulty detecting the type of document it is to render. This is usually caused by a PHP error, or even just a style tag that is output incorrectly on the page. See discussion in https://github.com/WordPress/gutenberg/pull/12455 and https://github.com/WordPress/gutenberg/issues/11378. The usual result is Gutenberg rendering incorrectly, notably with metaboxes overlapping content. The purpose of this PR is to help developers debug the issue and fix it at the root. As such, it adds a console warning, props @nickcernis for the text: ``` [Warning] Your browser is using Quirks Mode. This can cause rendering issues such as blocks overlaying meta boxes in the editor. Quirks Mode can be triggered by PHP errors or HTML code appearing before the opening <!DOCTYPE html>. Try checking the raw page source or your site's PHP error log and resolving errors there, removing any HTML before the doctype, or disabling plugins. ``` It also augments the documentation to add a note about this. * Move warning to index.js * Remove try/catch. * Tweak: Remove redundant [warning] in warn call * Organizing screenshot assets for the block tutorial inside the designers-developers directory in the repo (#12745) * Rename backwards compatiblity to backward compatibility (#12751) * Rename backwards compatiblity to backward compatibility * Remove package-lock from commit * Update CONTRIBUTING.md Co-Authored-By: mkaz <marcus@mkaz.com> * Update CONTRIBUTING.md Co-Authored-By: mkaz <marcus@mkaz.com> * Whitespace in manifest * Update node-sass to 4.11.0 to support Node.js 11 (#12541) ## Description Fixes #12539 by updating node-sass to support Node.js 11. ## How has this been tested? Running `npm install` on macOS 10.14 with Node.js 11.2 without problems. ## Types of changes Minor dependency bump to support Node.js 11. ## Checklist: - [x] My code is tested. - [x] My code follows the WordPress code style. <!-- Check code: `npm run lint`, Guidelines: https://make.wordpress.org/core/handbook/best-practices/coding-standards/javascript/ --> - [x] My code follows the accessibility standards. <!-- Guidelines: https://make.wordpress.org/core/handbook/best-practices/coding-standards/accessibility-coding-standards/ --> - [x] My code has proper inline documentation. <!-- Guidelines: https://make.wordpress.org/core/handbook/best-practices/inline-documentation-standards/javascript/ --> * Update native more block styling (#12767) * Update styling of more block * Fix CI test * Update spaces * Convert indentation to tabs * Fix lint issues * Remove key attribute * Set fixed width to the text input * Make sure to properly compare empty text and undefined text variable before resetting the eventCount field. (#12815) * Use the default "Read More" text on init only, giving the ability to user to empty the field and re-start with empty text (#12821) * Merge 'origin/master' into 'origin/mobile' (#12836) * RichText: fix onSetup doc (#12607) * Update broken links (#12660) * Update broken links There was 2 broken links. I changed same way with Data Module Reference page. * Update docs/designers-developers/developers/block-api/README.md Co-Authored-By: cagdasdag <cagdasdag81@gmail.com> * Update docs/designers-developers/developers/block-api/README.md Co-Authored-By: cagdasdag <cagdasdag81@gmail.com> * Docs: Update Glossary (#12479) * Update glossar with missing terms * Convert glossary to use dl/dt/dd dtags. Fixes #9976 * Fix TinyMCE link * remove spacing around tags * Add template definition, with link * Updates per review * Update docs/designers-developers/glossary.md Co-Authored-By: mkaz <marcus@mkaz.com> * Add documentation for `safeDecodeURI()` and `filterURLForDisplay()` (#12570) * Add documentation for `safeDecodeURI()` and `filterURLForDisplay()` * Whitespace * Consistant Capit… i mean capitalization * Oxford comma Co-Authored-By: georgeh <george@hotelling.net> * Update theme-support.md (#12661) * Fix: Undoing Image Selection in Image Block results in Broken Image (#12567) * Optimize isViewportMatch (#12542) * Cache createBlock call in isUnmodifiedDefaultBlock (#12521) * Cache createBlock call in isUnmodifiedDefaultBlock * Invalidate cache when default block name changes * Merge ifs * Font Size Picker: Use a menuitemradio role and better labels. (#12372) * Use a menuitemradio role and better labels. * Restore Button and remove MenuItem import. * Use template literals. * Set document title for preview interstitial (#12466) * Fix e2e tests after the WordPress 5.0 upgrade (#12715) * Fix e2e tests after the WordPress 5.0 upgrade * Remove the php unit tests testing the WP5.0 core instead of the plugin * Meta Boxes: Don't hide disabled meta boxes by modifying DOM (#12628) Hiding disabled meta boxes by setting `element.style.display = 'none'` interferes with plugins like ACF which rely on being able to show and hide meta boxes using `$.hide()` and `$.show()`. Hiding the meta box using a new `.edit-post-meta-boxes-area .is-hidden` class ensures that we don't interfere with third party code. * Add a word-wrap style to the facebook embed preview screen (#11890) * Add a word-break style to the facebook embed preview screen to prevent the long embed url from breaking the block boundary * Fix typo and missing space in scss comment * Adding @aldavigdis to the contributors list (#12686) * Make media & text block placeholder translatable (#12706) * Fix: Problems on Media & Text block resizing; Load wp-block-library styles before wp-edit-blocks. (#12619) * Get wordcount type from translation (#12586) * get wordcount type from translation * Add description to explain the options for wordcount type * Added mylsef into contributors.md. :) * Only render InserterWithShortcuts on hover (#12510) * Fix issue where default appender has icons overlaying the text (#12536) * Fix issue where default appender has icons overlaying the text This fixes #11425. It adds padding to the right of the default block appender to fit 3 icons. * chore: Tweak spelling * Classic Block: set correct focus back after blur (#12415) * Classic Block: set correct focus back after blur * Add e2e test * reset bookmark on mousedown and touchstart * e2e: Look for aria-label="Add Media" rather than "Insert Media" * RichText: only replace range and nodes if different (#12547) * RichText: only set range if different * Check rangeCount * Also compare nodes * Add e2e test * Simplify * RichText: Document isRangeEqual * Testing: RichText: Assure subscriber removal * Unsubscribe in page.evaluate * Mark temporary eslint-config package as private (#12734) * When a post is saved, check for tinymce and save any editors. (#12568) * When a post is saved, check for tinymce and save any editors. * Importing tinymce and using tinyMCE vs the object stored in window.tinymce. * Updated version number and changelog. * no longer importing tinymce since we use the tinyMCE global. tinyMCE.triggerSave works now. checking if tinyMCE exists before making the call just in case. * Using typeof to check for tinyMCE and fixed issues brought up in travis run. * using window.tinyMCE again to avoid warning RE undefined var * Restore the package.json version. * Add e2e tests for the custom wp_editor metaboxes * Rename functions, removing gutenberg_ prefix (#12326) * Rename functions, removing gutenberg_ and prefixing with wp_ * Remove wp_ prefix to match core * Remove function check per review * Annotations: Apply annotation className as string (#12741) * RichText: Ensure instance is selected before setting back selection (#12737) * Fix for #11663 (#12728) * Fixed Deleting an HTML Anchor attribute leaves an empty HTML id attribute * Fixed Deleting an HTML Anchor attribute leaves an empty * Update plugin version to 4.7.0-rc.1 (#12752) * Add an error state to the image block to allow upload errors to display (#10224) * Try: JS Console warning for when in Quirks Mode (#12575) * Try: JS Console warning for when in Quirks Mode This PR detects whether the browser is in Quirks Mode. Quirks Mode is a rendering method used when the doctype definition is missing or incorrectly placed in the HTML source, causing the browser to have difficulty detecting the type of document it is to render. This is usually caused by a PHP error, or even just a style tag that is output incorrectly on the page. See discussion in https://github.com/WordPress/gutenberg/pull/12455 and https://github.com/WordPress/gutenberg/issues/11378. The usual result is Gutenberg rendering incorrectly, notably with metaboxes overlapping content. The purpose of this PR is to help developers debug the issue and fix it at the root. As such, it adds a console warning, props @nickcernis for the text: ``` [Warning] Your browser is using Quirks Mode. This can cause rendering issues such as blocks overlaying meta boxes in the editor. Quirks Mode can be triggered by PHP errors or HTML code appearing before the opening <!DOCTYPE html>. Try checking the raw page source or your site's PHP error log and resolving errors there, removing any HTML before the doctype, or disabling plugins. ``` It also augments the documentation to add a note about this. * Move warning to index.js * Remove try/catch. * Tweak: Remove redundant [warning] in warn call * Organizing screenshot assets for the block tutorial inside the designers-developers directory in the repo (#12745) * Rename backwards compatiblity to backward compatibility (#12751) * Rename backwards compatiblity to backward compatibility * Remove package-lock from commit * Update CONTRIBUTING.md Co-Authored-By: mkaz <marcus@mkaz.com> * Update CONTRIBUTING.md Co-Authored-By: mkaz <marcus@mkaz.com> * Whitespace in manifest * Update node-sass to 4.11.0 to support Node.js 11 (#12541) ## Description Fixes #12539 by updating node-sass to support Node.js 11. ## How has this been tested? Running `npm install` on macOS 10.14 with Node.js 11.2 without problems. ## Types of changes Minor dependency bump to support Node.js 11. ## Checklist: - [x] My code is tested. - [x] My code follows the WordPress code style. <!-- Check code: `npm run lint`, Guidelines: https://make.wordpress.org/core/handbook/best-practices/coding-standards/javascript/ --> - [x] My code follows the accessibility standards. <!-- Guidelines: https://make.wordpress.org/core/handbook/best-practices/coding-standards/accessibility-coding-standards/ --> - [x] My code has proper inline documentation. <!-- Guidelines: https://make.wordpress.org/core/handbook/best-practices/inline-documentation-standards/javascript/ --> * Add attributes to ServerSideRender readme (#12793) * Add attributes to ServerSideRender readme Adds a code example demonstrating how to define attributes when registering a block that will use attributes in a ServerSideRender component. * Add whitespace and inline code markup to ServerSideRender readme Implements requested changes from code review. * Scripts: Add check-engines script to the package (#12721) * Scripts: Add check-engines script to the package * Update packages/scripts/CHANGELOG.md Co-Authored-By: gziolo <grzegorz@gziolo.pl> * Update packages/scripts/README.md Co-Authored-By: gziolo <grzegorz@gziolo.pl> * Update minimal node version to 10.x Co-Authored-By: gziolo <grzegorz@gziolo.pl> * Move devDependencies to root package.json file (#12720) * Chore: Remove unused npm dependencies from the root package.json file * Move devDependencies to root package.json file * Fix php notice from the recent comments block (#12812) * RichText: Fix React warning shown when unmounting a currently selected RichText. (#12817) * Packages: Reimplement ESLint config as plugin (#12763) * Packages: Move eslint-config to eslint-plugin (Fails pre-commit, but in effort to ensure history preservation) * eslint-plugin: Add npmrc to avoid package-lock.json * Framework: Update path references for eslint-config to -plugin * eslint-plugin: Reimplement ESLint config as plugin * eslint-plugin: Unmark as private * eslint-plugin: Undocument custom ruleset * 4.7 (#12819) * Bump plugin version to 4.7.0 * chore(release): publish - @wordpress/annotations@1.0.4 - @wordpress/api-fetch@2.2.6 - @wordpress/block-library@2.2.10 - @wordpress/block-serialization-default-parser@2.0.2 - @wordpress/block-serialization-spec-parser@2.0.2 - @wordpress/blocks@6.0.4 - @wordpress/components@7.0.4 - @wordpress/core-data@2.0.15 - @wordpress/data@4.1.0 - @wordpress/date@3.0.1 - @wordpress/edit-post@3.1.5 - @wordpress/editor@9.0.5 - @wordpress/eslint-plugin@1.0.0 - @wordpress/format-library@1.2.8 - @wordpress/html-entities@2.0.4 - @wordpress/list-reusable-blocks@1.1.17 - @wordpress/notices@1.1.1 - @wordpress/nux@3.0.5 - @wordpress/rich-text@3.0.3 - @wordpress/url@2.3.2 - @wordpress/viewport@2.0.13 * Update changelogs after 4.7 package releases * Add back package-lock.json * Remove the call that does `blur` the undelying native component when deselecting RichText or PlainText. (#12886) * [rnmobile]: Send blockType prop to RNAztecView (#12869) * Revert "Remove the call that does `blur` the undelying native component (#12946) * Revert "Remove the call that does `blur` the undelying native component when deselecting RichText or PlainText. (#12886)" This reverts commit d9fe45ed6251486f213daf3fc9a7568e2b4977c5. * Blur only if it is iOS * Fix imports * Fix lint errors in the mobile branch (#12990) * Revert changes to lib/load.php * Fix lint errors in More * Fix lint error for PlainText * Fix lint error in RichText --- .../block-library/src/more/edit.native.js | 75 +++++++++++++++---- .../block-library/src/more/editor.native.scss | 25 ++++--- .../block-library/src/nextpage/edit.native.js | 2 +- .../src/primitives/svg/index.native.js | 12 +-- .../default-block-appender/index.native.js | 75 +++++++++++++++++++ .../default-block-appender/style.native.scss | 14 ++++ .../editor/src/components/index.native.js | 1 + .../src/components/plain-text/index.native.js | 19 ++++- .../src/components/rich-text/index.native.js | 10 ++- 9 files changed, 189 insertions(+), 44 deletions(-) create mode 100644 packages/editor/src/components/default-block-appender/index.native.js create mode 100644 packages/editor/src/components/default-block-appender/style.native.scss diff --git a/packages/block-library/src/more/edit.native.js b/packages/block-library/src/more/edit.native.js index 5b85da44359d50..6a73a44c73b6a8 100644 --- a/packages/block-library/src/more/edit.native.js +++ b/packages/block-library/src/more/edit.native.js @@ -1,12 +1,17 @@ /** * External dependencies */ -import { View, Text } from 'react-native'; +import { View } from 'react-native'; /** * WordPress dependencies */ import { __ } from '@wordpress/i18n'; +import { Component } from '@wordpress/element'; +import { + getDefaultBlockName, + createBlock, +} from '@wordpress/blocks'; /** * Internal dependencies @@ -14,28 +19,68 @@ import { __ } from '@wordpress/i18n'; import { PlainText } from '@wordpress/editor'; import styles from './editor.scss'; -export default function MoreEdit( props ) { - const { attributes, setAttributes, onFocus, onBlur } = props; - const { customText } = attributes; - const defaultText = __( 'Read more' ); - const value = customText !== undefined ? customText : defaultText; +export default class MoreEdit extends Component { + constructor() { + super( ...arguments ); + this.onChangeInput = this.onChangeInput.bind( this ); - return ( - <View className={ styles[ 'block-library-more__container' ] }> - <View className={ styles[ 'block-library-more__sub-container' ] }> - <Text className={ styles[ 'block-library-more__left-marker' ] }>&lt;!--</Text> + this.state = { + defaultText: __( 'Read more' ), + }; + } + + onChangeInput( newValue ) { + // Detect Enter.key and add new empty block after. + // Note: This is detected after the fact, and the newline could be visible on the block + // for a very small time. This is OK for the alpha, we will revisit the logic later. + // See https://github.com/wordpress-mobile/gutenberg-mobile/issues/324 + if ( newValue.indexOf( '\n' ) !== -1 ) { + const { insertBlocksAfter } = this.props; + insertBlocksAfter( [ createBlock( getDefaultBlockName() ) ] ); + return; + } + // Set defaultText to an empty string, allowing the user to clear/replace the input field's text + this.setState( { + defaultText: '', + } ); + const value = newValue.length === 0 ? undefined : newValue; + this.props.setAttributes( { customText: value } ); + } + + renderLine() { + return <View style={ styles[ 'block-library-more__line' ] } />; + } + + renderText() { + const { attributes, onFocus, onBlur } = this.props; + const { customText } = attributes; + const { defaultText } = this.state; + const value = customText !== undefined ? customText : defaultText; + + return ( + <View> <PlainText - className={ styles[ 'block-library-more__plain-text' ] } + style={ styles[ 'block-library-more__text' ] } value={ value } multiline={ true } underlineColorAndroid="transparent" - onChange={ ( newValue ) => setAttributes( { customText: newValue } ) } + onChange={ this.onChangeInput } placeholder={ defaultText } - isSelected={ props.isSelected } + isSelected={ this.props.isSelected } onFocus={ onFocus } onBlur={ onBlur } /> - <Text className={ styles[ 'block-library-more__right-marker' ] }>--&gt;</Text> </View> - </View> ); + ); + } + + render() { + return ( + <View style={ styles[ 'block-library-more__container' ] }> + { this.renderLine() } + { this.renderText() } + { this.renderLine() } + </View> + ); + } } diff --git a/packages/block-library/src/more/editor.native.scss b/packages/block-library/src/more/editor.native.scss index 46d36fe08e321f..beb5ef423776ff 100644 --- a/packages/block-library/src/more/editor.native.scss +++ b/packages/block-library/src/more/editor.native.scss @@ -2,21 +2,22 @@ .block-library-more__container { align-items: center; - padding-left: 4; - padding-right: 4; - padding-top: 4; - padding-bottom: 4; -} - -.block-library-more__sub-container { - align-items: center; + padding: 4px; flex-direction: row; } -.block-library-more__left-marker { - padding-right: 4; +.block-library-more__line { + background-color: #555d66; + height: 2; + flex: 1; } -.block-library-more__right-marker { - padding-left: 4; +.block-library-more__text { + text-decoration-style: solid; + flex: 0; + width: 200; + text-align: center; + margin-left: 15; + margin-right: 15; + margin-bottom: 5; } diff --git a/packages/block-library/src/nextpage/edit.native.js b/packages/block-library/src/nextpage/edit.native.js index 03cd3bfe3c7d18..413fda53fe2172 100644 --- a/packages/block-library/src/nextpage/edit.native.js +++ b/packages/block-library/src/nextpage/edit.native.js @@ -18,7 +18,7 @@ export default function NextPageEdit( { attributes } ) { const { customText = __( 'Page break' ) } = attributes; return ( - <View className={ styles[ 'block-library-nextpage__container' ] }> + <View style={ styles[ 'block-library-nextpage__container' ] }> <Hr text={ customText } textStyle={ styles[ 'block-library-nextpage__text' ] } lineStyle={ styles[ 'block-library-nextpage__line' ] } /> diff --git a/packages/components/src/primitives/svg/index.native.js b/packages/components/src/primitives/svg/index.native.js index 47c49b1bb61289..b0272e6b5a7b9f 100644 --- a/packages/components/src/primitives/svg/index.native.js +++ b/packages/components/src/primitives/svg/index.native.js @@ -17,16 +17,8 @@ export { } from 'react-native-svg'; export const SVG = ( props ) => { - // We're using the react-native-classname-to-style plugin, so when a `className` prop is passed it gets converted to `style` here. - // Given it carries a string (as it was originally className) but an object is expected for `style`, - // we need to check whether `style` exists and is a string, and convert it to an object - - let styleValues = {}; - if ( typeof props.style === 'string' ) { - const oneStyle = props.style.split( ' ' ).map( ( element ) => styles[ element ] ).filter( Boolean ); - styleValues = Object.assign( styleValues, ...oneStyle ); - } - + const stylesFromClasses = ( props.className || '' ).split( ' ' ).map( ( element ) => styles[ element ] ).filter( Boolean ); + const styleValues = Object.assign( {}, props.style, ...stylesFromClasses ); const safeProps = { ...props, style: styleValues }; return ( diff --git a/packages/editor/src/components/default-block-appender/index.native.js b/packages/editor/src/components/default-block-appender/index.native.js new file mode 100644 index 00000000000000..436ecc5772b000 --- /dev/null +++ b/packages/editor/src/components/default-block-appender/index.native.js @@ -0,0 +1,75 @@ +/** + * External dependencies + */ +import { TextInput, TouchableWithoutFeedback, View } from 'react-native'; + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { compose } from '@wordpress/compose'; +import { decodeEntities } from '@wordpress/html-entities'; +import { withSelect, withDispatch } from '@wordpress/data'; + +import styles from './style.scss'; + +export function DefaultBlockAppender( { + isLocked, + isVisible, + onAppend, + placeholder, +} ) { + if ( isLocked || ! isVisible ) { + return null; + } + + const value = decodeEntities( placeholder ) || __( 'Start writing or press \u2295 to add content' ); + + return ( + <TouchableWithoutFeedback + onPress={ onAppend } + > + <View style={ styles.blockHolder } pointerEvents="box-only"> + <View style={ styles.blockContainer }> + <TextInput + style={ styles.textView } + textAlignVertical="top" + multiline + numberOfLines={ 0 } + value={ value } + /> + </View> + </View> + </TouchableWithoutFeedback> + ); +} + +export default compose( + withSelect( ( select, ownProps ) => { + const { getBlockCount, getEditorSettings, getTemplateLock } = select( 'core/editor' ); + + const isEmpty = ! getBlockCount( ownProps.rootClientId ); + const { bodyPlaceholder } = getEditorSettings(); + + return { + isVisible: isEmpty, + isLocked: !! getTemplateLock( ownProps.rootClientId ), + placeholder: bodyPlaceholder, + }; + } ), + withDispatch( ( dispatch, ownProps ) => { + const { + insertDefaultBlock, + startTyping, + } = dispatch( 'core/editor' ); + + return { + onAppend() { + const { rootClientId } = ownProps; + + insertDefaultBlock( undefined, rootClientId ); + startTyping(); + }, + }; + } ), +)( DefaultBlockAppender ); diff --git a/packages/editor/src/components/default-block-appender/style.native.scss b/packages/editor/src/components/default-block-appender/style.native.scss new file mode 100644 index 00000000000000..cc08f6c820ca92 --- /dev/null +++ b/packages/editor/src/components/default-block-appender/style.native.scss @@ -0,0 +1,14 @@ + +.blockHolder { + flex: 1 1 auto; +} + +.blockContainer { + background-color: $white; + padding: 8px; +} + +.textView { + color: #87a6bc; + font-size: 16px; +} diff --git a/packages/editor/src/components/index.native.js b/packages/editor/src/components/index.native.js index dc651f9e7e0a6b..229efa5879cc37 100644 --- a/packages/editor/src/components/index.native.js +++ b/packages/editor/src/components/index.native.js @@ -6,5 +6,6 @@ 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'; +export { default as DefaultBlockAppender } from './default-block-appender'; export { default as EditorHistoryRedo } from './editor-history/redo'; export { default as EditorHistoryUndo } from './editor-history/undo'; diff --git a/packages/editor/src/components/plain-text/index.native.js b/packages/editor/src/components/plain-text/index.native.js index e94f1939040f54..35ffd2782a1333 100644 --- a/packages/editor/src/components/plain-text/index.native.js +++ b/packages/editor/src/components/plain-text/index.native.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { TextInput } from 'react-native'; +import { TextInput, Platform } from 'react-native'; /** * WordPress dependencies @@ -14,6 +14,11 @@ import { Component } from '@wordpress/element'; import styles from './style.scss'; export default class PlainText extends Component { + constructor() { + super( ...arguments ); + this.isIOS = Platform.OS === 'ios'; + } + componentDidMount() { // if isSelected is true, we should request the focus on this TextInput if ( ( this._input.isFocused() === false ) && ( this._input.props.isSelected === true ) ) { @@ -21,6 +26,12 @@ export default class PlainText extends Component { } } + componentDidUpdate( prevProps ) { + if ( ! this.props.isSelected && prevProps.isSelected && this.isIOS ) { + this._input.blur(); + } + } + focus() { this._input.focus(); } @@ -28,12 +39,14 @@ export default class PlainText extends Component { render() { return ( <TextInput + { ...this.props } ref={ ( x ) => this._input = x } className={ [ styles[ 'editor-plain-text' ], this.props.className ] } - onChangeText={ ( text ) => this.props.onChange( text ) } + onChange={ ( event ) => { + this.props.onChange( event.nativeEvent.text ); + } } onFocus={ this.props.onFocus } // always assign onFocus as a props onBlur={ this.props.onBlur } // always assign onBlur as a props - { ...this.props } /> ); } diff --git a/packages/editor/src/components/rich-text/index.native.js b/packages/editor/src/components/rich-text/index.native.js index 362dc4b359d1a3..f6796a97fdbaa8 100644 --- a/packages/editor/src/components/rich-text/index.native.js +++ b/packages/editor/src/components/rich-text/index.native.js @@ -2,7 +2,7 @@ * External dependencies */ import RCTAztecView from 'react-native-aztec'; -import { View } from 'react-native'; +import { View, Platform } from 'react-native'; import { forEach, merge, @@ -66,6 +66,7 @@ export function getFormatValue( formatName ) { export class RichText extends Component { constructor() { super( ...arguments ); + this.isIOS = Platform.OS === 'ios'; this.onChange = this.onChange.bind( this ); this.onEnter = this.onEnter.bind( this ); this.onBackspace = this.onBackspace.bind( this ); @@ -274,8 +275,8 @@ export class RichText extends Component { // If the component is changed React side (undo/redo/merging/splitting/custom text actions) // we need to make sure the native is updated as well - if ( nextProps.value && - this.lastContent && + if ( ( typeof nextProps.value !== 'undefined' ) && + ( typeof this.lastContent !== 'undefined' ) && nextProps.value !== this.lastContent ) { this.lastEventCount = undefined; // force a refresh on the native side } @@ -292,6 +293,8 @@ export class RichText extends Component { componentDidUpdate( prevProps ) { if ( this.props.isSelected && ! prevProps.isSelected ) { this._editor.focus(); + } else if ( ! this.props.isSelected && prevProps.isSelected && this.isIOS ) { + this._editor.blur(); } } @@ -370,6 +373,7 @@ export class RichText extends Component { onContentSizeChange={ this.onContentSizeChange } onActiveFormatsChange={ this.onActiveFormatsChange } isSelected={ this.props.isSelected } + blockType={ { tag: tagName } } color={ 'black' } maxImagesWidth={ 200 } style={ style } From fa711e8c5acd3646b346977b9b4626cb8ece5576 Mon Sep 17 00:00:00 2001 From: torres126 <43215253+torres126@users.noreply.github.com> Date: Wed, 19 Dec 2018 11:21:32 +0000 Subject: [PATCH 252/254] Fix typo in docs/designers-developers/key-concepts.md (#13005) --- docs/designers-developers/key-concepts.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/designers-developers/key-concepts.md b/docs/designers-developers/key-concepts.md index 6ea644009b145d..ff9007185fe0e7 100644 --- a/docs/designers-developers/key-concepts.md +++ b/docs/designers-developers/key-concepts.md @@ -117,7 +117,7 @@ We chose instead to try and find a way to keep the formality and explicitness an Of these options a novel approach was suggested that by storing data in HTML comments we would know that we wouldn't break the rest of the HTML in the document, that browsers should ignore it, and that we could simplify our approach to parsing the document. -Unique to comments is that they cannot legitimately exist in ambiguous places, such as inside of HTML attributes like `<img alt='data-id="14"'>`. Comments are also quite permissive. Whereas HTML attributes are complicated to parse properly, comments are quite easily described by a leading `<!--` followed by anything except `--` until the first `-->`. This simplicity and permisiveness means that the parser can be implemented in several ways without needing to understand HTML properly and we have the liberty to use more convenient syntax inside of the comment—we only need to escape double-hyphen sequences. We take advantage of this in how we store block attributes: JSON literals inside the comment. +Unique to comments is that they cannot legitimately exist in ambiguous places, such as inside of HTML attributes like `<img alt='data-id="14"'>`. Comments are also quite permissive. Whereas HTML attributes are complicated to parse properly, comments are quite easily described by a leading `<!--` followed by anything except `--` until the first `-->`. This simplicity and permissiveness means that the parser can be implemented in several ways without needing to understand HTML properly and we have the liberty to use more convenient syntax inside of the comment—we only need to escape double-hyphen sequences. We take advantage of this in how we store block attributes: JSON literals inside the comment. After running this through the parser we're left with a simple object we can manipulate idiomatically and we don't have to worry about escaping or unescaping the data. It's handled for us through the serialization process. Because the comments are so different from other HTML tags and because we can perform a first-pass to extract the top-level blocks, we don't actually depend on having fully valid HTML! From 47950c3c3214a80401ee895413312a8691716e45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20=28Greg=29=20Zi=C3=B3=C5=82kowski?= <grzegorz@gziolo.pl> Date: Wed, 19 Dec 2018 12:40:52 +0100 Subject: [PATCH 253/254] Scripts: Add lint-style script based on stylelint (#12722) * Scripts: Add lint-css script based on stylelint * Update packages/scripts/README.md Co-Authored-By: gziolo <grzegorz@gziolo.pl> * Update default config for lint-style script * Scripts: Extend description for the package --- package-lock.json | 4 ++- package.json | 9 +++--- packages/scripts/CHANGELOG.md | 5 +-- packages/scripts/README.md | 30 +++++++++++++---- packages/scripts/config/.stylelintrc.json | 3 ++ packages/scripts/package.json | 6 ++-- packages/scripts/scripts/lint-style.js | 39 +++++++++++++++++++++++ 7 files changed, 80 insertions(+), 16 deletions(-) create mode 100644 packages/scripts/config/.stylelintrc.json create mode 100644 packages/scripts/scripts/lint-style.js diff --git a/package-lock.json b/package-lock.json index 4a190ed1cb0d28..eb6406cdd4b658 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2775,7 +2775,9 @@ "npm-package-json-lint": "^3.3.1", "puppeteer": "1.6.1", "read-pkg-up": "^1.0.1", - "resolve-bin": "^0.4.0" + "resolve-bin": "^0.4.0", + "stylelint": "^9.5.0", + "stylelint-config-wordpress": "^13.1.0" } }, "@wordpress/shortcode": { diff --git a/package.json b/package.json index a214f2987eca35..15e713f80c20d3 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,6 @@ "shallowequal": "1.1.0", "sprintf-js": "1.1.1", "source-map-loader": "0.2.3", - "stylelint": "9.5.0", "stylelint-config-wordpress": "13.1.0", "uuid": "3.3.2", "webpack": "4.8.3", @@ -164,11 +163,11 @@ "fixtures:regenerate": "npm run fixtures:clean && npm run fixtures:generate", "lint": "concurrently \"npm run lint-js\" \"npm run lint-pkg-json\" \"npm run lint-css\"", "lint-js": "wp-scripts lint-js .", - "lint-js:fix": "wp-scripts lint-js . --fix", + "lint-js:fix": "npm run lint-js -- --fix", "lint-php": "docker-compose run --rm composer run-script lint", "lint-pkg-json": "wp-scripts lint-pkg-json ./packages", - "lint-css": "stylelint '**/*.scss'", - "lint-css:fix": "stylelint '**/*.scss' --fix", + "lint-css": "wp-scripts lint-style '**/*.scss'", + "lint-css:fix": "npm run lint-css -- --fix", "package-plugin": "./bin/build-plugin-zip.sh", "postinstall": "npm run check-licenses && npm run build:packages", "pot-to-php": "./bin/pot-to-php.js", @@ -193,7 +192,7 @@ "wp-scripts lint-pkg-json" ], "*.scss": [ - "stylelint" + "wp-scripts lint-style" ], "*.js": [ "wp-scripts lint-js" diff --git a/packages/scripts/CHANGELOG.md b/packages/scripts/CHANGELOG.md index 119e734319d31c..3ff87bef238a4d 100644 --- a/packages/scripts/CHANGELOG.md +++ b/packages/scripts/CHANGELOG.md @@ -3,8 +3,9 @@ ### New Features - Added support for `check-engines` script ([#12721](https://github.com/WordPress/gutenberg/pull/12721)) -- Added support for `test-e2e` script ([#12437](https://github.com/WordPress/gutenberg/pull/12437)). -- Update default config provided for `lint-js` script ([#12845](https://github.com/WordPress/gutenberg/pull/12845)). +- Added support for `lint-style` script ([#12722](https://github.com/WordPress/gutenberg/pull/12722)) +- Added support for `test-e2e` script ([#12437](https://github.com/WordPress/gutenberg/pull/12437)) +- Update default config provided for `lint-js` script ([#12845](https://github.com/WordPress/gutenberg/pull/12845)) ## 2.4.4 (2018-11-20) diff --git a/packages/scripts/README.md b/packages/scripts/README.md index 2d578ef1b870bf..15a7405340dbea 100644 --- a/packages/scripts/README.md +++ b/packages/scripts/README.md @@ -1,6 +1,10 @@ # Scripts -Collection of JS scripts for WordPress development. +Collection of reusable scripts for WordPress development. + +Command-line interfaces help to turn working with an app into a pleasant experience, but it is still not enough to keep it easy to maintain in the long run. Developers are left on their own to keep all configurations and dependent tools up to date. This problem multiplies when they own more than one project which shares the same setup. Fortunately, there is a pattern that can simplify maintainers life – reusable scripts. This idea boils down to moving all the necessary configurations and scripts to one single tool dependency. In most cases, it should be possible to accomplish all tasks using the default settings, but some customization is allowed, too. With all that in place updating all projects should become a very straightforward task. + +_This package is inspired by [react-scripts](https://www.npmjs.com/package/react-scripts) and [kcd-scripts](https://www.npmjs.com/package/kcd-scripts)._ ## Installation @@ -21,6 +25,7 @@ _Example:_ "scripts": { "check-engines": "wp-scripts check-engines", "check-licenses": "wp-scripts check-licenses --production", + "lint:css": "wp-scripts lint-style '**/*.css'", "lint:js": "wp-scripts lint-js .", "lint:pkg-json": "wp-scripts lint-pkg-json .", "test:e2e": "wp-scripts test-e2e", @@ -84,7 +89,7 @@ _Example:_ ``` This is how you execute the script with presented setup: -* `npm run lint:js` - lints JavaScripts files in the whole project's. +* `npm run lint:js` - lints JavaScript files in the entire project's directories. ### `lint-pkg-json` @@ -103,6 +108,23 @@ _Example:_ This is how you execute those scripts using the presented setup: * `npm run lint:pkg-json` - lints `package.json` file in the project's root folder. +### `lint-style` + +Helps enforce coding style guidelines for your style files. It uses [stylelint](https://github.com/stylelint/stylelint) with the [stylelint-config-wordpress](https://github.com/WordPress-Coding-Standards/stylelint-config-wordpress) configuration per the [WordPress CSS Coding Standards](https://make.wordpress.org/core/handbook/best-practices/coding-standards/css/). You can override them with your own rules as described in [stylelint user guide](https://github.com/stylelint/stylelint/docs/user-guide.md). + +_Example:_ + +```json +{ + "scripts": { + "lint:css": "wp-scripts lint-style '**/*.css'" + } +} +``` + +This is how you execute the script with presented setup: +* `npm run lint:css` - lints CSS files in the whole project's directory. + ### `test-e2e` Launches the End-To-End (E2E) test runner. It uses [Jest](https://facebook.github.io/jest/) behind the scenes and you are able to utilize all of its [CLI options](https://facebook.github.io/jest/docs/en/cli.html). You can also run `./node_modules/.bin/wp-scripts test:e2e --help` or `npm run test:e2e:help` (as presented below) to view all of the available options. @@ -154,8 +176,4 @@ This is how you execute those scripts using the presented setup: * `npm run test:unit:help` - prints all available options to configure unit tests runner. * `npm run test:unit:watch` - runs all unit tests in the watch mode. -## Inspiration - -This package is inspired by [react-scripts](https://www.npmjs.com/package/react-scripts) and [kcd-scripts](https://www.npmjs.com/package/kcd-scripts). - <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/scripts/config/.stylelintrc.json b/packages/scripts/config/.stylelintrc.json new file mode 100644 index 00000000000000..e3113e5fb0b6a6 --- /dev/null +++ b/packages/scripts/config/.stylelintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "stylelint-config-wordpress" +} diff --git a/packages/scripts/package.json b/packages/scripts/package.json index 4ee9bdb00850cd..e6350aa34da1df 100644 --- a/packages/scripts/package.json +++ b/packages/scripts/package.json @@ -1,7 +1,7 @@ { "name": "@wordpress/scripts", "version": "2.4.4", - "description": "Collection of JS scripts for WordPress development.", + "description": "Collection of reusable scripts for WordPress development.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", "keywords": [ @@ -44,7 +44,9 @@ "npm-package-json-lint": "^3.3.1", "puppeteer": "1.6.1", "read-pkg-up": "^1.0.1", - "resolve-bin": "^0.4.0" + "resolve-bin": "^0.4.0", + "stylelint": "^9.5.0", + "stylelint-config-wordpress": "^13.1.0" }, "publishConfig": { "access": "public" diff --git a/packages/scripts/scripts/lint-style.js b/packages/scripts/scripts/lint-style.js new file mode 100644 index 00000000000000..d72a877fa93702 --- /dev/null +++ b/packages/scripts/scripts/lint-style.js @@ -0,0 +1,39 @@ +/** + * External dependencies + */ +const { sync: spawn } = require( 'cross-spawn' ); +const { sync: resolveBin } = require( 'resolve-bin' ); + +/** + * Internal dependencies + */ +const { + fromConfigRoot, + getCliArgs, + hasCliArg, + hasProjectFile, + hasPackageProp, +} = require( '../utils' ); + +const args = getCliArgs(); + +const hasStylelintConfig = hasCliArg( '--config' ) || + hasProjectFile( '.stylelintrc' ) || + hasProjectFile( '.stylelintrc.js' ) || + hasProjectFile( '.stylelintrc.json' ) || + hasProjectFile( '.stylelintrc.yaml' ) || + hasProjectFile( '.stylelintrc.yml' ) || + hasProjectFile( '.stylelint.config.js' ) || + hasPackageProp( 'stylelint' ); + +const config = ! hasStylelintConfig ? + [ '--config', fromConfigRoot( '.stylelintrc.json' ) ] : + []; + +const result = spawn( + resolveBin( 'stylelint' ), + [ ...config, ...args ], + { stdio: 'inherit' } +); + +process.exit( result.status ); From a163dcd8aa8359d7c2051665626b09b5af01b03b Mon Sep 17 00:00:00 2001 From: Alexis Lloyd <alexislloyd@gmail.com> Date: Tue, 18 Dec 2018 10:47:41 -0500 Subject: [PATCH 254/254] Update block design guidelines to include info on setup states Proposed guidelines for when to include a setup state in your block design. Would appreciate feedback on content and validation of markdown syntax. --- .../designers/block-design.md | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/docs/designers-developers/designers/block-design.md b/docs/designers-developers/designers/block-design.md index ae6abaf4c8a37f..0d9d098c96229c 100644 --- a/docs/designers-developers/designers/block-design.md +++ b/docs/designers-developers/designers/block-design.md @@ -19,6 +19,39 @@ Basic block settings won’t always make sense in the context of the placeholder The sidebar is not visible by default on a small / mobile screen, and may also be collapsed in a desktop view. Therefore, it should not be relied on for anything that is necessary for the basic operation of the block. Pick good defaults, make important actions available in the block toolbar, and think of the sidebar as something that only power users may discover. In addition, use sections and headers in the block sidebar if there are more than a handful of options, in order to allow users to easily scan and understand the options available. +## Setup state vs. live preview state + +Often a block will use the placeholder content to walk users through a setup process. The setup process gathers information from the user that is needed to render the block. A block’s setup state is indicated with a grey background to provide clear differentiation for the user. Not all blocks have setup states — for example, the paragraph block. + + +![An example of a gallery block’s setup state on a grey background](https://make.wordpress.org/design/files/2018/12/gallery-setup.png) + + +A setup state is **not** necessary if: + +- You can provide good default content in the block that will meet most people’s needs. +- That default content is easy to edit and customize. + +Use a setup state if: + +- There isn’t a clear default state that would work for most users. +- You need to gather input from the user that doesn’t have a 1-1 relationship with the live preview of the block (for example, if you need the user to input an API key to render content). +- You need more information from the user in order to render useful default content. + +For blocks that do have setup states, once the user has gone through the setup process, the placeholder is replaced with the live preview state of that block. + + +![An example of the image gallery’s live preview state](https://make.wordpress.org/design/files/2018/12/gallery-live-preview.png) + + +When the block is selected, additional controls may be revealed to customize the block’s contents. For example, when the image gallery is selected, it reveals controls to remove or add images. + + +![An example of additional controls being revealed on selection of a block.](https://make.wordpress.org/design/files/2018/12/gallery-additional-controls.png) + + +In most cases, a block’s setup state is only shown once and then further customization is done via the live preview state. However, in some cases it might be desirable to allow the user to return to the setup state — for example, if all the block content has been deleted or via a link from the block’s toolbar or sidebar. + ## Do's and Don'ts ### Blocks