From c3fd28087f6dd8a728fdf43b3e603cf88a095d63 Mon Sep 17 00:00:00 2001 From: System Administrator Date: Thu, 25 Jan 2018 04:49:20 +0530 Subject: [PATCH 01/43] Work in progress fixes for ticket #1054 This commit is towards rendering a front end preview for shortcode blocks. The shortcode block now implements a tabbed preview option, similar to HTML blocks. The user can edit their shortcodes, and previewing again will re-render the edited shortcode. Works for embed shortcodes too. Known issues - (1) playlist shortcode doesn't work (2) the iframe height/width in the preview tab needs to wrap content size. For example, the iframe is too big when previewing an audio player using audio shortcode (3) gallery shortcode preview stacks the images vertically instead of horizontally (4) video shortcode doesn't work for URLs supported by oembed --- blocks/library/shortcode/index.js | 195 ++++++++++++++++---- lib/class-wp-rest-shortcodes-controller.php | 116 ++++++++++++ lib/load.php | 1 + lib/register.php | 11 ++ 4 files changed, 290 insertions(+), 33 deletions(-) create mode 100755 lib/class-wp-rest-shortcodes-controller.php diff --git a/blocks/library/shortcode/index.js b/blocks/library/shortcode/index.js index 483b279a97b832..01bb9308411836 100644 --- a/blocks/library/shortcode/index.js +++ b/blocks/library/shortcode/index.js @@ -7,19 +7,23 @@ import TextareaAutosize from 'react-autosize-textarea'; * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { withInstanceId, Dashicon } from '@wordpress/components'; +import { withInstanceId, Dashicon, Button, Spinner, SandBox } from '@wordpress/components'; +import { Component, renderToString } from '@wordpress/element'; +import { addQueryArgs } from '@wordpress/url'; +import BlockControls from '../../block-controls'; +import Editable from '../../editable'; +import { getCurrentPostId } from '../../../editor/store/selectors'; /** * Internal dependencies */ import './editor.scss'; +import { registerBlockType } from '../../api'; -export const name = 'core/shortcode'; +registerBlockType('core/shortcode', { + title: __('Shortcode'), -export const settings = { - title: __( 'Shortcode' ), - - description: __( 'A shortcode is a WordPress-specific code snippet that is written between square brackets as [shortcode]. ' ), + description: __('A shortcode is a WordPress-specific code snippet that is written between square brackets as [shortcode]. '), icon: 'marker', @@ -47,7 +51,7 @@ export const settings = { attributes: { text: { type: 'string', - shortcode: ( attrs, { content } ) => { + shortcode: (attrs, { content }) => { return content; }, }, @@ -57,36 +61,161 @@ export const settings = { }, supports: { - customClassName: false, - className: false, - html: false, + + className: false + }, edit: withInstanceId( - ( { attributes, setAttributes, instanceId } ) => { - const inputId = `blocks-shortcode-input-${ instanceId }`; - - return ( -
- - setAttributes( { - text: event.target.value, - } ) } - /> -
- ); + class extends Component { + constructor() { + super() + this.state = { + html: '', + preview: false, + focus: false, + isFetching: false + } + this.doServerSideRender = this.doServerSideRender.bind( this ); + this.handlePreviewClick = this.handlePreviewClick.bind( this ) + this.getUriParameters = this.getUriParameters.bind( this ) + this.convertToJSON = this.convertToJSON.bind( this ) + } + + getUriParameters() { + //Function to parse the GET params to obtain the post ID + var paramString = window.location.search.substr( 1 ); + return paramString != null && paramString != "" ? this.convertToJSON( paramString ) : {}; + } + + convertToJSON ( paramString ) { + var params = {}; + var paramsArr = paramString.split( "&" ); + for ( var i = 0; i < paramsArr.length; i++ ) { + let tmpArr = paramsArr[i].split( "=" ); + params[tmpArr[0]] = tmpArr[1]; + } + return params; + } + + doServerSideRender( event ) { + //This function sends the shortcode content and post ID to the rest endpoint, + //and retrieves the filtered shortcode content to be rendered in Preview + + //Get post ID from GET params + var params = this.getUriParameters(); + if ( 0 === params.length || !( "post" in params ) ) { + //We don't have a post ID yet + this.setState( { html: __( 'Something went wrong. Try saving the post and try again' ) } ) + return null + } + let shortcode = this.props.attributes.text + shortcode = ( shortcode ) ? shortcode.trim() : "" + if ( 0 === shortcode.length ) { + this.setState( { html: __( 'Enter something to preview' ) } ) + return null + } + this.setState( { isFetching: true } ) + const apiUrl = addQueryArgs( wpApiSettings.root + 'gutenberg/v1/shortcodes', { + shortcode: shortcode, + postId : params["post"], + _wpnonce: wpApiSettings.nonce + + } ); + window.fetch( apiUrl, { + credentials: 'include', + } ).then( + ( response ) => { + + response.json().then( ( obj ) => { + obj = ( 0 < obj.length ) ? obj : __( "Sorry, couldn't render a preview" ) + this.setState( { html: obj, isFetching: false } ) + + } ) + + } + ); + + } + + handlePreviewClick() { + this.setState( { preview: true } ) + this.doServerSideRender() + } + + render() { + const { instanceId, setAttributes, attributes, focus, setFocus } = this.props + const inputId = `blocks-shortcode-input-${instanceId}`; + + return ( +
+
+ { + focus ? + +
+ + +
+
+ + : + null + } +
+
+ { this.state.preview ? + this.state.isFetching ? +
+ +

{ __( 'Loading...' ) }

+
+ : + +
+
+ setFocus() } + /> +
+
+ : +
+ + setAttributes( { + text: event.target.value, + } ) } + /> +
+ } +
+ +
+ ); + } } ), - - save( { attributes } ) { + save({ attributes }) { return attributes.text; }, -}; +}); \ No newline at end of file diff --git a/lib/class-wp-rest-shortcodes-controller.php b/lib/class-wp-rest-shortcodes-controller.php new file mode 100755 index 00000000000000..543f9949a39d3e --- /dev/null +++ b/lib/class-wp-rest-shortcodes-controller.php @@ -0,0 +1,116 @@ +namespace for the namespace keyword + $this->namespace = 'gutenberg/v1'; + $this->rest_base = 'shortcodes'; + } + + /** + * Registers the necessary REST API routes. + * + * @since 0.10.0 + * @access public + */ + public function register_routes() { + // @codingStandardsIgnoreLine - PHPCS mistakes $this->namespace for the namespace keyword + $namespace = $this->namespace; + + register_rest_route( $namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_shortcode_output' ), + 'permission_callback' => array( $this, 'get_shortcode_output_permissions_check' ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + } + + /** + * Checks if a given request has access to read shortcode blocks. + * + * @since 0.10.0 + * @access public + * + * @param WP_REST_Request $request Full details about the request. + * @return true|WP_Error True if the request has read access, WP_Error object otherwise. + */ + public function get_shortcode_output_permissions_check( $request ) { + if ( ! current_user_can( 'edit_posts' ) ) { + return new WP_Error( 'gutenberg_shortcode_block_cannot_read', __( 'Sorry, you are not allowed to read shortcode blocks as this user.', 'gutenberg' ), array( + 'status' => rest_authorization_required_code(), + ) ); + } + + return true; + } + + /** + * Filters shortcode content through their hooks. + * + * @since 0.10.0 + * @access public + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function get_shortcode_output( $request ) { + $args = $request->get_params(); + global $post; + global $wp_embed; + $post = get_post( $args['postId'] ); + setup_postdata( $post ); + + if ( has_shortcode( $args['shortcode'], 'embed' ) ) { + $data = do_shortcode( $wp_embed->run_shortcode( $args['shortcode'] )); + } else { + $data = do_shortcode( $args['shortcode'] ); + } + + return rest_ensure_response( $data ); + } + + /** + * Retrieves a reusable block's schema, conforming to JSON Schema. + * + * @since 0.10.0 + * @access public + * + * @return array Item schema data. + */ + public function get_item_schema() { + return array( + '$schema' => 'http://json-schema.org/schema#', + 'title' => 'reusable-block', + 'type' => 'object', + 'properties' => array( + 'shortcode' => array( + 'description' => __( 'The block\'s shortcode content.', 'gutenberg' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'required' => true, + ), + ), + ); + } +} diff --git a/lib/load.php b/lib/load.php index 0da31fddf577cc..3cb075dc23cfe3 100644 --- a/lib/load.php +++ b/lib/load.php @@ -13,6 +13,7 @@ require dirname( __FILE__ ) . '/class-wp-block-type.php'; require dirname( __FILE__ ) . '/class-wp-block-type-registry.php'; require dirname( __FILE__ ) . '/class-wp-rest-blocks-controller.php'; +require dirname( __FILE__ ) . '/class-wp-rest-shortcodes-controller.php'; require dirname( __FILE__ ) . '/blocks.php'; require dirname( __FILE__ ) . '/client-assets.php'; require dirname( __FILE__ ) . '/compat.php'; diff --git a/lib/register.php b/lib/register.php index 56f0d809d9b9fa..dd6c6a10647c10 100644 --- a/lib/register.php +++ b/lib/register.php @@ -409,6 +409,17 @@ function gutenberg_register_post_types() { } add_action( 'init', 'gutenberg_register_post_types' ); +/** + * Registers the REST API routes needed by the Gutenberg editor. + * + * @since 0.10.0 + */ +function gutenberg_register_rest_routes() { + $controller = new WP_REST_Shortcodes_Controller(); + $controller->register_routes(); +} +add_action( 'rest_api_init', 'gutenberg_register_rest_routes' ); + /** * Gets revisions details for the selected post. * From 8cec7fcf8becdd7895e28d6f378798367bc7840f Mon Sep 17 00:00:00 2001 From: System Administrator Date: Thu, 25 Jan 2018 14:24:28 +0530 Subject: [PATCH 02/43] eslint fixes and reordering for better readibility and conforming to standards --- blocks/library/shortcode/index.js | 245 ++++++++++++++---------------- 1 file changed, 118 insertions(+), 127 deletions(-) diff --git a/blocks/library/shortcode/index.js b/blocks/library/shortcode/index.js index 01bb9308411836..a6ad74b3c9f55b 100644 --- a/blocks/library/shortcode/index.js +++ b/blocks/library/shortcode/index.js @@ -7,12 +7,10 @@ import TextareaAutosize from 'react-autosize-textarea'; * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { withInstanceId, Dashicon, Button, Spinner, SandBox } from '@wordpress/components'; -import { Component, renderToString } from '@wordpress/element'; +import { withInstanceId, Dashicon, Spinner, SandBox } from '@wordpress/components'; +import { Component } from '@wordpress/element'; import { addQueryArgs } from '@wordpress/url'; import BlockControls from '../../block-controls'; -import Editable from '../../editable'; -import { getCurrentPostId } from '../../../editor/store/selectors'; /** * Internal dependencies @@ -20,10 +18,10 @@ import { getCurrentPostId } from '../../../editor/store/selectors'; import './editor.scss'; import { registerBlockType } from '../../api'; -registerBlockType('core/shortcode', { - title: __('Shortcode'), +registerBlockType( 'core/shortcode', { + title: __( 'Shortcode' ), - description: __('A shortcode is a WordPress-specific code snippet that is written between square brackets as [shortcode]. '), + description: __( 'A shortcode is a WordPress-specific code snippet that is written between square brackets as [shortcode]. ' ), icon: 'marker', @@ -51,7 +49,7 @@ registerBlockType('core/shortcode', { attributes: { text: { type: 'string', - shortcode: (attrs, { content }) => { + shortcode: ( attrs, { content } ) => { return content; }, }, @@ -62,160 +60,153 @@ registerBlockType('core/shortcode', { supports: { - className: false + className: false, }, edit: withInstanceId( class extends Component { constructor() { - super() + super(); this.state = { html: '', preview: false, focus: false, - isFetching: false - } + fetching: false, + }; this.doServerSideRender = this.doServerSideRender.bind( this ); - this.handlePreviewClick = this.handlePreviewClick.bind( this ) - this.getUriParameters = this.getUriParameters.bind( this ) - this.convertToJSON = this.convertToJSON.bind( this ) + this.handlePreviewClick = this.handlePreviewClick.bind( this ); + this.getUriParameters = this.getUriParameters.bind( this ); + this.convertToJSON = this.convertToJSON.bind( this ); } - getUriParameters() { + getUriParameters() { //Function to parse the GET params to obtain the post ID - var paramString = window.location.search.substr( 1 ); - return paramString != null && paramString != "" ? this.convertToJSON( paramString ) : {}; - } - - convertToJSON ( paramString ) { - var params = {}; - var paramsArr = paramString.split( "&" ); - for ( var i = 0; i < paramsArr.length; i++ ) { - let tmpArr = paramsArr[i].split( "=" ); - params[tmpArr[0]] = tmpArr[1]; - } - return params; - } - + const paramString = window.location.search.substr( 1 ); + return paramString !== null && paramString !== '' ? this.convertToJSON( paramString ) : {}; + } + + convertToJSON( paramString ) { + const params = {}; + const paramsArr = paramString.split( '&' ); + for ( let i = 0, paramsArrLength = paramsArr.length; i < paramsArrLength; i++ ) { + const tmpArr = paramsArr[ i ].split( '=' ); + params[ tmpArr[0] ] = tmpArr[1]; + } + return params; + } + doServerSideRender( event ) { //This function sends the shortcode content and post ID to the rest endpoint, //and retrieves the filtered shortcode content to be rendered in Preview - //Get post ID from GET params - var params = this.getUriParameters(); - if ( 0 === params.length || !( "post" in params ) ) { - //We don't have a post ID yet - this.setState( { html: __( 'Something went wrong. Try saving the post and try again' ) } ) - return null - } - let shortcode = this.props.attributes.text - shortcode = ( shortcode ) ? shortcode.trim() : "" - if ( 0 === shortcode.length ) { - this.setState( { html: __( 'Enter something to preview' ) } ) - return null + if ( event ) { + event.preventDefault(); } - this.setState( { isFetching: true } ) + + //Get post ID from GET params + const params = this.getUriParameters(); + let shortcode = this.props.attributes.text; const apiUrl = addQueryArgs( wpApiSettings.root + 'gutenberg/v1/shortcodes', { shortcode: shortcode, - postId : params["post"], - _wpnonce: wpApiSettings.nonce + postId: params.post, + _wpnonce: wpApiSettings.nonce, } ); + shortcode = ( shortcode ) ? shortcode.trim() : ''; + if ( 0 === params.length || ! ( 'post' in params ) ) { + //We don't have a post ID yet + this.setState( { html: __( 'Something went wrong. Try saving the post and try again' ) } ); + return null; + } + if ( 0 === shortcode.length ) { + this.setState( { html: __( 'Enter something to preview' ) } ); + return null; + } + this.setState( { fetching: true } ); window.fetch( apiUrl, { credentials: 'include', - } ).then( - ( response ) => { - - response.json().then( ( obj ) => { - obj = ( 0 < obj.length ) ? obj : __( "Sorry, couldn't render a preview" ) - this.setState( { html: obj, isFetching: false } ) - - } ) - - } - ); - + } ).then( ( response ) => { + response.json().then( ( obj ) => { + obj = ( 0 < obj.length ) ? obj : __( 'Sorry, couldn\'t render a preview' ); + this.setState( { html: obj, fetching: false } ); + } ); + } ); } handlePreviewClick() { - this.setState( { preview: true } ) - this.doServerSideRender() + this.setState( { preview: true } ); + this.doServerSideRender(); } render() { - const { instanceId, setAttributes, attributes, focus, setFocus } = this.props - const inputId = `blocks-shortcode-input-${instanceId}`; - - return ( -
-
- { - focus ? - -
- - -
-
- - : - null - } + const { fetching, preview, html } = this.state; + const { instanceId, setAttributes, attributes, focus, setFocus } = this.props; + const inputId = `blocks-shortcode-input-${ instanceId }`; + + const controls = focus && ( + +
+ +
-
- { this.state.preview ? - this.state.isFetching ? -
- -

{ __( 'Loading...' ) }

-
- : - -
-
- setFocus() } - /> -
-
- : -
- - setAttributes( { - text: event.target.value, - } ) } - /> -
- } -
- -
+ ); + if ( ! preview ) { + return [ + controls, +
+ + setAttributes( { + text: event.target.value, + } ) } + /> +
, + ]; + } + + if ( fetching ) { + return [ + controls, +
+ +

{ __( 'Loading...' ) }

+
, + ]; + } + + return [ + controls, +
+ setFocus() } + /> +
, + ]; } } ), - save({ attributes }) { + save( { attributes } ) { return attributes.text; }, -}); \ No newline at end of file +} ); From 9fc96f85393081f6377130e3ff5a41b681ad1321 Mon Sep 17 00:00:00 2001 From: System Administrator Date: Thu, 25 Jan 2018 18:08:43 +0530 Subject: [PATCH 03/43] Minor changes to the title and code comments to change language to shortcode --- lib/class-wp-rest-shortcodes-controller.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/class-wp-rest-shortcodes-controller.php b/lib/class-wp-rest-shortcodes-controller.php index 543f9949a39d3e..517de8a90c1213 100755 --- a/lib/class-wp-rest-shortcodes-controller.php +++ b/lib/class-wp-rest-shortcodes-controller.php @@ -1,6 +1,6 @@ 'http://json-schema.org/schema#', - 'title' => 'reusable-block', + 'title' => 'shortcode-block', 'type' => 'object', 'properties' => array( 'shortcode' => array( From 00abacaddd5ef4fe751cc8c324d917d33676ea17 Mon Sep 17 00:00:00 2001 From: System Administrator Date: Fri, 26 Jan 2018 00:30:09 +0530 Subject: [PATCH 04/43] Post ID is now retrieved from redux store, instead of from GET params Previously, the post ID was fetched by processing the post's URL and parsing the post ID GET parameter. Now, a better approach is taken by reading the post ID value from the redux store. --- blocks/library/shortcode/index.js | 39 ++++++++++++------------------- 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/blocks/library/shortcode/index.js b/blocks/library/shortcode/index.js index a6ad74b3c9f55b..91dbe85a8c6819 100644 --- a/blocks/library/shortcode/index.js +++ b/blocks/library/shortcode/index.js @@ -8,9 +8,11 @@ import TextareaAutosize from 'react-autosize-textarea'; */ import { __ } from '@wordpress/i18n'; import { withInstanceId, Dashicon, Spinner, SandBox } from '@wordpress/components'; -import { Component } from '@wordpress/element'; +import { Component, compose } from '@wordpress/element'; import { addQueryArgs } from '@wordpress/url'; import BlockControls from '../../block-controls'; +import { getCurrentPostId } from '../../../editor/store/selectors'; +import { connect } from 'react-redux'; /** * Internal dependencies @@ -64,7 +66,14 @@ registerBlockType( 'core/shortcode', { }, - edit: withInstanceId( + edit: compose( [ + connect( ( state ) => { + return { + postId: getCurrentPostId( state ), + }; + } ), + withInstanceId, + ] )( class extends Component { constructor() { super(); @@ -76,24 +85,6 @@ registerBlockType( 'core/shortcode', { }; this.doServerSideRender = this.doServerSideRender.bind( this ); this.handlePreviewClick = this.handlePreviewClick.bind( this ); - this.getUriParameters = this.getUriParameters.bind( this ); - this.convertToJSON = this.convertToJSON.bind( this ); - } - - getUriParameters() { - //Function to parse the GET params to obtain the post ID - const paramString = window.location.search.substr( 1 ); - return paramString !== null && paramString !== '' ? this.convertToJSON( paramString ) : {}; - } - - convertToJSON( paramString ) { - const params = {}; - const paramsArr = paramString.split( '&' ); - for ( let i = 0, paramsArrLength = paramsArr.length; i < paramsArrLength; i++ ) { - const tmpArr = paramsArr[ i ].split( '=' ); - params[ tmpArr[0] ] = tmpArr[1]; - } - return params; } doServerSideRender( event ) { @@ -104,17 +95,17 @@ registerBlockType( 'core/shortcode', { event.preventDefault(); } - //Get post ID from GET params - const params = this.getUriParameters(); + //Get post ID from redux store + const postId = this.props.postId; let shortcode = this.props.attributes.text; const apiUrl = addQueryArgs( wpApiSettings.root + 'gutenberg/v1/shortcodes', { shortcode: shortcode, - postId: params.post, + postId: postId, _wpnonce: wpApiSettings.nonce, } ); shortcode = ( shortcode ) ? shortcode.trim() : ''; - if ( 0 === params.length || ! ( 'post' in params ) ) { + if ( 0 === postId.length || null === postId ) { //We don't have a post ID yet this.setState( { html: __( 'Something went wrong. Try saving the post and try again' ) } ); return null; From 3646f61263055557d20eca558f737e87a42d4d4d Mon Sep 17 00:00:00 2001 From: System Administrator Date: Sun, 28 Jan 2018 01:09:58 +0530 Subject: [PATCH 05/43] Injects custom css and js that might be needed by iframe In this revision, custom css and js files are injected as props to the iframe sandbox. Custom css and js is needed in certain cases, for example, [gallery] shortcode needs the parent theme style.css. --- blocks/library/shortcode/index.js | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/blocks/library/shortcode/index.js b/blocks/library/shortcode/index.js index 91dbe85a8c6819..646cddf9a737cb 100644 --- a/blocks/library/shortcode/index.js +++ b/blocks/library/shortcode/index.js @@ -18,9 +18,10 @@ import { connect } from 'react-redux'; * Internal dependencies */ import './editor.scss'; -import { registerBlockType } from '../../api'; -registerBlockType( 'core/shortcode', { +export const name = 'core/shortcode'; + +export const settings = { title: __( 'Shortcode' ), description: __( 'A shortcode is a WordPress-specific code snippet that is written between square brackets as [shortcode]. ' ), @@ -79,6 +80,9 @@ registerBlockType( 'core/shortcode', { super(); this.state = { html: '', + js: '', + style: '', + type: '', preview: false, focus: false, fetching: false, @@ -118,9 +122,9 @@ registerBlockType( 'core/shortcode', { window.fetch( apiUrl, { credentials: 'include', } ).then( ( response ) => { - response.json().then( ( obj ) => { - obj = ( 0 < obj.length ) ? obj : __( 'Sorry, couldn\'t render a preview' ); - this.setState( { html: obj, fetching: false } ); + response.json().then( ( { html, type, style, js } ) => { + html = ( 0 < html.length ) ? html : __( 'Sorry, couldn\'t render a preview' ); + this.setState( { html: html, js: js, style: style, type: type, fetching: false } ); } ); } ); } @@ -131,7 +135,7 @@ registerBlockType( 'core/shortcode', { } render() { - const { fetching, preview, html } = this.state; + const { fetching, preview, html, type, js, style } = this.state; const { instanceId, setAttributes, attributes, focus, setFocus } = this.props; const inputId = `blocks-shortcode-input-${ instanceId }`; @@ -189,7 +193,9 @@ registerBlockType( 'core/shortcode', { setFocus() } /> , @@ -200,4 +206,4 @@ registerBlockType( 'core/shortcode', { save( { attributes } ) { return attributes.text; }, -} ); +}; From 2681b289f414a2a5edd72b83cf92543418386650 Mon Sep 17 00:00:00 2001 From: System Administrator Date: Sun, 28 Jan 2018 01:12:38 +0530 Subject: [PATCH 06/43] Checks shortcode content type and fetches custom css/js (if any) Shortcode content type (if it's a video or otherwise), and the shortcode's custom css and js (if any) are fetched and returned as parameters to the front end. In the case of [gallery] and [caption], for example, the theme's style.css is needed, else the shortcode preview will not render properly. [playlist] needs mediaelement JS to be able to render the playlist components. --- lib/class-wp-rest-shortcodes-controller.php | 70 +++++++++++++++++---- 1 file changed, 59 insertions(+), 11 deletions(-) diff --git a/lib/class-wp-rest-shortcodes-controller.php b/lib/class-wp-rest-shortcodes-controller.php index 517de8a90c1213..93248d6ca1069c 100755 --- a/lib/class-wp-rest-shortcodes-controller.php +++ b/lib/class-wp-rest-shortcodes-controller.php @@ -3,13 +3,13 @@ * Shortcode Blocks REST API: WP_REST_Shortcodes_Controller class * * @package gutenberg - * @since 0.10.0 + * @since 2.0.0 */ /** * Controller which provides a REST endpoint for Gutenberg to preview shortcode blocks. * - * @since 0.10.0 + * @since 2.0.0 * * @see WP_REST_Controller */ @@ -17,7 +17,7 @@ class WP_REST_Shortcodes_Controller extends WP_REST_Controller { /** * Constructs the controller. * - * @since 0.10.0 + * @since 2.0.0 * @access public */ public function __construct() { @@ -49,7 +49,7 @@ public function register_routes() { /** * Checks if a given request has access to read shortcode blocks. * - * @since 0.10.0 + * @since 2.0.0 * @access public * * @param WP_REST_Request $request Full details about the request. @@ -68,25 +68,59 @@ public function get_shortcode_output_permissions_check( $request ) { /** * Filters shortcode content through their hooks. * - * @since 0.10.0 + * @since 2.0.0 * @access public * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function get_shortcode_output( $request ) { - $args = $request->get_params(); global $post; global $wp_embed; + $args = $request->get_params(); $post = get_post( $args['postId'] ); setup_postdata( $post ); + $yt_pattern = '#https?://(?:www\.)?(?:youtube\.com/watch|youtu\.be/)#'; + $vimeo_pattern = '#https?://(.+\.)?vimeo\.com/.*#'; + $style = $js = ''; + //Since the [embed] shortcode needs to be run earlier than other shortcodes. if ( has_shortcode( $args['shortcode'], 'embed' ) ) { - $data = do_shortcode( $wp_embed->run_shortcode( $args['shortcode'] )); + $output = $wp_embed->run_shortcode( $args['shortcode'] ); } else { - $data = do_shortcode( $args['shortcode'] ); + $output = do_shortcode( $args['shortcode'] ); } + //Check if shortcode is returning a video. The video type will be used by the frontend to maintain 16:9 aspect ratio + //TODO: Extend embed video compare to other services too, such as videopress + if ( has_shortcode( $args['shortcode'], 'video' ) ) { + $type = 'video'; + } elseif ( has_shortcode( $args['shortcode'], 'embed' ) && preg_match( $yt_pattern, $args['shortcode'] ) ) { + $type = 'video'; + } elseif ( has_shortcode( $args['shortcode'], 'embed' ) && preg_match( $vimeo_pattern, $args['shortcode'] ) ) { + $type = 'video'; + } else { + $type = 'html'; + //Gallery and caption shortcodes need the theme style to be embedded in the shortcode preview iframe + if ( has_shortcode( $args['shortcode'], 'gallery' ) || has_shortcode( $args['shortcode'], 'caption' ) || has_shortcode( $args['shortcode'], 'wp_caption' ) ) { + $style = ''; + } + + //Playlist shortcodes need the playlist JS to be embedded in the shortcode preview iframe + if ( has_shortcode( $args['shortcode'], 'playlist' ) ) { + ob_start(); + wp_print_scripts( 'wp-playlist' ); + $js = ob_get_clean(); + + } + } + + $data = array( + 'html' => $output, + 'type' => $type, + 'style' => $style, + 'js' => $js, + ); return rest_ensure_response( $data ); } @@ -104,12 +138,26 @@ public function get_item_schema() { 'title' => 'shortcode-block', 'type' => 'object', 'properties' => array( - 'shortcode' => array( - 'description' => __( 'The block\'s shortcode content.', 'gutenberg' ), + 'html' => array( + 'description' => __( 'The block\'s content with shortcodes filtered through hooks.', 'gutenberg' ), 'type' => 'string', - 'context' => array( 'view', 'edit' ), 'required' => true, ), + 'type' => array( + 'description' => __( 'The filtered content type - video or otherwise', 'gutenberg' ), + 'type' => 'string', + 'required' => true, + ), + 'style' => array( + 'description' => __( 'Links to external style sheets needed to render the shortcode', 'gutenberg' ), + 'type' => 'string', + 'required' => true, + ), + 'js' => array( + 'description' => __( 'Links to external javascript and inline scripts needed to render the shortcode', 'gutenberg' ), + 'type' => 'string', + 'required' => true, + ), ), ); } From d7971cbd826ad146fbe2a0fda31c98a295ec3ed6 Mon Sep 17 00:00:00 2001 From: System Administrator Date: Sun, 28 Jan 2018 01:16:53 +0530 Subject: [PATCH 07/43] JS and CSS fixes to enable rendering of shortcode previews 1) Certain shortcodes, such as [gallery], [caption] and [playlist] need styles and JS inside of the sandbox iframe to be able to render content appropriately. The sandbox component didn't support injection of custom links for external stylesheets and scripts, so this revision attempts to fix that 2) [audio] shortcode fails to render since bounding client width is 0 (weird, I know). So, in this revision, I add a null check and set a minimum width if bounding client width or height is zero. --- components/sandbox/index.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/components/sandbox/index.js b/components/sandbox/index.js index d37206ff0b4831..4ad5a2dd567f75 100644 --- a/components/sandbox/index.js +++ b/components/sandbox/index.js @@ -96,6 +96,12 @@ export default class Sandbox extends Component { function sendResize() { var clientBoundingRect = document.body.getBoundingClientRect(); + if ( 0 === clientBoundingRect.height ) { + clientBoundingRect.height = document.getElementById( 'content' ).clientHeight; + } + if ( 0 === clientBoundingRect.width ) { + clientBoundingRect.width = document.getElementById( 'content' ).clientWidth; + } window.parent.postMessage( { action: 'resize', width: clientBoundingRect.width, @@ -141,12 +147,18 @@ export default class Sandbox extends Component { body { margin: 0; } + + body.html { + width: 100%; + } + body.video, body.video > div, body.video > div > iframe { width: 100%; height: 100%; } + body > div > * { margin-top: 0 !important; /* has to have !important to override inline styles */ margin-bottom: 0 !important; @@ -162,8 +174,11 @@ export default class Sandbox extends Component { + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/readme.md b/docs/readme.md index 6c251fd67a1f8a..44c365be4c1928 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -1,13 +1,17 @@ # Introduction "Gutenberg" is the codename for the 2017 WordPress editor focus. The goal of this focus 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: - + > 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. - + Key take-aways from parsing that paragraph: - + - Authoring richly laid out posts is a key strength of WordPress. - By embracing "the block", we can potentially unify multiple different interfaces into one. Instead of learning how to write shortcodes, custom HTML, or paste URLs to embed, you should do with just learning the block, and all the pieces should fall in place. - "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), and you can try [an early beta version today from the plugin repository](https://wordpress.org/plugins/gutenberg/). Though keep in mind it's not fully functional, feature complete, or production ready. + +## Logo +Released under GPL license, made by [Cristel Rossignol](https://twitter.com/cristelrossi). +[Gutenberg logo](https://github.com/WordPress/gutenberg/blob/master/docs/final-g-wapuu-black.svg). diff --git a/docs/talks.md b/docs/talks.md index 57ba8900af6b57..1d7c3736536c26 100644 --- a/docs/talks.md +++ b/docs/talks.md @@ -7,6 +7,7 @@ Talks given about Gutenberg, including slides and videos as they are available. - [Gutenberg Notes](http://haiku2.com/2017/09/bend-wordpress-meetup-gutenberg-notes/) at Bend WordPress Meetup (5. September 2017) - [Gutenberg and the Future of Content in WordPress](https://www.slideshare.net/andrewmduthie/gutenberg-and-the-future-of-content-in-wordpress) (20. September 2017) - [Head first into Gutenberg](https://speakerdeck.com/prtksxna/head-first-into-gutenberg) at the [WordPress Goa Meet-up](https://www.meetup.com/WordPressGoa/events/245275573/) (1. December 2017) +- [Gutenberg : vers une approche plus fine du contenu](https://imathi.eu/2018/02/16/gutenberg-vers-une-approche-plus-fine-du-contenu/) at [WP Paris](https://wpparis.fr/) (8. February 2018) ## Videos - [All `Gutenberg` tagged Talks at WordPress.tv](https://wordpress.tv/tag/gutenberg/) diff --git a/docs/templates.md b/docs/templates.md index c60c56f095bfd0..d136f21bedadae 100644 --- a/docs/templates.md +++ b/docs/templates.md @@ -15,7 +15,7 @@ Planned additions: ## API -Tempates can be declared in JS or in PHP as an array of blockTypes (block name and optional attributes). +Templates can be declared in JS or in PHP as an array of blockTypes (block name and optional attributes). ```js const template = [ @@ -84,4 +84,4 @@ function my_add_template_to_posts() { $post_type_object->template_lock = 'all'; } add_action( 'init', 'my_add_template_to_posts' ); -``` \ No newline at end of file +``` diff --git a/docs/themes.md b/docs/themes.md index bd402a4a17b698..b5b4fb7cc08d79 100644 --- a/docs/themes.md +++ b/docs/themes.md @@ -43,3 +43,13 @@ add_theme_support( 'editor-color-palette', ``` The colors will be shown in order on the palette, and there's no limit to how many can be specified. + +### Disabling custom colors in block Color Palettes + +By default, the color palette offered to blocks, allows the user to select a custom color different from the editor or theme default colors. +Themes can disable this feature using: +```php +add_theme_support( 'disable-custom-colors' ); +``` + +This flag will make sure users are only able to choose colors from the `editor-color-palette` the theme provided or from the editor default colors if the theme did not provide one. diff --git a/edit-post/api/index.js b/edit-post/api/index.js new file mode 100644 index 00000000000000..1252f8010ec3f4 --- /dev/null +++ b/edit-post/api/index.js @@ -0,0 +1,4 @@ +export { + registerSidebar, + activateSidebar, +} from './sidebar'; diff --git a/edit-post/api/sidebar.js b/edit-post/api/sidebar.js new file mode 100644 index 00000000000000..b2d52152d421e2 --- /dev/null +++ b/edit-post/api/sidebar.js @@ -0,0 +1,98 @@ +/* eslint no-console: [ 'error', { allow: [ 'error' ] } ] */ + +/* External dependencies */ +import { isFunction } from 'lodash'; + +/* Internal dependencies */ +import store from '../store'; +import { setGeneralSidebarActivePanel, openGeneralSidebar } from '../store/actions'; +import { applyFilters } from '@wordpress/hooks'; + +const sidebars = {}; + +/** + * Registers a sidebar to the editor. + * + * A button will be shown in the settings menu to open the sidebar. The sidebar + * can be manually opened by calling the `activateSidebar` function. + * + * @param {string} name The name of the sidebar. Should be in + * `[plugin]/[sidebar]` format. + * @param {Object} settings The settings for this sidebar. + * @param {string} settings.title The name to show in the settings menu. + * @param {Function} settings.render The function that renders the sidebar. + * + * @return {Object} The final sidebar settings object. + */ +export function registerSidebar( name, settings ) { + settings = { + name, + ...settings, + }; + + if ( typeof name !== 'string' ) { + console.error( + 'Sidebar names must be strings.' + ); + return null; + } + if ( ! /^[a-z][a-z0-9-]*\/[a-z][a-z0-9-]*$/.test( name ) ) { + console.error( + 'Sidebar names must contain a namespace prefix, include only lowercase alphanumeric characters or dashes, and start with a letter. Example: my-plugin/my-custom-sidebar.' + ); + return null; + } + if ( ! settings || ! isFunction( settings.render ) ) { + console.error( + 'The "render" property must be specified and must be a valid function.' + ); + return null; + } + if ( sidebars[ name ] ) { + console.error( + `Sidebar ${ name } is already registered.` + ); + } + + if ( ! settings.title ) { + console.error( + `The sidebar ${ name } must have a title.` + ); + return null; + } + if ( typeof settings.title !== 'string' ) { + console.error( + 'Sidebar titles must be strings.' + ); + return null; + } + + settings = applyFilters( 'editor.registerSidebar', settings, name ); + + return sidebars[ name ] = settings; +} + +/** + * Retrieves the sidebar settings object. + * + * @param {string} name The name of the sidebar to retrieve the settings for. + * + * @return {Object} The settings object of the sidebar. Or null if the + * sidebar doesn't exist. + */ +export function getSidebarSettings( name ) { + if ( ! sidebars.hasOwnProperty( name ) ) { + return null; + } + return sidebars[ name ]; +} +/** + * Activates the given sidebar. + * + * @param {string} name The name of the sidebar to activate. + * @return {void} + */ +export function activateSidebar( name ) { + store.dispatch( openGeneralSidebar( 'plugin' ) ); + store.dispatch( setGeneralSidebarActivePanel( 'plugin', name ) ); +} diff --git a/edit-post/assets/stylesheets/_mixins.scss b/edit-post/assets/stylesheets/_mixins.scss index 41339b23dab107..bac1daff4c66b3 100644 --- a/edit-post/assets/stylesheets/_mixins.scss +++ b/edit-post/assets/stylesheets/_mixins.scss @@ -115,6 +115,7 @@ $float-margin: calc( 50% - #{ $visual-editor-max-width-padding / 2 } ); * Button states and focus styles */ +// Buttons with rounded corners @mixin button-style__disabled { opacity: 0.6; cursor: default; @@ -132,11 +133,61 @@ $float-margin: calc( 50% - #{ $visual-editor-max-width-padding / 2 } ); } @mixin button-style__focus-active() { - outline: none; color: $dark-gray-900; box-shadow: inset 0 0 0 1px $dark-gray-300, inset 0 0 0 2px $white; + + // Windows High Contrast mode will show this outline, but not the box-shadow + outline: 2px solid transparent; + outline-offset: -2px; +} + +// Formatting Buttons +@mixin formatting-button-style__hover { + color: $dark-gray-500; + box-shadow: inset 0 0 0 1px $dark-gray-500, inset 0 0 0 2px $white; +} + +@mixin formatting-button-style__active() { + outline: none; + color: $white; + box-shadow: none; + background: $dark-gray-500; +} + +@mixin formatting-button-style__focus() { + box-shadow: inset 0 0 0 1px $dark-gray-500, inset 0 0 0 2px $white; + + // Windows High Contrast mode will show this outline, but not the box-shadow + outline: 2px solid transparent; + outline-offset: -2px; +} + +// Tabs, Inputs, Square buttons +@mixin square-style__neutral() { + outline-offset: -1px; +} + +@mixin square-style__focus-active() { + color: $dark-gray-900; + outline: 1px solid $dark-gray-300; +} + +// Menu items +@mixin menu-style__neutral() { + border: none; + box-shadow: none; +} + +@mixin menu-style__focus() { + color: $black; + border: none; + box-shadow: none; + outline-offset: -2px; + color: $dark-gray-900; + outline: 1px dotted $dark-gray-500; } +// Old @mixin tab-style__focus-active() { outline: none; color: $dark-gray-900; diff --git a/edit-post/assets/stylesheets/_z-index.scss b/edit-post/assets/stylesheets/_z-index.scss index 2c6e2e29a32516..5ce5a9b728468a 100644 --- a/edit-post/assets/stylesheets/_z-index.scss +++ b/edit-post/assets/stylesheets/_z-index.scss @@ -7,6 +7,7 @@ $z-layers: ( '.editor-block-list__block:before': -1, '.editor-block-list__block .wp-block-more:before': -1, '.editor-block-list__block {core/image aligned left or right}': 20, + '.editor-block-list__block {core/image aligned wide or fullwide}': 20, '.freeform-toolbar': 10, '.editor-warning': 1, '.components-form-toggle__input': 1, @@ -14,10 +15,10 @@ $z-layers: ( '.editor-inserter__tabs': 1, '.editor-inserter__tab.is-active': 1, '.components-panel__header': 1, - '.editor-meta-boxes-area.is-loading:before': 1, - '.editor-meta-boxes-area .spinner': 2, + '.edit-post-meta-boxes-area.is-loading:before': 1, + '.edit-post-meta-boxes-area .spinner': 2, '.blocks-format-toolbar__link-modal': 2, - '.editor-block-contextual-toolbar': 2, + '.editor-block-contextual-toolbar': 21, '.editor-block-switcher__menu': 2, '.components-popover__close': 2, '.editor-block-mover': 1, diff --git a/edit-post/assets/stylesheets/main.scss b/edit-post/assets/stylesheets/main.scss index d5c673831cb15a..a9d5f41a0157b8 100644 --- a/edit-post/assets/stylesheets/main.scss +++ b/edit-post/assets/stylesheets/main.scss @@ -47,10 +47,6 @@ body.gutenberg-editor-page { background: $white; - #update-nag, .update-nag { - display: none; - } - #wpcontent { padding-left: 0; } @@ -59,6 +55,12 @@ body.gutenberg-editor-page { padding-bottom: 0; } + /* We hide legacy notices in Gutenberg, because they were not designed in a way that scaled well. + Plugins can use Gutenberg notices if they need to pass on information to the user when they are editing. */ + #wpbody-content > div:not( .gutenberg ):not( #screen-meta ) { + display: none; + } + #wpfooter { display: none; } diff --git a/edit-post/components/header/editor-actions/index.js b/edit-post/components/header/editor-actions/index.js deleted file mode 100644 index 108c3aab64b45f..00000000000000 --- a/edit-post/components/header/editor-actions/index.js +++ /dev/null @@ -1,18 +0,0 @@ -/** - * WordPress dependencies - */ -import { MenuItemsGroup } from '@wordpress/components'; -import { applyFilters } from '@wordpress/hooks'; -import { __ } from '@wordpress/i18n'; - -export default function EditorActions() { - const tools = applyFilters( 'editor.EditorActions.tools', [] ); - return tools.length ? ( - - { tools } - - ) : null; -} diff --git a/edit-post/components/header/ellipsis-menu/style.scss b/edit-post/components/header/ellipsis-menu/style.scss deleted file mode 100644 index 6d16343f4d933d..00000000000000 --- a/edit-post/components/header/ellipsis-menu/style.scss +++ /dev/null @@ -1,20 +0,0 @@ -.edit-post-ellipsis-menu { - // the padding and margin of the ellipsis menu is intentionally non-standard - @include break-small() { - margin-left: 4px; - } - - .components-icon-button { - padding: 8px 4px; - width: auto; - } - - .components-button svg { - transform: rotate( 90deg ); - } -} - -.edit-post-ellipsis-menu__separator { - height: 1px; - background: $light-gray-500; -} diff --git a/edit-post/components/header/fixed-toolbar-toggle/index.js b/edit-post/components/header/fixed-toolbar-toggle/index.js index 82430d63dbe2d7..703fba930a8195 100644 --- a/edit-post/components/header/fixed-toolbar-toggle/index.js +++ b/edit-post/components/header/fixed-toolbar-toggle/index.js @@ -22,6 +22,7 @@ function FeatureToggle( { onToggle, active, onMobile } ) { return ( - { ! isPublishSidebarOpened && ( + { ! isPublishSidebarOpen && (
- + - +
) }
@@ -61,12 +78,15 @@ function Header( { export default connect( ( state ) => ( { - isDefaultSidebarOpened: isSidebarOpened( state ), - isPublishSidebarOpened: isSidebarOpened( state, 'publish' ), + isGeneralSidebarEditorOpen: getOpenedGeneralSidebar( state ) === 'editor', + isPublishSidebarOpen: isPublishSidebarOpened( state ), + hasActiveMetaboxes: hasMetaBoxes( state ), + isSaving: isSavingMetaBoxes( state ), } ), { - onToggleDefaultSidebar: () => toggleSidebar(), - onTogglePublishSidebar: () => toggleSidebar( 'publish' ), + onOpenGeneralSidebar: () => openGeneralSidebar( 'editor' ), + onCloseGeneralSidebar: closeGeneralSidebar, + onTogglePublishSidebar: togglePublishSidebar, }, undefined, { storeKey: 'edit-post' } diff --git a/edit-post/components/header/mode-switcher/index.js b/edit-post/components/header/mode-switcher/index.js index c16054d8846a07..9af936bb24a4d4 100644 --- a/edit-post/components/header/mode-switcher/index.js +++ b/edit-post/components/header/mode-switcher/index.js @@ -7,7 +7,7 @@ import { connect } from 'react-redux'; * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { MenuItemsGroup } from '@wordpress/components'; +import { MenuItemsChoice, MenuItemsGroup } from '@wordpress/components'; /** * Internal dependencies @@ -43,10 +43,14 @@ function ModeSwitcher( { onSwitch, mode } ) { return ( + filterName="editPost.MoreMenu.editor" + > + + ); } diff --git a/edit-post/components/header/ellipsis-menu/index.js b/edit-post/components/header/more-menu/index.js similarity index 60% rename from edit-post/components/header/ellipsis-menu/index.js rename to edit-post/components/header/more-menu/index.js index 7251e8f552dfb0..64a4d108f126f4 100644 --- a/edit-post/components/header/ellipsis-menu/index.js +++ b/edit-post/components/header/more-menu/index.js @@ -2,7 +2,7 @@ * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { IconButton, Dropdown } from '@wordpress/components'; +import { IconButton, Dropdown, MenuItemsGroup } from '@wordpress/components'; /** * Internal dependencies @@ -10,11 +10,10 @@ import { IconButton, Dropdown } from '@wordpress/components'; import './style.scss'; import ModeSwitcher from '../mode-switcher'; import FixedToolbarToggle from '../fixed-toolbar-toggle'; -import EditorActions from '../editor-actions'; -const element = ( +const MoreMenu = () => ( ( ) } renderContent={ ( { onClose } ) => ( -
+
-
-
- +
) } /> ); -function EllipsisMenu() { - return element; -} - -export default EllipsisMenu; +export default MoreMenu; diff --git a/edit-post/components/header/more-menu/style.scss b/edit-post/components/header/more-menu/style.scss new file mode 100644 index 00000000000000..d253e131b8f121 --- /dev/null +++ b/edit-post/components/header/more-menu/style.scss @@ -0,0 +1,21 @@ +.edit-post-more-menu { + // the padding and margin of the more menu is intentionally non-standard + @include break-small() { + margin-left: 4px; + } + + .components-icon-button { + padding: 8px 4px; + width: auto; + } + + .components-button svg { + transform: rotate(90deg); + } +} + +.edit-post-more-menu__content { + .components-menu-items__group:not(:last-child) { + border-bottom: 1px solid $light-gray-500; + } +} diff --git a/edit-post/components/layout/index.js b/edit-post/components/layout/index.js index 8bea1893d7a43b..5ec118509484be 100644 --- a/edit-post/components/layout/index.js +++ b/edit-post/components/layout/index.js @@ -3,6 +3,7 @@ */ import { connect } from 'react-redux'; import classnames from 'classnames'; +import { some } from 'lodash'; /** * WordPress dependencies @@ -10,7 +11,6 @@ import classnames from 'classnames'; import { Popover, navigateRegions } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { - MetaBoxes, AutosaveMonitor, UnsavedChangesWarning, EditorNotices, @@ -27,31 +27,58 @@ import Sidebar from '../sidebar'; import TextEditor from '../modes/text-editor'; import VisualEditor from '../modes/visual-editor'; import EditorModeKeyboardShortcuts from '../modes/keyboard-shortcuts'; +import MetaBoxes from '../meta-boxes'; +import { getMetaBoxContainer } from '../../utils/meta-boxes'; import { getEditorMode, - hasFixedToolbar, hasOpenSidebar, - isSidebarOpened, + isFeatureActive, + getOpenedGeneralSidebar, + isPublishSidebarOpened, + getActivePlugin, + getMetaBoxes, } from '../../store/selectors'; -import { toggleSidebar } from '../../store/actions'; +import { closePublishSidebar } from '../../store/actions'; +import PluginsPanel from '../../components/plugins-panel/index.js'; +import { getSidebarSettings } from '../../api/sidebar'; + +function GeneralSidebar( { openedGeneralSidebar } ) { + switch ( openedGeneralSidebar ) { + case 'editor': + return ; + case 'plugin': + return ; + default: + } + return null; +} function Layout( { mode, layoutHasOpenSidebar, - isDefaultSidebarOpened, - isPublishSidebarOpened, - fixedToolbarActive, - onClosePublishPanel, + publishSidebarOpen, + openedGeneralSidebar, + hasFixedToolbar, + onClosePublishSidebar, + plugin, + metaBoxes, } ) { + const isSidebarOpened = layoutHasOpenSidebar && + ( openedGeneralSidebar !== 'plugin' || getSidebarSettings( plugin ) ); const className = classnames( 'edit-post-layout', { - 'is-sidebar-opened': layoutHasOpenSidebar, - 'has-fixed-toolbar': fixedToolbarActive, + 'is-sidebar-opened': isSidebarOpened, + 'has-fixed-toolbar': hasFixedToolbar, } ); return (
- + { + return some( metaBoxes, ( metaBox, location ) => { + return metaBox.isActive && + jQuery( getMetaBoxContainer( location ) ).serialize() !== metaBox.data; + } ); + } } />
@@ -68,8 +95,11 @@ function Layout( {
- { isDefaultSidebarOpened && } - { isPublishSidebarOpened && } + { publishSidebarOpen && } + { + openedGeneralSidebar !== null && + }
); @@ -79,12 +109,14 @@ export default connect( ( state ) => ( { mode: getEditorMode( state ), layoutHasOpenSidebar: hasOpenSidebar( state ), - isDefaultSidebarOpened: isSidebarOpened( state ), - isPublishSidebarOpened: isSidebarOpened( state, 'publish' ), - fixedToolbarActive: hasFixedToolbar( state ), + openedGeneralSidebar: getOpenedGeneralSidebar( state ), + publishSidebarOpen: isPublishSidebarOpened( state ), + hasFixedToolbar: isFeatureActive( state, 'fixedToolbar' ), + plugin: getActivePlugin( state ), + metaBoxes: getMetaBoxes( state ), } ), { - onClosePublishPanel: () => toggleSidebar( 'publish', false ), + onClosePublishSidebar: closePublishSidebar, }, undefined, { storeKey: 'edit-post' } diff --git a/edit-post/components/layout/style.scss b/edit-post/components/layout/style.scss index 0cef72e1906e54..733827d2b8419f 100644 --- a/edit-post/components/layout/style.scss +++ b/edit-post/components/layout/style.scss @@ -62,7 +62,7 @@ padding: 10px 0 10px; clear: both; - .editor-meta-boxes-area { + .edit-post-meta-boxes-area { max-width: $visual-editor-max-width; margin: auto; } diff --git a/editor/components/meta-boxes/index.js b/edit-post/components/meta-boxes/index.js similarity index 74% rename from editor/components/meta-boxes/index.js rename to edit-post/components/meta-boxes/index.js index 3af2c12dc7471c..7e611e3a95b5cd 100644 --- a/editor/components/meta-boxes/index.js +++ b/edit-post/components/meta-boxes/index.js @@ -28,6 +28,11 @@ function MetaBoxes( { location, isActive, usePanel = false } ) { ); } -export default connect( ( state, ownProps ) => ( { - isActive: getMetaBox( state, ownProps.location ).isActive, -} ) )( MetaBoxes ); +export default connect( + ( state, ownProps ) => ( { + isActive: getMetaBox( state, ownProps.location ).isActive, + } ), + undefined, + undefined, + { storeKey: 'edit-post' } +)( MetaBoxes ); diff --git a/editor/components/meta-boxes/meta-boxes-area/index.js b/edit-post/components/meta-boxes/meta-boxes-area/index.js similarity index 83% rename from editor/components/meta-boxes/meta-boxes-area/index.js rename to edit-post/components/meta-boxes/meta-boxes-area/index.js index be78633428b7f2..fe6692a0c28053 100644 --- a/editor/components/meta-boxes/meta-boxes-area/index.js +++ b/edit-post/components/meta-boxes/meta-boxes-area/index.js @@ -60,7 +60,7 @@ class MetaBoxesArea extends Component { const { location, isSaving } = this.props; const classes = classnames( - 'editor-meta-boxes-area', + 'edit-post-meta-boxes-area', `is-${ location }`, { 'is-loading': isSaving, @@ -70,8 +70,8 @@ class MetaBoxesArea extends Component { return (
{ isSaving && } -
-
+
+
); } @@ -86,4 +86,9 @@ function mapStateToProps( state ) { }; } -export default connect( mapStateToProps )( MetaBoxesArea ); +export default connect( + mapStateToProps, + undefined, + undefined, + { storeKey: 'edit-post' } +)( MetaBoxesArea ); diff --git a/editor/components/meta-boxes/meta-boxes-area/style.scss b/edit-post/components/meta-boxes/meta-boxes-area/style.scss similarity index 87% rename from editor/components/meta-boxes/meta-boxes-area/style.scss rename to edit-post/components/meta-boxes/meta-boxes-area/style.scss index dffb73969e431e..2e00937b43e9f7 100644 --- a/editor/components/meta-boxes/meta-boxes-area/style.scss +++ b/edit-post/components/meta-boxes/meta-boxes-area/style.scss @@ -1,5 +1,5 @@ -.editor-meta-boxes-area { +.edit-post-meta-boxes-area { position: relative; /* Match width and positioning of the meta boxes. Override default styles. */ @@ -80,17 +80,17 @@ bottom: 0; content: ''; background: transparent; - z-index: z-index( '.editor-meta-boxes-area.is-loading:before'); + z-index: z-index( '.edit-post-meta-boxes-area.is-loading:before'); } .spinner { position: absolute; top: 10px; right: 20px; - z-index: z-index( '.editor-meta-boxes-area .spinner'); + z-index: z-index( '.edit-post-meta-boxes-area .spinner'); } } -.editor-meta-boxes-area__clear { +.edit-post-meta-boxes-area__clear { clear: both; } diff --git a/editor/components/meta-boxes/meta-boxes-panel/index.js b/edit-post/components/meta-boxes/meta-boxes-panel/index.js similarity index 86% rename from editor/components/meta-boxes/meta-boxes-panel/index.js rename to edit-post/components/meta-boxes/meta-boxes-panel/index.js index 4ed5ab13847975..5c90e14ceae364 100644 --- a/editor/components/meta-boxes/meta-boxes-panel/index.js +++ b/edit-post/components/meta-boxes/meta-boxes-panel/index.js @@ -41,14 +41,14 @@ class MetaBoxesPanel extends Component { const { children, opened } = this.props; const isOpened = opened === undefined ? this.state.opened : opened; const icon = `arrow-${ isOpened ? 'down' : 'right' }`; - const className = classnames( 'editor-meta-boxes-panel__body', { 'is-opened': isOpened } ); + const className = classnames( 'edit-post-meta-boxes-panel__body', { 'is-opened': isOpened } ); return ( - + @@ -48,16 +47,16 @@ const SidebarHeader = ( { panel, onSetPanel, onToggleSidebar, count } ) => { }; export default compose( - query( ( select ) => ( { + withSelect( ( select ) => ( { count: select( 'core/editor' ).getSelectedBlockCount(), } ) ), connect( ( state ) => ( { - panel: getActivePanel( state ), + panel: getActiveEditorPanel( state ), } ), { - onSetPanel: setActivePanel, - onToggleSidebar: toggleSidebar, + onSetPanel: setGeneralSidebarActivePanel.bind( null, 'editor' ), + onCloseSidebar: closeGeneralSidebar, }, undefined, { storeKey: 'edit-post' } diff --git a/edit-post/components/sidebar/index.js b/edit-post/components/sidebar/index.js index 7c046e7d1359d3..6a696b443b5fde 100644 --- a/edit-post/components/sidebar/index.js +++ b/edit-post/components/sidebar/index.js @@ -16,10 +16,40 @@ import './style.scss'; import PostSettings from './post-settings'; import BlockInspectorPanel from './block-inspector-panel'; import Header from './header'; +import { getActiveEditorPanel } from '../../store/selectors'; -import { getActivePanel } from '../../store/selectors'; +/** + * Returns the panel that should be rendered in the sidebar. + * + * @param {string} panel The currently active panel. + * + * @return {Object} The React element to render as a panel. + */ +function getPanel( panel ) { + switch ( panel ) { + case 'document': + return PostSettings; + case 'block': + return BlockInspectorPanel; + default: + return PostSettings; + } +} +/** + * Renders a sidebar with the relevant panel. + * + * @param {string} panel The currently active panel. + * + * @return {Object} The rendered sidebar. + */ const Sidebar = ( { panel } ) => { + const ActivePanel = getPanel( panel ); + + const props = { + panel, + }; + return (
{ tabIndex="-1" >
- { panel === 'document' && } - { panel === 'block' && } +
); }; @@ -37,7 +66,7 @@ const Sidebar = ( { panel } ) => { export default connect( ( state ) => { return { - panel: getActivePanel( state ), + panel: getActiveEditorPanel( state ), }; }, undefined, diff --git a/edit-post/components/sidebar/last-revision/style.scss b/edit-post/components/sidebar/last-revision/style.scss index c344f8477c9f46..772119aa43d5c0 100644 --- a/edit-post/components/sidebar/last-revision/style.scss +++ b/edit-post/components/sidebar/last-revision/style.scss @@ -1,5 +1,8 @@ -.edit-post-last-revision__panel .edit-post-post-last-revision__title { - box-sizing: content-box; - margin: #{ -1 * $panel-padding }; - padding: $panel-padding; +// Needs specificity, because this panel is just a button +.components-panel__body.is-opened.edit-post-last-revision__panel { + padding: 0; +} + +.editor-post-last-revision__title { + padding: #{ $panel-padding - 3px } $panel-padding; // subtract extra height of dashicon } diff --git a/edit-post/components/sidebar/page-attributes/index.js b/edit-post/components/sidebar/page-attributes/index.js index 87e01ca8b18708..8dd4a7f37759b5 100644 --- a/edit-post/components/sidebar/page-attributes/index.js +++ b/edit-post/components/sidebar/page-attributes/index.js @@ -11,12 +11,12 @@ import { __ } from '@wordpress/i18n'; import { PanelBody, PanelRow, withAPIData } from '@wordpress/components'; import { compose } from '@wordpress/element'; import { PageAttributesCheck, PageAttributesOrder, PageAttributesParent, PageTemplate } from '@wordpress/editor'; -import { query } from '@wordpress/data'; +import { withSelect } from '@wordpress/data'; /** * Internal dependencies */ -import { toggleSidebarPanel } from '../../../store/actions'; +import { toggleGeneralSidebarEditorPanel } from '../../../store/actions'; import { isEditorSidebarPanelOpened } from '../../../store/selectors'; /** @@ -45,7 +45,7 @@ export function PageAttributes( { isOpened, onTogglePanel, postType } ) { ); } -const applyQuery = query( ( select ) => ( { +const applyWithSelect = withSelect( ( select ) => ( { postTypeSlug: select( 'core/editor' ).getEditedPostAttribute( 'type' ), } ) ); @@ -57,7 +57,7 @@ const applyConnect = connect( }, { onTogglePanel() { - return toggleSidebarPanel( PANEL_NAME ); + return toggleGeneralSidebarEditorPanel( PANEL_NAME ); }, }, undefined, @@ -72,7 +72,7 @@ const applyWithAPIData = withAPIData( ( props ) => { } ); export default compose( - applyQuery, + applyWithSelect, applyConnect, applyWithAPIData, )( PageAttributes ); diff --git a/edit-post/components/sidebar/post-excerpt/index.js b/edit-post/components/sidebar/post-excerpt/index.js index b7ecc0f3926229..cf46d944e2c54d 100644 --- a/edit-post/components/sidebar/post-excerpt/index.js +++ b/edit-post/components/sidebar/post-excerpt/index.js @@ -14,7 +14,7 @@ import { PostExcerpt as PostExcerptForm, PostExcerptCheck } from '@wordpress/edi * Internal Dependencies */ import { isEditorSidebarPanelOpened } from '../../../store/selectors'; -import { toggleSidebarPanel } from '../../../store/actions'; +import { toggleGeneralSidebarEditorPanel } from '../../../store/actions'; /** * Module Constants @@ -39,7 +39,7 @@ export default connect( }, { onTogglePanel() { - return toggleSidebarPanel( PANEL_NAME ); + return toggleGeneralSidebarEditorPanel( PANEL_NAME ); }, }, undefined, diff --git a/edit-post/components/sidebar/post-settings/index.js b/edit-post/components/sidebar/post-settings/index.js index d90da822a43903..71a6aad75406e2 100644 --- a/edit-post/components/sidebar/post-settings/index.js +++ b/edit-post/components/sidebar/post-settings/index.js @@ -2,7 +2,6 @@ * WordPress dependencies */ import { Panel } from '@wordpress/components'; -import { MetaBoxes } from '@wordpress/editor'; /** * Internal Dependencies @@ -16,6 +15,7 @@ import DiscussionPanel from '../discussion-panel'; import LastRevision from '../last-revision'; import PageAttributes from '../page-attributes'; import DocumentOutlinePanel from '../document-outline-panel'; +import MetaBoxes from '../../meta-boxes'; const panel = ( diff --git a/edit-post/components/sidebar/post-status/index.js b/edit-post/components/sidebar/post-status/index.js index f8051e6ab34459..dbdf0a7056e13a 100644 --- a/edit-post/components/sidebar/post-status/index.js +++ b/edit-post/components/sidebar/post-status/index.js @@ -23,7 +23,7 @@ import PostPendingStatus from '../post-pending-status'; import { isEditorSidebarPanelOpened, } from '../../../store/selectors'; -import { toggleSidebarPanel } from '../../../store/actions'; +import { toggleGeneralSidebarEditorPanel } from '../../../store/actions'; /** * Module Constants @@ -50,7 +50,7 @@ export default connect( } ), { onTogglePanel() { - return toggleSidebarPanel( PANEL_NAME ); + return toggleGeneralSidebarEditorPanel( PANEL_NAME ); }, }, undefined, diff --git a/edit-post/components/sidebar/post-taxonomies/index.js b/edit-post/components/sidebar/post-taxonomies/index.js index 995197bb250f8b..aebae97031f711 100644 --- a/edit-post/components/sidebar/post-taxonomies/index.js +++ b/edit-post/components/sidebar/post-taxonomies/index.js @@ -14,7 +14,7 @@ import { PostTaxonomies as PostTaxonomiesForm, PostTaxonomiesCheck } from '@word * Internal dependencies */ import { isEditorSidebarPanelOpened } from '../../../store/selectors'; -import { toggleSidebarPanel } from '../../../store/actions'; +import { toggleGeneralSidebarEditorPanel } from '../../../store/actions'; /** * Module Constants @@ -43,7 +43,7 @@ export default connect( }, { onTogglePanel() { - return toggleSidebarPanel( PANEL_NAME ); + return toggleGeneralSidebarEditorPanel( PANEL_NAME ); }, }, undefined, diff --git a/edit-post/components/sidebar/style.scss b/edit-post/components/sidebar/style.scss index d7de73101db4e2..8c85fd91d5ef3a 100644 --- a/edit-post/components/sidebar/style.scss +++ b/edit-post/components/sidebar/style.scss @@ -29,14 +29,12 @@ overflow: auto; -webkit-overflow-scrolling: touch; height: 100%; - padding-top: $panel-header-height; margin-top: -1px; margin-bottom: -1px; @include break-small() { overflow: inherit; height: auto; - padding-top: 0; } } @@ -90,13 +88,16 @@ } } -.edit-post-layout.is-sidebar-opened .edit-post-sidebar { - /* Sidebar covers screen on mobile */ - width: 100%; +.edit-post-layout.is-sidebar-opened { + .edit-post-sidebar, + .edit-post-plugins-panel { + /* Sidebar covers screen on mobile */ + width: 100%; - /* Sidebar sits on the side on larger breakpoints */ - @include break-medium() { - width: $sidebar-width; + /* Sidebar sits on the side on larger breakpoints */ + @include break-medium() { + width: $sidebar-width; + } } } @@ -123,6 +124,7 @@ padding: 0 20px; margin-left: 0; font-weight: 400; + @include square-style__neutral; &.is-active { border-bottom-color: $blue-medium-500; @@ -130,6 +132,6 @@ } &:focus { - @include button-style__focus-active; + @include square-style__focus-active; } } diff --git a/editor/hooks/index.js b/edit-post/hooks/index.js similarity index 56% rename from editor/hooks/index.js rename to edit-post/hooks/index.js index 540437a892858f..043eab09ed2a90 100644 --- a/editor/hooks/index.js +++ b/edit-post/hooks/index.js @@ -1,4 +1,4 @@ /** * Internal dependencies */ -import './copy-content'; +import './more-menu'; diff --git a/editor/hooks/copy-content/index.js b/edit-post/hooks/more-menu/copy-content-menu-item/index.js similarity index 59% rename from editor/hooks/copy-content/index.js rename to edit-post/hooks/more-menu/copy-content-menu-item/index.js index e72c9ea1aab121..42596cdd8c5d1a 100644 --- a/editor/hooks/copy-content/index.js +++ b/edit-post/hooks/more-menu/copy-content-menu-item/index.js @@ -3,11 +3,10 @@ */ import { ClipboardButton, withState } from '@wordpress/components'; import { compose } from '@wordpress/element'; -import { query } from '@wordpress/data'; -import { addFilter } from '@wordpress/hooks'; +import { withSelect } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; -function CopyContentButton( { editedPostContent, hasCopied, setState } ) { +function CopyContentMenuItem( { editedPostContent, hasCopied, setState } ) { return ( ( { +export default compose( + withSelect( ( select ) => ( { editedPostContent: select( 'core/editor' ).getEditedPostAttribute( 'content' ), } ) ), withState( { hasCopied: false } ) -)( CopyContentButton ); - -const buttonElement = ; - -addFilter( - 'editor.EditorActions.tools', - 'core/copy-content/button', - ( children ) => [ ...children, buttonElement ] -); +)( CopyContentMenuItem ); diff --git a/edit-post/hooks/more-menu/index.js b/edit-post/hooks/more-menu/index.js new file mode 100644 index 00000000000000..52989ffc575da8 --- /dev/null +++ b/edit-post/hooks/more-menu/index.js @@ -0,0 +1,20 @@ +/** + * WordPress dependencies + */ +import { addFilter } from '@wordpress/hooks'; + +/** + * Internal dependencies + */ +import CopyContentMenuItem from './copy-content-menu-item'; + +const withCopyContentMenuItem = ( menuItems ) => [ + ...menuItems, + , +]; + +addFilter( + 'editPost.MoreMenu.tools', + 'core/edit-post/more-menu/withCopyContentMenuItem', + withCopyContentMenuItem +); diff --git a/edit-post/index.js b/edit-post/index.js index 3fb2777fe1bc01..ef9bf85f6463f6 100644 --- a/edit-post/index.js +++ b/edit-post/index.js @@ -16,8 +16,12 @@ import { EditorProvider, ErrorBoundary } from '@wordpress/editor'; * Internal dependencies */ import './assets/stylesheets/main.scss'; +import './hooks'; import Layout from './components/layout'; import store from './store'; +import { initializeMetaBoxState } from './store/actions'; + +export * from './api'; // Configure moment globally moment.locale( dateSettings.l10n.locale ); @@ -88,7 +92,7 @@ export function initializeEditor( id, post, settings ) { const reboot = reinitializeEditor.bind( null, target, settings ); const ReduxProvider = createProvider( 'edit-post' ); - const provider = render( + render( @@ -100,6 +104,8 @@ export function initializeEditor( id, post, settings ) { ); return { - initializeMetaBoxes: provider.initializeMetaBoxes, + initializeMetaBoxes( metaBoxes ) { + store.dispatch( initializeMetaBoxState( metaBoxes ) ); + }, }; } diff --git a/edit-post/store/actions.js b/edit-post/store/actions.js index 0e61b4a900cae8..c635ab8f76ebe6 100644 --- a/edit-post/store/actions.js +++ b/edit-post/store/actions.js @@ -1,52 +1,107 @@ /** - * Returns an action object used in signalling that the user toggled the - * sidebar. - * - * @param {string} sidebar Name of the sidebar to toggle - * (desktop, mobile or publish). - * @param {boolean?} forcedValue Force a sidebar state. + * Returns an action object used in signalling that the user switched the active + * sidebar tab panel. * - * @return {Object} Action object. + * @param {string} sidebar Sidebar name + * @param {string} panel Panel name + * @return {Object} Action object */ -export function toggleSidebar( sidebar, forcedValue ) { +export function setGeneralSidebarActivePanel( sidebar, panel ) { return { - type: 'TOGGLE_SIDEBAR', + type: 'SET_GENERAL_SIDEBAR_ACTIVE_PANEL', sidebar, - forcedValue, + panel, }; } /** - * Returns an action object used in signalling that the user switched the active - * sidebar tab panel. + * Returns an action object used in signalling that the user opened a sidebar. * - * @param {string} panel The panel name. + * @param {string} sidebar Sidebar to open. + * @param {string} [panel = null] Panel to open in the sidebar. Null if unchanged. + * @return {Object} Action object. + */ +export function openGeneralSidebar( sidebar, panel = null ) { + return { + type: 'OPEN_GENERAL_SIDEBAR', + sidebar, + panel, + }; +} + +/** + * Returns an action object signalling that the user closed the sidebar. * * @return {Object} Action object. */ -export function setActivePanel( panel ) { +export function closeGeneralSidebar() { return { - type: 'SET_ACTIVE_PANEL', - panel, + type: 'CLOSE_GENERAL_SIDEBAR', }; } /** - * Returns an action object used in signalling that the user toggled a - * sidebar panel. + * Returns an action object used in signalling that the user opened the publish + * sidebar. * - * @param {string} panel The panel name. + * @return {Object} Action object + */ +export function openPublishSidebar() { + return { + type: 'OPEN_PUBLISH_SIDEBAR', + }; +} + +/** + * Returns an action object used in signalling that the user closed the + * publish sidebar. * * @return {Object} Action object. */ -export function toggleSidebarPanel( panel ) { +export function closePublishSidebar() { + return { + type: 'CLOSE_PUBLISH_SIDEBAR', + }; +} + +/** + * Returns an action object used in signalling that the user toggles the publish sidebar + * + * @return {Object} Action object + */ +export function togglePublishSidebar() { + return { + type: 'TOGGLE_PUBLISH_SIDEBAR', + }; +} + +/** + * Returns an action object used in signalling that use toggled a panel in the editor. + * + * @param {string} panel The panel to toggle. + * @return {Object} Action object. +*/ +export function toggleGeneralSidebarEditorPanel( panel ) { return { - type: 'TOGGLE_SIDEBAR_PANEL', + type: 'TOGGLE_GENERAL_SIDEBAR_EDITOR_PANEL', panel, }; } +/** + * Returns an action object used in signalling that the viewport type preference should be set. + * + * @param {string} viewportType The viewport type (desktop or mobile). + * @return {Object} Action object. + */ +export function setViewportType( viewportType ) { + return { + type: 'SET_VIEWPORT_TYPE', + viewportType, + }; +} + /** * Returns an action object used to toggle a feature flag. * @@ -67,3 +122,61 @@ export function switchEditorMode( mode ) { mode, }; } + +/** + * Returns an action object used to check the state of meta boxes at a location. + * + * This should only be fired once to initialize meta box state. If a meta box + * area is empty, this will set the store state to indicate that React should + * not render the meta box area. + * + * Example: metaBoxes = { side: true, normal: false }. + * + * This indicates that the sidebar has a meta box but the normal area does not. + * + * @param {Object} metaBoxes Whether meta box locations are active. + * + * @return {Object} Action object. + */ +export function initializeMetaBoxState( metaBoxes ) { + return { + type: 'INITIALIZE_META_BOX_STATE', + metaBoxes, + }; +} + +/** + * Returns an action object used to request meta box update. + * + * @return {Object} Action object. + */ +export function requestMetaBoxUpdates() { + return { + type: 'REQUEST_META_BOX_UPDATES', + }; +} + +/** + * Returns an action object used signal a successfull meta nox update. + * + * @return {Object} Action object. + */ +export function metaBoxUpdatesSuccess() { + return { + type: 'META_BOX_UPDATES_SUCCESS', + }; +} + +/** + * Returns an action object used set the saved meta boxes data. + * This is used to check if the meta boxes have been touched when leaving the editor. + * + * @param {Object} dataPerLocation Meta Boxes Data per location. + * @return {Object} Action object. + */ +export function setMetaBoxSavedData( dataPerLocation ) { + return { + type: 'META_BOX_SET_SAVED_DATA', + dataPerLocation, + }; +} diff --git a/edit-post/store/defaults.js b/edit-post/store/defaults.js index bae11caa8e7b88..bb5277c4efb797 100644 --- a/edit-post/store/defaults.js +++ b/edit-post/store/defaults.js @@ -1,9 +1,10 @@ export const PREFERENCES_DEFAULTS = { - mode: 'visual', - sidebars: { - desktop: true, - mobile: false, - publish: false, + editorMode: 'visual', + viewportType: 'desktop', // 'desktop' | 'mobile' + activeGeneralSidebar: 'editor', // null | 'editor' | 'plugin' + activeSidebarPanel: { // The keys in this object should match activeSidebarPanel values + editor: null, // 'document' | 'block' + plugin: null, // pluginId }, panels: { 'post-status': true }, features: { diff --git a/edit-post/store/effects.js b/edit-post/store/effects.js new file mode 100644 index 00000000000000..5a275aa94c89ca --- /dev/null +++ b/edit-post/store/effects.js @@ -0,0 +1,88 @@ +/** + * External dependencies + */ +import { reduce, values, some } from 'lodash'; + +/** + * WordPress dependencies + */ +import { select, subscribe } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { + metaBoxUpdatesSuccess, + setMetaBoxSavedData, + requestMetaBoxUpdates, +} from './actions'; +import { getMetaBoxes } from './selectors'; +import { getMetaBoxContainer } from '../utils/meta-boxes'; + +const effects = { + INITIALIZE_META_BOX_STATE( action, store ) { + const hasActiveMetaBoxes = some( action.metaBoxes ); + + // Allow toggling metaboxes panels + if ( hasActiveMetaBoxes ) { + window.postboxes.add_postbox_toggles( 'post' ); + } + + // Initialize metaboxes state + const dataPerLocation = reduce( action.metaBoxes, ( memo, isActive, location ) => { + if ( isActive ) { + memo[ location ] = jQuery( getMetaBoxContainer( location ) ).serialize(); + } + return memo; + }, {} ); + store.dispatch( setMetaBoxSavedData( dataPerLocation ) ); + + // Saving metaboxes when saving posts + let previousIsSaving = select( 'core/editor' ).isSavingPost(); + subscribe( () => { + const isSavingPost = select( 'core/editor' ).isSavingPost(); + const shouldTriggerSaving = ! isSavingPost && previousIsSaving; + previousIsSaving = isSavingPost; + if ( shouldTriggerSaving ) { + store.dispatch( requestMetaBoxUpdates() ); + } + } ); + }, + REQUEST_META_BOX_UPDATES( action, store ) { + const state = store.getState(); + const dataPerLocation = reduce( getMetaBoxes( state ), ( memo, metabox, location ) => { + if ( metabox.isActive ) { + memo[ location ] = jQuery( getMetaBoxContainer( location ) ).serialize(); + } + return memo; + }, {} ); + store.dispatch( setMetaBoxSavedData( dataPerLocation ) ); + + // Additional data needed for backwards compatibility. + // If we do not provide this data the post will be overriden with the default values. + const post = select( 'core/editor' ).getCurrentPost( state ); + const additionalData = [ + post.comment_status && `comment_status=${ post.comment_status }`, + post.ping_status && `ping_status=${ post.ping_status }`, + ].filter( Boolean ); + + // To save the metaboxes, we serialize each one of the location forms and combine them + // We also add the "common" hidden fields from the base .metabox-base-form + const formData = values( dataPerLocation ) + .concat( jQuery( '.metabox-base-form' ).serialize() ) + .concat( additionalData ) + .join( '&' ); + const fetchOptions = { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: formData, + credentials: 'include', + }; + + // Save the metaboxes + window.fetch( window._wpMetaBoxUrl, fetchOptions ) + .then( () => store.dispatch( metaBoxUpdatesSuccess() ) ); + }, +}; + +export default effects; diff --git a/edit-post/store/index.js b/edit-post/store/index.js index 3dfc4b96916636..d96869e558bbc7 100644 --- a/edit-post/store/index.js +++ b/edit-post/store/index.js @@ -8,8 +8,8 @@ import { registerReducer, withRehydratation, loadAndPersist } from '@wordpress/d */ import reducer from './reducer'; import enhanceWithBrowserSize from './mobile'; -import applyMiddlewares from './middlewares'; import { BREAK_MEDIUM } from './constants'; +import applyMiddlewares from './middlewares'; /** * Module Constants @@ -20,6 +20,7 @@ const MODULE_KEY = 'core/edit-post'; const store = applyMiddlewares( registerReducer( MODULE_KEY, withRehydratation( reducer, 'preferences', STORAGE_KEY ) ) ); + loadAndPersist( store, reducer, 'preferences', STORAGE_KEY ); enhanceWithBrowserSize( store, BREAK_MEDIUM ); diff --git a/edit-post/store/middlewares.js b/edit-post/store/middlewares.js index 54d0d0968a3cfa..790b6e548786e9 100644 --- a/edit-post/store/middlewares.js +++ b/edit-post/store/middlewares.js @@ -2,11 +2,12 @@ * External dependencies */ import { flowRight } from 'lodash'; +import refx from 'refx'; /** * Internal dependencies */ -import { mobileMiddleware } from '../utils/mobile'; +import effects from './effects'; /** * Applies the custom middlewares used specifically in the editor module. @@ -17,7 +18,7 @@ import { mobileMiddleware } from '../utils/mobile'; */ function applyMiddlewares( store ) { const middlewares = [ - mobileMiddleware, + refx( effects ), ]; let enhancedDispatch = () => { diff --git a/edit-post/store/reducer.js b/edit-post/store/reducer.js index 8ab871fbc97b0d..1fa763747c6883 100644 --- a/edit-post/store/reducer.js +++ b/edit-post/store/reducer.js @@ -22,15 +22,30 @@ import { PREFERENCES_DEFAULTS } from './defaults'; */ export function preferences( state = PREFERENCES_DEFAULTS, action ) { switch ( action.type ) { - case 'TOGGLE_SIDEBAR': + case 'OPEN_GENERAL_SIDEBAR': + const activeSidebarPanel = action.panel ? action.panel : state.activeSidebarPanel[ action.sidebar ]; return { ...state, - sidebars: { - ...state.sidebars, - [ action.sidebar ]: action.forcedValue !== undefined ? action.forcedValue : ! state.sidebars[ action.sidebar ], + activeGeneralSidebar: action.sidebar, + activeSidebarPanel: { + ...state.activeSidebarPanel, + [ action.sidebar ]: activeSidebarPanel, }, }; - case 'TOGGLE_SIDEBAR_PANEL': + case 'SET_GENERAL_SIDEBAR_ACTIVE_PANEL': + return { + ...state, + activeSidebarPanel: { + ...state.activeSidebarPanel, + [ action.sidebar ]: action.panel, + }, + }; + case 'CLOSE_GENERAL_SIDEBAR': + return { + ...state, + activeGeneralSidebar: null, + }; + case 'TOGGLE_GENERAL_SIDEBAR_EDITOR_PANEL': return { ...state, panels: { @@ -38,10 +53,27 @@ export function preferences( state = PREFERENCES_DEFAULTS, action ) { [ action.panel ]: ! get( state, [ 'panels', action.panel ], false ), }, }; + case 'SET_VIEWPORT_TYPE': + return { + ...state, + viewportType: action.viewportType, + }; + case 'UPDATE_MOBILE_STATE': + if ( action.isMobile ) { + return { + ...state, + viewportType: 'mobile', + activeGeneralSidebar: null, + }; + } + return { + ...state, + viewportType: 'desktop', + }; case 'SWITCH_MODE': return { ...state, - mode: action.mode, + editorMode: action.mode, }; case 'TOGGLE_FEATURE': return { @@ -67,6 +99,18 @@ export function panel( state = 'document', action ) { return state; } +export function publishSidebarActive( state = false, action ) { + switch ( action.type ) { + case 'OPEN_PUBLISH_SIDEBAR': + return true; + case 'CLOSE_PUBLISH_SIDEBAR': + return false; + case 'TOGGLE_PUBLISH_SIDEBAR': + return ! state; + } + return state; +} + export function mobile( state = false, action ) { if ( action.type === 'UPDATE_MOBILE_STATE' ) { return action.isMobile; @@ -74,8 +118,80 @@ export function mobile( state = false, action ) { return state; } +const locations = [ + 'normal', + 'side', + 'advanced', +]; + +const defaultMetaBoxState = locations.reduce( ( result, key ) => { + result[ key ] = { + isActive: false, + }; + + return result; +}, {} ); + +/** + * Reducer keeping track of the meta boxes isSaving state. + * A "true" value means the meta boxes saving request is in-flight. + * + * + * @param {boolean} state Previous state. + * @param {Object} action Action Object. + * @return {Object} Updated state. + */ +export function isSavingMetaBoxes( state = false, action ) { + switch ( action.type ) { + case 'REQUEST_META_BOX_UPDATES': + return true; + case 'META_BOX_UPDATES_SUCCESS': + return false; + default: + return state; + } +} + +/** + * Reducer keeping track of the state of each meta box location. + * This includes: + * - isActive: Whether the location is active or not. + * - data: The last saved form data for this location. + * This is used to check whether the form is dirty + * before leaving the page. + * + * @param {boolean} state Previous state. + * @param {Object} action Action Object. + * @return {Object} Updated state. + */ +export function metaBoxes( state = defaultMetaBoxState, action ) { + switch ( action.type ) { + case 'INITIALIZE_META_BOX_STATE': + return locations.reduce( ( newState, location ) => { + newState[ location ] = { + ...state[ location ], + isActive: action.metaBoxes[ location ], + }; + return newState; + }, { ...state } ); + case 'META_BOX_SET_SAVED_DATA': + return locations.reduce( ( newState, location ) => { + newState[ location ] = { + ...state[ location ], + data: action.dataPerLocation[ location ], + }; + return newState; + }, { ...state } ); + default: + return state; + } +} + export default combineReducers( { preferences, panel, + publishSidebarActive, mobile, + metaBoxes, + isSavingMetaBoxes, } ); diff --git a/edit-post/store/selectors.js b/edit-post/store/selectors.js index a1300fe6f0e9bd..bbf325d6351931 100644 --- a/edit-post/store/selectors.js +++ b/edit-post/store/selectors.js @@ -1,3 +1,9 @@ +/** + * External dependencies + */ +import createSelector from 'rememo'; +import { some } from 'lodash'; + /** * Returns the current editing mode. * @@ -6,7 +12,7 @@ * @return {string} Editing mode. */ export function getEditorMode( state ) { - return getPreference( state, 'mode', 'visual' ); + return getPreference( state, 'editorMode', 'visual' ); } /** @@ -16,8 +22,18 @@ export function getEditorMode( state ) { * * @return {string} Active sidebar panel. */ -export function getActivePanel( state ) { - return state.panel; +export function getActiveEditorPanel( state ) { + return getPreference( state, 'activeSidebarPanel', {} ).editor; +} + +/** + * Returns the current active plugin for the plugin sidebar. + * + * @param {Object} state Global application state + * @return {string} Active plugin sidebar plugin + */ +export function getActivePlugin( state ) { + return getPreference( state, 'activeSidebarPanel', {} ).plugin; } /** @@ -46,20 +62,37 @@ export function getPreference( state, preferenceKey, defaultValue ) { } /** - * Returns true if the sidebar is open, or false otherwise. + * Returns the opened general sidebar and null if the sidebar is closed. * - * @param {Object} state Global application state. - * @param {string} sidebar Sidebar name (leave undefined for the default sidebar). + * @param {Object} state Global application state. + * @return {string} The opened general sidebar panel. + */ +export function getOpenedGeneralSidebar( state ) { + return getPreference( state, 'activeGeneralSidebar' ); +} + +/** + * Returns true if the panel is open in the currently opened sidebar. * - * @return {boolean} Whether the given sidebar is open. + * @param {Object} state Global application state + * @param {string} sidebar Sidebar name (leave undefined for the default sidebar) + * @param {string} panel Sidebar panel name (leave undefined for the default panel) + * @return {boolean} Whether the given general sidebar panel is open */ -export function isSidebarOpened( state, sidebar ) { - const sidebars = getPreference( state, 'sidebars' ); - if ( sidebar !== undefined ) { - return sidebars[ sidebar ]; - } +export function isGeneralSidebarPanelOpened( state, sidebar, panel ) { + const activeGeneralSidebar = getPreference( state, 'activeGeneralSidebar' ); + const activeSidebarPanel = getPreference( state, 'activeSidebarPanel' ); + return activeGeneralSidebar === sidebar && activeSidebarPanel === panel; +} - return isMobile( state ) ? sidebars.mobile : sidebars.desktop; +/** + * Returns true if the publish sidebar is opened. + * + * @param {Object} state Global application state + * @return {boolean} Whether the publish sidebar is open. + */ +export function isPublishSidebarOpened( state ) { + return state.publishSidebarActive; } /** @@ -70,19 +103,18 @@ export function isSidebarOpened( state, sidebar ) { * @return {boolean} Whether sidebar is open. */ export function hasOpenSidebar( state ) { - const sidebars = getPreference( state, 'sidebars' ); - return isMobile( state ) ? - sidebars.mobile || sidebars.publish : - sidebars.desktop || sidebars.publish; + const generalSidebarOpen = getPreference( state, 'activeGeneralSidebar' ) !== null; + const publishSidebarOpen = state.publishSidebarActive; + + return generalSidebarOpen || publishSidebarOpen; } /** * Returns true if the editor sidebar panel is open, or false otherwise. * - * @param {Object} state Global application state. - * @param {string} panel Sidebar panel name. - * - * @return {boolean} Whether sidebar is open. + * @param {Object} state Global application state. + * @param {string} panel Sidebar panel name. + * @return {boolean} Whether the sidebar panel is open. */ export function isEditorSidebarPanelOpened( state, panel ) { const panels = getPreference( state, 'panels' ); @@ -118,8 +150,55 @@ export function hasFixedToolbar( state ) { * @param {Object} state Global application state. * @param {string} feature Feature slug. * - * @return {booleean} Is active. + * @return {boolean} Is active. */ export function isFeatureActive( state, feature ) { return !! state.preferences.features[ feature ]; } + +/** + * Returns the state of legacy meta boxes. + * + * @param {Object} state Global application state. + * @return {Object} State of meta boxes. + */ +export function getMetaBoxes( state ) { + return state.metaBoxes; +} + +/** + * Returns the state of legacy meta boxes. + * + * @param {Object} state Global application state. + * @param {string} location Location of the meta box. + * + * @return {Object} State of meta box at specified location. + */ +export function getMetaBox( state, location ) { + return getMetaBoxes( state )[ location ]; +} + +/** + * Returns true if the post is using Meta Boxes + * + * @param {Object} state Global application state + * @return {boolean} Whether there are metaboxes or not. + */ +export const hasMetaBoxes = createSelector( + ( state ) => { + return some( getMetaBoxes( state ), ( metaBox ) => { + return metaBox.isActive; + } ); + }, + ( state ) => state.metaBoxes, +); + +/** + * Returns true if the the Meta Boxes are being saved. + * + * @param {Object} state Global application state. + * @return {boolean} Whether the metaboxes are being saved. + */ +export function isSavingMetaBoxes( state ) { + return state.isSavingMetaBoxes; +} diff --git a/edit-post/store/test/actions.js b/edit-post/store/test/actions.js index 013f65e39bc7d5..7c1ff16c09d15d 100644 --- a/edit-post/store/test/actions.js +++ b/edit-post/store/test/actions.js @@ -2,41 +2,94 @@ * Internal dependencies */ import { - toggleSidebar, - setActivePanel, - toggleSidebarPanel, + setGeneralSidebarActivePanel, + toggleGeneralSidebarEditorPanel, + openGeneralSidebar, + closeGeneralSidebar, + openPublishSidebar, + closePublishSidebar, + togglePublishSidebar, + setViewportType, toggleFeature, + requestMetaBoxUpdates, + initializeMetaBoxState, } from '../actions'; describe( 'actions', () => { - describe( 'toggleSidebar', () => { - it( 'should return TOGGLE_SIDEBAR action', () => { - expect( toggleSidebar( 'publish', true ) ).toEqual( { - type: 'TOGGLE_SIDEBAR', - sidebar: 'publish', - forcedValue: true, + describe( 'setGeneralSidebarActivePanel', () => { + it( 'should return SET_GENERAL_SIDEBAR_ACTIVE_PANEL action', () => { + expect( setGeneralSidebarActivePanel( 'editor', 'document' ) ).toEqual( { + type: 'SET_GENERAL_SIDEBAR_ACTIVE_PANEL', + sidebar: 'editor', + panel: 'document', } ); } ); } ); - describe( 'setActivePanel', () => { - const panel = 'panelName'; - expect( setActivePanel( panel ) ).toEqual( { - type: 'SET_ACTIVE_PANEL', - panel, + describe( 'openGeneralSidebar', () => { + it( 'should return OPEN_GENERAL_SIDEBAR action', () => { + const sidebar = 'sidebarName'; + const panel = 'panelName'; + expect( openGeneralSidebar( sidebar, panel ) ).toEqual( { + type: 'OPEN_GENERAL_SIDEBAR', + sidebar, + panel, + } ); + } ); + } ); + + describe( 'closeGeneralSidebar', () => { + it( 'should return CLOSE_GENERAL_SIDEBAR action', () => { + expect( closeGeneralSidebar() ).toEqual( { + type: 'CLOSE_GENERAL_SIDEBAR', + } ); + } ); + } ); + + describe( 'openPublishSidebar', () => { + it( 'should return an OPEN_PUBLISH_SIDEBAR action', () => { + expect( openPublishSidebar() ).toEqual( { + type: 'OPEN_PUBLISH_SIDEBAR', + } ); + } ); + } ); + + describe( 'closePublishSidebar', () => { + it( 'should return an CLOSE_PUBLISH_SIDEBAR action', () => { + expect( closePublishSidebar() ).toEqual( { + type: 'CLOSE_PUBLISH_SIDEBAR', + } ); + } ); + } ); + + describe( 'togglePublishSidebar', () => { + it( 'should return an TOGGLE_PUBLISH_SIDEBAR action', () => { + expect( togglePublishSidebar() ).toEqual( { + type: 'TOGGLE_PUBLISH_SIDEBAR', + } ); } ); } ); describe( 'toggleSidebarPanel', () => { - it( 'should return TOGGLE_SIDEBAR_PANEL action', () => { + it( 'should return TOGGLE_GENERAL_SIDEBAR_EDITOR_PANEL action', () => { const panel = 'panelName'; - expect( toggleSidebarPanel( panel ) ).toEqual( { - type: 'TOGGLE_SIDEBAR_PANEL', + expect( toggleGeneralSidebarEditorPanel( panel ) ).toEqual( { + type: 'TOGGLE_GENERAL_SIDEBAR_EDITOR_PANEL', panel, } ); } ); } ); + describe( 'setViewportType', () => { + it( 'should return SET_VIEWPORT_TYPE action', () => { + const viewportType = 'mobile'; + expect( setViewportType( viewportType ) ).toEqual( { + type: 'SET_VIEWPORT_TYPE', + viewportType, + } ); + } ); + } ); + describe( 'toggleFeature', () => { it( 'should return TOGGLE_FEATURE action', () => { const feature = 'name'; @@ -46,4 +99,27 @@ describe( 'actions', () => { } ); } ); } ); + + describe( 'requestMetaBoxUpdates', () => { + it( 'should return the REQUEST_META_BOX_UPDATES action', () => { + expect( requestMetaBoxUpdates() ).toEqual( { + type: 'REQUEST_META_BOX_UPDATES', + } ); + } ); + } ); + + describe( 'initializeMetaBoxState', () => { + it( 'should return the META_BOX_STATE_CHANGED action with a hasChanged flag', () => { + const metaBoxes = { + side: true, + normal: true, + advanced: false, + }; + + expect( initializeMetaBoxState( metaBoxes ) ).toEqual( { + type: 'INITIALIZE_META_BOX_STATE', + metaBoxes, + } ); + } ); + } ); } ); diff --git a/edit-post/store/test/reducer.js b/edit-post/store/test/reducer.js index 2e829713d2f9f2..4f6a1407155427 100644 --- a/edit-post/store/test/reducer.js +++ b/edit-post/store/test/reducer.js @@ -8,6 +8,8 @@ import deepFreeze from 'deep-freeze'; */ import { preferences, + isSavingMetaBoxes, + metaBoxes, } from '../reducer'; describe( 'state', () => { @@ -16,64 +18,42 @@ describe( 'state', () => { const state = preferences( undefined, {} ); expect( state ).toEqual( { - mode: 'visual', - sidebars: { - desktop: true, - mobile: false, - publish: false, + activeGeneralSidebar: 'editor', + activeSidebarPanel: { + editor: null, + plugin: null, }, + editorMode: 'visual', panels: { 'post-status': true }, features: { fixedToolbar: false }, + viewportType: 'desktop', } ); } ); - it( 'should toggle the given sidebar flag', () => { - const state = preferences( deepFreeze( { sidebars: { - mobile: true, - desktop: true, - } } ), { - type: 'TOGGLE_SIDEBAR', - sidebar: 'desktop', - } ); - - expect( state.sidebars ).toEqual( { - mobile: true, - desktop: false, - } ); - } ); - - it( 'should set the sidebar open flag to true if unset', () => { - const state = preferences( deepFreeze( { sidebars: { - mobile: true, - } } ), { - type: 'TOGGLE_SIDEBAR', - sidebar: 'desktop', - } ); - - expect( state.sidebars ).toEqual( { - mobile: true, - desktop: true, - } ); - } ); - - it( 'should force the given sidebar flag', () => { - const state = preferences( deepFreeze( { sidebars: { - mobile: true, - } } ), { - type: 'TOGGLE_SIDEBAR', - sidebar: 'desktop', - forcedValue: false, + it( 'should set the general sidebar active panel', () => { + const state = preferences( deepFreeze( { + activeGeneralSidebar: 'editor', + activeSidebarPanel: { + editor: null, + plugin: null, + }, + } ), { + type: 'SET_GENERAL_SIDEBAR_ACTIVE_PANEL', + sidebar: 'editor', + panel: 'document', } ); - - expect( state.sidebars ).toEqual( { - mobile: true, - desktop: false, + expect( state ).toEqual( { + activeGeneralSidebar: 'editor', + activeSidebarPanel: { + editor: 'document', + plugin: null, + }, } ); } ); it( 'should set the sidebar panel open flag to true if unset', () => { const state = preferences( deepFreeze( {} ), { - type: 'TOGGLE_SIDEBAR_PANEL', + type: 'TOGGLE_GENERAL_SIDEBAR_EDITOR_PANEL', panel: 'post-taxonomies', } ); @@ -82,7 +62,7 @@ describe( 'state', () => { it( 'should toggle the sidebar panel open flag', () => { const state = preferences( deepFreeze( { panels: { 'post-taxonomies': true } } ), { - type: 'TOGGLE_SIDEBAR_PANEL', + type: 'TOGGLE_GENERAL_SIDEBAR_EDITOR_PANEL', panel: 'post-taxonomies', } ); @@ -95,7 +75,7 @@ describe( 'state', () => { mode: 'text', } ); - expect( state ).toEqual( { mode: 'text' } ); + expect( state ).toEqual( { editorMode: 'text' } ); } ); it( 'should toggle a feature flag', () => { @@ -106,4 +86,92 @@ describe( 'state', () => { expect( state ).toEqual( { features: { chicken: false } } ); } ); } ); + + describe( 'isSavingMetaBoxes', () => { + it( 'should return default state', () => { + const actual = isSavingMetaBoxes( undefined, {} ); + expect( actual ).toBe( false ); + } ); + + it( 'should set saving flag to true', () => { + const action = { + type: 'REQUEST_META_BOX_UPDATES', + }; + const actual = isSavingMetaBoxes( false, action ); + + expect( actual ).toBe( true ); + } ); + + it( 'should set saving flag to false', () => { + const action = { + type: 'META_BOX_UPDATES_SUCCESS', + }; + const actual = isSavingMetaBoxes( true, action ); + + expect( actual ).toBe( false ); + } ); + } ); + + describe( 'metaBoxes()', () => { + it( 'should return default state', () => { + const actual = metaBoxes( undefined, {} ); + const expected = { + normal: { + isActive: false, + }, + side: { + isActive: false, + }, + advanced: { + isActive: false, + }, + }; + + expect( actual ).toEqual( expected ); + } ); + + it( 'should set the sidebar to active', () => { + const theMetaBoxes = { + normal: false, + advanced: false, + side: true, + }; + + const action = { + type: 'INITIALIZE_META_BOX_STATE', + metaBoxes: theMetaBoxes, + }; + + const actual = metaBoxes( undefined, action ); + const expected = { + normal: { + isActive: false, + }, + side: { + isActive: true, + }, + advanced: { + isActive: false, + }, + }; + + expect( actual ).toEqual( expected ); + } ); + + it( 'should set the meta boxes saved data', () => { + const action = { + type: 'META_BOX_SET_SAVED_DATA', + dataPerLocation: { + side: 'a=b', + }, + }; + + const theMetaBoxes = metaBoxes( { normal: { isActive: true }, side: { isActive: false } }, action ); + expect( theMetaBoxes ).toEqual( { + advanced: { data: undefined }, + normal: { isActive: true, data: undefined }, + side: { isActive: false, data: 'a=b' }, + } ); + } ); + } ); } ); diff --git a/edit-post/store/test/selectors.js b/edit-post/store/test/selectors.js index 581b0db3a0cbcd..689d30afdf6e44 100644 --- a/edit-post/store/test/selectors.js +++ b/edit-post/store/test/selectors.js @@ -4,12 +4,16 @@ import { getEditorMode, getPreference, - isSidebarOpened, + isGeneralSidebarPanelOpened, hasOpenSidebar, isEditorSidebarPanelOpened, isMobile, hasFixedToolbar, isFeatureActive, + getMetaBoxes, + hasMetaBoxes, + isSavingMetaBoxes, + getMetaBox, } from '../selectors'; jest.mock( '../constants', () => ( { @@ -20,7 +24,7 @@ describe( 'selectors', () => { describe( 'getEditorMode', () => { it( 'should return the selected editor mode', () => { const state = { - preferences: { mode: 'text' }, + preferences: { editorMode: 'text' }, }; expect( getEditorMode( state ) ).toEqual( 'text' ); @@ -61,143 +65,66 @@ describe( 'selectors', () => { } ); } ); - describe( 'isSidebarOpened', () => { - it( 'should return true when is not mobile and the normal sidebar is opened', () => { + describe( 'isGeneralSidebarPanelOpened', () => { + it( 'should return true when specified the sidebar panel is opened', () => { const state = { - mobile: false, - preferences: { - sidebars: { - desktop: true, - mobile: false, - }, - }, - }; - - expect( isSidebarOpened( state ) ).toBe( true ); - } ); - - it( 'should return false when is not mobile and the normal sidebar is closed', () => { - const state = { - mobile: false, - preferences: { - sidebars: { - desktop: false, - mobile: true, - }, - }, - }; - - expect( isSidebarOpened( state ) ).toBe( false ); - } ); - - it( 'should return true when is mobile and the mobile sidebar is opened', () => { - const state = { - mobile: true, preferences: { - sidebars: { - desktop: false, - mobile: true, - }, + activeGeneralSidebar: 'editor', + viewportType: 'desktop', + activeSidebarPanel: 'document', }, }; + const panel = 'document'; + const sidebar = 'editor'; - expect( isSidebarOpened( state ) ).toBe( true ); + expect( isGeneralSidebarPanelOpened( state, sidebar, panel ) ).toBe( true ); } ); - it( 'should return false when is mobile and the mobile sidebar is closed', () => { + it( 'should return false when another panel than the specified sidebar panel is opened', () => { const state = { - mobile: true, preferences: { - sidebars: { - desktop: true, - mobile: false, - }, + activeGeneralSidebar: 'editor', + viewportType: 'desktop', + activeSidebarPanel: 'blocks', }, }; + const panel = 'document'; + const sidebar = 'editor'; - expect( isSidebarOpened( state ) ).toBe( false ); + expect( isGeneralSidebarPanelOpened( state, sidebar, panel ) ).toBe( false ); } ); - it( 'should return true when the given is opened', () => { + it( 'should return false when no sidebar panel is opened', () => { const state = { preferences: { - sidebars: { - publish: true, - }, + activeGeneralSidebar: null, + viewportType: 'desktop', + activeSidebarPanel: null, }, }; + const panel = 'blocks'; + const sidebar = 'editor'; - expect( isSidebarOpened( state, 'publish' ) ).toBe( true ); - } ); - - it( 'should return false when the given is not opened', () => { - const state = { - preferences: { - sidebars: { - publish: false, - }, - }, - }; - - expect( isSidebarOpened( state, 'publish' ) ).toBe( false ); + expect( isGeneralSidebarPanelOpened( state, sidebar, panel ) ).toBe( false ); } ); } ); describe( 'hasOpenSidebar', () => { - it( 'should return true if at least one sidebar is open (using the desktop sidebar as default)', () => { - const state = { - mobile: false, - preferences: { - sidebars: { - desktop: true, - mobile: false, - publish: false, - }, - }, - }; - - expect( hasOpenSidebar( state ) ).toBe( true ); - } ); - - it( 'should return true if at no sidebar is open (using the desktop sidebar as default)', () => { - const state = { - mobile: false, - preferences: { - sidebars: { - desktop: false, - mobile: true, - publish: false, - }, - }, - }; - - expect( hasOpenSidebar( state ) ).toBe( false ); - } ); - - it( 'should return true if at least one sidebar is open (using the mobile sidebar as default)', () => { + it( 'should return true if at least one sidebar is open', () => { const state = { - mobile: true, preferences: { - sidebars: { - desktop: false, - mobile: true, - publish: false, - }, + activeSidebarPanel: null, }, }; expect( hasOpenSidebar( state ) ).toBe( true ); } ); - it( 'should return true if at no sidebar is open (using the mobile sidebar as default)', () => { + it( 'should return false if no sidebar is open', () => { const state = { - mobile: true, + publishSidebarActive: false, preferences: { - sidebars: { - desktop: true, - mobile: false, - publish: false, - }, + activeGeneralSidebar: null, }, }; @@ -339,4 +266,96 @@ describe( 'selectors', () => { expect( isFeatureActive( state, 'chicken' ) ).toBe( false ); } ); } ); + describe( 'hasMetaBoxes', () => { + it( 'should return true if there are active meta boxes', () => { + const state = { + metaBoxes: { + normal: { + isActive: false, + }, + side: { + isActive: true, + }, + }, + }; + + expect( hasMetaBoxes( state ) ).toBe( true ); + } ); + + it( 'should return false if there are no active meta boxes', () => { + const state = { + metaBoxes: { + normal: { + isActive: false, + }, + side: { + isActive: false, + }, + }, + }; + + expect( hasMetaBoxes( state ) ).toBe( false ); + } ); + } ); + + describe( 'isSavingMetaBoxes', () => { + it( 'should return true if some meta boxes are saving', () => { + const state = { + isSavingMetaBoxes: true, + }; + + expect( isSavingMetaBoxes( state ) ).toBe( true ); + } ); + + it( 'should return false if no meta boxes are saving', () => { + const state = { + isSavingMetaBoxes: false, + }; + + expect( isSavingMetaBoxes( state ) ).toBe( false ); + } ); + } ); + + describe( 'getMetaBoxes', () => { + it( 'should return the state of all meta boxes', () => { + const state = { + metaBoxes: { + normal: { + isActive: true, + }, + side: { + isActive: true, + }, + }, + }; + + expect( getMetaBoxes( state ) ).toEqual( { + normal: { + isActive: true, + }, + side: { + isActive: true, + }, + } ); + } ); + } ); + + describe( 'getMetaBox', () => { + it( 'should return the state of selected meta box', () => { + const state = { + metaBoxes: { + normal: { + isActive: false, + }, + side: { + isActive: true, + }, + }, + }; + + expect( getMetaBox( state, 'side' ) ).toEqual( { + isActive: true, + } ); + } ); + } ); } ); diff --git a/editor/utils/meta-boxes.js b/edit-post/utils/meta-boxes.js similarity index 80% rename from editor/utils/meta-boxes.js rename to edit-post/utils/meta-boxes.js index 6bf1e6d835b427..055a239f1c1d92 100644 --- a/editor/utils/meta-boxes.js +++ b/edit-post/utils/meta-boxes.js @@ -7,7 +7,7 @@ * @return {string} HTML content. */ export const getMetaBoxContainer = ( location ) => { - const area = document.querySelector( `.editor-meta-boxes-area.is-${ location } .metabox-location-${ location }` ); + const area = document.querySelector( `.edit-post-meta-boxes-area.is-${ location } .metabox-location-${ location }` ); if ( area ) { return area; } diff --git a/edit-post/utils/mobile/README.md b/edit-post/utils/mobile/README.md deleted file mode 100644 index fff39c3f681c85..00000000000000 --- a/edit-post/utils/mobile/README.md +++ /dev/null @@ -1,15 +0,0 @@ -mobileMiddleware -=========== - -`mobileMiddleware` is a very simple [redux middleware](https://redux.js.org/docs/advanced/Middleware.html) that sets the isSidebarOpened flag to false on REDUX_REHYDRATE payloads. -This useful to make isSidebarOpened false on mobile even if the value that was saved to local storage was true. -The middleware just needs to be added to the enhancers list: - -```js - const enhancers = [ - ... - applyMiddleware( mobileMiddleware ), - ]; - ... - const store = createStore( reducer, flowRight( enhancers ) ); -``` diff --git a/edit-post/utils/mobile/index.js b/edit-post/utils/mobile/index.js deleted file mode 100644 index 67e941bb2bd406..00000000000000 --- a/edit-post/utils/mobile/index.js +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Internal dependencies - */ -import { isMobile } from '../../store/selectors'; -import { toggleSidebar } from '../../store/actions'; - -/** - * Middleware - */ - -export const mobileMiddleware = ( { getState } ) => next => action => { - if ( action.type === 'TOGGLE_SIDEBAR' && action.sidebar === undefined ) { - return next( toggleSidebar( isMobile( getState() ) ? 'mobile' : 'desktop', action.forcedValue ) ); - } - return next( action ); -}; diff --git a/editor/components/block-list/block.js b/editor/components/block-list/block.js index 1faf219674d35a..6f00b36419e0d1 100644 --- a/editor/components/block-list/block.js +++ b/editor/components/block-list/block.js @@ -3,13 +3,20 @@ */ import { connect } from 'react-redux'; import classnames from 'classnames'; -import { get, reduce, size, castArray, noop } from 'lodash'; +import { get, reduce, size, castArray, noop, first, last } from 'lodash'; +import tinymce from 'tinymce'; /** * WordPress dependencies */ import { Component, findDOMNode, compose } from '@wordpress/element'; -import { keycodes } from '@wordpress/utils'; +import { + keycodes, + focus, + getScrollContainer, + placeCaretAtHorizontalEdge, + placeCaretAtVerticalEdge, +} from '@wordpress/utils'; import { BlockEdit, createBlock, @@ -25,7 +32,6 @@ import { __, sprintf } from '@wordpress/i18n'; /** * Internal dependencies */ -import { getScrollContainer } from '../../utils/dom'; import BlockMover from '../block-mover'; import BlockDropZone from '../block-drop-zone'; import BlockSettingsMenu from '../block-settings-menu'; @@ -60,7 +66,6 @@ import { getEditedPostAttribute, getNextBlockUid, getPreviousBlockUid, - isBlockHovered, isBlockMultiSelected, isBlockSelected, isFirstMultiSelectedBlock, @@ -68,6 +73,7 @@ import { isTyping, getBlockMode, getCurrentPostType, + getSelectedBlocksInitialCaretPosition, } from '../../store/selectors'; const { BACKSPACE, ESCAPE, DELETE, ENTER, UP, RIGHT, DOWN, LEFT } = keycodes; @@ -80,6 +86,7 @@ export class BlockListBlock extends Component { this.bindBlockNode = this.bindBlockNode.bind( this ); this.setAttributes = this.setAttributes.bind( this ); this.maybeHover = this.maybeHover.bind( this ); + this.hideHoverEffects = this.hideHoverEffects.bind( this ); this.maybeStartTyping = this.maybeStartTyping.bind( this ); this.stopTypingOnMouseMove = this.stopTypingOnMouseMove.bind( this ); this.mergeBlocks = this.mergeBlocks.bind( this ); @@ -99,6 +106,7 @@ export class BlockListBlock extends Component { this.state = { error: null, + isHovered: false, isSelectionCollapsed: true, }; } @@ -125,6 +133,10 @@ export class BlockListBlock extends Component { document.addEventListener( 'mousemove', this.stopTypingOnMouseMove ); } document.addEventListener( 'selectionchange', this.onSelectionChange ); + + if ( this.props.isSelected ) { + this.focusTabbable(); + } } componentWillReceiveProps( newProps ) { @@ -134,6 +146,10 @@ export class BlockListBlock extends Component { ) { this.previousOffset = this.node.getBoundingClientRect().top; } + + if ( newProps.isTyping || newProps.isSelected ) { + this.hideHoverEffects(); + } } componentDidUpdate( prevProps ) { @@ -157,6 +173,10 @@ export class BlockListBlock extends Component { this.removeStopTypingListener(); } } + + if ( this.props.isSelected && ! prevProps.isSelected ) { + this.focusTabbable(); + } } componentWillUnmount() { @@ -188,6 +208,47 @@ export class BlockListBlock extends Component { this.node = findDOMNode( node ); } + /** + * When a block becomces selected, transition focus to an inner tabbable. + */ + focusTabbable() { + const { initialPosition } = this.props; + + if ( this.node.contains( document.activeElement ) ) { + return; + } + + // Find all tabbables within node. + const tabbables = focus.tabbable.find( this.node ) + .filter( ( node ) => node !== this.node ); + + // If reversed (e.g. merge via backspace), use the last in the set of + // tabbables. + const isReverse = -1 === initialPosition; + const target = ( isReverse ? last : first )( tabbables ); + + if ( ! target ) { + return; + } + + target.focus(); + + // In reverse case, need to explicitly place caret position. + if ( isReverse ) { + // Special case RichText component because the placeCaret utilities + // aren't working correctly. When merging two paragraph blocks, the + // focus is not moved to the correct position. + const editor = tinymce.get( target.getAttribute( 'id' ) ); + if ( editor ) { + editor.selection.select( editor.getBody(), true ); + editor.selection.collapse( false ); + } else { + placeCaretAtHorizontalEdge( target, true ); + placeCaretAtVerticalEdge( target, true ); + } + } + } + setAttributes( attributes ) { const { block, onChange } = this.props; const type = getBlockType( block.name ); @@ -231,13 +292,26 @@ export class BlockListBlock extends Component { * @see https://developer.mozilla.org/en-US/docs/Web/Events/mouseenter */ maybeHover() { - const { isHovered, isMultiSelected, onHover } = this.props; + const { isMultiSelected, isSelected } = this.props; + const { isHovered } = this.state; - if ( isHovered || isMultiSelected || this.hadTouchStart ) { + if ( isHovered || isMultiSelected || isSelected || + this.props.isMultiSelecting || this.hadTouchStart ) { return; } - onHover(); + this.setState( { isHovered: true } ); + } + + /** + * Sets the block state as unhovered if currently hovering. There are cases + * where mouseleave may occur but the block is not hovered (multi-select), + * so to avoid unnecesary renders, the state is only set if hovered. + */ + hideHoverEffects() { + if ( this.state.isHovered ) { + this.setState( { isHovered: false } ); + } } maybeStartTyping() { @@ -301,6 +375,14 @@ export class BlockListBlock extends Component { * @return {void} */ onFocus( event ) { + // Firefox-specific: Firefox will redirect focus of an already-focused + // node to its parent, but assign a property before doing so. If that + // property exists, ensure that it is the node, or abort. + const { explicitOriginalTarget } = event.nativeEvent; + if ( explicitOriginalTarget && explicitOriginalTarget !== this.node ) { + return; + } + if ( event.target === this.node && ! this.props.isSelected ) { this.props.onSelect(); } @@ -435,11 +517,12 @@ export class BlockListBlock extends Component { rootUID, layout, renderBlockMenu, - isHovered, isSelected, isMultiSelected, isFirstMultiSelected, + isLastInSelection, } = this.props; + const isHovered = this.state.isHovered && ! this.props.isMultiSelecting; const { name: blockName, isValid } = block; const blockType = getBlockType( blockName ); // translators: %s: Type of block (i.e. Text, Image etc) @@ -459,6 +542,11 @@ export class BlockListBlock extends Component { const shouldShowMobileToolbar = shouldAppearSelected; const { error } = this.state; + // Insertion point can only be made visible when the side inserter is + // not present, and either the block is at the extent of a selection or + // is the last block in the top-level list rendering. + const shouldShowInsertionPoint = ! showSideInserter && ( isLastInSelection || ( isLast && ! rootUID ) ); + // Generate the wrapper class names handling the different states of the block. const wrapperClassName = classnames( 'editor-block-list__block', { 'has-warning': ! isValid || !! error, @@ -468,12 +556,15 @@ export class BlockListBlock extends Component { 'is-reusable': isReusableBlock( blockType ), } ); - const { onMouseLeave, onReplace } = this.props; + const { onReplace } = this.props; // Determine whether the block has props to apply to the wrapper. - let wrapperProps; + let wrapperProps = this.props.wrapperProps; if ( blockType.getEditWrapperProps ) { - wrapperProps = blockType.getEditWrapperProps( block.attributes ); + wrapperProps = { + ...wrapperProps, + ...blockType.getEditWrapperProps( block.attributes ), + }; } // Disable reasons: @@ -489,7 +580,7 @@ export class BlockListBlock extends Component { } { !! error && } - { ! showSideInserter && ( + { shouldShowInsertionPoint && ( ) } { showSideInserter && ( @@ -594,7 +684,8 @@ const mapStateToProps = ( state, { uid, rootUID } ) => { block: getBlock( state, uid ), isMultiSelected: isBlockMultiSelected( state, uid ), isFirstMultiSelected: isFirstMultiSelectedBlock( state, uid ), - isHovered: isBlockHovered( state, uid ) && ! isMultiSelecting( state ), + isMultiSelecting: isMultiSelecting( state ), + isLastInSelection: state.blockSelection.end === uid, // 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. isTyping: isSelected && isTyping( state ), @@ -603,6 +694,7 @@ const mapStateToProps = ( state, { uid, rootUID } ) => { mode: getBlockMode( state, uid ), isSelectionEnabled: isSelectionEnabled( state ), postType: getCurrentPostType( state ), + initialPosition: getSelectedBlocksInitialCaretPosition( state ), isSelected, }; }; @@ -628,21 +720,6 @@ const mapDispatchToProps = ( dispatch, ownProps ) => ( { dispatch( stopTyping() ); }, - onHover() { - dispatch( { - type: 'TOGGLE_BLOCK_HOVERED', - hovered: true, - uid: ownProps.uid, - } ); - }, - onMouseLeave() { - dispatch( { - type: 'TOGGLE_BLOCK_HOVERED', - hovered: false, - uid: ownProps.uid, - } ); - }, - onInsertBlocks( blocks, index ) { const { rootUID, layout } = ownProps; diff --git a/editor/components/block-list/insertion-point.js b/editor/components/block-list/insertion-point.js index 919df2c499d4a6..e109cad8bb53a7 100644 --- a/editor/components/block-list/insertion-point.js +++ b/editor/components/block-list/insertion-point.js @@ -10,6 +10,7 @@ import { getBlockIndex, getBlockInsertionPoint, isBlockInsertionPointVisible, + getBlockCount, } from '../../store/selectors'; function BlockInsertionPoint( { showInsertionPoint } ) { @@ -21,14 +22,16 @@ function BlockInsertionPoint( { showInsertionPoint } ) { } export default connect( - ( state, { uid, rootUID, layout } ) => { + ( state, { uid, rootUID } ) => { const blockIndex = uid ? getBlockIndex( state, uid, rootUID ) : -1; - const insertIndex = blockIndex > -1 ? blockIndex + 1 : 0; + const insertIndex = blockIndex > -1 ? blockIndex + 1 : getBlockCount( state ); + const insertionPoint = getBlockInsertionPoint( state ); return { showInsertionPoint: ( - isBlockInsertionPointVisible( state, rootUID, layout ) && - getBlockInsertionPoint( state, rootUID ) === insertIndex + isBlockInsertionPointVisible( state ) && + insertionPoint.index === insertIndex && + insertionPoint.rootUID === rootUID ), }; }, diff --git a/editor/components/block-list/invalid-block-warning.js b/editor/components/block-list/invalid-block-warning.js index 7caf10850510c4..c6b60d90ed7422 100644 --- a/editor/components/block-list/invalid-block-warning.js +++ b/editor/components/block-list/invalid-block-warning.js @@ -6,12 +6,12 @@ import { connect } from 'react-redux'; /** * WordPress dependencies */ -import { __, sprintf } from '@wordpress/i18n'; +import { __ } from '@wordpress/i18n'; import { Button } from '@wordpress/components'; import { getBlockType, - getUnknownTypeHandlerName, createBlock, + rawHandler, } from '@wordpress/blocks'; /** @@ -20,46 +20,19 @@ import { import { replaceBlock } from '../../store/actions'; import Warning from '../warning'; -function InvalidBlockWarning( { ignoreInvalid, switchToBlockType } ) { - const htmlBlockName = 'core/html'; - const defaultBlockType = getBlockType( getUnknownTypeHandlerName() ); - const htmlBlockType = getBlockType( htmlBlockName ); - const switchTo = ( blockType ) => () => switchToBlockType( blockType ); +function InvalidBlockWarning( { convertToHTML, convertToBlocks } ) { + const hasHTMLBlock = !! getBlockType( 'core/html' ); return ( -

{ defaultBlockType && htmlBlockType && sprintf( __( - 'This block appears to have been modified externally. ' + - 'Overwrite the changes or Convert to %s or %s to keep ' + - 'your changes.' - ), defaultBlockType.title, htmlBlockType.title ) }

+

{ __( 'This block appears to have been modified externally.' ) }

- - { defaultBlockType && ( - - ) } - - { htmlBlockType && ( - ) }

@@ -69,24 +42,17 @@ function InvalidBlockWarning( { ignoreInvalid, switchToBlockType } ) { export default connect( null, - ( dispatch, ownProps ) => { - return { - ignoreInvalid() { - const { block } = ownProps; - const { name, attributes } = block; - const nextBlock = createBlock( name, attributes ); - dispatch( replaceBlock( block.uid, nextBlock ) ); - }, - switchToBlockType( blockType ) { - const { block } = ownProps; - if ( blockType && block ) { - const nextBlock = createBlock( blockType.name, { - content: block.originalContent, - } ); - - dispatch( replaceBlock( block.uid, nextBlock ) ); - } - }, - }; - } + ( dispatch, { block } ) => ( { + convertToHTML() { + dispatch( replaceBlock( block.uid, createBlock( 'core/html', { + content: block.originalContent, + } ) ) ); + }, + convertToBlocks() { + dispatch( replaceBlock( block.uid, rawHandler( { + HTML: block.originalContent, + mode: 'BLOCKS', + } ) ) ); + }, + } ) )( InvalidBlockWarning ); diff --git a/editor/components/block-list/layout.js b/editor/components/block-list/layout.js index 784988a6be11df..7a89328cf0ce5f 100644 --- a/editor/components/block-list/layout.js +++ b/editor/components/block-list/layout.js @@ -29,6 +29,8 @@ import DefaultBlockAppender from '../default-block-appender'; import { isSelectionEnabled, isMultiSelecting, + getMultiSelectedBlocksStartUid, + getMultiSelectedBlocksEndUid, } from '../../store/selectors'; import { startMultiSelect, stopMultiSelect, multiSelect, selectBlock } from '../../store/actions'; @@ -128,6 +130,11 @@ class BlockListLayout extends Component { window.addEventListener( 'mouseup', this.onSelectionEnd ); } + /** + * Handles multi-selection changes in response to pointer move. + * + * @param {string} uid Block under cursor in multi-select drag. + */ onSelectionChange( uid ) { const { onMultiSelect, selectionStart, selectionEnd } = this.props; const { selectionAtStart } = this; @@ -137,10 +144,13 @@ class BlockListLayout extends Component { return; } + // If multi-selecting and cursor extent returns to the start of + // selection, cancel multi-select. if ( isAtStart && selectionStart ) { onMultiSelect( null, null ); } + // Expand multi-selection to block under cursor. if ( ! isAtStart && selectionEnd !== uid ) { onMultiSelect( selectionAtStart, uid ); } @@ -199,7 +209,7 @@ class BlockListLayout extends Component { defaultLayout = layout; } - const classes = classnames( { + const classes = classnames( 'editor-block-list__layout', { [ `layout-${ layout }` ]: layout, } ); @@ -236,6 +246,8 @@ export default connect( // Reference block selection value directly, since current selectors // assume either multi-selection (getMultiSelectedBlocksStartUid) or // singular-selection (getSelectedBlock) exclusively. + selectionStart: getMultiSelectedBlocksStartUid( state ), + selectionEnd: getMultiSelectedBlocksEndUid( state ), selectionStartUID: state.blockSelection.start, isSelectionEnabled: isSelectionEnabled( state ), isMultiSelecting: isMultiSelecting( state ), diff --git a/editor/components/block-list/style.scss b/editor/components/block-list/style.scss index 68b409229c8049..05d1db6af689f1 100644 --- a/editor/components/block-list/style.scss +++ b/editor/components/block-list/style.scss @@ -1,13 +1,29 @@ -.edit-post-visual-editor .editor-block-list__block { - margin-bottom: $block-spacing; +.editor-block-list__layout .editor-default-block-appender, +.editor-block-list__layout .editor-block-list__block { position: relative; - padding: $block-padding; + margin-bottom: $block-spacing; + padding-left: $block-padding; + padding-right: $block-padding; @include break-small { // The block mover needs to stay inside the block to allow clicks when hovering the block - padding: $block-padding $block-padding + $block-mover-padding-visible; + padding-left: $block-padding + $block-mover-padding-visible; + padding-right: $block-padding + $block-mover-padding-visible; + } + + // Prevent collapsing margins + // This allows us control over block boundaries and how blocks fit together visually + // It makes things a lot simpler, however it also means block margins and paddings have to be tuned (halved) for the editor. + padding-top: .05px; + padding-bottom: .05px; + + // Space every block using margin instead of padding + .editor-block-list__block-edit { + margin-top: $block-padding; + margin-bottom: $block-padding; } + // Block outline container &:before { z-index: z-index( '.editor-block-list__block:before' ); content: ''; @@ -24,6 +40,7 @@ } } + // Block warnings &.has-warning .editor-block-list__block-edit { position: relative; min-height: 250px; @@ -121,85 +138,71 @@ display: none; } - // Alignments + + /** + * Alignments + */ + &[data-align="left"], &[data-align="right"] { // Without z-index, won't be clickable as "above" adjacent content z-index: z-index( '.editor-block-list__block {core/image aligned left or right}' ); width: 100%; - max-width: 370px; // Needed for blocks with no intrinsic width, like Cover Image or Gallery - } - - &[data-align="left"] { - float: left; - // mobile, and no sidebars - margin-right: $block-padding; + // When images are floated, the block itself should collapse to zero height + margin-bottom: 0; - // sidebar (folded) - .auto-fold .edit-post-layout:not( .is-sidebar-opened ) & { - @include editor-width( $admin-sidebar-width-collapsed + $visual-editor-max-width - $block-padding ) { - margin-left: $float-margin; - } + // Hide block outline when an image is floated + &:before { + content: none; } + } - // sidebar (sticky) - .sticky-menu .edit-post-layout:not( .is-sidebar-opened ) & { - @include editor-width( $admin-sidebar-width + $visual-editor-max-width - $block-padding ) { - margin-left: $float-margin; - } + // Apply max-width to floated items that have no intrinsic width, like Cover Image or Gallery + &[data-align="left"], + &[data-align="right"] { + > .editor-block-list__block-edit { + max-width: 360px; + width: 100%; } - // sidebar (sticky) and post settings - .sticky-menu .edit-post-layout & { - @include editor-width( $admin-sidebar-width + $visual-editor-max-width + $sidebar-width - $block-padding ) { - margin-left: $float-margin; - } + // reset when data-resized + &[data-resized="true"] > .editor-block-list__block-edit { + max-width: none; + width: auto; } + } - // sidebar and post settings - .auto-fold .is-sidebar-opened & { - @include editor-width( $admin-sidebar-width + $visual-editor-max-width + $sidebar-width ) { - margin-left: $float-margin; - } + // Left + &[data-align="left"] { + .editor-block-list__block-edit { // This is in the editor only, on the frontend, the img should be floated + float: left; + margin-right: 2em; } } + // Right &[data-align="right"] { - float: right; - - // mobile, and no sidebars - margin-right: $block-padding; - - // sidebar (folded) - .auto-fold .edit-post-layout:not( .is-sidebar-opened ) & { - @include editor-width( $admin-sidebar-width-collapsed + $visual-editor-max-width - $block-padding ) { - margin-right: $float-margin; - } + .editor-block-list__block-edit { // This is in the editor only, on the frontend, the img should be floated + float: right; + margin-left: 2em; } - // sidebar (sticky) - .sticky-menu .edit-post-layout:not( .is-sidebar-opened ) & { - @include editor-width( $admin-sidebar-width + $visual-editor-max-width - $block-padding ) { - margin-right: $float-margin; - } + .editor-block-toolbar { + float: right; } + } - // sidebar (sticky) and post settings - .sticky-menu .edit-post-layout & { - @include editor-width( $admin-sidebar-width + $visual-editor-max-width + $sidebar-width - $block-padding ) { - margin-right: $float-margin; - } - } + // Wide and full-wide + &[data-align="full"], + &[data-align="wide"] { + clear: both; - // sidebar and post settings - .auto-fold .is-sidebar-opened & { - @include editor-width( $admin-sidebar-width + $visual-editor-max-width + $sidebar-width ) { - margin-right: $float-margin; - } - } + // Without z-index, the block toolbar will be below an adjecent float + z-index: z-index( '.editor-block-list__block {core/image aligned wide or fullwide}' ); } + // Full-wide &[data-align="full"] { padding-left: 0; padding-right: 0; @@ -249,10 +252,12 @@ } } + // Clear floats &[data-clear="true"] { float: none; } + // Dropzones & > .components-drop-zone { border: none; top: -4px; @@ -362,10 +367,6 @@ color: $dark-gray-300; margin: 4px 0 0 -4px; // align better with text blocks } - - .editor-inserter__toggle.components-icon-button:not(:disabled):hover { - @include button-style__hover; - } } .editor-block-list__insertion-point { @@ -414,19 +415,31 @@ } } -$sticky-bottom-offset: 20px; + +/** + * Block Toolbar + */ .editor-block-contextual-toolbar { position: sticky; z-index: z-index( '.editor-block-contextual-toolbar' ); - margin-top: - $block-toolbar-height - $block-padding - 1px; - margin-bottom: $block-padding + $sticky-bottom-offset; white-space: nowrap; text-align: left; pointer-events: none; height: $block-toolbar-height; - // put toolbar snugly to edge on mobile + // Position the contextual toolbar above the block, add 1px to each to stack borders + margin-top: -$block-toolbar-height - 1px; + margin-bottom: $block-padding + 1px; + + // Floated items have special needs for the contextual toolbar position + .edit-post-visual-editor .editor-block-list__block[data-align="left"] &, + .edit-post-visual-editor .editor-block-list__block[data-align="right"] & { + margin-bottom: 1px; + margin-top: -$block-toolbar-height - 1px; + } + + // put toolbar snugly to side edges on mobile margin-left: -$block-padding - 1px; // stack borders margin-right: -$block-padding - 1px; @include break-small() { @@ -444,6 +457,15 @@ $sticky-bottom-offset: 20px; border: 1px solid $light-gray-500; width: 100%; + // this prevents floats from messing up the position + position: absolute; + left: 0; + + .editor-block-list__block[data-align="right"] & { + left: auto; + right: 0; + } + // remove stacked borders in inline toolbar > div:first-child { margin-left: -1px; @@ -469,12 +491,6 @@ $sticky-bottom-offset: 20px; } } -.editor-block-contextual-toolbar + div { - // prevent collapsing margins between block and toolbar, matches the 20px bottom offset + display: flex; - margin-top: - $sticky-bottom-offset - 1px; - padding-top: 2px; -} - .editor-block-list__side-inserter { position: absolute; top: 10px; diff --git a/editor/components/block-settings-menu/style.scss b/editor/components/block-settings-menu/style.scss index e70c2a1b3053ef..765faac7cf62e4 100644 --- a/editor/components/block-settings-menu/style.scss +++ b/editor/components/block-settings-menu/style.scss @@ -30,7 +30,8 @@ .editor-block-settings-menu__section { margin-top: $item-spacing; - padding-top: $item-spacing; + margin-bottom: -$item-spacing; + padding: $item-spacing 0; border-top: 1px solid $light-gray-500; } @@ -44,14 +45,12 @@ border-radius: 0; color: $dark-gray-500; cursor: pointer; - border: 1px solid transparent; + @include menu-style__neutral; &:hover, &:focus, &:not(:disabled):hover { - box-shadow: none; - color: $dark-gray-500; - border: 1px solid $dark-gray-500; + @include menu-style__focus; } .dashicon { diff --git a/editor/components/copy-handler/index.js b/editor/components/copy-handler/index.js index 9dc685ba9a8fa5..dfc3981d69716c 100644 --- a/editor/components/copy-handler/index.js +++ b/editor/components/copy-handler/index.js @@ -8,11 +8,11 @@ import { connect } from 'react-redux'; */ import { Component } from '@wordpress/element'; import { serialize } from '@wordpress/blocks'; +import { documentHasSelection } from '@wordpress/utils'; /** * Internal dependencies */ -import { documentHasSelection } from '../../utils/dom'; import { removeBlocks } from '../../store/actions'; import { getMultiSelectedBlocks, diff --git a/editor/components/default-block-appender/index.js b/editor/components/default-block-appender/index.js index 6dc5f6dbaaa377..80e41082ca7a04 100644 --- a/editor/components/default-block-appender/index.js +++ b/editor/components/default-block-appender/index.js @@ -2,13 +2,14 @@ * External dependencies */ import { connect } from 'react-redux'; -import { get } from 'lodash'; /** * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { getDefaultBlockName } from '@wordpress/blocks'; +import { compose } from '@wordpress/element'; +import { isUnmodifiedDefaultBlock } from '@wordpress/blocks'; +import { withContext } from '@wordpress/components'; /** * Internal dependencies @@ -18,8 +19,8 @@ import BlockDropZone from '../block-drop-zone'; import { appendDefaultBlock, startTyping } from '../../store/actions'; import { getBlock, getBlockCount } from '../../store/selectors'; -export function DefaultBlockAppender( { isVisible, onAppend, showPrompt } ) { - if ( ! isVisible ) { +export function DefaultBlockAppender( { isLocked, isVisible, onAppend, showPrompt } ) { + if ( isLocked || ! isVisible ) { return null; } @@ -38,29 +39,37 @@ export function DefaultBlockAppender( { isVisible, onAppend, showPrompt } ) {
); } +export default compose( + connect( + ( state, ownProps ) => { + const isEmpty = ! getBlockCount( state, ownProps.rootUID ); + const lastBlock = getBlock( state, ownProps.lastBlockUID ); + const isLastBlockEmptyDefault = lastBlock && isUnmodifiedDefaultBlock( lastBlock ); -export default connect( - ( state, ownProps ) => { - const isEmpty = ! getBlockCount( state, ownProps.rootUID ); - const lastBlock = getBlock( state, ownProps.lastBlockUID ); - const isLastBlockDefault = get( lastBlock, 'name' ) === getDefaultBlockName(); + return { + isVisible: isEmpty || ! isLastBlockEmptyDefault, + showPrompt: isEmpty, + }; + }, + ( dispatch, ownProps ) => ( { + onAppend() { + const { layout, rootUID } = ownProps; - return { - isVisible: isEmpty || ! isLastBlockDefault, - showPrompt: isEmpty, - }; - }, - ( dispatch, ownProps ) => ( { - onAppend() { - const { layout, rootUID } = ownProps; + let attributes; + if ( layout ) { + attributes = { layout }; + } - let attributes; - if ( layout ) { - attributes = { layout }; - } + dispatch( appendDefaultBlock( attributes, rootUID ) ); + dispatch( startTyping() ); + }, + } ) + ), + withContext( 'editor' )( ( settings ) => { + const { templateLock } = settings; - dispatch( appendDefaultBlock( attributes, rootUID ) ); - dispatch( startTyping() ); - }, + return { + isLocked: !! templateLock, + }; } ), )( DefaultBlockAppender ); diff --git a/editor/components/editor-global-keyboard-shortcuts/index.js b/editor/components/editor-global-keyboard-shortcuts/index.js index cb591b7b1104f2..1d0fc82d14d86e 100644 --- a/editor/components/editor-global-keyboard-shortcuts/index.js +++ b/editor/components/editor-global-keyboard-shortcuts/index.js @@ -7,20 +7,28 @@ import { first, last } from 'lodash'; /** * WordPress dependencies */ -import { Component, compose } from '@wordpress/element'; +import { Component, Fragment, compose } from '@wordpress/element'; import { KeyboardShortcuts, withContext } from '@wordpress/components'; /** * Internal dependencies */ import { getBlockOrder, getMultiSelectedBlockUids } from '../../store/selectors'; -import { clearSelectedBlock, multiSelect, redo, undo, removeBlocks } from '../../store/actions'; +import { + clearSelectedBlock, + multiSelect, + redo, + undo, + autosave, + removeBlocks, +} from '../../store/actions'; class EditorGlobalKeyboardShortcuts extends Component { constructor() { super( ...arguments ); this.selectAll = this.selectAll.bind( this ); this.undoOrRedo = this.undoOrRedo.bind( this ); + this.save = this.save.bind( this ); this.deleteSelectedBlocks = this.deleteSelectedBlocks.bind( this ); } @@ -41,6 +49,11 @@ class EditorGlobalKeyboardShortcuts extends Component { event.preventDefault(); } + save( event ) { + event.preventDefault(); + this.props.onSave(); + } + deleteSelectedBlocks( event ) { const { multiSelectedBlockUids, onRemove, isLocked } = this.props; if ( multiSelectedBlockUids.length ) { @@ -53,14 +66,24 @@ class EditorGlobalKeyboardShortcuts extends Component { render() { return ( - + + + + ); } } @@ -79,6 +102,7 @@ export default compose( onRedo: redo, onUndo: undo, onRemove: removeBlocks, + onSave: autosave, } ), withContext( 'editor' )( ( settings ) => { diff --git a/editor/components/index.js b/editor/components/index.js index f77b2c0b955baa..46350d874cef02 100644 --- a/editor/components/index.js +++ b/editor/components/index.js @@ -7,7 +7,6 @@ export { default as EditorGlobalKeyboardShortcuts } from './editor-global-keyboa export { default as EditorHistoryRedo } from './editor-history/redo'; export { default as EditorHistoryUndo } from './editor-history/undo'; export { default as EditorNotices } from './editor-notices'; -export { default as MetaBoxes } from './meta-boxes'; export { default as PageAttributesCheck } from './page-attributes/check'; export { default as PageAttributesOrder } from './page-attributes/order'; export { default as PageAttributesParent } from './page-attributes/parent'; diff --git a/editor/components/inserter/group.js b/editor/components/inserter/group.js index 1bcb25971b0cd1..4a7be1f9661a96 100644 --- a/editor/components/inserter/group.js +++ b/editor/components/inserter/group.js @@ -72,6 +72,8 @@ export default class InserterGroup extends Component { disabled={ item.isDisabled } onMouseEnter={ this.createToggleBlockHover( item ) } onMouseLeave={ this.createToggleBlockHover( null ) } + onFocus={ this.createToggleBlockHover( item ) } + onBlur={ this.createToggleBlockHover( null ) } > { item.title } diff --git a/editor/components/inserter/index.js b/editor/components/inserter/index.js index ff1fb1df9ea3d5..82372dd82fe990 100644 --- a/editor/components/inserter/index.js +++ b/editor/components/inserter/index.js @@ -1,7 +1,6 @@ /** * External dependencies */ -import { connect } from 'react-redux'; import { isEmpty } from 'lodash'; /** @@ -11,18 +10,12 @@ import { __ } from '@wordpress/i18n'; import { Dropdown, IconButton, withContext } from '@wordpress/components'; import { createBlock, isUnmodifiedDefaultBlock } from '@wordpress/blocks'; import { Component, compose } from '@wordpress/element'; +import { withSelect, withDispatch } from '@wordpress/data'; /** * Internal dependencies */ import InserterMenu from './menu'; -import { getBlockInsertionPoint, getSelectedBlock } from '../../store/selectors'; -import { - insertBlock, - hideInsertionPoint, - showInsertionPoint, - replaceBlocks, -} from '../../store/actions'; class Inserter extends Component { constructor() { @@ -51,7 +44,6 @@ class Inserter extends Component { position, children, onInsertBlock, - insertionPoint, hasSupportedBlocks, isLocked, } = this.props; @@ -80,7 +72,7 @@ class Inserter extends Component { ) } renderContent={ ( { onClose } ) => { const onSelect = ( item ) => { - onInsertBlock( item, insertionPoint ); + onInsertBlock( item ); onClose(); }; @@ -93,35 +85,24 @@ class Inserter extends Component { } export default compose( [ - connect( - ( state, ownProps ) => { - return { - insertionPoint: getBlockInsertionPoint( state, ownProps.rootUID ), - selectedBlock: getSelectedBlock( state ), - }; + withSelect( ( select ) => ( { + insertionPoint: select( 'core/editor' ).getBlockInsertionPoint, + selectedBlock: select( 'core/editor' ).getSelectedBlock, + } ) ), + withDispatch( ( dispatch, ownProps ) => ( { + showInsertionPoint: dispatch( 'core/editor' ).showInsertionPoint, + hideInsertionPoint: dispatch( 'core/editor' ).hideInsertionPoint, + onInsertBlock: ( item ) => { + const { insertionPoint, selectedBlock } = ownProps; + const { index, rootUID, layout } = insertionPoint; + const { name, initialAttributes } = item; + const insertedBlock = createBlock( name, { ...initialAttributes, layout } ); + if ( selectedBlock && isUnmodifiedDefaultBlock( selectedBlock ) ) { + return dispatch( 'core/editor' ).replaceBlocks( selectedBlock.uid, insertedBlock ); + } + return dispatch( 'core/editor' ).insertBlock( insertedBlock, index, rootUID ); }, - { - showInsertionPoint, - hideInsertionPoint, - insertBlock, - replaceBlocks, - }, - ( { selectedBlock, ...stateProps }, dispatchProps, { layout, rootUID, ...ownProps } ) => ( { - ...stateProps, - ...ownProps, - showInsertionPoint: dispatchProps.showInsertionPoint, - hideInsertionPoint: dispatchProps.hideInsertionPoint, - onInsertBlock( item, index ) { - const { name, initialAttributes } = item; - const insertedBlock = createBlock( name, { ...initialAttributes, layout } ); - if ( selectedBlock && isUnmodifiedDefaultBlock( selectedBlock ) ) { - dispatchProps.replaceBlocks( selectedBlock.uid, insertedBlock ); - return; - } - dispatchProps.insertBlock( insertedBlock, index, rootUID ); - }, - } ) - ), + } ) ), withContext( 'editor' )( ( settings ) => { const { blockTypes, templateLock } = settings; diff --git a/editor/components/inserter/menu.js b/editor/components/inserter/menu.js index b0b9fae56ae097..0ff036a9342416 100644 --- a/editor/components/inserter/menu.js +++ b/editor/components/inserter/menu.js @@ -33,6 +33,7 @@ import { keycodes } from '@wordpress/utils'; * Internal dependencies */ import './style.scss'; +import NoBlocks from './no-blocks'; import { getInserterItems, getRecentInserterItems } from '../../store/selectors'; import { fetchReusableBlocks } from '../../store/actions'; @@ -201,9 +202,7 @@ export class InserterMenu extends Component { renderCategories( visibleItemsByCategory ) { if ( isEmpty( visibleItemsByCategory ) ) { return ( - - { __( 'No blocks found' ) } - + ); } @@ -229,9 +228,9 @@ export class InserterMenu extends Component { // If the Saved tab is selected and we have no results, display a friendly message if ( 'saved' === tab && itemsForTab.length === 0 ) { return ( -

+ { __( 'No saved blocks.' ) } -

+
); } diff --git a/editor/components/inserter/no-blocks.js b/editor/components/inserter/no-blocks.js new file mode 100644 index 00000000000000..f70e6e5819f4f3 --- /dev/null +++ b/editor/components/inserter/no-blocks.js @@ -0,0 +1,14 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +function NoBlocks( { children } ) { + return ( + + { !! children ? children : __( 'No blocks found.' ) } + + ); +} + +export default NoBlocks; diff --git a/editor/components/inserter/style.scss b/editor/components/inserter/style.scss index 99070c07ea1a3a..f432f8c5ffb42c 100644 --- a/editor/components/inserter/style.scss +++ b/editor/components/inserter/style.scss @@ -35,6 +35,7 @@ input[type="search"].editor-inserter__search { z-index: 1; border: none; box-shadow: 0 1px 0 0 $light-gray-500; + @include square-style__neutral; // fonts smaller than 16px causes mobile safari to zoom font-size: $mobile-text-min-font-size; @@ -43,7 +44,7 @@ input[type="search"].editor-inserter__search { } &:focus { - @include input-style__focus-active; + @include square-style__focus-active; } } @@ -132,7 +133,7 @@ input[type="search"].editor-inserter__search { } } -.editor-inserter__no-results { +.editor-inserter__no-blocks { display: block; text-align: center; font-style: italic; @@ -164,12 +165,7 @@ input[type="search"].editor-inserter__search { border-bottom: 1px solid $light-gray-500; flex-shrink: 0; margin-top: 1px; - } - - .editor-inserter__no-tab-content-message { - font-style: italic; - margin-top: 3em; - text-align: center; + @include square-style__neutral; } } diff --git a/editor/components/multi-select-scroll-into-view/index.js b/editor/components/multi-select-scroll-into-view/index.js index 4903d80eebc90a..4be0e9579d6fd8 100644 --- a/editor/components/multi-select-scroll-into-view/index.js +++ b/editor/components/multi-select-scroll-into-view/index.js @@ -7,12 +7,8 @@ import scrollIntoView from 'dom-scroll-into-view'; * WordPress dependencies */ import { Component } from '@wordpress/element'; -import { query } from '@wordpress/data'; - -/** - * Internal dependencies - */ -import { getScrollContainer } from '../../utils/dom'; +import { withSelect } from '@wordpress/data'; +import { getScrollContainer } from '@wordpress/utils'; class MultiSelectScrollIntoView extends Component { componentDidUpdate() { @@ -56,7 +52,7 @@ class MultiSelectScrollIntoView extends Component { } } -export default query( ( select ) => { +export default withSelect( ( select ) => { return { extentUID: select( 'core/editor' ).getLastMultiSelectedBlockUid(), }; diff --git a/editor/components/page-attributes/parent.js b/editor/components/page-attributes/parent.js index 8e50bbdac6a7a1..576f9fb882f801 100644 --- a/editor/components/page-attributes/parent.js +++ b/editor/components/page-attributes/parent.js @@ -9,10 +9,9 @@ import { stringify } from 'querystringify'; * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { withInstanceId, withAPIData } from '@wordpress/components'; +import { TreeSelect, withInstanceId, withAPIData } from '@wordpress/components'; import { compose } from '@wordpress/element'; import { buildTermsTree } from '@wordpress/utils'; -import { TermTreeSelect } from '@wordpress/blocks'; /** * Internal dependencies @@ -34,11 +33,11 @@ export function PageAttributesParent( { parent, postType, items, onUpdateParent name: item.title.raw ? item.title.raw : `#${ item.id } (${ __( 'no title' ) })`, } ) ) ); return ( - ); diff --git a/editor/components/post-last-revision/style.scss b/editor/components/post-last-revision/style.scss index 08b35cb99c25c8..17c9de56816c22 100644 --- a/editor/components/post-last-revision/style.scss +++ b/editor/components/post-last-revision/style.scss @@ -1,5 +1,4 @@ .editor-post-last-revision__title { - padding: 0; width: 100%; font-weight: 600; @@ -7,3 +6,16 @@ margin-right: 5px; } } + +// Needs specificity +.components-icon-button:not(:disabled).editor-post-last-revision__title { + + &:hover, + &:active { + @include menu-style__neutral; + } + + &:focus { + @include menu-style__focus; + } +} diff --git a/editor/components/post-publish-panel/index.js b/editor/components/post-publish-panel/index.js index 49497486cc890a..73d44549cf25d8 100644 --- a/editor/components/post-publish-panel/index.js +++ b/editor/components/post-publish-panel/index.js @@ -18,7 +18,12 @@ import './style.scss'; import PostPublishButton from '../post-publish-button'; import PostPublishPanelPrepublish from './prepublish'; import PostPublishPanelPostpublish from './postpublish'; -import { getCurrentPostType, isCurrentPostPublished, isSavingPost } from '../../store/selectors'; +import { + getCurrentPostType, + isCurrentPostPublished, + isSavingPost, + isEditedPostDirty, +} from '../../store/selectors'; class PostPublishPanel extends Component { constructor() { @@ -43,6 +48,14 @@ class PostPublishPanel extends Component { } } + componentDidUpdate( prevProps ) { + // Automatically collapse the publish sidebar when a post + // is published and the user makes an edit. + if ( prevProps.isPublished && this.props.isDirty ) { + this.props.onClose(); + } + } + onPublish() { this.setState( { loading: true } ); } @@ -85,6 +98,7 @@ const applyConnect = connect( postType: getCurrentPostType( state ), isPublished: isCurrentPostPublished( state ), isSaving: isSavingPost( state ), + isDirty: isEditedPostDirty( state ), }; }, ); diff --git a/editor/components/post-saved-state/index.js b/editor/components/post-saved-state/index.js index 034d5de3ed357c..841a3bddf0a6f7 100644 --- a/editor/components/post-saved-state/index.js +++ b/editor/components/post-saved-state/index.js @@ -15,7 +15,7 @@ import { Dashicon, Button } from '@wordpress/components'; */ import './style.scss'; import PostSwitchToDraftButton from '../post-switch-to-draft-button'; -import { editPost, savePost } from '../../store/actions'; +import { savePost } from '../../store/actions'; import { isEditedPostNew, isCurrentPostPublished, @@ -23,8 +23,6 @@ import { isSavingPost, isEditedPostSaveable, getCurrentPost, - getEditedPostAttribute, - hasMetaBoxes, } from '../../store/selectors'; /** @@ -33,7 +31,7 @@ import { * @param {Object} Props Component Props. * @return {WPElement} WordPress Element. */ -export function PostSavedState( { hasActiveMetaboxes, isNew, isPublished, isDirty, isSaving, isSaveable, status, onStatusChange, onSave } ) { +export function PostSavedState( { isNew, isPublished, isDirty, isSaving, isSaveable, onSave } ) { const className = 'editor-post-saved-state'; if ( isSaving ) { @@ -52,7 +50,7 @@ export function PostSavedState( { hasActiveMetaboxes, isNew, isPublished, isDirt return null; } - if ( ! isNew && ! isDirty && ! hasActiveMetaboxes ) { + if ( ! isNew && ! isDirty ) { return ( @@ -61,16 +59,8 @@ export function PostSavedState( { hasActiveMetaboxes, isNew, isPublished, isDirt ); } - const onClick = () => { - if ( 'auto-draft' === status ) { - onStatusChange( 'draft' ); - } - - onSave(); - }; - return ( - @@ -78,18 +68,15 @@ export function PostSavedState( { hasActiveMetaboxes, isNew, isPublished, isDirt } export default connect( - ( state ) => ( { + ( state, { forceIsDirty, forceIsSaving } ) => ( { post: getCurrentPost( state ), isNew: isEditedPostNew( state ), isPublished: isCurrentPostPublished( state ), - isDirty: isEditedPostDirty( state ), - isSaving: isSavingPost( state ), + isDirty: forceIsDirty || isEditedPostDirty( state ), + isSaving: forceIsSaving || isSavingPost( state ), isSaveable: isEditedPostSaveable( state ), - status: getEditedPostAttribute( state, 'status' ), - hasActiveMetaboxes: hasMetaBoxes( state ), } ), { - onStatusChange: ( status ) => editPost( { status } ), onSave: savePost, } )( PostSavedState ); diff --git a/editor/components/post-saved-state/test/index.js b/editor/components/post-saved-state/test/index.js index 524d5bc02a23db..bd45a24b8aa74b 100644 --- a/editor/components/post-saved-state/test/index.js +++ b/editor/components/post-saved-state/test/index.js @@ -52,29 +52,7 @@ describe( 'PostSavedState', () => { expect( wrapper.childAt( 1 ).text() ).toBe( 'Saved' ); } ); - it( 'should edit auto-draft post to draft before save', () => { - const statusSpy = jest.fn(); - const saveSpy = jest.fn(); - const wrapper = shallow( - - ); - - expect( wrapper.name() ).toBe( 'Button' ); - expect( wrapper.childAt( 0 ).text() ).toBe( 'Save' ); - wrapper.simulate( 'click' ); - expect( statusSpy ).toHaveBeenCalledWith( 'draft' ); - expect( saveSpy ).toHaveBeenCalled(); - } ); - it( 'should return Save button if edits to be saved', () => { - const statusSpy = jest.fn(); const saveSpy = jest.fn(); const wrapper = shallow( { isDirty={ true } isSaving={ false } isSaveable={ true } - onStatusChange={ statusSpy } onSave={ saveSpy } /> ); expect( wrapper.name() ).toBe( 'Button' ); expect( wrapper.childAt( 0 ).text() ).toBe( 'Save' ); wrapper.simulate( 'click' ); - expect( statusSpy ).not.toHaveBeenCalled(); expect( saveSpy ).toHaveBeenCalled(); } ); } ); diff --git a/editor/components/post-switch-to-draft-button/index.js b/editor/components/post-switch-to-draft-button/index.js index ebaf2265d30edf..275d0543706462 100644 --- a/editor/components/post-switch-to-draft-button/index.js +++ b/editor/components/post-switch-to-draft-button/index.js @@ -23,11 +23,18 @@ function PostSwitchToDraftButton( { className, isSaving, isPublished, onClick } return null; } + const onSwitch = () => { + // eslint-disable-next-line no-alert + if ( window.confirm( __( 'Are you sure you want to unpublish this post?' ) ) ) { + onClick(); + } + }; + return ( ) } /> diff --git a/blocks/index.js b/blocks/index.js index f4f350de37a395..ca965030e23470 100644 --- a/blocks/index.js +++ b/blocks/index.js @@ -15,24 +15,20 @@ import './hooks'; export * from './api'; export { registerCoreBlocks } from './library'; export { default as AlignmentToolbar } from './alignment-toolbar'; +export { default as Autocomplete } from './autocomplete'; export { default as BlockAlignmentToolbar } from './block-alignment-toolbar'; export { default as BlockControls } from './block-controls'; -export { default as BlockDescription } from './block-description'; export { default as BlockEdit } from './block-edit'; export { default as BlockIcon } from './block-icon'; export { default as ColorPalette } from './color-palette'; -export { default as Editable } from './rich-text/editable'; export { default as ImagePlaceholder } from './image-placeholder'; export { default as InnerBlocks } from './inner-blocks'; export { default as InspectorControls } from './inspector-controls'; +export { default as InspectorAdvancedControls } from './inspector-advanced-controls'; export { default as PlainText } from './plain-text'; export { default as MediaUpload } from './media-upload'; -export { default as MediaUploadButton } from './media-upload/button'; export { default as RichText } from './rich-text'; export { default as RichTextProvider } from './rich-text/provider'; export { default as UrlInput } from './url-input'; export { default as UrlInputButton } from './url-input/button'; - -// Deprecated matchers -import { attr, prop, text, html, query, node, children } from './hooks/matchers'; -export const source = { attr, prop, text, html, query, node, children }; +export { default as EditorSettings, withEditorSettings } from './editor-settings'; diff --git a/blocks/inspector-controls/index.js b/blocks/inspector-controls/index.js index c0bd29a4a79105..4a91e43d798fee 100644 --- a/blocks/inspector-controls/index.js +++ b/blocks/inspector-controls/index.js @@ -1,64 +1,6 @@ -/** - * External dependencies - */ -import { forEach } from 'lodash'; - /** * WordPress dependencies */ -import { - BaseControl, - CheckboxControl, - Fill, - RadioControl, - RangeControl, - SelectControl, - TextControl, - TextareaControl, - ToggleControl, -} from '@wordpress/components'; -import { Component } from '@wordpress/element'; -import { deprecated } from '@wordpress/utils'; - -export default function InspectorControls( { children } ) { - return ( - - { children } - - ); -} - -const withDeprecation = ( componentName ) => ( OriginalComponent ) => { - class WrappedComponent extends Component { - componentDidMount() { - deprecated( `wp.blocks.InspectorControls.${ componentName }`, { - version: '2.4', - alternative: `wp.components.${ componentName }`, - plugin: 'Gutenberg', - } ); - } - - render() { - return ( - - ); - } - } - return WrappedComponent; -}; +import { createSlotFill } from '@wordpress/components'; -forEach( - { - BaseControl, - CheckboxControl, - RadioControl, - RangeControl, - SelectControl, - TextControl, - TextareaControl, - ToggleControl, - }, - ( component, componentName ) => { - InspectorControls[ componentName ] = withDeprecation( componentName )( component ); - } -); +export default createSlotFill( 'InspectorControls' ); diff --git a/blocks/library/audio/editor.scss b/blocks/library/audio/editor.scss index 6fe2a8575745e4..273a512be9a259 100644 --- a/blocks/library/audio/editor.scss +++ b/blocks/library/audio/editor.scss @@ -11,7 +11,6 @@ } .wp-block-audio .components-placeholder__fieldset { - display: block; max-width: 400px; form { diff --git a/blocks/library/audio/index.js b/blocks/library/audio/index.js index 13e0ccb9b9e483..af28501c38b564 100644 --- a/blocks/library/audio/index.js +++ b/blocks/library/audio/index.js @@ -6,8 +6,15 @@ * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { Button, IconButton, Placeholder, Toolbar } from '@wordpress/components'; +import { + Button, + FormFileUpload, + IconButton, + Placeholder, + Toolbar, +} from '@wordpress/components'; import { Component } from '@wordpress/element'; +import { mediaUpload } from '@wordpress/utils'; /** * Internal dependencies @@ -51,20 +58,20 @@ export const settings = { }, edit: class extends Component { - constructor( { className } ) { + constructor() { super( ...arguments ); // edit component has its own src in the state so it can be edited // without setting the actual value outside of the edit UI this.state = { editing: ! this.props.attributes.src, src: this.props.attributes.src, - className, }; } + render() { const { caption, id } = this.props.attributes; - const { setAttributes, isSelected } = this.props; - const { editing, className, src } = this.state; + const { setAttributes, isSelected, className } = this.props; + const { editing, src } = this.state; const switchToEditing = () => { this.setState( { editing: true } ); }; @@ -85,24 +92,12 @@ export const settings = { } return false; }; - const controls = isSelected && ( - - - - - - ); + const setAudio = ( [ audio ] ) => onSelectAudio( audio ); + const uploadFromFiles = ( event ) => mediaUpload( event.target.files, setAudio, 'audio' ); if ( editing ) { - return [ - controls, + return ( + + { __( 'Upload' ) } + ( ) } /> - , - ]; + + ); } /* eslint-disable jsx-a11y/no-static-element-interactions, jsx-a11y/onclick-has-role, jsx-a11y/click-events-have-key-events */ return [ - controls, + isSelected && ( + + + + + + ),
`; diff --git a/blocks/library/audio/test/index.js b/blocks/library/audio/test/index.js index 3acc03865f117b..9ab9bd66b155c1 100644 --- a/blocks/library/audio/test/index.js +++ b/blocks/library/audio/test/index.js @@ -4,8 +4,6 @@ import { name, settings } from '../'; import { blockEditRender } from 'blocks/test/helpers'; -jest.mock( 'blocks/media-upload', () => () => '*** Mock(Media upload button) ***' ); - describe( 'core/audio', () => { test( 'block edit matches snapshot', () => { const wrapper = blockEditRender( name, settings ); diff --git a/blocks/library/block/edit-panel/index.js b/blocks/library/block/edit-panel/index.js index c09ebab9aaae35..d05596c88aaa92 100644 --- a/blocks/library/block/edit-panel/index.js +++ b/blocks/library/block/edit-panel/index.js @@ -2,6 +2,7 @@ * WordPress dependencies */ import { Button } from '@wordpress/components'; +import { Component, Fragment } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { keycodes } from '@wordpress/utils'; @@ -10,66 +11,97 @@ import { keycodes } from '@wordpress/utils'; */ import './style.scss'; +/** + * Module constants + */ const { ESCAPE } = keycodes; -function ReusableBlockEditPanel( props ) { - const { isEditing, title, isSaving, onEdit, onChangeTitle, onSave, onCancel } = props; +class SharedBlockEditPanel extends Component { + constructor() { + super( ...arguments ); - return [ - ( ! isEditing && ! isSaving ) && ( -
- - { title } - - -
- ), - ( isEditing || isSaving ) && ( -
{ - event.preventDefault(); - onSave(); - } }> - onChangeTitle( event.target.value ) } - onKeyDown={ ( event ) => { - if ( event.keyCode === ESCAPE ) { - event.stopPropagation(); - onCancel(); - } - } } /> - - -
- ), - ]; -} + this.bindTitleRef = this.bindTitleRef.bind( this ); + this.handleFormSubmit = this.handleFormSubmit.bind( this ); + this.handleTitleChange = this.handleTitleChange.bind( this ); + this.handleTitleKeyDown = this.handleTitleKeyDown.bind( this ); + } + + componentDidMount() { + if ( this.props.isEditing ) { + this.titleRef.select(); + } + } + + bindTitleRef( ref ) { + this.titleRef = ref; + } -export default ReusableBlockEditPanel; + handleFormSubmit( event ) { + event.preventDefault(); + this.props.onSave(); + } + + handleTitleChange( event ) { + this.props.onChangeTitle( event.target.value ); + } + + handleTitleKeyDown( event ) { + if ( event.keyCode === ESCAPE ) { + event.stopPropagation(); + this.props.onCancel(); + } + } + + render() { + const { isEditing, title, isSaving, onEdit, onSave, onCancel } = this.props; + + return ( + + { ( ! isEditing && ! isSaving ) && ( +
+ + { title } + + +
+ ) } + { ( isEditing || isSaving ) && ( +
+ + + +
+ ) } +
+ ); + } +} +export default SharedBlockEditPanel; diff --git a/blocks/library/block/edit-panel/style.scss b/blocks/library/block/edit-panel/style.scss index 3f8259415e8dc9..e9717da5c3a238 100644 --- a/blocks/library/block/edit-panel/style.scss +++ b/blocks/library/block/edit-panel/style.scss @@ -1,4 +1,4 @@ -.reusable-block-edit-panel { +.shared-block-edit-panel { align-items: center; background: $light-gray-100; color: $dark-gray-500; @@ -6,18 +6,20 @@ font-family: $default-font; font-size: $default-font-size; justify-content: flex-end; - margin: $block-padding (-$block-padding) (-$block-padding); + margin: 0 (-$block-padding); padding: 10px $block-padding; + position: relative; + top: $block-padding; - .reusable-block-edit-panel__spinner { + .shared-block-edit-panel__spinner { margin: 0 5px; } - .reusable-block-edit-panel__info { + .shared-block-edit-panel__info { margin-right: auto; } - .reusable-block-edit-panel__title { + .shared-block-edit-panel__title { flex-grow: 1; font-size: 14px; height: 30px; @@ -26,7 +28,7 @@ } // Needs specificity to override the margin-bottom set by .button - .wp-core-ui & .reusable-block-edit-panel__button { + .wp-core-ui & .shared-block-edit-panel__button { margin: 0 0 0 5px; } } diff --git a/blocks/library/block/index.js b/blocks/library/block/index.js index 31efe46ad53bf5..531f6fb5d3ef12 100644 --- a/blocks/library/block/index.js +++ b/blocks/library/block/index.js @@ -1,165 +1,178 @@ /** * External dependencies */ -import { pickBy, noop } from 'lodash'; -import { connect } from 'react-redux'; +import { noop, partial } from 'lodash'; /** * WordPress dependencies */ -import { Component } from '@wordpress/element'; -import { Placeholder, Spinner } from '@wordpress/components'; +import { Component, Fragment, compose } from '@wordpress/element'; +import { Placeholder, Spinner, Disabled } from '@wordpress/components'; +import { withSelect, withDispatch } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ import BlockEdit from '../../block-edit'; -import ReusableBlockEditPanel from './edit-panel'; +import SharedBlockEditPanel from './edit-panel'; +import SharedBlockIndicator from './indicator'; -class ReusableBlockEdit extends Component { - constructor() { +class SharedBlockEdit extends Component { + constructor( { sharedBlock } ) { super( ...arguments ); this.startEditing = this.startEditing.bind( this ); this.stopEditing = this.stopEditing.bind( this ); this.setAttributes = this.setAttributes.bind( this ); this.setTitle = this.setTitle.bind( this ); - this.updateReusableBlock = this.updateReusableBlock.bind( this ); + this.save = this.save.bind( this ); this.state = { - isEditing: false, + isEditing: !! ( sharedBlock && sharedBlock.isTemporary ), title: null, - attributes: null, + changedAttributes: null, }; } componentDidMount() { - if ( ! this.props.reusableBlock ) { - this.props.fetchReusableBlock(); - } - } - - /** - * @inheritdoc - */ - componentWillReceiveProps( nextProps ) { - if ( this.props.focus && ! nextProps.focus ) { - this.stopEditing(); + if ( ! this.props.sharedBlock ) { + this.props.fetchSharedBlock(); } } startEditing() { - this.setState( { isEditing: true } ); + const { sharedBlock } = this.props; + + this.setState( { + isEditing: true, + title: sharedBlock.title, + changedAttributes: {}, + } ); } stopEditing() { this.setState( { isEditing: false, title: null, - attributes: null, + changedAttributes: null, } ); } setAttributes( attributes ) { - this.setState( ( prevState ) => ( { - attributes: { ...prevState.attributes, ...attributes }, - } ) ); + this.setState( ( prevState ) => { + if ( prevState.changedAttributes !== null ) { + return { changedAttributes: { ...prevState.changedAttributes, ...attributes } }; + } + } ); } setTitle( title ) { this.setState( { title } ); } - updateReusableBlock() { - const { title, attributes } = this.state; + save() { + const { sharedBlock, onUpdateTitle, updateAttributes, block, onSave } = this.props; + const { title, changedAttributes } = this.state; - // Use pickBy to include only changed (assigned) values in payload - const payload = pickBy( { - title, - attributes, - } ); + if ( title !== sharedBlock.title ) { + onUpdateTitle( title ); + } + + updateAttributes( block.uid, changedAttributes ); + onSave(); - this.props.updateReusableBlock( payload ); - this.props.saveReusableBlock(); this.stopEditing(); } render() { - const { isSelected, reusableBlock, isFetching, isSaving } = this.props; - const { isEditing, title, attributes } = this.state; + const { isSelected, sharedBlock, block, isFetching, isSaving } = this.props; + const { isEditing, title, changedAttributes } = this.state; - if ( ! reusableBlock && isFetching ) { + if ( ! sharedBlock && isFetching ) { return ; } - if ( ! reusableBlock ) { + if ( ! sharedBlock || ! block ) { return { __( 'Block has been deleted or is unavailable.' ) }; } - const reusableBlockAttributes = { ...reusableBlock.attributes, ...attributes }; - - return [ - // We fake the block being read-only by wrapping it with an element that has pointer-events: none -
- -
, - isSelected && ( - - ), - ]; + let element = ( + + ); + + if ( ! isEditing ) { + element = { element }; + } + + return ( + + { element } + { ( isSelected || isEditing ) && ( + + ) } + { ! isSelected && ! isEditing && } + + ); } } -const ConnectedReusableBlockEdit = connect( - ( state, ownProps ) => ( { - reusableBlock: state.reusableBlocks.data[ ownProps.attributes.ref ], - isFetching: state.reusableBlocks.isFetching[ ownProps.attributes.ref ], - isSaving: state.reusableBlocks.isSaving[ ownProps.attributes.ref ], +const EnhancedSharedBlockEdit = compose( [ + withSelect( ( select, ownProps ) => { + const { + getSharedBlock, + isFetchingSharedBlock, + isSavingSharedBlock, + getBlock, + } = select( 'core/editor' ); + const { ref } = ownProps.attributes; + const sharedBlock = getSharedBlock( ref ); + + return { + sharedBlock, + isFetching: isFetchingSharedBlock( ref ), + isSaving: isSavingSharedBlock( ref ), + block: sharedBlock ? getBlock( sharedBlock.uid ) : null, + }; } ), - ( dispatch, ownProps ) => ( { - fetchReusableBlock() { - dispatch( { - type: 'FETCH_REUSABLE_BLOCKS', - id: ownProps.attributes.ref, - } ); - }, - updateReusableBlock( reusableBlock ) { - dispatch( { - type: 'UPDATE_REUSABLE_BLOCK', - id: ownProps.attributes.ref, - reusableBlock, - } ); - }, - saveReusableBlock() { - dispatch( { - type: 'SAVE_REUSABLE_BLOCK', - id: ownProps.attributes.ref, - } ); - }, - } ) -)( ReusableBlockEdit ); + withDispatch( ( dispatch, ownProps ) => { + const { + fetchSharedBlocks, + updateBlockAttributes, + updateSharedBlockTitle, + saveSharedBlock, + } = dispatch( 'core/editor' ); + const { ref } = ownProps.attributes; + + return { + fetchSharedBlock: partial( fetchSharedBlocks, ref ), + updateAttributes: updateBlockAttributes, + onUpdateTitle: partial( updateSharedBlockTitle, ref ), + onSave: partial( saveSharedBlock, ref ), + }; + } ), +] )( SharedBlockEdit ); export const name = 'core/block'; export const settings = { - title: __( 'Reusable Block' ), - category: 'reusable-blocks', + title: __( 'Shared Block' ), + category: 'shared', isPrivate: true, attributes: { @@ -173,6 +186,6 @@ export const settings = { html: false, }, - edit: ConnectedReusableBlockEdit, + edit: EnhancedSharedBlockEdit, save: () => null, }; diff --git a/blocks/library/block/index.php b/blocks/library/block/index.php index 98807d0bd04284..dd1e1fbc43903e 100644 --- a/blocks/library/block/index.php +++ b/blocks/library/block/index.php @@ -12,17 +12,17 @@ * * @return string Rendered HTML of the referenced block. */ -function gutenberg_render_block_core_reusable_block( $attributes ) { +function render_block_core_block( $attributes ) { if ( empty( $attributes['ref'] ) ) { return ''; } - $reusable_block = get_post( $attributes['ref'] ); - if ( ! $reusable_block || 'wp_block' !== $reusable_block->post_type ) { + $shared_block = get_post( $attributes['ref'] ); + if ( ! $shared_block || 'wp_block' !== $shared_block->post_type ) { return ''; } - $blocks = gutenberg_parse_blocks( $reusable_block->post_content ); + $blocks = gutenberg_parse_blocks( $shared_block->post_content ); $block = array_shift( $blocks ); if ( ! $block ) { @@ -39,5 +39,5 @@ function gutenberg_render_block_core_reusable_block( $attributes ) { ), ), - 'render_callback' => 'gutenberg_render_block_core_reusable_block', + 'render_callback' => 'render_block_core_block', ) ); diff --git a/blocks/library/button/index.js b/blocks/library/button/index.js index fdcf21f6f38442..c35f362b284020 100644 --- a/blocks/library/button/index.js +++ b/blocks/library/button/index.js @@ -3,7 +3,14 @@ */ import { __ } from '@wordpress/i18n'; import { Component } from '@wordpress/element'; -import { Dashicon, IconButton, PanelColor, ToggleControl, withFallbackStyles } from '@wordpress/components'; +import { + Dashicon, + IconButton, + PanelBody, + PanelColor, + ToggleControl, + withFallbackStyles, +} from '@wordpress/components'; /** * Internal dependencies @@ -96,29 +103,31 @@ class ButtonBlock extends Component { /> { isSelected && - - - setAttributes( { color: colorValue } ) } + + - - - setAttributes( { textColor: colorValue } ) } - /> - - { this.nodeRef && } + + setAttributes( { color: colorValue } ) } + /> + + + setAttributes( { textColor: colorValue } ) } + /> + + { this.nodeRef && } + } , diff --git a/blocks/library/button/style.scss b/blocks/library/button/style.scss index 326c7a04a17869..ff5f7fee05a583 100644 --- a/blocks/library/button/style.scss +++ b/blocks/library/button/style.scss @@ -8,7 +8,7 @@ $blocks-button__line-height: $big-font-size + 6px; background-color: $dark-gray-700; border: none; border-radius: $blocks-button__height / 2; - box-shadow: none !important; + box-shadow: none; color: $white; cursor: pointer; display: inline-block; @@ -17,9 +17,9 @@ $blocks-button__line-height: $big-font-size + 6px; margin: 0; padding: ( $blocks-button__height - $blocks-button__line-height ) / 2 24px; text-align: center; - text-decoration: none !important; - white-space: nowrap; - word-break: break-word; + text-decoration: none; + white-space: normal; + word-break: break-all; &:hover, &:focus, diff --git a/blocks/library/button/test/__snapshots__/index.js.snap b/blocks/library/button/test/__snapshots__/index.js.snap index c90afe81797fe8..05d5185451e859 100644 --- a/blocks/library/button/test/__snapshots__/index.js.snap +++ b/blocks/library/button/test/__snapshots__/index.js.snap @@ -7,17 +7,29 @@ exports[`core/button block edit matches snapshot 1`] = `
- - - Add text… - +
+
+
+
+
+
`; diff --git a/blocks/library/categories/block.js b/blocks/library/categories/block.js index 5a9faaa621a5ca..e2cc937fe7bae0 100644 --- a/blocks/library/categories/block.js +++ b/blocks/library/categories/block.js @@ -2,7 +2,8 @@ * WordPress dependencies */ import { Component } from '@wordpress/element'; -import { Placeholder, Spinner, ToggleControl, withAPIData } from '@wordpress/components'; +import { PanelBody, Placeholder, Spinner, ToggleControl } from '@wordpress/components'; +import { withSelect } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; import { times, unescape } from 'lodash'; @@ -46,7 +47,7 @@ class CategoriesBlock extends Component { } getCategories( parentId = null ) { - const categories = this.props.categories.data; + const categories = this.props.categories; if ( ! categories || ! categories.length ) { return []; } @@ -142,32 +143,32 @@ class CategoriesBlock extends Component { } render() { - const { attributes, focus, setAttributes } = this.props; + const { attributes, focus, setAttributes, isRequesting } = this.props; const { align, displayAsDropdown, showHierarchy, showPostCounts } = attributes; - const categories = this.getCategories(); const inspectorControls = focus && ( -

{ __( 'Categories Settings' ) }

- - - + + + + +
); - if ( ! categories.length ) { + if ( isRequesting ) { return [ inspectorControls, { +export default withSelect( ( select ) => { + const { getCategories, isRequestingCategories } = select( 'core' ); + return { - categories: '/wp/v2/categories', + categories: getCategories(), + isRequesting: isRequestingCategories(), }; } )( CategoriesBlock ); diff --git a/blocks/library/code/editor.scss b/blocks/library/code/editor.scss index 632b987be6783f..d354bdb7e2912d 100644 --- a/blocks/library/code/editor.scss +++ b/blocks/library/code/editor.scss @@ -3,7 +3,6 @@ font-size: $text-editor-font-size; color: $dark-gray-800; padding: .8em 1.6em; - overflow-x: auto !important; border: 1px solid $light-gray-500; border-radius: 4px; } diff --git a/blocks/library/columns/index.js b/blocks/library/columns/index.js index 896fe5d8452331..011cfa37702c28 100644 --- a/blocks/library/columns/index.js +++ b/blocks/library/columns/index.js @@ -9,12 +9,13 @@ import memoize from 'memize'; * WordPress dependencies */ import { __, sprintf } from '@wordpress/i18n'; -import { RangeControl } from '@wordpress/components'; +import { PanelBody, RangeControl } from '@wordpress/components'; /** * Internal dependencies */ import './style.scss'; +import './editor.scss'; import InspectorControls from '../../inspector-controls'; import BlockControls from '../../block-controls'; import BlockAlignmentToolbar from '../../block-alignment-toolbar'; @@ -83,17 +84,19 @@ export const settings = { /> , - { - setAttributes( { - columns: nextColumns, - } ); - } } - min={ 2 } - max={ 6 } - /> + + { + setAttributes( { + columns: nextColumns, + } ); + } } + min={ 2 } + max={ 6 } + /> + , ] : [],
diff --git a/blocks/library/cover-image/editor.scss b/blocks/library/cover-image/editor.scss index a9109c61172d83..29db22108e5d96 100644 --- a/blocks/library/cover-image/editor.scss +++ b/blocks/library/cover-image/editor.scss @@ -1,12 +1,10 @@ .wp-block-cover-image { + margin: 0; + .blocks-rich-text__tinymce[data-is-empty="true"]:before { position: inherit; } - .blocks-rich-text__tinymce a { - color: white; - } - .blocks-rich-text__tinymce:focus a[data-mce-selected] { padding: 0 2px; margin: 0 -2px; diff --git a/blocks/library/cover-image/index.js b/blocks/library/cover-image/index.js index b7b4be7d82d0e9..c5dfd482d5933a 100644 --- a/blocks/library/cover-image/index.js +++ b/blocks/library/cover-image/index.js @@ -26,6 +26,35 @@ import InspectorControls from '../../inspector-controls'; const validAlignments = [ 'left', 'center', 'right', 'wide', 'full' ]; +const blockAttributes = { + title: { + type: 'array', + source: 'children', + selector: 'p', + }, + url: { + type: 'string', + }, + align: { + type: 'string', + }, + contentAlign: { + type: 'string', + default: 'center', + }, + id: { + type: 'number', + }, + hasParallax: { + type: 'boolean', + default: false, + }, + dimRatio: { + type: 'number', + default: 50, + }, +}; + export const name = 'core/cover-image'; export const settings = { @@ -33,38 +62,11 @@ export const settings = { description: __( 'Cover Image is a bold image block with an optional title.' ), - icon: 'format-image', + icon: 'cover-image', category: 'common', - attributes: { - title: { - type: 'array', - source: 'children', - selector: 'h2', - }, - url: { - type: 'string', - }, - align: { - type: 'string', - }, - contentAlign: { - type: 'string', - default: 'center', - }, - id: { - type: 'number', - }, - hasParallax: { - type: 'boolean', - default: false, - }, - dimRatio: { - type: 'number', - default: 50, - }, - }, + attributes: blockAttributes, transforms: { from: [ @@ -101,9 +103,7 @@ export const settings = { const toggleParallax = () => setAttributes( { hasParallax: ! hasParallax } ); const setDimRatio = ( ratio ) => setAttributes( { dimRatio: ratio } ); - const style = url ? - { backgroundImage: `url(${ url })` } : - undefined; + const style = backgroundImageStyles( url ); const classes = classnames( className, contentAlign !== 'center' && `has-${ contentAlign }-content`, @@ -147,20 +147,21 @@ export const settings = { , -

{ __( 'Cover Image Settings' ) }

- - + + + + { alignmentToolbar } @@ -190,7 +191,7 @@ export const settings = { return [ controls, -
{ title || isSelected ? ( setAttributes( { title: value } ) } @@ -206,15 +208,13 @@ export const settings = { inlineToolbar /> ) : null } -
, +
, ]; }, save( { attributes, className } ) { const { url, title, hasParallax, dimRatio, align, contentAlign } = attributes; - const style = url ? - { backgroundImage: `url(${ url })` } : - undefined; + const style = backgroundImageStyles( url ); const classes = classnames( className, dimRatioToClass( dimRatio ), @@ -227,11 +227,44 @@ export const settings = { ); return ( -
-

{ title }

-
+
+ { title && title.length > 0 && ( +

{ title }

+ ) } +
); }, + + deprecated: [ { + attributes: { + ...blockAttributes, + title: { + type: 'array', + source: 'children', + selector: 'h2', + }, + }, + + save( { attributes, className } ) { + const { url, title, hasParallax, dimRatio, align } = attributes; + const style = backgroundImageStyles( url ); + const classes = classnames( + className, + dimRatioToClass( dimRatio ), + { + 'has-background-dim': dimRatio !== 0, + 'has-parallax': hasParallax, + }, + align ? `align${ align }` : null, + ); + + return ( +
+

{ title }

+
+ ); + }, + } ], }; function dimRatioToClass( ratio ) { @@ -239,3 +272,9 @@ function dimRatioToClass( ratio ) { null : 'has-background-dim-' + ( 10 * Math.round( ratio / 10 ) ); } + +function backgroundImageStyles( url ) { + return url ? + { backgroundImage: `url(${ url })` } : + undefined; +} diff --git a/blocks/library/cover-image/style.scss b/blocks/library/cover-image/style.scss index 4ec232e2d20907..67bfb21089f23f 100644 --- a/blocks/library/cover-image/style.scss +++ b/blocks/library/cover-image/style.scss @@ -1,16 +1,18 @@ .wp-block-cover-image { position: relative; background-size: cover; - height: 430px; + min-height: 430px; width: 100%; - margin: 0; + margin: 0 0 1.5em 0; display: flex; justify-content: center; align-items: center; &.has-left-content { justify-content: flex-start; - h2 { + + h2, + .wp-block-cover-image-text { margin-left: 0; text-align: left; } @@ -18,20 +20,31 @@ &.has-right-content { justify-content: flex-end; - h2 { + + h2, + .wp-block-cover-image-text { margin-right: 0; text-align: right; } } - h2 { + h2, + .wp-block-cover-image-text { color: white; - font-size: 24pt; - line-height: 1em; + font-size: 2em; + line-height: 1.25; z-index: 1; - max-width: $visual-editor-max-width; + margin-bottom: 0; + max-width: $content-width; padding: $block-padding; text-align: center; + + a, + a:hover, + a:focus, + a:active { + color: white; + } } &.has-parallax { @@ -45,7 +58,7 @@ left: 0; bottom: 0; right: 0; - background: rgba( black, 0.5 ); + background-color: rgba( black, 0.5 ); } @for $i from 1 through 10 { @@ -58,4 +71,12 @@ height: inherit; } + // Apply max-width to floated items that have no intrinsic width + [data-align="left"] &, + [data-align="right"] &, + &.alignleft, + &.alignright { + max-width: $content-width / 2; + width: 100%; + } } diff --git a/blocks/library/cover-image/test/__snapshots__/index.js.snap b/blocks/library/cover-image/test/__snapshots__/index.js.snap index 1c3b482fb5353b..ee701c6e9620cf 100644 --- a/blocks/library/cover-image/test/__snapshots__/index.js.snap +++ b/blocks/library/cover-image/test/__snapshots__/index.js.snap @@ -87,7 +87,6 @@ exports[`core/cover-image block edit matches snapshot 1`] = ` type="file" />
- *** Mock(Media upload button) ***
`; diff --git a/blocks/library/cover-image/test/index.js b/blocks/library/cover-image/test/index.js index 4cd1d315f7c3de..90efbf7a169199 100644 --- a/blocks/library/cover-image/test/index.js +++ b/blocks/library/cover-image/test/index.js @@ -4,8 +4,6 @@ import { name, settings } from '../'; import { blockEditRender } from 'blocks/test/helpers'; -jest.mock( 'blocks/media-upload', () => () => '*** Mock(Media upload button) ***' ); - describe( 'core/cover-image', () => { test( 'block edit matches snapshot', () => { const wrapper = blockEditRender( name, settings ); diff --git a/blocks/library/embed/index.js b/blocks/library/embed/index.js index a8fd4081a9927d..51f8e7c2d94d2c 100644 --- a/blocks/library/embed/index.js +++ b/blocks/library/embed/index.js @@ -3,6 +3,7 @@ */ import { parse } from 'url'; import { includes, kebabCase, toLower } from 'lodash'; +import { stringify } from 'querystring'; /** * WordPress dependencies @@ -10,7 +11,6 @@ import { includes, kebabCase, toLower } from 'lodash'; import { __, sprintf } from '@wordpress/i18n'; import { Component, renderToString } from '@wordpress/element'; import { Button, Placeholder, Spinner, SandBox } from '@wordpress/components'; -import { addQueryArgs } from '@wordpress/url'; import classnames from 'classnames'; /** @@ -71,7 +71,9 @@ function getEmbedBlockSettings( { title, icon, category = 'embed', transforms, k edit: class extends Component { constructor() { super( ...arguments ); + this.doServerSideRender = this.doServerSideRender.bind( this ); + this.state = { html: '', type: '', @@ -109,20 +111,15 @@ function getEmbedBlockSettings( { title, icon, category = 'embed', transforms, k } const { url } = this.props.attributes; const { setAttributes } = this.props; - const apiURL = addQueryArgs( wpApiSettings.root + 'oembed/1.0/proxy', { - url: url, - _wpnonce: wpApiSettings.nonce, - } ); this.setState( { error: false, fetching: true } ); - window.fetch( apiURL, { - credentials: 'include', - } ).then( - ( response ) => { - if ( this.unmounting ) { - return; - } - response.json().then( ( obj ) => { + wp.apiRequest( { path: `/oembed/1.0/proxy?${ stringify( { url } ) }` } ) + .then( + ( obj ) => { + if ( this.unmounting ) { + return; + } + const { html, provider_name: providerName } = obj; const providerNameSlug = kebabCase( toLower( providerName ) ); let { type } = obj; @@ -136,19 +133,19 @@ function getEmbedBlockSettings( { title, icon, category = 'embed', transforms, k } else if ( 'photo' === type ) { this.setState( { html: this.getPhotoHtml( obj ), type, providerNameSlug } ); setAttributes( { type, providerNameSlug } ); - } else { - this.setState( { error: true } ); } this.setState( { fetching: false } ); - } ); - } - ); + }, + () => { + this.setState( { fetching: false, error: true } ); + } + ); } render() { const { html, type, error, fetching } = this.state; const { align, url, caption } = this.props.attributes; - const { setAttributes, isSelected } = this.props; + const { setAttributes, isSelected, className } = this.props; const updateAlignment = ( nextAlign ) => setAttributes( { align: nextAlign } ); const controls = isSelected && ( @@ -212,14 +209,10 @@ function getEmbedBlockSettings( { title, icon, category = 'embed', transforms, k /> ); - let typeClassName = 'wp-block-embed'; - if ( 'video' === type ) { - typeClassName += ' is-video'; - } return [ controls, -
+
{ ( cannotPreview ) ? (

{ url }

@@ -245,11 +238,11 @@ function getEmbedBlockSettings( { title, icon, category = 'embed', transforms, k const { url, caption, align, type, providerNameSlug } = attributes; if ( ! url ) { - return; + return null; } const embedClassName = classnames( 'wp-block-embed', { - [ `is-align${ align }` ]: align, + [ `align${ align }` ]: align, [ `is-type-${ type }` ]: type, [ `is-provider-${ providerNameSlug }` ]: providerNameSlug, } ); @@ -273,7 +266,7 @@ export const settings = getEmbedBlockSettings( { from: [ { type: 'raw', - isMatch: ( node ) => node.nodeName === 'P' && /^\s*(https?:\/\/\S+)\s*/i.test( node.textContent ), + isMatch: ( node ) => node.nodeName === 'P' && /^\s*(https?:\/\/\S+)\s*$/i.test( node.textContent ), transform: ( node ) => { return createBlock( 'core/embed', { url: node.textContent.trim(), diff --git a/blocks/library/embed/style.scss b/blocks/library/embed/style.scss index be8071374195d6..184ca12045b5a2 100644 --- a/blocks/library/embed/style.scss +++ b/blocks/library/embed/style.scss @@ -4,3 +4,12 @@ text-align: center; font-size: $default-font-size; } + +// Apply max-width to floated items that have no intrinsic width +.editor-block-list__block[data-type="core/embed"][data-align="left"] .editor-block-list__block-edit, +.editor-block-list__block[data-type="core/embed"][data-align="right"] .editor-block-list__block-edit, +.wp-block-embed.alignleft, +.wp-block-embed.alignright { + max-width: $content-width / 2; + width: 100%; +} diff --git a/blocks/library/freeform/editor.scss b/blocks/library/freeform/editor.scss index f9c19c5c3432a4..cba0ec09fbb678 100644 --- a/blocks/library/freeform/editor.scss +++ b/blocks/library/freeform/editor.scss @@ -16,7 +16,7 @@ blockquote { margin: 0; - box-shadow: inset 0px 0px 0px 0px $light-gray-500; + box-shadow: inset 0 0 0 0 $light-gray-500; border-left: 4px solid $black; padding-left: 1em; } @@ -116,26 +116,47 @@ } } -// freeform toolbar +.editor-block-list__layout .editor-block-list__block[data-type="core/freeform"] { + .editor-block-list__block-edit:before { + outline: 1px solid #e2e4e7; + } + + // Don't show normal block toolbar + .editor-block-contextual-toolbar { + display: none; + } + + // Don't show block type label for classic block + &.is-hovered .editor-block-breadcrumb { + display: none; + } +} + + +div[data-type="core/freeform"] .editor-block-contextual-toolbar + div { + margin-top: 0; + padding-top: 0; +} + .freeform-toolbar { - position: sticky; width: auto; - top: -1px; // stack borders - margin-top: -$block-controls-height - 16px; - border: 1px solid $light-gray-500; - z-index: z-index( '.freeform-toolbar' ); - background-color: $white; - min-height: $block-controls-height; - margin-bottom: 14px; - - @include break-small() { - margin-left: - $block-padding - 1px; - margin-right: - $block-padding - 1px; - } + margin: -$block-padding; + margin-bottom: $block-padding; } -.freeform-toolbar.has-advanced-toolbar { - margin-top: -89px; // pull upwards the classic block toolbar when enabled, height is manually entered +.freeform-toolbar:empty { + height: $block-toolbar-height; + background: #f5f5f5; + border-bottom: 1px solid #e2e4e7; + + &:before { + font-family: $default-font; + font-size: $default-font-size; + content: attr( data-placeholder ); + color: #555d66; + line-height: 37px; + padding: $block-padding; + } } // Overwrite inline styles. @@ -149,145 +170,19 @@ width: 100% !important; } -.freeform-toolbar .mce-tinymce-inline .mce-flow-layout { - white-space: normal; -} - .freeform-toolbar .mce-container-body.mce-abs-layout { overflow: visible; } -.freeform-toolbar .mce-menubar { - position: static; -} - +.freeform-toolbar .mce-menubar, .freeform-toolbar div.mce-toolbar-grp { - background-color: transparent; - border: none; position: static; } -.freeform-toolbar div.mce-toolbar-grp > div { - padding: 0; -} - .freeform-toolbar .mce-toolbar-grp .mce-toolbar:not(:first-child) { display: none; - border-top: 1px solid $light-gray-500; } .freeform-toolbar.has-advanced-toolbar .mce-toolbar-grp .mce-toolbar { display: block; } - -.freeform-toolbar div.mce-btn-group { - padding: 0; - margin: 0; - border: 0; -} - -.freeform-toolbar .mce-toolbar-grp .mce-toolbar .mce-btn { - margin: 0; - padding: 3px; - background: none; - outline: none; - color: $dark-gray-500; - cursor: pointer; - position: relative; - min-width: $icon-button-size; - height: $icon-button-size; - - // Overwrite - border: none; - box-sizing: border-box; - box-shadow: none; - border-radius: 0; - - &:hover, - /* the ":not(:disabled)" is needed to make it specific enough */ - &:hover:not(:disabled), - &:focus, - &:focus:active { - color: $dark-gray-500; - outline: none; - box-shadow: none; - background: inherit; - } - - &.mce-active, - &.mce-active:hover { - background: none; - color: $white; - } - - &:disabled { - cursor: default; - } - - &> button { - border: 1px solid transparent; - padding: 4px; - box-sizing: content-box; - } - - &:hover button, - &:focus button { - color: $dark-gray-500; - } - - &:not(:disabled) { - &.mce-active button, - &:hover button, - &:focus button { - border: 1px solid $dark-gray-500; - } - } - - &.mce-active button, - &.mce-active:hover button { - background-color: $dark-gray-500; - color: $white; - } - - &.mce-active .mce-ico { - color: $white; - } - - &.mce-listbox, - &.mce-colorbutton { - width: auto; - border: none; - box-shadow: none; - } - - &.mce-colorbutton .mce-preview { - bottom: 8px; - left: 8px; - } -} - -.components-toolbar__control .dashicon { - display: block; -} - - -.mce-widget.mce-tooltip { - display: block; - opacity: initial; - - .mce-tooltip-inner { - padding: 4px 12px; - background: $dark-gray-400; - border-width: 0; - color: white; - white-space: nowrap; - box-shadow: none; - font-size: $default-font-size; - border-radius: 0; - } - - .mce-tooltip-arrow { - border-top-color: $dark-gray-400; - border-bottom-color: $dark-gray-400; - } -} diff --git a/blocks/library/freeform/old-editor.js b/blocks/library/freeform/old-editor.js index 73d30c743e1299..40bfaaefa7bc4b 100644 --- a/blocks/library/freeform/old-editor.js +++ b/blocks/library/freeform/old-editor.js @@ -28,6 +28,7 @@ export default class OldEditor extends Component { super( props ); this.initialize = this.initialize.bind( this ); this.onSetup = this.onSetup.bind( this ); + this.focus = this.focus.bind( this ); } componentDidMount() { @@ -78,6 +79,8 @@ export default class OldEditor extends Component { const { attributes: { content }, setAttributes } = this.props; const { ref } = this; + this.editor = editor; + if ( content ) { editor.on( 'loadContent', () => editor.setContent( content ) ); } @@ -109,18 +112,38 @@ export default class OldEditor extends Component { editor.dom.toggleClass( ref, 'has-advanced-toolbar', active ); }, } ); + + editor.on( 'init', () => { + const rootNode = this.editor.getBody(); + + // Create the toolbar by refocussing the editor. + if ( document.activeElement === rootNode ) { + rootNode.blur(); + this.editor.focus(); + } + } ); + } + + focus() { + if ( this.editor ) { + this.editor.focus(); + } } render() { - const { isSelected, id } = this.props; + const { id } = this.props; return [ + // Disable reason: Clicking on this visual placeholder should create + // the toolbar, it can also be created by focussing the field below. + /* eslint-disable jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */
this.ref = ref } className="freeform-toolbar" - style={ ! isSelected ? { display: 'none' } : {} } + onClick={ this.focus } + data-placeholder={ __( 'Classic' ) } />,
`; diff --git a/blocks/library/gallery/test/index.js b/blocks/library/gallery/test/index.js index 1256a4d6aa6429..b64ac373dd8d5e 100644 --- a/blocks/library/gallery/test/index.js +++ b/blocks/library/gallery/test/index.js @@ -4,8 +4,6 @@ import { name, settings } from '../'; import { blockEditRender } from 'blocks/test/helpers'; -jest.mock( 'blocks/media-upload', () => () => '*** Mock(Media upload button) ***' ); - describe( 'core/gallery', () => { test( 'block edit matches snapshot', () => { const wrapper = blockEditRender( name, settings ); diff --git a/blocks/library/heading/editor.scss b/blocks/library/heading/editor.scss index 9ea90441b5405b..e410681721783b 100644 --- a/blocks/library/heading/editor.scss +++ b/blocks/library/heading/editor.scss @@ -8,27 +8,28 @@ margin: 0; } + // These follow a Major Third type scale h1 { - font-size: 2em; + font-size: 2.44em; } h2 { - font-size: 1.6em; + font-size: 1.95em; } h3 { - font-size: 1.4em; + font-size: 1.56em; } h4 { - font-size: 1.2em; + font-size: 1.25em; } h5 { - font-size: 1.1em; + font-size: 1em; } h6 { - font-size: 1em; + font-size: 0.8em; } } diff --git a/blocks/library/heading/index.js b/blocks/library/heading/index.js index 74f2bcbfcb069e..cfe9eedd435a53 100644 --- a/blocks/library/heading/index.js +++ b/blocks/library/heading/index.js @@ -3,7 +3,7 @@ */ import { __, sprintf } from '@wordpress/i18n'; import { concatChildren } from '@wordpress/element'; -import { Toolbar } from '@wordpress/components'; +import { PanelBody, Toolbar } from '@wordpress/components'; /** * Internal dependencies @@ -101,7 +101,7 @@ export const settings = { }; }, - edit( { attributes, setAttributes, isSelected, mergeBlocks, insertBlocksAfter, onReplace } ) { + edit( { attributes, setAttributes, isSelected, mergeBlocks, insertBlocksAfter, onReplace, className } ) { const { align, content, nodeName, placeholder } = attributes; return [ @@ -121,26 +121,27 @@ export const settings = { ), isSelected && ( -

{ __( 'Heading Settings' ) }

-

{ __( 'Level' ) }

- ( { - icon: 'heading', - title: sprintf( __( 'Heading %s' ), level ), - isActive: 'H' + level === nodeName, - onClick: () => setAttributes( { nodeName: 'H' + level } ), - subscript: level, - } ) ) - } - /> -

{ __( 'Text Alignment' ) }

- { - setAttributes( { align: nextAlign } ); - } } - /> + +

{ __( 'Level' ) }

+ ( { + icon: 'heading', + title: sprintf( __( 'Heading %s' ), level ), + isActive: 'H' + level === nodeName, + onClick: () => setAttributes( { nodeName: 'H' + level } ), + subscript: level, + } ) ) + } + /> +

{ __( 'Text Alignment' ) }

+ { + setAttributes( { align: nextAlign } ); + } } + /> +
), onReplace( [] ) } style={ { textAlign: align } } + className={ className } placeholder={ placeholder || __( 'Write heading…' ) } isSelected={ isSelected } />, diff --git a/blocks/library/heading/test/__snapshots__/index.js.snap b/blocks/library/heading/test/__snapshots__/index.js.snap index b609495cafebb5..3da981c56f1bd2 100644 --- a/blocks/library/heading/test/__snapshots__/index.js.snap +++ b/blocks/library/heading/test/__snapshots__/index.js.snap @@ -4,16 +4,28 @@ exports[`core/heading block edit matches snapshot 1`] = `
-

-

- Write heading… -

+
+
+
+

+ Write heading… +

+
+
+
`; diff --git a/blocks/library/html/editor.scss b/blocks/library/html/editor.scss index 40409e2c819c4a..6a76fd7d376570 100644 --- a/blocks/library/html/editor.scss +++ b/blocks/library/html/editor.scss @@ -1,9 +1,25 @@ -.wp-block-html.blocks-plain-text { - font-family: $editor-html-font; - font-size: $text-editor-font-size; - color: $dark-gray-800; - padding: .8em 1.6em; - overflow-x: auto !important; - border: 1px solid $light-gray-500; - border-radius: 4px; +.gutenberg .wp-block-html { + iframe { + display: block; + + // Disable pointer events so that we can click on the block to select it + pointer-events: none; + } + + .CodeMirror { + border-radius: 4px; + border: 1px solid $light-gray-500; + font-family: $editor-html-font; + font-size: $text-editor-font-size; + height: auto; + } + + .CodeMirror-gutters { + background: $white; + border-right: none; + } + + .CodeMirror-lines { + padding: 8px 8px 8px 0; + } } diff --git a/blocks/library/html/index.js b/blocks/library/html/index.js index 9ed3e582b72944..8ed57744305e32 100644 --- a/blocks/library/html/index.js +++ b/blocks/library/html/index.js @@ -3,14 +3,13 @@ */ import { RawHTML } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; -import { withState } from '@wordpress/components'; +import { withState, SandBox, CodeEditor } from '@wordpress/components'; /** * Internal dependencies */ import './editor.scss'; import BlockControls from '../../block-controls'; -import PlainText from '../../plain-text'; export const name = 'core/html'; @@ -49,35 +48,38 @@ export const settings = { edit: withState( { preview: false, - } )( ( { attributes, setAttributes, setState, isSelected, preview } ) => [ - isSelected && ( - -
- - -
-
- ), - preview ? -
: - setAttributes( { content } ) } - aria-label={ __( 'HTML' ) } - />, - ] ), + } )( ( { attributes, setAttributes, setState, isSelected, toggleSelection, preview } ) => ( + <div className="wp-block-html"> + { isSelected && ( + <BlockControls> + <div className="components-toolbar"> + <button + className={ `components-tab-button ${ ! preview ? 'is-active' : '' }` } + onClick={ () => setState( { preview: false } ) } + > + <span>HTML</span> + </button> + <button + className={ `components-tab-button ${ preview ? 'is-active' : '' }` } + onClick={ () => setState( { preview: true } ) } + > + <span>{ __( 'Preview' ) }</span> + </button> + </div> + </BlockControls> + ) } + { preview ? ( + <SandBox html={ attributes.content } /> + ) : ( + <CodeEditor + value={ attributes.content } + focus={ isSelected } + onFocus={ toggleSelection } + onChange={ content => setAttributes( { content } ) } + /> + ) } + </div> + ) ), save( { attributes } ) { return <RawHTML>{ attributes.content }</RawHTML>; diff --git a/blocks/library/html/test/__snapshots__/index.js.snap b/blocks/library/html/test/__snapshots__/index.js.snap index ab2e253b0e5722..465266d93baeef 100644 --- a/blocks/library/html/test/__snapshots__/index.js.snap +++ b/blocks/library/html/test/__snapshots__/index.js.snap @@ -1,9 +1,22 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`core/html block edit matches snapshot 1`] = ` -<textarea - aria-label="HTML" - class="blocks-plain-text wp-block-html" - rows="1" -/> +<div + class="wp-block-html" +> + <div + class="components-placeholder" + > + <div + class="components-placeholder__label" + /> + <div + class="components-placeholder__fieldset" + > + <span + class="spinner is-active" + /> + </div> + </div> +</div> `; diff --git a/blocks/library/image/block.js b/blocks/library/image/block.js index 301faa17a6aed1..7629ab5c58f0e8 100644 --- a/blocks/library/image/block.js +++ b/blocks/library/image/block.js @@ -8,6 +8,7 @@ import { isEmpty, map, get, + pick, } from 'lodash'; /** @@ -15,15 +16,15 @@ import { */ import { __ } from '@wordpress/i18n'; import { Component, compose } from '@wordpress/element'; -import { createMediaFromFile, getBlobByURL, revokeBlobURL, viewPort } from '@wordpress/utils'; +import { getBlobByURL, revokeBlobURL, viewPort } from '@wordpress/utils'; import { IconButton, + PanelBody, SelectControl, - TextControl, + TextareaControl, Toolbar, - withAPIData, - withContext, } from '@wordpress/components'; +import { withSelect } from '@wordpress/data'; /** * Internal dependencies @@ -36,6 +37,8 @@ import BlockControls from '../../block-controls'; import BlockAlignmentToolbar from '../../block-alignment-toolbar'; import UrlInputButton from '../../url-input/button'; import ImageSize from './image-size'; +import { mediaUpload } from '../../../utils/mediaupload'; +import { withEditorSettings } from '../../editor-settings'; /** * Module constants @@ -64,13 +67,16 @@ class ImageBlock extends Component { if ( ! id && url.indexOf( 'blob:' ) === 0 ) { getBlobByURL( url ) - .then( createMediaFromFile ) - .then( ( media ) => { - setAttributes( { - id: media.id, - url: media.source_url, - } ); - } ); + .then( + ( file ) => + mediaUpload( + [ file ], + ( [ image ] ) => { + setAttributes( { ...image } ); + }, + 'image' + ) + ); } } @@ -92,11 +98,7 @@ class ImageBlock extends Component { } onSelectImage( media ) { - const attributes = { url: media.url, alt: media.alt, id: media.id }; - if ( media.caption ) { - attributes.caption = [ media.caption ]; - } - this.props.setAttributes( attributes ); + this.props.setAttributes( pick( media, [ 'alt', 'id', 'caption', 'url' ] ) ); } onSetHref( value ) { @@ -135,7 +137,7 @@ class ImageBlock extends Component { } getAvailableSizes() { - return get( this.props.image, [ 'data', 'media_details', 'sizes' ], {} ); + return get( this.props.image, [ 'media_details', 'sizes' ], {} ); } render() { @@ -200,19 +202,25 @@ class ImageBlock extends Component { controls, isSelected && ( <InspectorControls key="inspector"> - <h2>{ __( 'Image Settings' ) }</h2> - <TextControl label={ __( 'Textual Alternative' ) } value={ alt } onChange={ this.updateAlt } help={ __( 'Describe the purpose of the image. Leave empty if the image is not a key part of the content.' ) } /> - { ! isEmpty( availableSizes ) && ( - <SelectControl - label={ __( 'Size' ) } - value={ url } - options={ map( availableSizes, ( size, name ) => ( { - value: size.source_url, - label: startCase( name ), - } ) ) } - onChange={ this.updateImageURL } + <PanelBody title={ __( 'Image Settings' ) }> + <TextareaControl + label={ __( 'Textual Alternative' ) } + value={ alt } + onChange={ this.updateAlt } + help={ __( 'Describe the purpose of the image. Leave empty if the image is not a key part of the content.' ) } /> - ) } + { ! isEmpty( availableSizes ) && ( + <SelectControl + label={ __( 'Size' ) } + value={ url } + options={ map( availableSizes, ( size, name ) => ( { + value: size.source_url, + label: startCase( name ), + } ) ) } + onChange={ this.updateImageURL } + /> + ) } + </PanelBody> </InspectorControls> ), <figure key="image" className={ classes } style={ figureStyle }> @@ -279,7 +287,7 @@ class ImageBlock extends Component { <RichText tagName="figcaption" placeholder={ __( 'Write caption…' ) } - value={ caption } + value={ caption || [] } onFocus={ this.onFocusCaption } onChange={ ( value ) => setAttributes( { caption: value } ) } isSelected={ this.state.captionFocused } @@ -293,17 +301,13 @@ class ImageBlock extends Component { } export default compose( [ - withContext( 'editor' )( ( settings ) => { - return { settings }; - } ), - withAPIData( ( props ) => { + withEditorSettings(), + withSelect( ( select, props ) => { + const { getMedia } = select( 'core' ); const { id } = props.attributes; - if ( ! id ) { - return {}; - } return { - image: `/wp/v2/media/${ id }`, + image: id ? getMedia( id ) : null, }; } ), ] )( ImageBlock ); diff --git a/blocks/library/image/editor.scss b/blocks/library/image/editor.scss index 3c698fdd864dfa..db51468bc901e3 100644 --- a/blocks/library/image/editor.scss +++ b/blocks/library/image/editor.scss @@ -59,10 +59,15 @@ .blocks-format-toolbar__link-modal { top: 0; left: 0; + position: absolute; + border: none; + width: 100%; } } -.wp-core-ui .wp-block-image__upload-button.button { +.wp-core-ui .wp-block-audio__upload-button.button, +.wp-core-ui .wp-block-image__upload-button.button, +.wp-core-ui .wp-block-video__upload-button.button { margin-right: 5px; .dashicon { @@ -82,7 +87,7 @@ margin-right: auto; } - &[data-resized="false"] .wp-block-image div { + &[data-resized="false"] .wp-block-image > div { margin-left: auto; margin-right: auto; } diff --git a/blocks/library/image/index.js b/blocks/library/image/index.js index 8fcd736fef2514..ea04c238926ca4 100644 --- a/blocks/library/image/index.js +++ b/blocks/library/image/index.js @@ -2,7 +2,6 @@ * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { createMediaFromFile, preloadImage } from '@wordpress/utils'; /** * Internal dependencies @@ -36,7 +35,7 @@ const blockAttributes = { href: { type: 'string', source: 'attribute', - selector: 'a', + selector: 'figure > a', attribute: 'href', }, id: { @@ -89,17 +88,15 @@ export const settings = { isMatch( files ) { return files.length === 1 && files[ 0 ].type.indexOf( 'image/' ) === 0; }, - transform( files, onChange ) { + transform( files ) { const file = files[ 0 ]; + // We don't need to upload the media directly here + // It's already done as part of the `componentDidMount` + // int the image block const block = createBlock( 'core/image', { url: window.URL.createObjectURL( file ), } ); - createMediaFromFile( file ) - .then( ( media ) => preloadImage( media.source_url ).then( - () => onChange( block.uid, { id: media.id, url: media.source_url } ) - ) ); - return block; }, }, @@ -173,16 +170,8 @@ export const settings = { /> ); - let figureStyle = {}; - - if ( width ) { - figureStyle = { width }; - } else if ( align === 'left' || align === 'right' ) { - figureStyle = { maxWidth: '50%' }; - } - return ( - <figure className={ align ? `align${ align }` : null } style={ figureStyle }> + <figure className={ align ? `align${ align }` : null }> { href ? <a href={ href }>{ image }</a> : image } { caption && caption.length > 0 && <figcaption>{ caption }</figcaption> } </figure> diff --git a/blocks/library/image/style.scss b/blocks/library/image/style.scss index c5ce93a482c36e..bd22c450854ce4 100644 --- a/blocks/library/image/style.scss +++ b/blocks/library/image/style.scss @@ -1,6 +1,16 @@ -.wp-block-image figcaption { - margin-top: 0.5em; - color: $dark-gray-300; - text-align: center; - font-size: $default-font-size; +.wp-block-image { + width: fit-content; + figcaption { + margin-top: 0.5em; + color: $dark-gray-300; + text-align: center; + font-size: $default-font-size; + } + + &.aligncenter { + display: block; + margin-left: auto; + margin-right: auto; + text-align: center; + } } diff --git a/blocks/library/index.js b/blocks/library/index.js index b55e5b2025cf50..777e9aa0325a0f 100644 --- a/blocks/library/index.js +++ b/blocks/library/index.js @@ -6,6 +6,11 @@ import { setDefaultBlockName, setUnknownTypeHandlerName, } from '../api'; +import * as paragraph from './paragraph'; +import * as image from './image'; +import * as heading from './heading'; +import * as quote from './quote'; +import * as gallery from './gallery'; import * as audio from './audio'; import * as button from './button'; import * as categories from './categories'; @@ -14,18 +19,14 @@ import * as columns from './columns'; import * as coverImage from './cover-image'; import * as embed from './embed'; import * as freeform from './freeform'; -import * as gallery from './gallery'; -import * as heading from './heading'; import * as html from './html'; -import * as image from './image'; import * as latestPosts from './latest-posts'; import * as list from './list'; import * as more from './more'; -import * as paragraph from './paragraph'; +import * as nextpage from './nextpage'; import * as preformatted from './preformatted'; import * as pullquote from './pullquote'; -import * as quote from './quote'; -import * as reusableBlock from './block'; +import * as sharedBlock from './block'; import * as separator from './separator'; import * as shortcode from './shortcode'; import * as subhead from './subhead'; @@ -36,26 +37,17 @@ import * as video from './video'; export const registerCoreBlocks = () => { [ - // FIXME: Temporary fix. - // - // The Shortcode block declares a catch-all shortcode transform, - // meaning it will attempt to intercept pastes and block conversions of - // any valid shortcode-like content. Other blocks (e.g. Gallery) may - // declare specific shortcode transforms (e.g. `[gallery]`), with which - // this block would conflict. Thus, the Shortcode block needs to be - // registered as early as possible, so that any other block types' - // shortcode transforms can be honoured. - // - // This isn't a proper solution, as it is at odds with the - // specification of shortcode conversion, in the sense that conversion - // is explicitly independent of block order. Thus, concurrent parse - // rules (i.e. a same text input can yield two different transforms, - // like `[gallery] -> { Gallery, Shortcode }`) are unsupported, - // yielding non-deterministic results. A proper solution could be to - // let the editor (or site owners) determine a default block handler of - // unknown shortcodes — see `setUnknownTypeHandlerName`. - shortcode, + // Common blocks are grouped at the top to prioritize their display + // in various contexts — like the inserter and auto-complete components. + paragraph, + image, + heading, + gallery, + list, + quote, + // Register all remaining core blocks. + shortcode, audio, button, categories, @@ -66,19 +58,14 @@ export const registerCoreBlocks = () => { ...embed.common, ...embed.others, freeform, - gallery, - heading, html, - image, - list, latestPosts, more, - paragraph, + nextpage, preformatted, pullquote, - quote, - reusableBlock, separator, + sharedBlock, subhead, table, textColumns, diff --git a/blocks/library/latest-posts/block.js b/blocks/library/latest-posts/block.js index 0ca30c391bd821..2feae16fa2d46c 100644 --- a/blocks/library/latest-posts/block.js +++ b/blocks/library/latest-posts/block.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { isUndefined, pickBy } from 'lodash'; +import { get, isUndefined, pickBy } from 'lodash'; import moment from 'moment'; import classnames from 'classnames'; import { stringify } from 'querystringify'; @@ -11,7 +11,9 @@ import { stringify } from 'querystringify'; */ import { Component } from '@wordpress/element'; import { + PanelBody, Placeholder, + QueryControls, RangeControl, Spinner, ToggleControl, @@ -26,7 +28,6 @@ import { decodeEntities } from '@wordpress/utils'; */ import './editor.scss'; import './style.scss'; -import QueryPanel from '../../query-panel'; import InspectorControls from '../../inspector-controls'; import BlockControls from '../../block-controls'; import BlockAlignmentToolbar from '../../block-alignment-toolbar'; @@ -49,35 +50,37 @@ class LatestPostsBlock extends Component { render() { const latestPosts = this.props.latestPosts.data; - const { attributes, isSelected, setAttributes } = this.props; + const { attributes, categoriesList, isSelected, setAttributes } = this.props; const { displayPostDate, align, layout, columns, order, orderBy, categories, postsToShow } = attributes; const inspectorControls = isSelected && ( <InspectorControls key="inspector"> - <h3>{ __( 'Latest Posts Settings' ) }</h3> - <QueryPanel - { ...{ order, orderBy } } - numberOfItems={ postsToShow } - category={ categories } - onOrderChange={ ( value ) => setAttributes( { order: value } ) } - onOrderByChange={ ( value ) => setAttributes( { orderBy: value } ) } - onCategoryChange={ ( value ) => setAttributes( { categories: '' !== value ? value : undefined } ) } - onNumberOfItemsChange={ ( value ) => setAttributes( { postsToShow: value } ) } - /> - <ToggleControl - label={ __( 'Display post date' ) } - checked={ displayPostDate } - onChange={ this.toggleDisplayPostDate } - /> - { layout === 'grid' && - <RangeControl - label={ __( 'Columns' ) } - value={ columns } - onChange={ ( value ) => setAttributes( { columns: value } ) } - min={ 2 } - max={ ! hasPosts ? MAX_POSTS_COLUMNS : Math.min( MAX_POSTS_COLUMNS, latestPosts.length ) } + <PanelBody title={ __( 'Latest Posts Settings' ) }> + <QueryControls + { ...{ order, orderBy } } + numberOfItems={ postsToShow } + categoriesList={ get( categoriesList, 'data', {} ) } + selectedCategoryId={ categories } + onOrderChange={ ( value ) => setAttributes( { order: value } ) } + onOrderByChange={ ( value ) => setAttributes( { orderBy: value } ) } + onCategoryChange={ ( value ) => setAttributes( { categories: '' !== value ? value : undefined } ) } + onNumberOfItemsChange={ ( value ) => setAttributes( { postsToShow: value } ) } /> - } + <ToggleControl + label={ __( 'Display post date' ) } + checked={ displayPostDate } + onChange={ this.toggleDisplayPostDate } + /> + { layout === 'grid' && + <RangeControl + label={ __( 'Columns' ) } + value={ columns } + onChange={ ( value ) => setAttributes( { columns: value } ) } + min={ 2 } + max={ ! hasPosts ? MAX_POSTS_COLUMNS : Math.min( MAX_POSTS_COLUMNS, latestPosts.length ) } + /> + } + </PanelBody> </InspectorControls> ); @@ -155,14 +158,19 @@ class LatestPostsBlock extends Component { export default withAPIData( ( props ) => { const { postsToShow, order, orderBy, categories } = props.attributes; - const queryString = stringify( pickBy( { + const latestPostsQuery = stringify( pickBy( { categories, order, orderBy, per_page: postsToShow, _fields: [ 'date_gmt', 'link', 'title' ], }, value => ! isUndefined( value ) ) ); + const categoriesListQuery = stringify( { + per_page: 100, + _fields: [ 'id', 'name', 'parent' ], + } ); return { - latestPosts: `/wp/v2/posts?${ queryString }`, + latestPosts: `/wp/v2/posts?${ latestPostsQuery }`, + categoriesList: `/wp/v2/categories?${ categoriesListQuery }`, }; } )( LatestPostsBlock ); diff --git a/blocks/library/list/index.js b/blocks/library/list/index.js index 1c4d846344269a..40093bb84ea724 100644 --- a/blocks/library/list/index.js +++ b/blocks/library/list/index.js @@ -234,6 +234,7 @@ export const settings = { setAttributes, mergeBlocks, onReplace, + className, } = this.props; const { nodeName, values } = attributes; @@ -276,6 +277,7 @@ export const settings = { onChange={ this.setNextValues } value={ values } wrapperClassName="blocks-list" + className={ className } placeholder={ __( 'Write list…' ) } onMerge={ mergeBlocks } onSplit={ diff --git a/blocks/library/list/test/__snapshots__/index.js.snap b/blocks/library/list/test/__snapshots__/index.js.snap index be2068fdf123f7..0dfeabf5c9554d 100644 --- a/blocks/library/list/test/__snapshots__/index.js.snap +++ b/blocks/library/list/test/__snapshots__/index.js.snap @@ -4,18 +4,29 @@ exports[`core/list block edit matches snapshot 1`] = ` <div class="blocks-list blocks-rich-text" > - <ul - aria-label="Write list…" - class="blocks-rich-text__tinymce" - contenteditable="true" - data-is-placeholder-visible="true" - /> - <ul - class="blocks-rich-text__tinymce" - > - <li> - Write list… - </li> - </ul> + <div> + <div> + <div + class="components-autocomplete" + > + <ul + aria-autocomplete="list" + aria-expanded="false" + aria-label="Write list…" + aria-multiline="true" + class="blocks-rich-text__tinymce" + contenteditable="true" + data-is-placeholder-visible="true" + /> + <ul + class="blocks-rich-text__tinymce" + > + <li> + Write list… + </li> + </ul> + </div> + </div> + </div> </div> `; diff --git a/blocks/library/more/editor.scss b/blocks/library/more/editor.scss index 73f74aa4419fd0..261e549b11be34 100644 --- a/blocks/library/more/editor.scss +++ b/blocks/library/more/editor.scss @@ -3,31 +3,40 @@ text-align: center; } -.gutenberg .wp-block-more { +.gutenberg .wp-block-more { // needs specificity + display: block; + text-align: center; + white-space: nowrap; + + // Label input { font-size: 12px; text-transform: uppercase; font-weight: 600; font-family: $default-font; color: $dark-gray-300; - padding-left: 8px; - padding-right: 8px; - background: $white; border: none; box-shadow: none; white-space: nowrap; + text-align: center; + margin: 0; + border-radius: 4px; + background: $white; + padding: 6px 8px; + height: $icon-button-size-small; &:focus { box-shadow: none; } } + // Dashed line &:before { content: ''; position: absolute; top: calc( 50% ); - left: $block-mover-padding-visible + $block-padding; - right: $block-mover-padding-visible + $block-padding; + left: 0; + right: 0; border-top: 3px dashed $light-gray-700; z-index: z-index( '.editor-block-list__block .wp-block-more:before' ); } diff --git a/blocks/library/more/index.js b/blocks/library/more/index.js index b99d21dc975d7c..cbaddeefe52c5a 100644 --- a/blocks/library/more/index.js +++ b/blocks/library/more/index.js @@ -7,8 +7,8 @@ import { compact } from 'lodash'; * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { ToggleControl } from '@wordpress/components'; -import { RawHTML } from '@wordpress/element'; +import { PanelBody, ToggleControl } from '@wordpress/components'; +import { Component, RawHTML } from '@wordpress/element'; /** * Internal dependencies @@ -68,33 +68,57 @@ export const settings = { ], }, - edit( { attributes, setAttributes, isSelected } ) { - const { customText, noTeaser } = attributes; - - const toggleNoTeaser = () => setAttributes( { noTeaser: ! noTeaser } ); - const defaultText = __( 'Read more' ); - const value = customText !== undefined ? customText : defaultText; - const inputLength = value.length ? value.length + 1 : 1; - - return [ - isSelected && ( - <InspectorControls key="inspector"> - <ToggleControl - label={ __( 'Hide the teaser before the "More" tag' ) } - checked={ !! noTeaser } - onChange={ toggleNoTeaser } + edit: class extends Component { + constructor() { + super( ...arguments ); + this.onChangeInput = this.onChangeInput.bind( this ); + + this.state = { + defaultText: __( 'Read more' ), + }; + } + + onChangeInput( event ) { + // Set defaultText to an empty string, allowing the user to clear/replace the input field's text + this.setState( { + defaultText: '', + } ); + + const value = event.target.value.length === 0 ? undefined : event.target.value; + this.props.setAttributes( { customText: value } ); + } + + render() { + const { customText, noTeaser } = this.props.attributes; + const { setAttributes, isSelected } = this.props; + + const toggleNoTeaser = () => setAttributes( { noTeaser: ! noTeaser } ); + const { defaultText } = this.state; + const value = customText !== undefined ? customText : defaultText; + const inputLength = value.length + 1; + + return [ + isSelected && ( + <InspectorControls key="inspector"> + <PanelBody> + <ToggleControl + label={ __( 'Hide the teaser before the "More" tag' ) } + checked={ !! noTeaser } + onChange={ toggleNoTeaser } + /> + </PanelBody> + </InspectorControls> + ), + <div key="more-tag" className="wp-block-more"> + <input + type="text" + value={ value } + size={ inputLength } + onChange={ this.onChangeInput } /> - </InspectorControls> - ), - <div key="more-tag" className="wp-block-more"> - <input - type="text" - value={ value } - size={ inputLength } - onChange={ ( event ) => setAttributes( { customText: event.target.value } ) } - /> - </div>, - ]; + </div>, + ]; + } }, save( { attributes } ) { diff --git a/blocks/library/paragraph/editor.scss b/blocks/library/paragraph/editor.scss index a50c04177658f0..a7a87b04151266 100644 --- a/blocks/library/paragraph/editor.scss +++ b/blocks/library/paragraph/editor.scss @@ -1,3 +1,11 @@ -.editor-block-list__block:not( .is-multi-selected ) .wp-block-paragraph { - background: white; +.blocks-font-size__main { + display: flex; + justify-content: space-between; +} + +.blocks-paragraph__custom-size-slider { + .components-range-control__slider + .dashicon { + width: 30px; + height: 30px; + } } diff --git a/blocks/library/paragraph/index.js b/blocks/library/paragraph/index.js index f31fa3d1374d69..c9cbb73bdbe8f0 100644 --- a/blocks/library/paragraph/index.js +++ b/blocks/library/paragraph/index.js @@ -2,18 +2,20 @@ * External dependencies */ import classnames from 'classnames'; +import { findKey, isFinite, map, omit } from 'lodash'; /** * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { concatChildren, Component } from '@wordpress/element'; +import { concatChildren, Component, RawHTML } from '@wordpress/element'; import { - Autocomplete, PanelBody, PanelColor, RangeControl, ToggleControl, + Button, + ButtonGroup, withFallbackStyles, } from '@wordpress/components'; @@ -23,7 +25,8 @@ import { import './editor.scss'; import './style.scss'; import { createBlock } from '../../api'; -import { blockAutocompleter, userAutocompleter } from '../../autocompleters'; +import { blockAutocompleter } from '../../autocompleters'; +import { defaultAutocompleters } from '../../hooks/default-autocompleters'; import AlignmentToolbar from '../../alignment-toolbar'; import BlockAlignmentToolbar from '../../block-alignment-toolbar'; import BlockControls from '../../block-controls'; @@ -34,25 +37,34 @@ import ContrastChecker from '../../contrast-checker'; const { getComputedStyle } = window; -const ContrastCheckerWithFallbackStyles = withFallbackStyles( ( node, ownProps ) => { - const { textColor, backgroundColor } = ownProps; - //avoid the use of querySelector if both colors are known and verify if node is available. - const editableNode = ( ! textColor || ! backgroundColor ) && node ? node.querySelector( '[contenteditable="true"]' ) : null; +const FallbackStyles = withFallbackStyles( ( node, ownProps ) => { + const { textColor, backgroundColor, fontSize, customFontSize } = ownProps.attributes; + const editableNode = node.querySelector( '[contenteditable="true"]' ); //verify if editableNode is available, before using getComputedStyle. const computedStyles = editableNode ? getComputedStyle( editableNode ) : null; return { fallbackBackgroundColor: backgroundColor || ! computedStyles ? undefined : computedStyles.backgroundColor, fallbackTextColor: textColor || ! computedStyles ? undefined : computedStyles.color, + fallbackFontSize: fontSize || customFontSize || ! computedStyles ? undefined : parseInt( computedStyles.fontSize ) || undefined, }; -} )( ContrastChecker ); +} ); + +const FONT_SIZES = { + small: 14, + regular: 16, + large: 36, + larger: 48, +}; + +const autocompleters = [ blockAutocompleter, ...defaultAutocompleters ]; class ParagraphBlock extends Component { constructor() { super( ...arguments ); - this.nodeRef = null; - this.bindRef = this.bindRef.bind( this ); this.onReplace = this.onReplace.bind( this ); this.toggleDropCap = this.toggleDropCap.bind( this ); + this.getFontSize = this.getFontSize.bind( this ); + this.setFontSize = this.setFontSize.bind( this ); } onReplace( blocks ) { @@ -74,11 +86,31 @@ class ParagraphBlock extends Component { setAttributes( { dropCap: ! attributes.dropCap } ); } - bindRef( node ) { - if ( ! node ) { + getFontSize() { + const { customFontSize, fontSize } = this.props.attributes; + if ( fontSize ) { + return FONT_SIZES[ fontSize ]; + } + + if ( customFontSize ) { + return customFontSize; + } + } + + setFontSize( fontSizeValue ) { + const { setAttributes } = this.props; + const thresholdFontSize = findKey( FONT_SIZES, ( size ) => size === fontSizeValue ); + if ( thresholdFontSize ) { + setAttributes( { + fontSize: thresholdFontSize, + customFontSize: undefined, + } ); return; } - this.nodeRef = node; + setAttributes( { + fontSize: undefined, + customFontSize: fontSizeValue, + } ); } render() { @@ -89,6 +121,10 @@ class ParagraphBlock extends Component { isSelected, mergeBlocks, onReplace, + className, + fallbackBackgroundColor, + fallbackTextColor, + fallbackFontSize, } = this.props; const { @@ -96,13 +132,12 @@ class ParagraphBlock extends Component { content, dropCap, placeholder, - fontSize, backgroundColor, textColor, width, } = attributes; - const className = dropCap ? 'has-drop-cap' : null; + const fontSize = this.getFontSize(); return [ isSelected && ( @@ -117,21 +152,49 @@ class ParagraphBlock extends Component { ), isSelected && ( <InspectorControls key="inspector"> - <PanelBody title={ __( 'Text Settings' ) }> + <PanelBody title={ __( 'Text Settings' ) } className="blocks-font-size"> + <div className="blocks-font-size__main"> + <ButtonGroup aria-label={ __( 'Font Size' ) }> + { map( { + S: 'small', + M: 'regular', + L: 'large', + XL: 'larger', + }, ( size, label ) => ( + <Button + key={ label } + isLarge + isPrimary={ fontSize === FONT_SIZES[ size ] } + aria-pressed={ fontSize === FONT_SIZES[ size ] } + onClick={ () => this.setFontSize( FONT_SIZES[ size ] ) } + > + { label } + </Button> + ) ) } + </ButtonGroup> + <Button + isLarge + onClick={ () => this.setFontSize( undefined ) } + > + { __( 'Reset' ) } + </Button> + </div> + <RangeControl + className="blocks-paragraph__custom-size-slider" + label={ __( 'Custom Size' ) } + value={ fontSize || '' } + initialPosition={ fallbackFontSize } + onChange={ ( value ) => this.setFontSize( value ) } + min={ 12 } + max={ 100 } + beforeIcon="editor-textcolor" + afterIcon="editor-textcolor" + /> <ToggleControl label={ __( 'Drop Cap' ) } checked={ !! dropCap } onChange={ this.toggleDropCap } /> - <RangeControl - label={ __( 'Font Size' ) } - value={ fontSize || '' } - onChange={ ( value ) => setAttributes( { fontSize: value } ) } - min={ 10 } - max={ 200 } - beforeIcon="editor-textcolor" - allowReset - /> </PanelBody> <PanelColor title={ __( 'Background Color' ) } colorValue={ backgroundColor } initialOpen={ false }> <ColorPalette @@ -145,12 +208,15 @@ class ParagraphBlock extends Component { onChange={ ( colorValue ) => setAttributes( { textColor: colorValue } ) } /> </PanelColor> - { this.nodeRef && <ContrastCheckerWithFallbackStyles - node={ this.nodeRef } - textColor={ textColor } - backgroundColor={ backgroundColor } + <ContrastChecker + { ...{ + textColor, + backgroundColor, + fallbackBackgroundColor, + fallbackTextColor, + } } isLargeText={ fontSize >= 18 } - /> } + /> <PanelBody title={ __( 'Block Alignment' ) }> <BlockAlignmentToolbar value={ width } @@ -159,51 +225,42 @@ class ParagraphBlock extends Component { </PanelBody> </InspectorControls> ), - <div key="editable" ref={ this.bindRef }> - <Autocomplete completers={ [ - blockAutocompleter( { onReplace } ), - userAutocompleter(), - ] }> - { ( { isExpanded, listBoxId, activeId } ) => ( - <RichText - tagName="p" - className={ classnames( 'wp-block-paragraph', className, { - 'has-background': backgroundColor, - } ) } - style={ { - backgroundColor: backgroundColor, - color: textColor, - fontSize: fontSize ? fontSize + 'px' : undefined, - textAlign: align, - } } - value={ content } - onChange={ ( nextContent ) => { - setAttributes( { - content: nextContent, - } ); - } } - onSplit={ insertBlocksAfter ? - ( before, after, ...blocks ) => { - setAttributes( { content: before } ); - insertBlocksAfter( [ - ...blocks, - createBlock( 'core/paragraph', { content: after } ), - ] ); - } : - undefined - } - onMerge={ mergeBlocks } - onReplace={ this.onReplace } - onRemove={ () => onReplace( [] ) } - placeholder={ placeholder || __( 'Add text or type / to add content' ) } - aria-autocomplete="list" - aria-expanded={ isExpanded } - aria-owns={ listBoxId } - aria-activedescendant={ activeId } - isSelected={ isSelected } - /> - ) } - </Autocomplete> + <div key="editable"> + <RichText + tagName="p" + className={ classnames( 'wp-block-paragraph', className, { + 'has-background': backgroundColor, + 'has-drop-cap': dropCap, + } ) } + style={ { + backgroundColor: backgroundColor, + color: textColor, + fontSize: fontSize ? fontSize + 'px' : undefined, + textAlign: align, + } } + value={ content } + onChange={ ( nextContent ) => { + setAttributes( { + content: nextContent, + } ); + } } + onSplit={ insertBlocksAfter ? + ( before, after, ...blocks ) => { + setAttributes( { content: before } ); + insertBlocksAfter( [ + ...blocks, + createBlock( 'core/paragraph', { content: after } ), + ] ); + } : + undefined + } + onMerge={ mergeBlocks } + onReplace={ this.onReplace } + onRemove={ () => onReplace( [] ) } + placeholder={ placeholder || __( 'Add text or type / to add content' ) } + isSelected={ isSelected } + autocompleters={ autocompleters } + /> </div>, ]; } @@ -240,6 +297,9 @@ const schema = { type: 'string', }, fontSize: { + type: 'string', + }, + customFontSize: { type: 'number', }, }; @@ -265,6 +325,7 @@ export const settings = { from: [ { type: 'raw', + priority: 20, isMatch: ( node ) => ( node.nodeName === 'P' && // Do not allow embedded content. @@ -275,6 +336,40 @@ export const settings = { }, deprecated: [ + { + supports, + attributes: omit( { + ...schema, + fontSize: { + type: 'number', + }, + }, 'customFontSize' ), + save( { attributes } ) { + const { width, align, content, dropCap, backgroundColor, textColor, fontSize } = attributes; + const className = classnames( { + [ `align${ width }` ]: width, + 'has-background': backgroundColor, + 'has-drop-cap': dropCap, + } ); + const styles = { + backgroundColor: backgroundColor, + color: textColor, + fontSize: fontSize, + textAlign: align, + }; + + return <p style={ styles } className={ className ? className : undefined }>{ content }</p>; + }, + migrate( attributes ) { + if ( isFinite( attributes.fontSize ) ) { + return omit( { + ...attributes, + customFontSize: attributes.fontSize, + }, 'fontSize' ); + } + return attributes; + }, + }, { supports, attributes: { @@ -285,12 +380,14 @@ export const settings = { }, }, save( { attributes } ) { - return attributes.content; + return <RawHTML>{ attributes.content }</RawHTML>; }, migrate( attributes ) { return { ...attributes, - content: [ attributes.content ], + content: [ + <RawHTML key="html">{ attributes.content }</RawHTML>, + ], }; }, }, @@ -309,19 +406,31 @@ export const settings = { } }, - edit: ParagraphBlock, + edit: FallbackStyles( ParagraphBlock ), save( { attributes } ) { - const { width, align, content, dropCap, backgroundColor, textColor, fontSize } = attributes; + const { + width, + align, + content, + dropCap, + backgroundColor, + textColor, + fontSize, + customFontSize, + } = attributes; + const className = classnames( { [ `align${ width }` ]: width, 'has-background': backgroundColor, 'has-drop-cap': dropCap, + [ `is-${ fontSize }-text` ]: fontSize && FONT_SIZES[ fontSize ], } ); + const styles = { backgroundColor: backgroundColor, color: textColor, - fontSize: fontSize, + fontSize: ! fontSize && customFontSize ? customFontSize : undefined, textAlign: align, }; diff --git a/blocks/library/paragraph/style.scss b/blocks/library/paragraph/style.scss index ba590421b23b99..b67e036fde3868 100644 --- a/blocks/library/paragraph/style.scss +++ b/blocks/library/paragraph/style.scss @@ -1,13 +1,31 @@ -p.has-drop-cap { - &:first-letter { - float: left; - font-size: 4.1em; - line-height: 0.7; - font-family: serif; - font-weight: bold; - margin: .07em .23em 0 0; - text-transform: uppercase; - font-style: normal; +p { + &.is-small-text { + font-size: 14px; + } + + &.is-regular-text { + font-size: 16px; + } + + &.is-large-text { + font-size: 36px; + } + + &.is-larger-text { + font-size: 48px; + } + + &.has-drop-cap { + &:first-letter { + float: left; + font-size: 4.1em; + line-height: 0.7; + font-family: serif; + font-weight: 600; + margin: .07em .23em 0 0; + text-transform: uppercase; + font-style: normal; + } } } diff --git a/blocks/library/paragraph/test/__snapshots__/index.js.snap b/blocks/library/paragraph/test/__snapshots__/index.js.snap index 70db105dfef59e..0369b0add114df 100644 --- a/blocks/library/paragraph/test/__snapshots__/index.js.snap +++ b/blocks/library/paragraph/test/__snapshots__/index.js.snap @@ -2,28 +2,36 @@ exports[`core/paragraph block edit matches snapshot 1`] = ` <div> + <div> <div - class="components-autocomplete" + class="blocks-rich-text" > - <div - class="blocks-rich-text" - > - <p - aria-autocomplete="list" - aria-expanded="false" - aria-label="Add text or type / to add content" - class="wp-block-paragraph blocks-rich-text__tinymce" - contenteditable="true" - data-is-placeholder-visible="true" - /> - <p - class="blocks-rich-text__tinymce wp-block-paragraph" - > - Add text or type / to add content - </p> + <div> + <div> + <div + class="components-autocomplete" + > + <p + aria-autocomplete="list" + aria-expanded="false" + aria-label="Add text or type / to add content" + aria-multiline="false" + class="wp-block-paragraph blocks-rich-text__tinymce" + contenteditable="true" + data-is-placeholder-visible="true" + role="textbox" + /> + <p + class="blocks-rich-text__tinymce wp-block-paragraph" + > + Add text or type / to add content + </p> + </div> + </div> </div> </div> </div> + </div> `; diff --git a/blocks/library/preformatted/test/__snapshots__/index.js.snap b/blocks/library/preformatted/test/__snapshots__/index.js.snap index 309ddbdef8a9d5..89317044d5b733 100644 --- a/blocks/library/preformatted/test/__snapshots__/index.js.snap +++ b/blocks/library/preformatted/test/__snapshots__/index.js.snap @@ -4,16 +4,28 @@ exports[`core/preformatted block edit matches snapshot 1`] = ` <div class="wp-block-preformatted blocks-rich-text" > - <pre - aria-label="Write preformatted text…" - class="blocks-rich-text__tinymce" - contenteditable="true" - data-is-placeholder-visible="true" - /> - <pre - class="blocks-rich-text__tinymce" - > - Write preformatted text… - </pre> + <div> + <div> + <div + class="components-autocomplete" + > + <pre + aria-autocomplete="list" + aria-expanded="false" + aria-label="Write preformatted text…" + aria-multiline="false" + class="blocks-rich-text__tinymce" + contenteditable="true" + data-is-placeholder-visible="true" + role="textbox" + /> + <pre + class="blocks-rich-text__tinymce" + > + Write preformatted text… + </pre> + </div> + </div> + </div> </div> `; diff --git a/blocks/library/pullquote/index.js b/blocks/library/pullquote/index.js index 8116d44d63ded6..d0169d59b8ca6f 100644 --- a/blocks/library/pullquote/index.js +++ b/blocks/library/pullquote/index.js @@ -90,6 +90,7 @@ export const settings = { value: fromRichTextValue( nextValue ), } ) } + /* translators: the text of the quotation */ placeholder={ __( 'Write quote…' ) } wrapperClassName="blocks-pullquote__content" isSelected={ isSelected && editable === 'content' } @@ -99,7 +100,8 @@ export const settings = { <RichText tagName="cite" value={ citation } - placeholder={ __( 'Write caption…' ) } + /* translators: the individual or entity quoted */ + placeholder={ __( 'Write citation…' ) } onChange={ ( nextCitation ) => setAttributes( { citation: nextCitation, diff --git a/blocks/library/pullquote/style.scss b/blocks/library/pullquote/style.scss index fa750312ff6ca8..a99cb172322036 100644 --- a/blocks/library/pullquote/style.scss +++ b/blocks/library/pullquote/style.scss @@ -26,6 +26,6 @@ position: relative; font-weight: 900; text-transform: uppercase; - font-size: 13px; + font-size: $default-font-size; } } diff --git a/blocks/library/pullquote/test/__snapshots__/index.js.snap b/blocks/library/pullquote/test/__snapshots__/index.js.snap index d77c0faf63f18a..f8bdb27de8f502 100644 --- a/blocks/library/pullquote/test/__snapshots__/index.js.snap +++ b/blocks/library/pullquote/test/__snapshots__/index.js.snap @@ -7,18 +7,30 @@ exports[`core/pullquote block edit matches snapshot 1`] = ` <div class="blocks-pullquote__content blocks-rich-text" > - <div - aria-label="Write quote…" - class="blocks-rich-text__tinymce" - contenteditable="true" - data-is-placeholder-visible="true" - /> - <div - class="blocks-rich-text__tinymce" - > - <p> - Write quote… - </p> + <div> + <div> + <div + class="components-autocomplete" + > + <div + aria-autocomplete="list" + aria-expanded="false" + aria-label="Write quote…" + aria-multiline="true" + class="blocks-rich-text__tinymce" + contenteditable="true" + data-is-placeholder-visible="true" + role="textbox" + /> + <div + class="blocks-rich-text__tinymce" + > + <p> + Write quote… + </p> + </div> + </div> + </div> </div> </div> </blockquote> diff --git a/blocks/library/quote/index.js b/blocks/library/quote/index.js index 5487416b90824d..98371b07561b81 100644 --- a/blocks/library/quote/index.js +++ b/blocks/library/quote/index.js @@ -210,6 +210,7 @@ export const settings = { onReplace( [] ); } } } + /* translators: the text of the quotation */ placeholder={ __( 'Write quote…' ) } isSelected={ isSelected && editable === 'content' } onFocus={ onSetActiveEditable( 'content' ) } @@ -223,6 +224,7 @@ export const settings = { citation: nextCitation, } ) } + /* translators: the individual or entity quoted */ placeholder={ __( 'Write citation…' ) } isSelected={ isSelected && editable === 'cite' } onFocus={ onSetActiveEditable( 'cite' ) } diff --git a/blocks/library/quote/test/__snapshots__/index.js.snap b/blocks/library/quote/test/__snapshots__/index.js.snap index 54b03c5e572298..010e122e054ab7 100644 --- a/blocks/library/quote/test/__snapshots__/index.js.snap +++ b/blocks/library/quote/test/__snapshots__/index.js.snap @@ -7,18 +7,30 @@ exports[`core/quote block edit matches snapshot 1`] = ` <div class="blocks-rich-text" > - <div - aria-label="Write quote…" - class="blocks-rich-text__tinymce" - contenteditable="true" - data-is-placeholder-visible="true" - /> - <div - class="blocks-rich-text__tinymce" - > - <p> - Write quote… - </p> + <div> + <div> + <div + class="components-autocomplete" + > + <div + aria-autocomplete="list" + aria-expanded="false" + aria-label="Write quote…" + aria-multiline="true" + class="blocks-rich-text__tinymce" + contenteditable="true" + data-is-placeholder-visible="true" + role="textbox" + /> + <div + class="blocks-rich-text__tinymce" + > + <p> + Write quote… + </p> + </div> + </div> + </div> </div> </div> </blockquote> diff --git a/blocks/library/shortcode/editor.scss b/blocks/library/shortcode/editor.scss index 0ca68bb1dd4e4f..864856a84ccb19 100644 --- a/blocks/library/shortcode/editor.scss +++ b/blocks/library/shortcode/editor.scss @@ -1,8 +1,10 @@ .wp-block-shortcode { display: flex; flex-direction: row; - padding: 1em; + padding: $block-padding; background-color: $light-gray-100; + font-size: $default-font-size; + font-family: $default-font; label { flex-basis: 0; @@ -10,18 +12,15 @@ align-items: center; margin-right: $item-spacing; white-space: nowrap; + font-weight: 600; } .blocks-plain-text { + /* + * Unit required on zero value to work around IE bug. + * https://github.com/philipwalton/flexbugs#flexbug-4 + */ flex: 1 0 0%; - padding: 4px 8px; - border: 1px solid $light-gray-500; - font-family: $default-font; - font-size: 13px; - - &:focus { - border: 1px solid $dark-gray-500; - } } .dashicon { diff --git a/blocks/library/shortcode/test/__snapshots__/index.js.snap b/blocks/library/shortcode/test/__snapshots__/index.js.snap index 75e3c259352368..f45b727243d033 100644 --- a/blocks/library/shortcode/test/__snapshots__/index.js.snap +++ b/blocks/library/shortcode/test/__snapshots__/index.js.snap @@ -9,7 +9,7 @@ exports[`core/shortcode block edit matches snapshot 1`] = ` > <svg aria-hidden="true" - class="dashicon dashicons-editor-code" + class="dashicon dashicons-shortcode" focusable="false" height="20" role="img" @@ -18,13 +18,13 @@ exports[`core/shortcode block edit matches snapshot 1`] = ` xmlns="http://www.w3.org/2000/svg" > <path - d="M9 6l-4 4 4 4-1 2-6-6 6-6zm2 8l4-4-4-4 1-2 6 6-6 6z" + d="M6 14H4V6h2V4H2v12h4M7.1 17h2.1l3.7-14h-2.1M14 4v2h2v8h-2v2h4V4" /> </svg> Shortcode </label> <textarea - class="blocks-plain-text" + class="blocks-plain-text input-control" id="blocks-shortcode-input-0" placeholder="Write shortcode here…" rows="1" diff --git a/blocks/library/table/index.js b/blocks/library/table/index.js index 8295d1673222b8..2c59482c07e8e8 100644 --- a/blocks/library/table/index.js +++ b/blocks/library/table/index.js @@ -72,6 +72,7 @@ export const settings = { } } content={ content } className={ className } + isSelected={ isSelected } />, ]; }, diff --git a/blocks/library/table/test/__snapshots__/index.js.snap b/blocks/library/table/test/__snapshots__/index.js.snap index f1f7656c6e267a..77337c6c8b1fb6 100644 --- a/blocks/library/table/test/__snapshots__/index.js.snap +++ b/blocks/library/table/test/__snapshots__/index.js.snap @@ -4,28 +4,39 @@ exports[`core/embed block edit matches snapshot 1`] = ` <div class="wp-block-table blocks-rich-text" > - <table - class="blocks-rich-text__tinymce" - contenteditable="true" - > - <tbody> - <tr> - <td> - <br /> - </td> - <td> - <br /> - </td> - </tr> - <tr> - <td> - <br /> - </td> - <td> - <br /> - </td> - </tr> - </tbody> - </table> + <div> + <div> + <div + class="components-autocomplete" + > + <table + aria-autocomplete="list" + aria-expanded="false" + aria-multiline="false" + class="blocks-rich-text__tinymce" + contenteditable="true" + > + <tbody> + <tr> + <td> + <br /> + </td> + <td> + <br /> + </td> + </tr> + <tr> + <td> + <br /> + </td> + <td> + <br /> + </td> + </tr> + </tbody> + </table> + </div> + </div> + </div> </div> `; diff --git a/blocks/library/text-columns/index.js b/blocks/library/text-columns/index.js index c0ad471fe68f7d..6d86451e18a893 100644 --- a/blocks/library/text-columns/index.js +++ b/blocks/library/text-columns/index.js @@ -7,7 +7,7 @@ import { times } from 'lodash'; * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { RangeControl } from '@wordpress/components'; +import { PanelBody, RangeControl } from '@wordpress/components'; /** * Internal dependencies @@ -73,13 +73,15 @@ export const settings = { ), isSelected && ( <InspectorControls key="inspector"> - <RangeControl - label={ __( 'Columns' ) } - value={ columns } - onChange={ ( value ) => setAttributes( { columns: value } ) } - min={ 2 } - max={ 4 } - /> + <PanelBody> + <RangeControl + label={ __( 'Columns' ) } + value={ columns } + onChange={ ( value ) => setAttributes( { columns: value } ) } + min={ 2 } + max={ 4 } + /> + </PanelBody> </InspectorControls> ), <div className={ `${ className } align${ width } columns-${ columns }` } key="block"> diff --git a/blocks/library/text-columns/test/__snapshots__/index.js.snap b/blocks/library/text-columns/test/__snapshots__/index.js.snap index 0ccf70650bd90e..c1572062af8c07 100644 --- a/blocks/library/text-columns/test/__snapshots__/index.js.snap +++ b/blocks/library/text-columns/test/__snapshots__/index.js.snap @@ -10,17 +10,29 @@ exports[`core/text-columns block edit matches snapshot 1`] = ` <div class="blocks-rich-text" > - <p - aria-label="New Column" - class="blocks-rich-text__tinymce" - contenteditable="true" - data-is-placeholder-visible="true" - /> - <p - class="blocks-rich-text__tinymce" - > - New Column - </p> + <div> + <div> + <div + class="components-autocomplete" + > + <p + aria-autocomplete="list" + aria-expanded="false" + aria-label="New Column" + aria-multiline="false" + class="blocks-rich-text__tinymce" + contenteditable="true" + data-is-placeholder-visible="true" + role="textbox" + /> + <p + class="blocks-rich-text__tinymce" + > + New Column + </p> + </div> + </div> + </div> </div> </div> <div @@ -29,17 +41,29 @@ exports[`core/text-columns block edit matches snapshot 1`] = ` <div class="blocks-rich-text" > - <p - aria-label="New Column" - class="blocks-rich-text__tinymce" - contenteditable="true" - data-is-placeholder-visible="true" - /> - <p - class="blocks-rich-text__tinymce" - > - New Column - </p> + <div> + <div> + <div + class="components-autocomplete" + > + <p + aria-autocomplete="list" + aria-expanded="false" + aria-label="New Column" + aria-multiline="false" + class="blocks-rich-text__tinymce" + contenteditable="true" + data-is-placeholder-visible="true" + role="textbox" + /> + <p + class="blocks-rich-text__tinymce" + > + New Column + </p> + </div> + </div> + </div> </div> </div> </div> diff --git a/blocks/library/verse/test/__snapshots__/index.js.snap b/blocks/library/verse/test/__snapshots__/index.js.snap index 9b11da6a1dfa3c..7e514a4fb36fda 100644 --- a/blocks/library/verse/test/__snapshots__/index.js.snap +++ b/blocks/library/verse/test/__snapshots__/index.js.snap @@ -4,16 +4,28 @@ exports[`core/verse block edit matches snapshot 1`] = ` <div class="wp-block-verse blocks-rich-text" > - <pre - aria-label="Write…" - class="blocks-rich-text__tinymce" - contenteditable="true" - data-is-placeholder-visible="true" - /> - <pre - class="blocks-rich-text__tinymce" - > - Write… - </pre> + <div> + <div> + <div + class="components-autocomplete" + > + <pre + aria-autocomplete="list" + aria-expanded="false" + aria-label="Write…" + aria-multiline="false" + class="blocks-rich-text__tinymce" + contenteditable="true" + data-is-placeholder-visible="true" + role="textbox" + /> + <pre + class="blocks-rich-text__tinymce" + > + Write… + </pre> + </div> + </div> + </div> </div> `; diff --git a/blocks/library/video/editor.scss b/blocks/library/video/editor.scss index 253c4f3838b7b7..ec44bb62c7bd5a 100644 --- a/blocks/library/video/editor.scss +++ b/blocks/library/video/editor.scss @@ -11,10 +11,13 @@ } .wp-block-video .components-placeholder__fieldset { - display: block; max-width: 400px; form { max-width: none; } } + +.editor-block-list__block[data-align="center"] { + text-align: center; +} diff --git a/blocks/library/video/index.js b/blocks/library/video/index.js index 5e81bd8909e7c7..b91ecb707ec637 100644 --- a/blocks/library/video/index.js +++ b/blocks/library/video/index.js @@ -6,8 +6,15 @@ * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { Placeholder, Toolbar, IconButton, Button } from '@wordpress/components'; +import { + Button, + FormFileUpload, + IconButton, + Placeholder, + Toolbar, +} from '@wordpress/components'; import { Component } from '@wordpress/element'; +import { mediaUpload } from '@wordpress/utils'; /** * Internal dependencies @@ -52,27 +59,26 @@ export const settings = { getEditWrapperProps( attributes ) { const { align } = attributes; - if ( 'left' === align || 'right' === align || 'wide' === align || 'full' === align ) { + if ( 'left' === align || 'center' === align || 'right' === align || 'wide' === align || 'full' === align ) { return { 'data-align': align }; } }, edit: class extends Component { - constructor( { className } ) { + constructor() { super( ...arguments ); // edit component has its own src in the state so it can be edited // without setting the actual value outside of the edit UI this.state = { editing: ! this.props.attributes.src, src: this.props.attributes.src, - className, }; } render() { const { align, caption, id } = this.props.attributes; - const { setAttributes, isSelected } = this.props; - const { editing, className, src } = this.state; + const { setAttributes, isSelected, className } = this.props; + const { editing, src } = this.state; const updateAlignment = ( nextAlign ) => setAttributes( { align: nextAlign } ); const switchToEditing = () => { this.setState( { editing: true } ); @@ -94,20 +100,24 @@ export const settings = { } return false; }; + const setVideo = ( [ audio ] ) => onSelectVideo( audio ); + const uploadFromFiles = ( event ) => mediaUpload( event.target.files, setVideo, 'video' ); const controls = isSelected && ( <BlockControls key="controls"> <BlockAlignmentToolbar value={ align } onChange={ updateAlignment } /> - <Toolbar> - <IconButton - className="components-icon-button components-toolbar__control" - label={ __( 'Edit video' ) } - onClick={ switchToEditing } - icon="edit" - /> - </Toolbar> + { ! editing && ( + <Toolbar> + <IconButton + className="components-icon-button components-toolbar__control" + label={ __( 'Edit video' ) } + onClick={ switchToEditing } + icon="edit" + /> + </Toolbar> + ) } </BlockControls> ); @@ -133,13 +143,21 @@ export const settings = { { __( 'Use URL' ) } </Button> </form> + <FormFileUpload + isLarge + className="wp-block-video__upload-button" + onChange={ uploadFromFiles } + accept="video/*" + > + { __( 'Upload' ) } + </FormFileUpload> <MediaUpload onSelect={ onSelectVideo } type="video" id={ id } render={ ( { open } ) => ( <Button isLarge onClick={ open } > - { __( 'Add from Media Library' ) } + { __( 'Media Library' ) } </Button> ) } /> diff --git a/blocks/library/video/style.scss b/blocks/library/video/style.scss index 9edf6b7a3e12cc..44e0b375652d86 100644 --- a/blocks/library/video/style.scss +++ b/blocks/library/video/style.scss @@ -1,6 +1,14 @@ -.wp-block-video figcaption { - margin-top: 0.5em; - color: $dark-gray-300; - text-align: center; - font-size: $default-font-size; +.wp-block-video { + margin: 0; + + figcaption { + margin-top: 0.5em; + color: $dark-gray-300; + text-align: center; + font-size: $default-font-size; + } + + &.aligncenter { + text-align: center; + } } diff --git a/blocks/library/video/test/__snapshots__/index.js.snap b/blocks/library/video/test/__snapshots__/index.js.snap index fa8000f75f57c4..ccc89e10953a6b 100644 --- a/blocks/library/video/test/__snapshots__/index.js.snap +++ b/blocks/library/video/test/__snapshots__/index.js.snap @@ -45,7 +45,35 @@ exports[`core/video block edit matches snapshot 1`] = ` Use URL </button> </form> - *** Mock(Media upload button) *** + <div + class="components-form-file-upload" + > + <button + class="components-button components-icon-button wp-block-video__upload-button button button-large" + type="button" + > + <svg + aria-hidden="true" + class="dashicon dashicons-upload" + focusable="false" + height="20" + role="img" + viewBox="0 0 20 20" + width="20" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M8 14V8H5l5-6 5 6h-3v6H8zm-2 2v-6H4v8h12.01v-8H14v6H6z" + /> + </svg> + Upload + </button> + <input + accept="video/*" + style="display:none" + type="file" + /> + </div> </div> </div> `; diff --git a/blocks/library/video/test/index.js b/blocks/library/video/test/index.js index d3e53da7625341..2c21fb5fe7f0aa 100644 --- a/blocks/library/video/test/index.js +++ b/blocks/library/video/test/index.js @@ -4,8 +4,6 @@ import { name, settings } from '../'; import { blockEditRender } from 'blocks/test/helpers'; -jest.mock( 'blocks/media-upload', () => () => '*** Mock(Media upload button) ***' ); - describe( 'core/video', () => { test( 'block edit matches snapshot', () => { const wrapper = blockEditRender( name, settings ); diff --git a/blocks/media-upload/README.md b/blocks/media-upload/README.md index 31330514dbcda9..e590177cbb593c 100644 --- a/blocks/media-upload/README.md +++ b/blocks/media-upload/README.md @@ -3,6 +3,25 @@ MediaUpload MediaUpload is a React component used to render a button that opens a the WordPress media modal. +## Setup + +This is a placeholder component necessary to make it possible to provide an integration with the core blocks that handle media files. By default it renders nothing but it provides a way to have it overridden with the `components.MediaUpload` filter. + +```jsx +import { addFilter } from '@wordpress/hooks'; +import MediaUpload from './media-upload'; + +const replaceMediaUpload = () => MediaUpload; + +addFilter( + 'blocks.MediaUpload', + 'core/edit-post/blocks/media-upload/replaceMediaUpload', + replaceMediaUpload +); +``` + +You can check how this component is implemented for the edit post page using `wp.media` module in [edit-post](../../edit-post/hooks/blocks/media-upload/index.js). + ## Usage @@ -56,7 +75,7 @@ Media ID (or media IDs if multiple is true) to be selected by default when openi Callback called when the media modal is closed, the selected media are passed as an argument. -- Type: `Func` +- Type: `Function` - Required: Yes ## render diff --git a/blocks/media-upload/index.js b/blocks/media-upload/index.js index 3b8031e94856ad..1a6e1e46657c92 100644 --- a/blocks/media-upload/index.js +++ b/blocks/media-upload/index.js @@ -1,156 +1,15 @@ /** * WordPress dependencies */ -import { Component } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; -import { pick } from 'lodash'; +import { withFilters } from '@wordpress/components'; -// Getter for the sake of unit tests. -const getGalleryDetailsMediaFrame = () => { - /** - * Custom gallery details frame. - * - * @link https://github.com/xwp/wp-core-media-widgets/blob/905edbccfc2a623b73a93dac803c5335519d7837/wp-admin/js/widgets/media-gallery-widget.js - * @class GalleryDetailsMediaFrame - * @constructor - */ - return wp.media.view.MediaFrame.Post.extend( { - - /** - * Create the default states. - * - * @return {void} - */ - createStates: function createStates() { - this.states.add( [ - new wp.media.controller.Library( { - id: 'gallery', - title: wp.media.view.l10n.createGalleryTitle, - priority: 40, - toolbar: 'main-gallery', - filterable: 'uploaded', - multiple: 'add', - editable: false, - - library: wp.media.query( _.defaults( { - type: 'image', - }, this.options.library ) ), - } ), - - new wp.media.controller.GalleryEdit( { - library: this.options.selection, - editing: this.options.editing, - menu: 'gallery', - displaySettings: false, - } ), - - new wp.media.controller.GalleryAdd(), - ] ); - }, - } ); -}; - -// the media library image object contains numerous attributes -// we only need this set to display the image in the library -const slimImageObject = ( img ) => { - const attrSet = [ 'sizes', 'mime', 'type', 'subtype', 'id', 'url', 'alt', 'link', 'caption' ]; - return pick( img, attrSet ); -}; - -class MediaUpload extends Component { - constructor( { multiple = false, type, gallery = false, title = __( 'Select or Upload Media' ), modalClass } ) { - super( ...arguments ); - this.openModal = this.openModal.bind( this ); - this.onSelect = this.onSelect.bind( this ); - this.onUpdate = this.onUpdate.bind( this ); - this.onOpen = this.onOpen.bind( this ); - const frameConfig = { - title, - button: { - text: __( 'Select' ), - }, - multiple, - selection: new wp.media.model.Selection( [] ), - }; - if ( !! type ) { - frameConfig.library = { type }; - } - - if ( gallery ) { - const GalleryDetailsMediaFrame = getGalleryDetailsMediaFrame(); - this.frame = new GalleryDetailsMediaFrame( { - frame: 'select', - mimeType: type, - state: 'gallery', - } ); - wp.media.frame = this.frame; - } else { - this.frame = wp.media( frameConfig ); - } - - if ( modalClass ) { - this.frame.$el.addClass( modalClass ); - } - - // When an image is selected in the media frame... - this.frame.on( 'select', this.onSelect ); - this.frame.on( 'update', this.onUpdate ); - this.frame.on( 'open', this.onOpen ); - } - - componentWillUnmount() { - this.frame.remove(); - } - - onUpdate( selections ) { - const { onSelect, multiple = false } = this.props; - const state = this.frame.state(); - const selectedImages = selections || state.get( 'selection' ); - - if ( ! selectedImages || ! selectedImages.models.length ) { - return; - } - if ( multiple ) { - onSelect( selectedImages.models.map( ( model ) => slimImageObject( model.toJSON() ) ) ); - } else { - onSelect( slimImageObject( selectedImages.models[ 0 ].toJSON() ) ); - } - } - - onSelect() { - const { onSelect, multiple = false } = this.props; - // Get media attachment details from the frame state - const attachment = this.frame.state().get( 'selection' ).toJSON(); - onSelect( multiple ? attachment : attachment[ 0 ] ); - } - - onOpen() { - const selection = this.frame.state().get( 'selection' ); - const addMedia = ( id ) => { - const attachment = wp.media.attachment( id ); - attachment.fetch(); - selection.add( attachment ); - }; - - if ( ! this.props.value ) { - return; - } - - if ( this.props.multiple ) { - this.props.value.map( addMedia ); - } else { - addMedia( this.props.value ); - } - } - - openModal() { - this.frame.open(); - } - - render() { - return this.props.render( { open: this.openModal } ); - } -} - -export default MediaUpload; +/** + * This is a placeholder for the media upload component necessary to make it possible to provide + * an integration with the core blocks that handle media files. By default it renders nothing but + * it provides a way to have it overridden with the `blocks.MediaUpload` filter. + * + * @return {WPElement} Media upload element. + */ +const MediaUpload = () => null; +export default withFilters( 'blocks.MediaUpload' )( MediaUpload ); diff --git a/blocks/query-panel/index.js b/blocks/query-panel/index.js deleted file mode 100644 index 667409a3b23a66..00000000000000 --- a/blocks/query-panel/index.js +++ /dev/null @@ -1,88 +0,0 @@ -/** - * External dependencies - */ -import { noop } from 'lodash'; - -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; -import { RangeControl, SelectControl } from '@wordpress/components'; - -/** - * Internal dependencies - */ -import CategorySelect from './category-select'; - -const DEFAULT_MIN_ITEMS = 1; -const DEFAULT_MAX_ITEMS = 100; - -export default function QueryPanel( { - category, - numberOfItems, - order, - orderBy, - maxItems = DEFAULT_MAX_ITEMS, - minItems = DEFAULT_MIN_ITEMS, - onCategoryChange, - onNumberOfItemsChange, - onOrderChange = noop, - onOrderByChange = noop, -} ) { - return [ - ( onOrderChange || onOrderByChange ) && ( - <SelectControl - key="query-panel-select" - label={ __( 'Order by' ) } - value={ `${ orderBy }/${ order }` } - options={ [ - { - label: __( 'Newest to Oldest' ), - value: 'date/desc', - }, - { - label: __( 'Oldest to Newest' ), - value: 'date/asc', - }, - { - /* translators: label for ordering posts by title in ascending order */ - label: __( 'A → Z' ), - value: 'title/asc', - }, - { - /* translators: label for ordering posts by title in descending order */ - label: __( 'Z → A' ), - value: 'title/desc', - }, - ] } - onChange={ ( value ) => { - const [ newOrderBy, newOrder ] = value.split( '/' ); - if ( newOrder !== order ) { - onOrderChange( newOrder ); - } - if ( newOrderBy !== orderBy ) { - onOrderByChange( newOrderBy ); - } - } } - /> - ), - onCategoryChange && ( - <CategorySelect - key="query-panel-category-select" - label={ __( 'Category' ) } - noOptionLabel={ __( 'All' ) } - selectedCategory={ category } - onChange={ onCategoryChange } - /> ), - onNumberOfItemsChange && ( - <RangeControl - key="query-panel-range-control" - label={ __( 'Number of items' ) } - value={ numberOfItems } - onChange={ onNumberOfItemsChange } - min={ minItems } - max={ maxItems } - /> - ), - ]; -} diff --git a/blocks/rich-text/README.md b/blocks/rich-text/README.md index c98aa659f266f2..a6bc71a973caab 100644 --- a/blocks/rich-text/README.md +++ b/blocks/rich-text/README.md @@ -59,6 +59,10 @@ a traditional `input` field, usually when the user exits the field. *Optional.* By default, the placeholder will hide as soon as the editable field receives focus. With this setting it can be be kept while the field is focussed and empty. +### `autocompleters: Array<Completer>` + +*Optional.* A list of autocompleters to use instead of the default. + ## Example {% codetabs %} diff --git a/blocks/rich-text/format-toolbar/index.js b/blocks/rich-text/format-toolbar/index.js index edc1c54a61b749..e7594faa545529 100644 --- a/blocks/rich-text/format-toolbar/index.js +++ b/blocks/rich-text/format-toolbar/index.js @@ -3,7 +3,13 @@ */ import { __ } from '@wordpress/i18n'; import { Component } from '@wordpress/element'; -import { IconButton, Toolbar, withSpokenMessages, Fill } from '@wordpress/components'; +import { + Fill, + IconButton, + ToggleControl, + Toolbar, + withSpokenMessages, +} from '@wordpress/components'; import { keycodes } from '@wordpress/utils'; /** @@ -32,8 +38,6 @@ const FORMATTING_CONTROLS = [ format: 'strikethrough', }, { - icon: 'admin-links', - title: __( 'Link' ), format: 'link', }, ]; @@ -47,10 +51,11 @@ const stopKeyPropagation = ( event ) => event.stopPropagation(); class FormatToolbar extends Component { constructor() { super( ...arguments ); - this.state = { isAddingLink: false, isEditingLink: false, + settingsVisible: false, + opensInNewWindow: false, newLinkValue: '', }; @@ -60,11 +65,13 @@ class FormatToolbar extends Component { this.submitLink = this.submitLink.bind( this ); this.onKeyDown = this.onKeyDown.bind( this ); this.onChangeLinkValue = this.onChangeLinkValue.bind( this ); + this.toggleLinkSettingsVisibility = this.toggleLinkSettingsVisibility.bind( this ); + this.setLinkTarget = this.setLinkTarget.bind( this ); } onKeyDown( event ) { if ( event.keyCode === ESCAPE ) { - if ( this.state.isEditingLink ) { + if ( this.state.isEditingLink || this.state.isAddingLink ) { event.stopPropagation(); this.dropLink(); } @@ -79,6 +86,8 @@ class FormatToolbar extends Component { this.setState( { isAddingLink: false, isEditingLink: false, + settingsVisible: false, + opensInNewWindow: !! nextProps.formats.link && !! nextProps.formats.link.target, newLinkValue: '', } ); } @@ -96,6 +105,17 @@ class FormatToolbar extends Component { }; } + toggleLinkSettingsVisibility() { + this.setState( ( state ) => ( { settingsVisible: ! state.settingsVisible } ) ); + } + + setLinkTarget( opensInNewWindow ) { + this.setState( { opensInNewWindow } ); + if ( this.props.formats.link ) { + this.props.onChange( { link: { value: this.props.formats.link.value, target: opensInNewWindow ? '_blank' : '' } } ); + } + } + addLink() { this.setState( { isEditingLink: false, isAddingLink: true, newLinkValue: '' } ); } @@ -112,7 +132,8 @@ class FormatToolbar extends Component { submitLink( event ) { event.preventDefault(); - this.props.onChange( { link: { value: this.state.newLinkValue } } ); + this.setState( { isEditingLink: false, isAddingLink: false, newLinkValue: '' } ); + this.props.onChange( { link: { value: this.state.newLinkValue, target: this.state.opensInNewWindow ? '_blank' : '' } } ); if ( this.state.isAddingLink ) { this.props.speak( __( 'Link added.' ), 'assertive' ); } @@ -124,70 +145,101 @@ class FormatToolbar extends Component { render() { const { formats, focusPosition, enabledControls = DEFAULT_CONTROLS, customControls = [] } = this.props; - const { isAddingLink, isEditingLink, newLinkValue } = this.state; - const linkStyle = focusPosition ? - { position: 'absolute', ...focusPosition } : - null; + const { isAddingLink, isEditingLink, newLinkValue, settingsVisible, opensInNewWindow } = this.state; const toolbarControls = FORMATTING_CONTROLS.concat( customControls ) .filter( control => enabledControls.indexOf( control.format ) !== -1 ) .map( ( control ) => { - const isLink = control.format === 'link'; + if ( control.format === 'link' ) { + const isFormatActive = this.isFormatActive( 'link' ); + const isActive = isFormatActive || isAddingLink; + return { + ...control, + icon: isFormatActive ? 'editor-unlink' : 'admin-links', // TODO: Need proper unlink icon + title: isFormatActive ? __( 'Unlink' ) : __( 'Link' ), + onClick: isActive ? this.dropLink : this.addLink, + isActive, + }; + } + return { ...control, - onClick: isLink ? this.addLink : this.toggleFormat( control.format ), - isActive: this.isFormatActive( control.format ) || ( isLink && isAddingLink ), + onClick: this.toggleFormat( control.format ), + isActive: this.isFormatActive( control.format ), }; } ); + const linkSettings = settingsVisible && ( + <div className="blocks-format-toolbar__link-modal-line blocks-format-toolbar__link-settings"> + <ToggleControl + label={ __( 'Open in new window' ) } + checked={ opensInNewWindow } + onChange={ this.setLinkTarget } /> + </div> + ); + return ( <div className="blocks-format-toolbar"> <Toolbar controls={ toolbarControls } /> - { ( isAddingLink || isEditingLink ) && - // Disable reason: KeyPress must be suppressed so the block doesn't hide the toolbar - /* eslint-disable jsx-a11y/no-noninteractive-element-interactions */ - <Fill name="RichText.Siblings"> - <form - className="blocks-format-toolbar__link-modal" - style={ linkStyle } - onKeyPress={ stopKeyPropagation } - onKeyDown={ this.onKeyDown } - onSubmit={ this.submitLink }> - <div className="blocks-format-toolbar__link-modal-line"> - <UrlInput value={ newLinkValue } onChange={ this.onChangeLinkValue } /> - <IconButton icon="editor-break" label={ __( 'Apply' ) } type="submit" /> - <IconButton icon="editor-unlink" label={ __( 'Remove link' ) } onClick={ this.dropLink } /> - </div> - </form> - </Fill> - /* eslint-enable jsx-a11y/no-noninteractive-element-interactions */ - } - - { !! formats.link && ! isAddingLink && ! isEditingLink && - // Disable reason: KeyPress must be suppressed so the block doesn't hide the toolbar - /* eslint-disable jsx-a11y/no-static-element-interactions */ + { ( isAddingLink || isEditingLink || formats.link ) && ( <Fill name="RichText.Siblings"> - <div - className="blocks-format-toolbar__link-modal" - style={ linkStyle } - onKeyPress={ stopKeyPropagation } - > - <div className="blocks-format-toolbar__link-modal-line"> - <a - className="blocks-format-toolbar__link-value" - href={ formats.link.value } - target="_blank" + <div style={ { position: 'absolute', ...focusPosition } }> + { ( isAddingLink || isEditingLink ) && ( + // Disable reason: KeyPress must be suppressed so the block doesn't hide the toolbar + /* eslint-disable jsx-a11y/no-noninteractive-element-interactions */ + <form + className="blocks-format-toolbar__link-modal" + onKeyPress={ stopKeyPropagation } + onKeyDown={ this.onKeyDown } + onSubmit={ this.submitLink }> + <div className="blocks-format-toolbar__link-modal-line"> + <UrlInput value={ newLinkValue } onChange={ this.onChangeLinkValue } /> + <IconButton icon="editor-break" label={ __( 'Apply' ) } type="submit" /> + <IconButton + className="blocks-format-toolbar__link-settings-toggle" + icon="ellipsis" + label={ __( 'Link Settings' ) } + onClick={ this.toggleLinkSettingsVisibility } + aria-expanded={ settingsVisible } + /> + </div> + { linkSettings } + </form> + /* eslint-enable jsx-a11y/no-noninteractive-element-interactions */ + ) } + + { formats.link && ! isAddingLink && ! isEditingLink && ( + // Disable reason: KeyPress must be suppressed so the block doesn't hide the toolbar + /* eslint-disable jsx-a11y/no-static-element-interactions */ + <div + className="blocks-format-toolbar__link-modal" + onKeyPress={ stopKeyPropagation } > - { formats.link.value && filterURLForDisplay( decodeURI( formats.link.value ) ) } - </a> - <IconButton icon="edit" label={ __( 'Edit' ) } onClick={ this.editLink } /> - <IconButton icon="editor-unlink" label={ __( 'Remove link' ) } onClick={ this.dropLink } /> - </div> + <div className="blocks-format-toolbar__link-modal-line"> + <a + className="blocks-format-toolbar__link-value" + href={ formats.link.value } + target="_blank" + > + { formats.link.value && filterURLForDisplay( decodeURI( formats.link.value ) ) } + </a> + <IconButton icon="edit" label={ __( 'Edit' ) } onClick={ this.editLink } /> + <IconButton + className="blocks-format-toolbar__link-settings-toggle" + icon="ellipsis" + label={ __( 'Link Settings' ) } + onClick={ this.toggleLinkSettingsVisibility } + aria-expanded={ settingsVisible } + /> + </div> + { linkSettings } + </div> + /* eslint-enable jsx-a11y/no-static-element-interactions */ + ) } </div> </Fill> - /* eslint-enable jsx-a11y/no-static-element-interactions */ - } + ) } </div> ); } diff --git a/blocks/rich-text/format-toolbar/style.scss b/blocks/rich-text/format-toolbar/style.scss index 609255d2e3ffc8..1a150ffc21db8b 100644 --- a/blocks/rich-text/format-toolbar/style.scss +++ b/blocks/rich-text/format-toolbar/style.scss @@ -3,11 +3,11 @@ } .blocks-format-toolbar__link-modal { - position: absolute; - box-shadow: 0px 3px 20px rgba( 18, 24, 30, .1 ), 0px 1px 3px rgba( 18, 24, 30, .1 ); - border: 1px solid #e0e5e9; - background: #fff; - width: 300px; + position: relative; + left: -50%; + box-shadow: $shadow-popover; + border: 1px solid $light-gray-500; + background: $white; display: flex; flex-direction: column; font-family: $default-font; @@ -17,16 +17,48 @@ } .blocks-format-toolbar__link-modal-line { + $button-size: 36px; + $input-padding: 10px; + $input-size: 230px; + display: flex; flex-direction: row; flex-grow: 1; flex-shrink: 1; min-width: 0; - align-items: center; + align-items: flex-start; .components-button { flex-shrink: 0; + width: $button-size; + height: $button-size; + } + + .blocks-url-input { + width: $input-size; + + input { + padding: $input-padding; + margin: 0; + } + } + + .blocks-url-input__suggestions { + border-top: 1px solid $light-gray-500; + box-shadow: none; + padding: 4px 0; + position: relative; + width: $input-size + $button-size * 2; } + + .blocks-url-input__suggestion { + color: $dark-gray-100; + padding: 4px ( $button-size + $input-padding ); + } +} + +.blocks-format-toolbar__link-settings-toggle .dashicon { + transform: rotate(90deg); } .blocks-format-toolbar__link-value { @@ -34,11 +66,21 @@ flex-grow: 1; flex-shrink: 1; overflow: hidden; + text-overflow: ellipsis; position: relative; white-space: nowrap; - min-width: 0; + min-width: 150px; + max-width: 500px; +} + +.blocks-format-toolbar__link-settings { + padding: 7px 8px; + border-top: 1px solid $light-gray-500; + padding-top: 8px; // add 1px for the border - &:after { - @include long-content-fade( $size: 40% ); + .components-base-control { + margin: 0; + flex-grow: 1; + flex-shrink: 1; } } diff --git a/blocks/rich-text/index.js b/blocks/rich-text/index.js index 4908d87c61cb66..507b5ed35b1131 100644 --- a/blocks/rich-text/index.js +++ b/blocks/rich-text/index.js @@ -20,15 +20,17 @@ import 'element-closest'; /** * WordPress dependencies */ -import { createElement, Component, renderToString } from '@wordpress/element'; -import { keycodes, createBlobURL, isHorizontalEdge } from '@wordpress/utils'; +import { createElement, Component, renderToString, Fragment, compose } from '@wordpress/element'; +import { keycodes, createBlobURL, isHorizontalEdge, getRectangleFromRange, getScrollContainer } from '@wordpress/utils'; import { withSafeTimeout, Slot, Fill } from '@wordpress/components'; +import { withSelect } from '@wordpress/data'; /** * Internal dependencies */ import './style.scss'; import { rawHandler } from '../api'; +import Autocomplete from '../autocomplete'; import FormatToolbar from './format-toolbar'; import TinyMCE from './tinymce'; import { pickAriaProps } from './aria'; @@ -101,7 +103,7 @@ export function getFormatProperties( formatName, parents ) { switch ( formatName ) { case 'link' : { const anchor = find( parents, node => node.nodeName.toLowerCase() === 'a' ); - return !! anchor ? { value: anchor.getAttribute( 'href' ) || '', node: anchor } : {}; + return !! anchor ? { value: anchor.getAttribute( 'href' ) || '', target: anchor.getAttribute( 'target' ) || '', node: anchor } : {}; } default: return {}; @@ -406,36 +408,6 @@ export class RichText extends Component { this.context.onCreateUndoLevel(); } - /** - * Determines the DOM rectangle for the selection in the editor. - * - * @return {DOMRect} The DOMRect based on the selection in the editor. - */ - getEditorSelectionRect() { - let range = this.editor.selection.getRng(); - - // getBoundingClientRect doesn't work in Safari when range is collapsed - if ( range.collapsed ) { - const { startContainer, startOffset } = range; - range = document.createRange(); - - if ( ( ! startContainer.nodeValue ) || startContainer.nodeValue.length === 0 ) { - // container has no text content, select node (empty block) - range.selectNode( startContainer ); - } else if ( startOffset === startContainer.nodeValue.length ) { - // at end of text content, select last character - range.setStart( startContainer, startContainer.nodeValue.length - 1 ); - range.setEnd( startContainer, startContainer.nodeValue.length ); - } else { - // select 1 character from current position - range.setStart( startContainer, startOffset ); - range.setEnd( startContainer, startOffset + 1 ); - } - } - - return range.getBoundingClientRect(); - } - /** * Calculates the relative position where the link toolbar should be. * @@ -444,11 +416,11 @@ export class RichText extends Component { * absolutely position the toolbar. It does this by finding the closest * relative element. * + * @param {DOMRect} position Caret range rectangle. + * * @return {{top: number, left: number}} The desired position of the toolbar. */ - getFocusPosition() { - const position = this.getEditorSelectionRect(); - + getFocusPosition( position ) { // Find the parent "relative" or "absolute" positioned container const findRelativeParent = ( node ) => { const style = window.getComputedStyle( node ); @@ -460,11 +432,10 @@ export class RichText extends Component { const container = findRelativeParent( this.editor.getBody() ); const containerPosition = container.getBoundingClientRect(); const toolbarOffset = { top: 10, left: 0 }; - const linkModalWidth = 298; return { top: position.top - containerPosition.top + ( position.height ) + toolbarOffset.top, - left: position.left - containerPosition.left - ( linkModalWidth / 2 ) + ( position.width / 2 ) + toolbarOffset.left, + left: position.left - containerPosition.left + ( position.width / 2 ) + toolbarOffset.left, }; } @@ -558,6 +529,40 @@ export class RichText extends Component { if ( keyCode === BACKSPACE ) { this.onChange(); } + + // `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.editor.getBody() ); + + 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, + ); + } } /** @@ -661,8 +666,17 @@ export class RichText extends Component { return accFormats; }, {} ); - const focusPosition = this.getFocusPosition(); + const rect = getRectangleFromRange( this.editor.selection.getRng() ); + const focusPosition = this.getFocusPosition( rect ); + this.setState( { formats, focusPosition, selectedNodeId: this.state.selectedNodeId + 1 } ); + + if ( this.props.isViewportSmall ) { + // 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 ); + } } updateContent() { @@ -685,10 +699,6 @@ export class RichText extends Component { return nodeListToReact( this.editor.getBody().childNodes || [], createTinyMCEElement ); } - componentWillUnmount() { - this.onChange(); - } - componentDidUpdate( prevProps ) { // The `savedContent` var allows us to avoid updating the content right after an `onChange` call if ( @@ -696,6 +706,10 @@ export class RichText extends Component { this.props.tagName === prevProps.tagName && this.props.value !== prevProps.value && this.props.value !== this.savedContent && + + // Comparing using isEqual is necessary especially to avoid unnecessary updateContent calls + // This fixes issues in multi richText blocks like quotes when moving the focus between + // the different editables. ! isEqual( this.props.value, prevProps.value ) && ! isEqual( this.props.value, this.savedContent ) ) { @@ -734,7 +748,7 @@ export class RichText extends Component { if ( ! anchor ) { this.removeFormat( 'link' ); } - this.applyFormat( 'link', { href: formatValue.value }, anchor ); + this.applyFormat( 'link', { href: formatValue.value, target: formatValue.target }, anchor ); } else { this.editor.execCommand( 'Unlink' ); } @@ -761,7 +775,7 @@ export class RichText extends Component { * @param {Array} after content after the split position * @param {?Array} blocks blocks to insert at the split position */ - restoreContentAndSplit( before, after, blocks ) { + restoreContentAndSplit( before, after, blocks = [] ) { this.updateContent(); this.props.onSplit( before, after, ...blocks ); } @@ -780,9 +794,10 @@ export class RichText extends Component { keepPlaceholderOnFocus = false, isSelected = false, formatters, + autocompleters, } = this.props; - const ariaProps = pickAriaProps( this.props ); + const ariaProps = { ...pickAriaProps( this.props ), 'aria-multiline': !! MultilineTag }; // Generating a key that includes `tagName` ensures that if the tag // changes, we unmount and destroy the previous TinyMCE element, then @@ -814,27 +829,37 @@ export class RichText extends Component { { formatToolbar } </div> } - <TinyMCE - tagName={ Tagname } - getSettings={ this.getSettings } - onSetup={ this.onSetup } - style={ style } - defaultValue={ value } - isPlaceholderVisible={ isPlaceholderVisible } - aria-label={ placeholder } - { ...ariaProps } - className={ className } - key={ key } - /> - { isPlaceholderVisible && - <Tagname - className={ classnames( 'blocks-rich-text__tinymce', className ) } - style={ style } - > - { MultilineTag ? <MultilineTag>{ placeholder }</MultilineTag> : placeholder } - </Tagname> - } - { isSelected && <Slot name="RichText.Siblings" /> } + <Autocomplete onReplace={ this.props.onReplace } completers={ autocompleters }> + { ( { isExpanded, listBoxId, activeId } ) => ( + <Fragment> + <TinyMCE + tagName={ Tagname } + getSettings={ this.getSettings } + onSetup={ this.onSetup } + style={ style } + defaultValue={ value } + isPlaceholderVisible={ isPlaceholderVisible } + aria-label={ placeholder } + aria-autocomplete="list" + aria-expanded={ isExpanded } + aria-owns={ listBoxId } + aria-activedescendant={ activeId } + { ...ariaProps } + className={ className } + key={ key } + /> + { isPlaceholderVisible && + <Tagname + className={ classnames( 'blocks-rich-text__tinymce', className ) } + style={ style } + > + { MultilineTag ? <MultilineTag>{ placeholder }</MultilineTag> : placeholder } + </Tagname> + } + { isSelected && <Slot name="RichText.Siblings" /> } + </Fragment> + ) } + </Autocomplete> </div> ); } @@ -852,4 +877,12 @@ RichText.defaultProps = { formatters: [], }; -export default withSafeTimeout( RichText ); +export default compose( [ + withSelect( ( select ) => { + const { isViewportMatch = identity } = select( 'core/viewport' ) || {}; + return { + isViewportSmall: isViewportMatch( '< small' ), + }; + } ), + withSafeTimeout, +] )( RichText ); diff --git a/blocks/rich-text/patterns.js b/blocks/rich-text/patterns.js index fd20d944527de7..5277e3531d876b 100644 --- a/blocks/rich-text/patterns.js +++ b/blocks/rich-text/patterns.js @@ -2,7 +2,7 @@ * External dependencies */ import tinymce from 'tinymce'; -import { find, get, escapeRegExp, groupBy, drop } from 'lodash'; +import { filter, escapeRegExp, groupBy, drop } from 'lodash'; /** * WordPress dependencies @@ -12,7 +12,7 @@ import { keycodes } from '@wordpress/utils'; /** * Internal dependencies */ -import { getBlockTypes } from '../api/registration'; +import { getBlockTransforms, findTransform } from '../api/factory'; const { ESCAPE, ENTER, SPACE, BACKSPACE } = keycodes; @@ -26,11 +26,7 @@ export default function( editor ) { const { enter: enterPatterns, undefined: spacePatterns, - } = groupBy( getBlockTypes().reduce( ( acc, blockType ) => { - const transformsFrom = get( blockType, 'transforms.from', [] ); - const transforms = transformsFrom.filter( ( { type } ) => type === 'pattern' ); - return [ ...acc, ...transforms ]; - }, [] ), 'trigger' ); + } = groupBy( filter( getBlockTransforms( 'from' ), { type: 'pattern' } ), 'trigger' ); const inlinePatterns = settings.inline || [ { delimiter: '`', format: 'code' }, @@ -56,7 +52,7 @@ export default function( editor ) { } if ( keyCode === ENTER ) { - enter(); + enter( event ); // Wait for the browser to insert the character. } else if ( keyCode === SPACE ) { setTimeout( () => searchFirstText( spacePatterns ) ); @@ -181,17 +177,16 @@ export default function( editor ) { const firstText = content[ 0 ]; - const { result, pattern } = patterns.reduce( ( acc, item ) => { - return acc.result ? acc : { - result: item.regExp.exec( firstText ), - pattern: item, - }; - }, {} ); + const transformation = findTransform( patterns, ( item ) => { + return item.regExp.test( firstText ); + } ); - if ( ! result ) { + if ( ! transformation ) { return; } + const result = firstText.match( transformation.regExp ); + const range = editor.selection.getRng(); const matchLength = result[ 0 ].length; const remainingText = firstText.slice( matchLength ); @@ -201,7 +196,7 @@ export default function( editor ) { return; } - const block = pattern.transform( { + const block = transformation.transform( { content: [ remainingText, ...drop( content ) ], match: result, } ); @@ -209,7 +204,7 @@ export default function( editor ) { onReplace( [ block ] ); } - function enter() { + function enter( event ) { if ( ! onReplace ) { return; } @@ -223,14 +218,18 @@ export default function( editor ) { return; } - const pattern = find( enterPatterns, ( { regExp } ) => regExp.test( content[ 0 ] ) ); + const pattern = findTransform( enterPatterns, ( { regExp } ) => regExp.test( content[ 0 ] ) ); if ( ! pattern ) { return; } const block = pattern.transform( { content } ); + onReplace( [ block ] ); - editor.once( 'keyup', () => onReplace( [ block ] ) ); + // We call preventDefault to prevent additional newlines. + event.preventDefault(); + // stopImmediatePropagation is called to prevent TinyMCE's own processing of keydown which conflicts with the block replacement. + event.stopImmediatePropagation(); } } diff --git a/blocks/rich-text/style.scss b/blocks/rich-text/style.scss index daabfbd14d954c..1f16f43498a306 100644 --- a/blocks/rich-text/style.scss +++ b/blocks/rich-text/style.scss @@ -37,6 +37,10 @@ background: $light-gray-200; font-family: $editor-html-font; font-size: 14px; + + .is-multi-selected & { + background: darken( $blue-medium-highlight, 15% ); + } } &:focus code[data-mce-selected] { @@ -82,7 +86,7 @@ font-size: 4.1em; line-height: 0.7; font-family: serif; - font-weight: bold; + font-weight: 600; margin: .07em .23em 0 0; text-transform: uppercase; font-style: normal; diff --git a/blocks/rich-text/test/index.js b/blocks/rich-text/test/index.js index 192f1a1866b47d..f1fe726cf973ab 100644 --- a/blocks/rich-text/test/index.js +++ b/blocks/rich-text/test/index.js @@ -145,6 +145,7 @@ describe( 'getFormatProperties', () => { nodeName: 'A', attributes: { href: 'https://www.testing.com', + target: '_blank', }, }; @@ -156,7 +157,7 @@ describe( 'getFormatProperties', () => { expect( getFormatProperties( formatName, [ { ...node, nodeName: 'P' } ] ) ).toEqual( {} ); } ); - test( 'should return an object of value and node for a link', () => { + test( 'should return a populated object', () => { const mockNode = { ...node, getAttribute: jest.fn().mockImplementation( ( attr ) => mockNode.attributes[ attr ] ), @@ -168,11 +169,12 @@ describe( 'getFormatProperties', () => { expect( getFormatProperties( formatName, parents ) ).toEqual( { value: 'https://www.testing.com', + target: '_blank', node: mockNode, } ); } ); - test( 'should return an object of value and node of empty values when no values are found.', () => { + test( 'should return an object with empty values when no link is found', () => { const mockNode = { ...node, attributes: {}, @@ -185,6 +187,7 @@ describe( 'getFormatProperties', () => { expect( getFormatProperties( formatName, parents ) ).toEqual( { value: '', + target: '', node: mockNode, } ); } ); @@ -197,7 +200,7 @@ describe( 'RichText', () => { const options = { type: 'inline-style', style: { - 'font-weight': 'bold', + 'font-weight': '600', }, }; diff --git a/blocks/rich-text/tinymce.js b/blocks/rich-text/tinymce.js index 049605688c796f..6dbf1499071dd3 100644 --- a/blocks/rich-text/tinymce.js +++ b/blocks/rich-text/tinymce.js @@ -98,6 +98,9 @@ export default class TinyMCE extends Component { render() { const { tagName = 'div', style, defaultValue, className, isPlaceholderVisible } = this.props; const ariaProps = pickAriaProps( this.props ); + if ( [ 'ul', 'ol', 'table' ].indexOf( tagName ) === -1 ) { + ariaProps.role = 'textbox'; + } // If a default value is provided, render it into the DOM even before // TinyMCE finishes initializing. This avoids a short delay by allowing diff --git a/blocks/test/fixtures/core__audio.serialized.html b/blocks/test/fixtures/core__audio.serialized.html index d851452c2d2b2d..f7b69332aa8827 100644 --- a/blocks/test/fixtures/core__audio.serialized.html +++ b/blocks/test/fixtures/core__audio.serialized.html @@ -1,3 +1,3 @@ <!-- wp:audio {"align":"right"} --> -<figure class="wp-block-audio alignright"><audio controls="" src="https://media.simplecast.com/episodes/audio/80564/draft-podcast-51-livePublish2.mp3"></audio></figure> +<figure class="wp-block-audio alignright"><audio controls src="https://media.simplecast.com/episodes/audio/80564/draft-podcast-51-livePublish2.mp3"></audio></figure> <!-- /wp:audio --> diff --git a/blocks/test/fixtures/core__code.serialized.html b/blocks/test/fixtures/core__code.serialized.html index 3a45062483ea3f..651254a7cab493 100644 --- a/blocks/test/fixtures/core__code.serialized.html +++ b/blocks/test/fixtures/core__code.serialized.html @@ -1,5 +1,5 @@ <!-- wp:code --> <pre class="wp-block-code"><code>export default function MyButton() { - return &lt;Button&gt;Click Me!&lt;/Button&gt;; + return &lt;Button>Click Me!&lt;/Button>; }</code></pre> <!-- /wp:code --> diff --git a/blocks/test/fixtures/core__cover-image.html b/blocks/test/fixtures/core__cover-image.html index 935452ed8f0f46..86b5915701b0b0 100644 --- a/blocks/test/fixtures/core__cover-image.html +++ b/blocks/test/fixtures/core__cover-image.html @@ -1,5 +1,5 @@ <!-- wp:core/cover-image {"url":"https://cldup.com/uuUqE_dXzy.jpg","dimRatio":40} --> -<section class="wp-block-cover-image has-background-dim-40 has-background-dim" style="background-image:url(https://cldup.com/uuUqE_dXzy.jpg)"> - <h2>Guten Berg!</h2> -</section> +<div class="wp-block-cover-image has-background-dim-40 has-background-dim" style="background-image:url(https://cldup.com/uuUqE_dXzy.jpg)"> + <p class="wp-block-cover-image-text">Guten Berg!</p> +</div> <!-- /wp:core/cover-image --> diff --git a/blocks/test/fixtures/core__cover-image.json b/blocks/test/fixtures/core__cover-image.json index f001fe32e4ef43..7f12304be6f8ab 100644 --- a/blocks/test/fixtures/core__cover-image.json +++ b/blocks/test/fixtures/core__cover-image.json @@ -13,6 +13,6 @@ "dimRatio": 40 }, "innerBlocks": [], - "originalContent": "<section class=\"wp-block-cover-image has-background-dim-40 has-background-dim\" style=\"background-image:url(https://cldup.com/uuUqE_dXzy.jpg)\">\n <h2>Guten Berg!</h2>\n</section>" + "originalContent": "<div class=\"wp-block-cover-image has-background-dim-40 has-background-dim\" style=\"background-image:url(https://cldup.com/uuUqE_dXzy.jpg)\">\n <p class=\"wp-block-cover-image-text\">Guten Berg!</p>\n</div>" } ] diff --git a/blocks/test/fixtures/core__cover-image.parsed.json b/blocks/test/fixtures/core__cover-image.parsed.json index 401e758f295803..25808e4279256f 100644 --- a/blocks/test/fixtures/core__cover-image.parsed.json +++ b/blocks/test/fixtures/core__cover-image.parsed.json @@ -6,7 +6,7 @@ "dimRatio": 40 }, "innerBlocks": [], - "innerHTML": "\n<section class=\"wp-block-cover-image has-background-dim-40 has-background-dim\" style=\"background-image:url(https://cldup.com/uuUqE_dXzy.jpg)\">\n <h2>Guten Berg!</h2>\n</section>\n" + "innerHTML": "\n<div class=\"wp-block-cover-image has-background-dim-40 has-background-dim\" style=\"background-image:url(https://cldup.com/uuUqE_dXzy.jpg)\">\n <p class=\"wp-block-cover-image-text\">Guten Berg!</p>\n</div>\n" }, { "attrs": {}, diff --git a/blocks/test/fixtures/core__cover-image.serialized.html b/blocks/test/fixtures/core__cover-image.serialized.html index 35427b2cd64b89..160d3aa203dbf1 100644 --- a/blocks/test/fixtures/core__cover-image.serialized.html +++ b/blocks/test/fixtures/core__cover-image.serialized.html @@ -1,5 +1,5 @@ <!-- wp:cover-image {"url":"https://cldup.com/uuUqE_dXzy.jpg","dimRatio":40} --> -<section class="wp-block-cover-image has-background-dim-40 has-background-dim" style="background-image:url(https://cldup.com/uuUqE_dXzy.jpg)"> - <h2>Guten Berg!</h2> -</section> +<div class="wp-block-cover-image has-background-dim-40 has-background-dim" style="background-image:url(https://cldup.com/uuUqE_dXzy.jpg)"> + <p class="wp-block-cover-image-text">Guten Berg!</p> +</div> <!-- /wp:cover-image --> diff --git a/blocks/test/fixtures/core__image__center-caption.serialized.html b/blocks/test/fixtures/core__image__center-caption.serialized.html index 7b7acfcd4d09d0..47d89c059a193b 100644 --- a/blocks/test/fixtures/core__image__center-caption.serialized.html +++ b/blocks/test/fixtures/core__image__center-caption.serialized.html @@ -1,5 +1,5 @@ <!-- wp:image {"align":"center"} --> <figure class="wp-block-image aligncenter"><img src="https://cldup.com/YLYhpou2oq.jpg" alt="" /> - <figcaption>Give it a try. Press the &quot;really wide&quot; button on the image toolbar.</figcaption> + <figcaption>Give it a try. Press the "really wide" button on the image toolbar.</figcaption> </figure> <!-- /wp:image --> diff --git a/blocks/test/fixtures/core__paragraph__deprecated.html b/blocks/test/fixtures/core__paragraph__deprecated.html index d29a3bf4629d74..96ec545b64fdae 100644 --- a/blocks/test/fixtures/core__paragraph__deprecated.html +++ b/blocks/test/fixtures/core__paragraph__deprecated.html @@ -1,3 +1,3 @@ <!-- wp:paragraph --> -Unwrapped is still valid. +Unwrapped is <em>still</em> valid. <!-- /wp:paragraph --> diff --git a/blocks/test/fixtures/core__paragraph__deprecated.json b/blocks/test/fixtures/core__paragraph__deprecated.json index 3d7ce0d436581b..7fe227a9e14cb4 100644 --- a/blocks/test/fixtures/core__paragraph__deprecated.json +++ b/blocks/test/fixtures/core__paragraph__deprecated.json @@ -5,11 +5,19 @@ "isValid": true, "attributes": { "content": [ - "Unwrapped is still valid." + { + "key": "html", + "ref": null, + "props": { + "children": "Unwrapped is <em>still</em> valid." + }, + "_owner": null, + "_store": {} + } ], "dropCap": false }, "innerBlocks": [], - "originalContent": "Unwrapped is still valid." + "originalContent": "Unwrapped is <em>still</em> valid." } ] diff --git a/blocks/test/fixtures/core__paragraph__deprecated.parsed.json b/blocks/test/fixtures/core__paragraph__deprecated.parsed.json index cd3f20a738b932..11217876b3cb4a 100644 --- a/blocks/test/fixtures/core__paragraph__deprecated.parsed.json +++ b/blocks/test/fixtures/core__paragraph__deprecated.parsed.json @@ -3,7 +3,7 @@ "blockName": "core/paragraph", "attrs": null, "innerBlocks": [], - "innerHTML": "\nUnwrapped is still valid.\n" + "innerHTML": "\nUnwrapped is <em>still</em> valid.\n" }, { "attrs": {}, diff --git a/blocks/test/fixtures/core__paragraph__deprecated.serialized.html b/blocks/test/fixtures/core__paragraph__deprecated.serialized.html index b60a1c44a3a340..ddc0a9d9a1cf47 100644 --- a/blocks/test/fixtures/core__paragraph__deprecated.serialized.html +++ b/blocks/test/fixtures/core__paragraph__deprecated.serialized.html @@ -1,3 +1,3 @@ <!-- wp:paragraph --> -<p>Unwrapped is still valid.</p> +<p>Unwrapped is <em>still</em> valid.</p> <!-- /wp:paragraph --> diff --git a/blocks/test/fixtures/core__video.serialized.html b/blocks/test/fixtures/core__video.serialized.html index 99abb6e0cf8823..48dca62aecd32a 100644 --- a/blocks/test/fixtures/core__video.serialized.html +++ b/blocks/test/fixtures/core__video.serialized.html @@ -1,3 +1,3 @@ <!-- wp:video --> -<figure class="wp-block-video"><video controls="" src="https://awesome-fake.video/file.mp4"></video></figure> +<figure class="wp-block-video"><video controls src="https://awesome-fake.video/file.mp4"></video></figure> <!-- /wp:video --> diff --git a/blocks/test/helpers/index.js b/blocks/test/helpers/index.js index d5386212e91483..f46573240df852 100644 --- a/blocks/test/helpers/index.js +++ b/blocks/test/helpers/index.js @@ -11,8 +11,8 @@ import { createBlock, getBlockType, registerBlockType, - BlockEdit, } from '../..'; +import { BlockEdit } from '../../block-edit'; export const blockEditRender = ( name, settings ) => { if ( ! getBlockType( name ) ) { @@ -26,6 +26,8 @@ export const blockEditRender = ( name, settings ) => { isSelected={ false } attributes={ block.attributes } setAttributes={ noop } + user={ {} } + createInnerBlockList={ noop } /> ); }; diff --git a/blocks/url-input/index.js b/blocks/url-input/index.js index a98d0df14025aa..de1064948e248f 100644 --- a/blocks/url-input/index.js +++ b/blocks/url-input/index.js @@ -4,6 +4,7 @@ import { throttle } from 'lodash'; import classnames from 'classnames'; import scrollIntoView from 'dom-scroll-into-view'; +import { stringify } from 'querystringify'; /** * WordPress dependencies @@ -66,11 +67,13 @@ class UrlInput extends Component { selectedSuggestion: null, loading: true, } ); - this.suggestionsRequest = new wp.api.collections.Posts().fetch( { data: { - search: value, - per_page: 20, - orderby: 'relevance', - } } ); + this.suggestionsRequest = wp.apiRequest( { + path: `/wp/v2/posts?${ stringify( { + search: value, + per_page: 20, + orderby: 'relevance', + } ) }`, + } ); this.suggestionsRequest .then( @@ -175,7 +178,7 @@ class UrlInput extends Component { } render() { - const { value, instanceId } = this.props; + const { value = '', instanceId } = this.props; const { showSuggestions, posts, selectedSuggestion, loading } = this.state; /* eslint-disable jsx-a11y/no-autofocus */ return ( diff --git a/blocks/url-input/style.scss b/blocks/url-input/style.scss index 19ca2db56e6181..4037d4273ee360 100644 --- a/blocks/url-input/style.scss +++ b/blocks/url-input/style.scss @@ -41,7 +41,7 @@ transition: all .15s ease-in-out; list-style: none; margin: 0; - box-shadow: 0px 3px 20px rgba( 18, 24, 30, .1 ), 0px 1px 3px rgba( 18, 24, 30, .1 ); + box-shadow: 0 3px 20px rgba( 18, 24, 30, .1 ), 0 1px 3px rgba( 18, 24, 30, .1 ); z-index: z-index( '.blocks-url-input__suggestions' ) } diff --git a/components/autocomplete/index.js b/components/autocomplete/index.js index 13cb632ac404fd..bdbb371b892b59 100644 --- a/components/autocomplete/index.js +++ b/components/autocomplete/index.js @@ -2,7 +2,7 @@ * External dependencies */ import classnames from 'classnames'; -import { escapeRegExp, find, filter, map } from 'lodash'; +import { escapeRegExp, find, filter, map, debounce } from 'lodash'; /** * WordPress dependencies @@ -15,6 +15,7 @@ import { __, _n, sprintf } from '@wordpress/i18n'; * Internal dependencies */ import './style.scss'; +import { isDeprecatedCompleter, toCompatibleCompleter } from './completer-compat'; import withFocusOutside from '../higher-order/with-focus-outside'; import Button from '../button'; import Popover from '../popover'; @@ -23,6 +24,81 @@ import withSpokenMessages from '../higher-order/with-spoken-messages'; const { ENTER, ESCAPE, UP, DOWN, LEFT, RIGHT, SPACE } = keycodes; +/** + * A raw completer option. + * @typedef {*} CompleterOption + */ + +/** + * @callback FnGetOptions + * + * @returns {(CompleterOption[]|Promise.<CompleterOption[]>)} The completer options or a promise for them. + */ + +/** + * @callback FnGetOptionKeywords + * @param {CompleterOption} option a completer option. + * + * @returns {string[]} list of key words to search. + */ + +/** + * @callback FnGetOptionLabel + * @param {CompleterOption} option a completer option. + * + * @returns {(string|Array.<(string|Component)>)} list of react components to render. + */ + +/** + * @callback FnAllowNode + * @param {Node} textNode check if the completer can handle this text node. + * + * @returns {boolean} true if the completer can handle this text node. + */ + +/** + * @callback FnAllowContext + * @param {Range} before the range before the auto complete trigger and query. + * @param {Range} after the range after the autocomplete trigger and query. + * + * @returns {boolean} true if the completer can handle these ranges. + */ + +/** + * @typedef {Object} OptionCompletion + * @property {('insert-at-caret', 'replace')} action the intended placement of the completion. + * @property {OptionCompletionValue} value the completion value. + */ + +/** + * A completion value. + * @typedef {(String|WPElement|Object)} OptionCompletionValue + */ + +/** + * @callback FnGetOptionCompletion + * @param {CompleterOption} value the value of the completer option. + * @param {Range} range the nodes included in the autocomplete trigger and query. + * @param {String} query the text value of the autocomplete query. + * + * @returns {(OptionCompletion|OptionCompletionValue)} the completion for the given option. If an + * OptionCompletionValue is returned, the + * completion action defaults to `insert-at-caret`. + */ + +/** + * @typedef {Object} Completer + * @property {String} name a way to identify a completer, useful for selective overriding. + * @property {?String} className A class to apply to the popup menu. + * @property {String} triggerPrefix the prefix that will display the menu. + * @property {(CompleterOption[]|FnGetOptions)} options the completer options or a function to get them. + * @property {?FnGetOptionKeywords} getOptionKeywords get the keywords for a given option. + * @property {FnGetOptionLabel} getOptionLabel get the label for a given option. + * @property {?FnAllowNode} allowNode filter the allowed text nodes in the autocomplete. + * @property {?FnAllowContext} allowContext filter the context under which the autocomplete activates. + * @property {FnGetOptionCompletion} getOptionCompletion get the completion associated with a given option. + */ + /** * Recursively select the firstChild until hitting a leaf node. * @@ -131,6 +207,36 @@ export class Autocomplete extends Component { }; } + /* + * NOTE: This is necessary for backwards compatibility with the + * previous completer interface. Once we no longer support the + * old interface, we should be able to use the `completers` prop + * directly. + */ + static getDerivedStateFromProps( nextProps, prevState ) { + const { completers: nextCompleters } = nextProps; + const { lastAppliedCompleters } = prevState; + + if ( nextCompleters !== lastAppliedCompleters ) { + let completers = nextCompleters; + + if ( completers.some( isDeprecatedCompleter ) ) { + completers = completers.map( completer => { + return isDeprecatedCompleter( completer ) ? + toCompatibleCompleter( completer ) : + completer; + } ); + } + + return { + completers, + lastAppliedCompleters: nextCompleters, + }; + } + + return null; + } + constructor() { super( ...arguments ); @@ -141,6 +247,7 @@ export class Autocomplete extends Component { this.search = this.search.bind( this ); this.handleKeyDown = this.handleKeyDown.bind( this ); this.getWordRect = this.getWordRect.bind( this ); + this.debouncedLoadOptions = debounce( this.loadOptions, 250 ); this.state = this.constructor.getInitialState(); } @@ -149,7 +256,7 @@ export class Autocomplete extends Component { this.node = node; } - replace( range, replacement ) { + insertCompletion( range, replacement ) { const container = document.createElement( 'div' ); container.innerHTML = renderToString( replacement ); while ( container.firstChild ) { @@ -162,15 +269,32 @@ export class Autocomplete extends Component { } select( option ) { + const { onReplace } = this.props; const { open, range, query } = this.state; - const { onSelect } = open || {}; + const { getOptionCompletion } = open || {}; this.reset(); - if ( onSelect ) { - const replacement = onSelect( option.value, range, query ); - if ( replacement !== undefined ) { - this.replace( range, replacement ); + if ( getOptionCompletion ) { + const completion = getOptionCompletion( option.value, range, query ); + + const { action, value } = + ( undefined === completion.action || undefined === completion.value ) ? + { action: 'insert-at-caret', value: completion } : + completion; + + if ( 'replace' === action ) { + onReplace( [ value ] ); + } else if ( 'insert-at-caret' === action ) { + this.insertCompletion( range, value ); + } else if ( 'backcompat' === action ) { + // NOTE: This block should be removed once we no longer support the old completer interface. + const onSelect = value; + const deprecatedOptionObject = option.value; + const selectionResult = onSelect( deprecatedOptionObject.value, range, query ); + if ( selectionResult !== undefined ) { + this.insertCompletion( range, selectionResult ); + } } } } @@ -231,13 +355,33 @@ export class Autocomplete extends Component { } } - loadOptions( index ) { - this.props.completers[ index ].getOptions().then( ( options ) => { - const keyedOptions = map( options, ( option, i ) => ( { ...option, key: index + '-' + i } ) ); + /** + * Load options for an autocompleter. + * + * @param {Completer} completer The autocompleter. + * @param {string} query The query, if any. + */ + loadOptions( completer, query ) { + const { options } = completer; + + /* + * We support both synchronous and asynchronous retrieval of completer options + * but internally treat all as async so we maintain a single, consistent code path. + */ + Promise.resolve( + typeof options === 'function' ? options( query ) : options + ).then( optionsData => { + const keyedOptions = optionsData.map( ( optionData, optionIndex ) => ( { + key: `${ completer.idx }-${ optionIndex }`, + value: optionData, + label: completer.getOptionLabel( optionData ), + keywords: completer.getOptionKeywords ? completer.getOptionKeywords( optionData ) : [], + } ) ); + const filteredOptions = filterOptions( this.state.search, keyedOptions ); const selectedIndex = filteredOptions.length === this.state.filteredOptions.length ? this.state.selectedIndex : 0; this.setState( { - [ 'options_' + index ]: keyedOptions, + [ 'options_' + completer.idx ]: keyedOptions, filteredOptions, selectedIndex, } ); @@ -322,9 +466,9 @@ export class Autocomplete extends Component { } search( event ) { - const { open: wasOpen, suppress: wasSuppress } = this.state; - const { completers } = this.props; + const { completers, open: wasOpen, suppress: wasSuppress, query: wasQuery } = this.state; const container = event.target; + // ensure that the cursor location is unambiguous const cursor = this.getCursor( container ); if ( ! cursor ) { @@ -334,8 +478,12 @@ export class Autocomplete extends Component { const match = this.findMatch( container, cursor, completers, wasOpen ); const { open, query, range } = match || {}; // asynchronously load the options for the open completer - if ( open && ( ! wasOpen || open.idx !== wasOpen.idx ) ) { - this.loadOptions( open.idx ); + if ( open && ( ! wasOpen || open.idx !== wasOpen.idx || query !== wasQuery ) ) { + if ( open.isDebounced ) { + this.debouncedLoadOptions( open, query ); + } else { + this.loadOptions( open, query ); + } } // create a regular expression to filter the options const search = open ? new RegExp( '(?:\\b|\\s|^)' + escapeRegExp( query ), 'i' ) : /./; @@ -452,6 +600,7 @@ export class Autocomplete extends Component { componentWillUnmount() { this.toggleKeyEvents( false ); + this.debouncedLoadOptions.cancel(); } render() { diff --git a/components/autocomplete/test/index.js b/components/autocomplete/test/index.js index 6abba94aeb5b1b..e64299aeabbcdb 100644 --- a/components/autocomplete/test/index.js +++ b/components/autocomplete/test/index.js @@ -3,6 +3,7 @@ */ import { mount } from 'enzyme'; import { Component } from '../../../element'; +import { noop } from 'lodash'; /** * WordPress dependencies @@ -37,18 +38,24 @@ class FakeEditor extends Component { } } -function makeAutocompleter( completers, AutocompleteComponent = Autocomplete ) { +function makeAutocompleter( completers, { + AutocompleteComponent = Autocomplete, + onReplace = noop, +} = {} ) { return mount( - <AutocompleteComponent instanceId="1" completers={ completers }>{ - ( { isExpanded, listBoxId, activeId } ) => ( + <AutocompleteComponent instanceId="1" + completers={ completers } + onReplace={ onReplace } + > + { ( { isExpanded, listBoxId, activeId } ) => ( <FakeEditor aria-autocomplete="list" aria-expanded={ isExpanded } aria-owns={ listBoxId } aria-activedescendant={ activeId } /> - ) - }</AutocompleteComponent> + ) } + </AutocompleteComponent> ); } @@ -134,29 +141,31 @@ function expectInitialState( wrapper ) { describe( 'Autocomplete', () => { const options = [ { - value: 1, + id: 1, label: 'Bananas', keywords: [ 'fruit' ], }, { - value: 2, + id: 2, label: 'Apple', keywords: [ 'fruit' ], }, { - value: 3, + id: 3, label: 'Avocado', keywords: [ 'fruit' ], }, ]; const basicCompleter = { - getOptions: () => Promise.resolve( options ), + options, + getOptionLabel: option => option.label, + getOptionKeywords: option => option.keywords, }; const slashCompleter = { triggerPrefix: '/', - getOptions: () => Promise.resolve( options ), + ...basicCompleter, }; let realGetCursor, realCreateRange; @@ -215,7 +224,7 @@ describe( 'Autocomplete', () => { expectInitialState( wrapper ); // simulate typing 'b' simulateInput( wrapper, [ par( tx( 'b' ) ) ] ); - // wait for getOptions promise + // wait for async popover display process.nextTick( function() { wrapper.update(); expect( wrapper.state( 'open' ) ).toBeDefined(); @@ -223,7 +232,7 @@ describe( 'Autocomplete', () => { expect( wrapper.state( 'query' ) ).toEqual( 'b' ); expect( wrapper.state( 'search' ) ).toEqual( /(?:\b|\s|^)b/i ); expect( wrapper.state( 'filteredOptions' ) ).toEqual( [ - { key: '0-0', value: 1, label: 'Bananas', keywords: [ 'fruit' ] }, + { key: '0-0', value: options[ 0 ], label: 'Bananas', keywords: [ 'fruit' ] }, ] ); expect( wrapper.find( 'Popover' ) ).toHaveLength( 1 ); expect( wrapper.find( 'Popover' ).prop( 'focusOnMount' ) ).toBe( false ); @@ -237,7 +246,7 @@ describe( 'Autocomplete', () => { expectInitialState( wrapper ); // simulate typing 'zzz' simulateInput( wrapper, [ tx( 'zzz' ) ] ); - // wait for getOptions promise + // wait for async popover display process.nextTick( () => { wrapper.update(); // now check that we've opened the popup and filtered the options to empty @@ -256,7 +265,7 @@ describe( 'Autocomplete', () => { expectInitialState( wrapper ); // simulate typing 'b' simulateInput( wrapper, [ par( tx( 'b' ) ) ] ); - // wait for getOptions promise + // wait for async popover display process.nextTick( () => { wrapper.update(); // now check that the popup is not open @@ -270,7 +279,7 @@ describe( 'Autocomplete', () => { expectInitialState( wrapper ); // simulate typing '/' simulateInput( wrapper, [ par( tx( '/' ) ) ] ); - // wait for getOptions promise + // wait for async popover display process.nextTick( () => { wrapper.update(); // now check that we've opened the popup and filtered the options @@ -279,9 +288,9 @@ describe( 'Autocomplete', () => { expect( wrapper.state( 'query' ) ).toEqual( '' ); expect( wrapper.state( 'search' ) ).toEqual( /(?:\b|\s|^)/i ); expect( wrapper.state( 'filteredOptions' ) ).toEqual( [ - { key: '0-0', value: 1, label: 'Bananas', keywords: [ 'fruit' ] }, - { key: '0-1', value: 2, label: 'Apple', keywords: [ 'fruit' ] }, - { key: '0-2', value: 3, label: 'Avocado', keywords: [ 'fruit' ] }, + { key: '0-0', value: options[ 0 ], label: 'Bananas', keywords: [ 'fruit' ] }, + { key: '0-1', value: options[ 1 ], label: 'Apple', keywords: [ 'fruit' ] }, + { key: '0-2', value: options[ 2 ], label: 'Avocado', keywords: [ 'fruit' ] }, ] ); expect( wrapper.find( 'Popover' ) ).toHaveLength( 1 ); expect( wrapper.find( 'button.components-autocomplete__result' ) ).toHaveLength( 3 ); @@ -294,7 +303,7 @@ describe( 'Autocomplete', () => { expectInitialState( wrapper ); // simulate typing fruit (split over 2 text nodes because these things happen) simulateInput( wrapper, [ par( tx( 'fru' ), tx( 'it' ) ) ] ); - // wait for getOptions promise + // wait for async popover display process.nextTick( () => { wrapper.update(); // now check that we've opened the popup and filtered the options @@ -303,9 +312,9 @@ describe( 'Autocomplete', () => { expect( wrapper.state( 'query' ) ).toEqual( 'fruit' ); expect( wrapper.state( 'search' ) ).toEqual( /(?:\b|\s|^)fruit/i ); expect( wrapper.state( 'filteredOptions' ) ).toEqual( [ - { key: '0-0', value: 1, label: 'Bananas', keywords: [ 'fruit' ] }, - { key: '0-1', value: 2, label: 'Apple', keywords: [ 'fruit' ] }, - { key: '0-2', value: 3, label: 'Avocado', keywords: [ 'fruit' ] }, + { key: '0-0', value: options[ 0 ], label: 'Bananas', keywords: [ 'fruit' ] }, + { key: '0-1', value: options[ 1 ], label: 'Apple', keywords: [ 'fruit' ] }, + { key: '0-2', value: options[ 2 ], label: 'Avocado', keywords: [ 'fruit' ] }, ] ); expect( wrapper.find( 'Popover' ) ).toHaveLength( 1 ); expect( wrapper.find( 'button.components-autocomplete__result' ) ).toHaveLength( 3 ); @@ -318,7 +327,7 @@ describe( 'Autocomplete', () => { expectInitialState( wrapper ); // simulate typing 'a' simulateInput( wrapper, [ tx( 'a' ) ] ); - // wait for getOptions promise + // wait for async popover display process.nextTick( () => { wrapper.update(); // now check that we've opened the popup and all options are displayed @@ -327,8 +336,8 @@ describe( 'Autocomplete', () => { expect( wrapper.state( 'query' ) ).toEqual( 'a' ); expect( wrapper.state( 'search' ) ).toEqual( /(?:\b|\s|^)a/i ); expect( wrapper.state( 'filteredOptions' ) ).toEqual( [ - { key: '0-1', value: 2, label: 'Apple', keywords: [ 'fruit' ] }, - { key: '0-2', value: 3, label: 'Avocado', keywords: [ 'fruit' ] }, + { key: '0-1', value: options[ 1 ], label: 'Apple', keywords: [ 'fruit' ] }, + { key: '0-2', value: options[ 2 ], label: 'Avocado', keywords: [ 'fruit' ] }, ] ); expect( wrapper.find( 'Popover' ) ).toHaveLength( 1 ); expect( wrapper.find( 'button.components-autocomplete__result' ) ).toHaveLength( 2 ); @@ -340,7 +349,7 @@ describe( 'Autocomplete', () => { expect( wrapper.state( 'query' ) ).toEqual( 'ap' ); expect( wrapper.state( 'search' ) ).toEqual( /(?:\b|\s|^)ap/i ); expect( wrapper.state( 'filteredOptions' ) ).toEqual( [ - { key: '0-1', value: 2, label: 'Apple', keywords: [ 'fruit' ] }, + { key: '0-1', value: options[ 1 ], label: 'Apple', keywords: [ 'fruit' ] }, ] ); expect( wrapper.find( 'Popover' ) ).toHaveLength( 1 ); expect( wrapper.find( 'button.components-autocomplete__result' ) ).toHaveLength( 1 ); @@ -352,6 +361,80 @@ describe( 'Autocomplete', () => { } ); } ); + it( 'renders options provided via array', ( done ) => { + const wrapper = makeAutocompleter( [ + { ...slashCompleter, options }, + ] ); + expectInitialState( wrapper ); + // simulate typing '/' + simulateInput( wrapper, [ par( tx( '/' ) ) ] ); + // wait for async popover display + process.nextTick( () => { + wrapper.update(); + + expect( wrapper.find( 'Popover' ) ).toHaveLength( 1 ); + + const itemWrappers = wrapper.find( 'button.components-autocomplete__result' ); + expect( itemWrappers ).toHaveLength( 3 ); + + const expectedLabelContent = options.map( o => o.label ); + const actualLabelContent = itemWrappers.map( itemWrapper => itemWrapper.text() ); + expect( actualLabelContent ).toEqual( expectedLabelContent ); + + done(); + } ); + } ); + it( 'renders options provided via function that returns array', ( done ) => { + const optionsMock = jest.fn( () => options ); + + const wrapper = makeAutocompleter( [ + { ...slashCompleter, options: optionsMock }, + ] ); + expectInitialState( wrapper ); + // simulate typing '/' + simulateInput( wrapper, [ par( tx( '/' ) ) ] ); + // wait for async popover display + process.nextTick( () => { + wrapper.update(); + + expect( wrapper.find( 'Popover' ) ).toHaveLength( 1 ); + + const itemWrappers = wrapper.find( 'button.components-autocomplete__result' ); + expect( itemWrappers ).toHaveLength( 3 ); + + const expectedLabelContent = options.map( o => o.label ); + const actualLabelContent = itemWrappers.map( itemWrapper => itemWrapper.text() ); + expect( actualLabelContent ).toEqual( expectedLabelContent ); + + done(); + } ); + } ); + it( 'renders options provided via function that returns promise', ( done ) => { + const optionsMock = jest.fn( () => Promise.resolve( options ) ); + + const wrapper = makeAutocompleter( [ + { ...slashCompleter, options: optionsMock }, + ] ); + expectInitialState( wrapper ); + // simulate typing '/' + simulateInput( wrapper, [ par( tx( '/' ) ) ] ); + // wait for async popover display + process.nextTick( () => { + wrapper.update(); + + expect( wrapper.find( 'Popover' ) ).toHaveLength( 1 ); + + const itemWrappers = wrapper.find( 'button.components-autocomplete__result' ); + expect( itemWrappers ).toHaveLength( 3 ); + + const expectedLabelContent = options.map( o => o.label ); + const actualLabelContent = itemWrappers.map( itemWrapper => itemWrapper.text() ); + expect( actualLabelContent ).toEqual( expectedLabelContent ); + + done(); + } ); + } ); + it( 'navigates options by arrow keys', ( done ) => { const wrapper = makeAutocompleter( [ slashCompleter ] ); // listen to keydown events on the editor to see if it gets them @@ -367,7 +450,7 @@ describe( 'Autocomplete', () => { editorKeydown.mockClear(); // simulate typing '/', the menu is open so the editor should not get key down events simulateInput( wrapper, [ par( tx( '/' ) ) ] ); - // wait for getOptions promise + // wait for async popover display process.nextTick( () => { wrapper.update(); expect( wrapper.state( 'selectedIndex' ) ).toBe( 0 ); @@ -395,7 +478,7 @@ describe( 'Autocomplete', () => { expectInitialState( wrapper ); // simulate typing '/' simulateInput( wrapper, [ par( tx( '/' ) ) ] ); - // wait for getOptions promise + // wait for async popover display process.nextTick( () => { wrapper.update(); expect( wrapper.state( 'selectedIndex' ) ).toBe( 0 ); @@ -423,7 +506,7 @@ describe( 'Autocomplete', () => { editorKeydown.mockClear(); // simulate typing '/' simulateInput( wrapper, [ par( tx( '/' ) ) ] ); - // wait for getOptions promise + // wait for async popover display process.nextTick( () => { wrapper.update(); // menu should be open with all options @@ -433,17 +516,17 @@ describe( 'Autocomplete', () => { expect( wrapper.state( 'query' ) ).toEqual( '' ); expect( wrapper.state( 'search' ) ).toEqual( /(?:\b|\s|^)/i ); expect( wrapper.state( 'filteredOptions' ) ).toEqual( [ - { key: '0-0', value: 1, label: 'Bananas', keywords: [ 'fruit' ] }, - { key: '0-1', value: 2, label: 'Apple', keywords: [ 'fruit' ] }, - { key: '0-2', value: 3, label: 'Avocado', keywords: [ 'fruit' ] }, + { key: '0-0', value: options[ 0 ], label: 'Bananas', keywords: [ 'fruit' ] }, + { key: '0-1', value: options[ 1 ], label: 'Apple', keywords: [ 'fruit' ] }, + { key: '0-2', value: options[ 2 ], label: 'Avocado', keywords: [ 'fruit' ] }, ] ); // pressing escape should suppress the dialog but it maintains the state simulateKeydown( wrapper, ESCAPE ); expect( wrapper.state( 'suppress' ) ).toEqual( 0 ); expect( wrapper.state( 'filteredOptions' ) ).toEqual( [ - { key: '0-0', value: 1, label: 'Bananas', keywords: [ 'fruit' ] }, - { key: '0-1', value: 2, label: 'Apple', keywords: [ 'fruit' ] }, - { key: '0-2', value: 3, label: 'Avocado', keywords: [ 'fruit' ] }, + { key: '0-0', value: options[ 0 ], label: 'Bananas', keywords: [ 'fruit' ] }, + { key: '0-1', value: options[ 1 ], label: 'Apple', keywords: [ 'fruit' ] }, + { key: '0-2', value: options[ 2 ], label: 'Avocado', keywords: [ 'fruit' ] }, ] ); expect( wrapper.find( 'Popover' ) ).toHaveLength( 0 ); // the editor should not have gotten the event @@ -455,7 +538,9 @@ describe( 'Autocomplete', () => { it( 'closes by blur', () => { jest.spyOn( Autocomplete.prototype, 'handleFocusOutside' ); - const wrapper = makeAutocompleter( [], EnhancedAutocomplete ); + const wrapper = makeAutocompleter( [], { + AutocompleteComponent: EnhancedAutocomplete, + } ); simulateInput( wrapper, [ par( tx( '/' ) ) ] ); wrapper.find( '.fake-editor' ).simulate( 'blur' ); @@ -465,8 +550,11 @@ describe( 'Autocomplete', () => { } ); it( 'selects by enter', ( done ) => { - const onSelect = jest.fn(); - const wrapper = makeAutocompleter( [ { ...slashCompleter, onSelect } ] ); + const getOptionCompletion = jest.fn().mockReturnValue( { + action: 'non-existent-action', + value: 'dummy-value', + } ); + const wrapper = makeAutocompleter( [ { ...slashCompleter, getOptionCompletion } ] ); // listen to keydown events on the editor to see if it gets them const editorKeydown = jest.fn(); const fakeEditor = wrapper.getDOMNode().querySelector( '.fake-editor' ); @@ -480,7 +568,7 @@ describe( 'Autocomplete', () => { editorKeydown.mockClear(); // simulate typing '/' simulateInput( wrapper, [ par( tx( '/' ) ) ] ); - // wait for getOptions promise + // wait for async popover display process.nextTick( () => { wrapper.update(); // menu should be open with all options @@ -489,14 +577,14 @@ describe( 'Autocomplete', () => { expect( wrapper.state( 'query' ) ).toEqual( '' ); expect( wrapper.state( 'search' ) ).toEqual( /(?:\b|\s|^)/i ); expect( wrapper.state( 'filteredOptions' ) ).toEqual( [ - { key: '0-0', value: 1, label: 'Bananas', keywords: [ 'fruit' ] }, - { key: '0-1', value: 2, label: 'Apple', keywords: [ 'fruit' ] }, - { key: '0-2', value: 3, label: 'Avocado', keywords: [ 'fruit' ] }, + { key: '0-0', value: options[ 0 ], label: 'Bananas', keywords: [ 'fruit' ] }, + { key: '0-1', value: options[ 1 ], label: 'Apple', keywords: [ 'fruit' ] }, + { key: '0-2', value: options[ 2 ], label: 'Avocado', keywords: [ 'fruit' ] }, ] ); - // pressing enter should reset and call onSelect + // pressing enter should reset and call getOptionCompletion simulateKeydown( wrapper, ENTER ); expectInitialState( wrapper ); - expect( onSelect ).toHaveBeenCalled(); + expect( getOptionCompletion ).toHaveBeenCalled(); // the editor should not have gotten the event expect( editorKeydown ).not.toHaveBeenCalled(); done(); @@ -518,13 +606,16 @@ describe( 'Autocomplete', () => { done(); } ); - it( 'selects by click on result', ( done ) => { - const onSelect = jest.fn(); - const wrapper = makeAutocompleter( [ { ...slashCompleter, onSelect } ] ); + it( 'selects by click', ( done ) => { + const getOptionCompletion = jest.fn().mockReturnValue( { + action: 'non-existent-action', + value: 'dummy-value', + } ); + const wrapper = makeAutocompleter( [ { ...slashCompleter, getOptionCompletion } ] ); expectInitialState( wrapper ); // simulate typing '/' simulateInput( wrapper, [ par( tx( '/' ) ) ] ); - // wait for getOptions promise + // wait for async popover display process.nextTick( () => { wrapper.update(); // menu should be open with all options @@ -533,15 +624,126 @@ describe( 'Autocomplete', () => { expect( wrapper.state( 'query' ) ).toEqual( '' ); expect( wrapper.state( 'search' ) ).toEqual( /(?:\b|\s|^)/i ); expect( wrapper.state( 'filteredOptions' ) ).toEqual( [ - { key: '0-0', value: 1, label: 'Bananas', keywords: [ 'fruit' ] }, - { key: '0-1', value: 2, label: 'Apple', keywords: [ 'fruit' ] }, - { key: '0-2', value: 3, label: 'Avocado', keywords: [ 'fruit' ] }, + { key: '0-0', value: options[ 0 ], label: 'Bananas', keywords: [ 'fruit' ] }, + { key: '0-1', value: options[ 1 ], label: 'Apple', keywords: [ 'fruit' ] }, + { key: '0-2', value: options[ 2 ], label: 'Avocado', keywords: [ 'fruit' ] }, ] ); // clicking should reset and select the item wrapper.find( '.components-autocomplete__result Button' ).at( 0 ).simulate( 'click' ); wrapper.update(); expectInitialState( wrapper ); - expect( onSelect ).toHaveBeenCalled(); + expect( getOptionCompletion ).toHaveBeenCalled(); + done(); + } ); + } ); + + it( 'calls insertCompletion for a completion with action `insert-at-caret`', ( done ) => { + const getOptionCompletion = jest.fn() + .mockReturnValueOnce( { + action: 'insert-at-caret', + value: 'expected-value', + } ); + + const insertCompletion = jest.fn(); + + const wrapper = makeAutocompleter( + [ { ...slashCompleter, getOptionCompletion } ], + { + AutocompleteComponent: class extends Autocomplete { + insertCompletion( ...args ) { + return insertCompletion( ...args ); + } + }, + } + ); + expectInitialState( wrapper ); + + // simulate typing '/' + simulateInput( wrapper, [ par( tx( '/' ) ) ] ); + + // wait for async popover display + process.nextTick( () => { + wrapper.update(); + // menu should be open with at least one option + expect( wrapper.state( 'open' ) ).toBeDefined(); + expect( wrapper.state( 'filteredOptions' ).length ).toBeGreaterThanOrEqual( 1 ); + + // clicking should reset and select the item + wrapper.find( '.components-autocomplete__result Button' ).at( 0 ).simulate( 'click' ); + wrapper.update(); + + expect( insertCompletion ).toHaveBeenCalledTimes( 1 ); + expect( insertCompletion.mock.calls[ 0 ][ 1 ] ).toBe( 'expected-value' ); + done(); + } ); + } ); + + it( 'calls insertCompletion for a completion without an action property', ( done ) => { + const getOptionCompletion = jest.fn().mockReturnValueOnce( 'expected-value' ); + + const insertCompletion = jest.fn(); + + const wrapper = makeAutocompleter( + [ { ...slashCompleter, getOptionCompletion } ], + { + AutocompleteComponent: class extends Autocomplete { + insertCompletion( ...args ) { + return insertCompletion( ...args ); + } + }, + } + ); + expectInitialState( wrapper ); + + // simulate typing '/' + simulateInput( wrapper, [ par( tx( '/' ) ) ] ); + + // wait for async popover display + process.nextTick( () => { + wrapper.update(); + // menu should be open with at least one option + expect( wrapper.state( 'open' ) ).toBeDefined(); + expect( wrapper.state( 'filteredOptions' ).length ).toBeGreaterThanOrEqual( 1 ); + + // clicking should reset and select the item + wrapper.find( '.components-autocomplete__result Button' ).at( 0 ).simulate( 'click' ); + wrapper.update(); + + expect( insertCompletion ).toHaveBeenCalledTimes( 1 ); + expect( insertCompletion.mock.calls[ 0 ][ 1 ] ).toBe( 'expected-value' ); + done(); + } ); + } ); + + it( 'calls onReplace for a completion with action `replace`', ( done ) => { + const getOptionCompletion = jest.fn() + .mockReturnValueOnce( { + action: 'replace', + value: 'expected-value', + } ); + + const onReplace = jest.fn(); + + const wrapper = makeAutocompleter( + [ { ...slashCompleter, getOptionCompletion } ], + { onReplace } ); + expectInitialState( wrapper ); + // simulate typing '/' + simulateInput( wrapper, [ par( tx( '/' ) ) ] ); + + // wait for async popover display + process.nextTick( () => { + wrapper.update(); + // menu should be open with at least one option + expect( wrapper.state( 'open' ) ).toBeDefined(); + expect( wrapper.state( 'filteredOptions' ).length ).toBeGreaterThanOrEqual( 1 ); + + // clicking should reset and select the item + wrapper.find( '.components-autocomplete__result Button' ).at( 0 ).simulate( 'click' ); + wrapper.update(); + + expect( onReplace ).toHaveBeenCalledTimes( 1 ); + expect( onReplace ).toHaveBeenLastCalledWith( [ 'expected-value' ] ); done(); } ); } ); diff --git a/components/base-control/README.md b/components/base-control/README.md index acc23a9cf82a08..ac2dc6249714a1 100644 --- a/components/base-control/README.md +++ b/components/base-control/README.md @@ -49,8 +49,8 @@ If this property is added, a help text will be generated using help property as ### className -The class that will be added with "blocks-base-control" to the classes of the wrapper div. -If no className is passed only blocks-base-control is used. +The class that will be added with "components-base-control" to the classes of the wrapper div. +If no className is passed only components-base-control is used. - Type: `String` - Required: No diff --git a/components/base-control/index.js b/components/base-control/index.js index 2e21c38f494546..695293cec35eba 100644 --- a/components/base-control/index.js +++ b/components/base-control/index.js @@ -10,10 +10,12 @@ import './style.scss'; function BaseControl( { id, label, help, className, children } ) { return ( - <div className={ classnames( 'blocks-base-control', className ) }> - { label && <label className="blocks-base-control__label" htmlFor={ id }>{ label }</label> } - { children } - { !! help && <p id={ id + '__help' } className="blocks-base-control__help">{ help }</p> } + <div className={ classnames( 'components-base-control', className ) }> + <div className="components-base-control__field"> + { label && <label className="components-base-control__label" htmlFor={ id }>{ label }</label> } + { children } + </div> + { !! help && <p id={ id + '__help' } className="components-base-control__help">{ help }</p> } </div> ); } diff --git a/components/base-control/style.scss b/components/base-control/style.scss index cf7f4c77014fc2..1a423e21a6e7c6 100644 --- a/components/base-control/style.scss +++ b/components/base-control/style.scss @@ -1,12 +1,12 @@ -.blocks-base-control { - margin: 0 0 1.5em 0; +.components-base-control { + margin: 0 0 1.5em; } -.blocks-base-control__label { +.components-base-control__label { display: block; margin-bottom: 5px; } -.blocks-base-control__help { +.components-base-control__help { font-style: italic; } diff --git a/components/button/style.scss b/components/button/style.scss index 1777641ae44403..f891e353530881 100644 --- a/components/button/style.scss +++ b/components/button/style.scss @@ -37,6 +37,10 @@ background-image: linear-gradient( -45deg, $blue-medium-500 28%, $blue-dark-900 28%, $blue-dark-900 72%, $blue-medium-500 72%) !important; border-color: $blue-dark-900 !important; } + + .wp-core-ui.gutenberg-editor-page & { + font-size: $default-font-size; + } } @keyframes components-button__busy-animation { diff --git a/components/checkbox-control/index.js b/components/checkbox-control/index.js index ffac318d6fae24..c231143f4f88cf 100644 --- a/components/checkbox-control/index.js +++ b/components/checkbox-control/index.js @@ -7,13 +7,13 @@ import './style.scss'; function CheckboxControl( { label, heading, checked, help, instanceId, onChange, ...props } ) { const id = `inspector-checkbox-control-${ instanceId }`; - const onChangeValue = ( event ) => onChange( event.target.value ); + const onChangeValue = ( event ) => onChange( event.target.checked ); return ( <BaseControl label={ heading } id={ id } help={ help }> <input id={ id } - className="blocks-checkbox-control__input" + className="components-checkbox-control__input" type="checkbox" value="1" onChange={ onChangeValue } @@ -21,7 +21,7 @@ function CheckboxControl( { label, heading, checked, help, instanceId, onChange, aria-describedby={ !! help ? id + '__help' : undefined } { ...props } /> - <label className="blocks-checkbox-control__label" htmlFor={ id }> + <label className="components-checkbox-control__label" htmlFor={ id }> { label } </label> </BaseControl> diff --git a/components/checkbox-control/style.scss b/components/checkbox-control/style.scss index 90f1f24fe4ca7e..9971776a521124 100644 --- a/components/checkbox-control/style.scss +++ b/components/checkbox-control/style.scss @@ -1,4 +1,4 @@ -.blocks-checkbox-control__input[type=checkbox] { +.components-checkbox-control__input[type=checkbox] { margin-top: 0; margin-right: 6px; } \ No newline at end of file diff --git a/components/dashicon/index.js b/components/dashicon/index.js index ac5616576bb8de..6f066d96762508 100644 --- a/components/dashicon/index.js +++ b/components/dashicon/index.js @@ -5,6 +5,11 @@ DO NOT EDIT THAT FILE! EDIT index-header.jsx and index-footer.jsx instead OR if you're looking to change now SVGs get output, you'll need to edit strings in the Gruntfile :) !!! */ +/** + * Internal dependencies + */ +import './style.scss'; + /** * External dependencies */ @@ -241,8 +246,14 @@ export default class Dashicon extends wp.element.Component { case 'clock': path = 'M10 2c4.42 0 8 3.58 8 8s-3.58 8-8 8-8-3.58-8-8 3.58-8 8-8zm0 14c3.31 0 6-2.69 6-6s-2.69-6-6-6-6 2.69-6 6 2.69 6 6 6zm-.71-5.29c.07.05.14.1.23.15l-.02.02L14 13l-3.03-3.19L10 5l-.97 4.81h.01c0 .02-.01.05-.02.09S9 9.97 9 10c0 .28.1.52.29.71z'; break; + case 'cloud-saved': + path = 'M14.8 9c.1-.3.2-.6.2-1 0-2.2-1.8-4-4-4-1.5 0-2.9.9-3.5 2.2-.3-.1-.7-.2-1-.2C5.1 6 4 7.1 4 8.5c0 .2 0 .4.1.5-1.8.3-3.1 1.7-3.1 3.5C1 14.4 2.6 16 4.5 16h10c1.9 0 3.5-1.6 3.5-3.5 0-1.8-1.4-3.3-3.2-3.5zm-6.3 5.9l-3.2-3.2 1.4-1.4 1.8 1.8 3.8-3.8 1.4 1.4-5.2 5.2z'; + break; + case 'cloud-upload': + path = 'M14.8 9c.1-.3.2-.6.2-1 0-2.2-1.8-4-4-4-1.5 0-2.9.9-3.5 2.2-.3-.1-.7-.2-1-.2C5.1 6 4 7.1 4 8.5c0 .2 0 .4.1.5-1.8.3-3.1 1.7-3.1 3.5C1 14.4 2.6 16 4.5 16H8v-3H5l4.5-4.5L14 13h-3v3h3.5c1.9 0 3.5-1.6 3.5-3.5 0-1.8-1.4-3.3-3.2-3.5z'; + break; case 'cloud': - path = 'M14.85 10.03c1.76.18 3.15 1.66 3.15 3.47 0 1.93-1.57 3.5-3.5 3.5h-10C2.57 17 1 15.43 1 13.5c0-1.79 1.34-3.24 3.06-3.46C4.02 9.87 4 9.69 4 9.5 4 8.12 5.12 7 6.5 7c.34 0 .66.07.95.19C8.11 5.89 9.45 5 11 5c2.21 0 4 1.79 4 4 0 .36-.06.7-.15 1.03z'; + path = 'M14.9 9c1.8.2 3.1 1.7 3.1 3.5 0 1.9-1.6 3.5-3.5 3.5h-10C2.6 16 1 14.4 1 12.5 1 10.7 2.3 9.3 4.1 9 4 8.9 4 8.7 4 8.5 4 7.1 5.1 6 6.5 6c.3 0 .7.1.9.2C8.1 4.9 9.4 4 11 4c2.2 0 4 1.8 4 4 0 .4-.1.7-.1 1z'; break; case 'columns': path = 'M3 15h6V5H3v10zm8 0h6V5h-6v10z'; @@ -274,6 +285,9 @@ export default class Dashicon extends wp.element.Component { case 'controls-volumeon': path = 'M2 7h4l5-4v14l-5-4H2V7zm12.69-2.46C14.82 4.59 18 5.92 18 10s-3.18 5.41-3.31 5.46c-.06.03-.13.04-.19.04-.2 0-.39-.12-.46-.31-.11-.26.02-.55.27-.65.11-.05 2.69-1.15 2.69-4.54 0-3.41-2.66-4.53-2.69-4.54-.25-.1-.38-.39-.27-.65.1-.25.39-.38.65-.27zM16 10c0 2.57-2.23 3.43-2.32 3.47-.06.02-.12.03-.18.03-.2 0-.39-.12-.47-.32-.1-.26.04-.55.29-.65.07-.02 1.68-.67 1.68-2.53s-1.61-2.51-1.68-2.53c-.25-.1-.38-.39-.29-.65.1-.25.39-.39.65-.29.09.04 2.32.9 2.32 3.47z'; break; + case 'cover-image': + path = 'M2.2 1h15.5c.7 0 1.3.6 1.3 1.2v11.5c0 .7-.6 1.2-1.2 1.2H2.2c-.6.1-1.2-.5-1.2-1.1V2.2C1 1.6 1.6 1 2.2 1zM17 13V3H3v10h14zm-4-4s0-5 3-5v7c0 .6-.4 1-1 1H5c-.6 0-1-.4-1-1V7c2 0 3 4 3 4s1-4 3-4 3 2 3 2zM4 17h12v2H4z'; + break; case 'dashboard': path = 'M3.76 16h12.48c1.1-1.37 1.76-3.11 1.76-5 0-4.42-3.58-8-8-8s-8 3.58-8 8c0 1.89.66 3.63 1.76 5zM10 4c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zM6 6c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zm8 0c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zm-5.37 5.55L12 7v6c0 1.1-.9 2-2 2s-2-.9-2-2c0-.57.24-1.08.63-1.45zM4 10c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zm12 0c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zm-5 3c0-.55-.45-1-1-1s-1 .45-1 1 .45 1 1 1 1-.45 1-1z'; break; @@ -337,6 +351,9 @@ export default class Dashicon extends wp.element.Component { case 'editor-ltr': path = 'M5.52 2h7.43c.55 0 1 .45 1 1s-.45 1-1 1h-1v13c0 .55-.45 1-1 1s-1-.45-1-1V5c0-.55-.45-1-1-1s-1 .45-1 1v12c0 .55-.45 1-1 1s-1-.45-1-1v-5.96h-.43C3.02 11.04 1 9.02 1 6.52S3.02 2 5.52 2zM14 14l5-4-5-4v8z'; break; + case 'editor-ol-rtl': + path = 'M15.025 8.75a1.048 1.048 0 0 1 .45-.1.507.507 0 0 1 .35.11.455.455 0 0 1 .13.36.803.803 0 0 1-.06.3 1.448 1.448 0 0 1-.19.33c-.09.11-.29.32-.58.62l-.99 1v.58h2.76v-.7h-1.72v-.04l.51-.48a7.276 7.276 0 0 0 .7-.71 1.75 1.75 0 0 0 .3-.49 1.254 1.254 0 0 0 .1-.51.968.968 0 0 0-.16-.56 1.007 1.007 0 0 0-.44-.37 1.512 1.512 0 0 0-.65-.14 1.98 1.98 0 0 0-.51.06 1.9 1.9 0 0 0-.42.15 3.67 3.67 0 0 0-.48.35l.45.54a2.505 2.505 0 0 1 .45-.3zM16.695 15.29a1.29 1.29 0 0 0-.74-.3v-.02a1.203 1.203 0 0 0 .65-.37.973.973 0 0 0 .23-.65.81.81 0 0 0-.37-.71 1.72 1.72 0 0 0-1-.26 2.185 2.185 0 0 0-1.33.4l.4.6a1.79 1.79 0 0 1 .46-.23 1.18 1.18 0 0 1 .41-.07c.38 0 .58.15.58.46a.447.447 0 0 1-.22.43 1.543 1.543 0 0 1-.7.12h-.31v.66h.31a1.764 1.764 0 0 1 .75.12.433.433 0 0 1 .23.41.55.55 0 0 1-.2.47 1.084 1.084 0 0 1-.63.15 2.24 2.24 0 0 1-.57-.08 2.671 2.671 0 0 1-.52-.2v.74a2.923 2.923 0 0 0 1.18.22 1.948 1.948 0 0 0 1.22-.33 1.077 1.077 0 0 0 .43-.92.836.836 0 0 0-.26-.64zM15.005 4.17c.06-.05.16-.14.3-.28l-.02.42V7h.84V3h-.69l-1.29 1.03.4.51zM4.02 5h9v1h-9zM4.02 10h9v1h-9zM4.02 15h9v1h-9z'; + break; case 'editor-ol': path = 'M6 7V3h-.69L4.02 4.03l.4.51.46-.37c.06-.05.16-.14.3-.28l-.02.42V7H6zm2-2h9v1H8V5zm-1.23 6.95v-.7H5.05v-.04l.51-.48c.33-.31.57-.54.7-.71.14-.17.24-.33.3-.49.07-.16.1-.33.1-.51 0-.21-.05-.4-.16-.56-.1-.16-.25-.28-.44-.37s-.41-.14-.65-.14c-.19 0-.36.02-.51.06-.15.03-.29.09-.42.15-.12.07-.29.19-.48.35l.45.54c.16-.13.31-.23.45-.3.15-.07.3-.1.45-.1.14 0 .26.03.35.11s.13.2.13.36c0 .1-.02.2-.06.3s-.1.21-.19.33c-.09.11-.29.32-.58.62l-.99 1v.58h2.76zM8 10h9v1H8v-1zm-1.29 3.95c0-.3-.12-.54-.37-.71-.24-.17-.58-.26-1-.26-.52 0-.96.13-1.33.4l.4.6c.17-.11.32-.19.46-.23.14-.05.27-.07.41-.07.38 0 .58.15.58.46 0 .2-.07.35-.22.43s-.38.12-.7.12h-.31v.66h.31c.34 0 .59.04.75.12.15.08.23.22.23.41 0 .22-.07.37-.2.47-.14.1-.35.15-.63.15-.19 0-.38-.03-.57-.08s-.36-.12-.52-.2v.74c.34.15.74.22 1.18.22.53 0 .94-.11 1.22-.33.29-.22.43-.52.43-.92 0-.27-.09-.48-.26-.64s-.42-.26-.74-.3v-.02c.27-.06.49-.19.65-.37.15-.18.23-.39.23-.65zM8 15h9v1H8v-1z'; break; @@ -529,6 +546,9 @@ export default class Dashicon extends wp.element.Component { case 'insert': path = 'M10 1c-5 0-9 4-9 9s4 9 9 9 9-4 9-9-4-9-9-9zm0 16c-3.9 0-7-3.1-7-7s3.1-7 7-7 7 3.1 7 7-3.1 7-7 7zm1-11H9v3H6v2h3v3h2v-3h3V9h-3V6z'; break; + case 'instagram': + path = 'M12.67 10A2.67 2.67 0 1 0 10 12.67 2.68 2.68 0 0 0 12.67 10zm1.43 0A4.1 4.1 0 1 1 10 5.9a4.09 4.09 0 0 1 4.1 4.1zm1.13-4.27a1 1 0 1 1-1-1 1 1 0 0 1 1 1zM10 3.44c-1.17 0-3.67-.1-4.72.32a2.67 2.67 0 0 0-1.52 1.52c-.42 1-.32 3.55-.32 4.72s-.1 3.67.32 4.72a2.74 2.74 0 0 0 1.52 1.52c1 .42 3.55.32 4.72.32s3.67.1 4.72-.32a2.83 2.83 0 0 0 1.52-1.52c.42-1.05.32-3.55.32-4.72s.1-3.67-.32-4.72a2.74 2.74 0 0 0-1.52-1.52c-1.05-.42-3.55-.32-4.72-.32zM18 10c0 1.1 0 2.2-.05 3.3a4.84 4.84 0 0 1-1.29 3.36A4.8 4.8 0 0 1 13.3 18H6.7a4.84 4.84 0 0 1-3.36-1.29 4.84 4.84 0 0 1-1.29-3.41C2 12.2 2 11.1 2 10V6.7a4.84 4.84 0 0 1 1.34-3.36A4.8 4.8 0 0 1 6.7 2.05C7.8 2 8.9 2 10 2h3.3a4.84 4.84 0 0 1 3.36 1.29A4.8 4.8 0 0 1 18 6.7V10z'; + break; case 'laptop': path = 'M3 3h14c.6 0 1 .4 1 1v10c0 .6-.4 1-1 1H3c-.6 0-1-.4-1-1V4c0-.6.4-1 1-1zm13 2H4v8h12V5zm-3 1H5v4zm6 11v-1H1v1c0 .6.5 1 1.1 1h15.8c.6 0 1.1-.4 1.1-1z'; break; diff --git a/components/date-time/style.scss b/components/date-time/style.scss index a38470f9a5690d..4f90e25ed23e04 100644 --- a/components/date-time/style.scss +++ b/components/date-time/style.scss @@ -1,4 +1,44 @@ -@import '~react-datepicker/dist/react-datepicker'; +$datepicker__background-color: $light-gray-300; +$datepicker__border-color: $light-gray-500; +$datepicker__highlighted-color: $blue-wordpress; +$datepicker__muted-color: #ccc; +$datepicker__selected-color: $blue-wordpress; +$datepicker__text-color: $dark-gray-500; +$datepicker__header-color: $black; +$datepicker__navigation-disabled-color: lighten($datepicker__muted-color, 10%); + +$datepicker__border-radius: 0; +$datepicker__day-margin: 0.166rem; +$datepicker__font-size: 13px; +$datepicker__font-family: inherit; +$datepicker__item-size: 28px; +$datepicker__margin: 0.4rem; +$datepicker__navigation-size: 6px; +$datepicker__triangle-size: 6px; + +@import '~react-datepicker/src/stylesheets/datepicker'; + +.react-datepicker__month-container { + float: none; +} + +.react-datepicker-time__header, +.react-datepicker__current-month { + font-size: $datepicker__font-size; +} + +.react-datepicker__navigation { + top: 12px; + + &--previous, + &--previous:hover { + border-right-color: $black; + } + &--next, + &--next:hover { + border-left-color: $black; + } +} .components-time-picker { display: flex; diff --git a/components/drop-zone/index.js b/components/drop-zone/index.js index 1d439572eb5dd5..1b2f00516d5892 100644 --- a/components/drop-zone/index.js +++ b/components/drop-zone/index.js @@ -21,14 +21,12 @@ class DropZone extends Component { super( ...arguments ); this.setZoneNode = this.setZoneNode.bind( this ); - this.onDrop = this.onDrop.bind( this ); - this.onFilesDrop = this.onFilesDrop.bind( this ); - this.onHTMLDrop = this.onHTMLDrop.bind( this ); this.state = { isDraggingOverDocument: false, isDraggingOverElement: false, position: null, + type: null, }; } @@ -36,9 +34,9 @@ class DropZone extends Component { this.context.dropzones.add( { element: this.zone, updateState: this.setState.bind( this ), - onDrop: this.onDrop, - onFilesDrop: this.onFilesDrop, - onHTMLDrop: this.onHTMLDrop, + onDrop: this.props.onDrop, + onFilesDrop: this.props.onFilesDrop, + onHTMLDrop: this.props.onHTMLDrop, } ); } @@ -46,31 +44,13 @@ class DropZone extends Component { this.context.dropzones.remove( this.zone ); } - onDrop() { - if ( this.props.onDrop ) { - this.props.onDrop( ...arguments ); - } - } - - onFilesDrop() { - if ( this.props.onFilesDrop ) { - this.props.onFilesDrop( ...arguments ); - } - } - - onHTMLDrop() { - if ( this.props.onHTMLDrop ) { - this.props.onHTMLDrop( ...arguments ); - } - } - setZoneNode( node ) { this.zone = node; } render() { const { className, label } = this.props; - const { isDraggingOverDocument, isDraggingOverElement, position } = this.state; + const { isDraggingOverDocument, isDraggingOverElement, position, type } = this.state; const classes = classnames( 'components-drop-zone', className, { 'is-active': isDraggingOverDocument || isDraggingOverElement, 'is-dragging-over-document': isDraggingOverDocument, @@ -79,6 +59,7 @@ class DropZone extends Component { 'is-close-to-bottom': position && position.y === 'bottom', 'is-close-to-left': position && position.x === 'left', 'is-close-to-right': position && position.x === 'right', + [ `is-dragging-${ type }` ]: !! type, } ); return ( diff --git a/components/drop-zone/provider.js b/components/drop-zone/provider.js index 0dc85da2b6b504..439139e83aff80 100644 --- a/components/drop-zone/provider.js +++ b/components/drop-zone/provider.js @@ -1,12 +1,13 @@ /** * External dependencies */ -import { isEqual, without, some, filter, findIndex, noop, throttle } from 'lodash'; +import { isEqual, find, some, filter, noop, throttle } from 'lodash'; +import isShallowEqual from 'shallowequal'; /** * WordPress dependencies */ -import { Component } from '@wordpress/element'; +import { Component, findDOMNode } from '@wordpress/element'; class DropZoneProvider extends Component { constructor() { @@ -27,7 +28,7 @@ class DropZoneProvider extends Component { } dragOverListener( event ) { - this.toggleDraggingOverDocument( event ); + this.toggleDraggingOverDocument( event, this.getDragEventType( event ) ); event.preventDefault(); } @@ -48,6 +49,10 @@ class DropZoneProvider extends Component { window.addEventListener( 'dragover', this.dragOverListener ); window.addEventListener( 'drop', this.onDrop ); window.addEventListener( 'mouseup', this.resetDragState ); + + // Disable reason: Can't use a ref since this component just renders its children + // eslint-disable-next-line react/no-find-dom-node + this.container = findDOMNode( this ); } componentWillUnmount() { @@ -76,11 +81,34 @@ class DropZoneProvider extends Component { isDraggingOverDocument: false, isDraggingOverElement: false, position: null, + type: null, } ); } ); } - toggleDraggingOverDocument( event ) { + getDragEventType( event ) { + if ( event.dataTransfer ) { + if ( event.dataTransfer.types.indexOf( 'Files' ) !== -1 ) { + return 'file'; + } + + if ( event.dataTransfer.types.indexOf( 'text/html' ) !== -1 ) { + return 'html'; + } + } + + return 'default'; + } + + doesDropzoneSupportType( dropzone, type ) { + return ( + ( type === 'file' && dropzone.onFilesDrop ) || + ( type === 'html' && dropzone.onHTMLDrop ) || + ( type === 'default' && dropzone.onDrop ) + ); + } + + toggleDraggingOverDocument( event, dragEventType ) { // In some contexts, it may be necessary to capture and redirect the // drag event (e.g. atop an `iframe`). To accommodate this, you can // create an instance of CustomEvent with the original event specified @@ -90,14 +118,23 @@ class DropZoneProvider extends Component { const detail = window.CustomEvent && event instanceof window.CustomEvent ? event.detail : event; // Index of hovered dropzone. - const hoveredDropZone = findIndex( this.dropzones, ( { element } ) => - this.isWithinZoneBounds( element, detail.clientX, detail.clientY ) + + const hoveredDropZones = filter( this.dropzones, ( dropzone ) => + this.doesDropzoneSupportType( dropzone, dragEventType ) && + this.isWithinZoneBounds( dropzone.element, detail.clientX, detail.clientY ) ); + // Find the leaf dropzone not containing another dropzone + const hoveredDropZone = find( hoveredDropZones, zone => ( + ! some( hoveredDropZones, subZone => subZone !== zone && zone.element.parentElement.contains( subZone.element ) ) + ) ); + + const hoveredDropZoneIndex = this.dropzones.indexOf( hoveredDropZone ); + let position = null; - if ( hoveredDropZone !== -1 ) { - const rect = this.dropzones[ hoveredDropZone ].element.getBoundingClientRect(); + if ( hoveredDropZone ) { + const rect = hoveredDropZone.element.getBoundingClientRect(); position = { x: detail.clientX - rect.left < rect.right - detail.clientX ? 'left' : 'right', @@ -110,38 +147,41 @@ class DropZoneProvider extends Component { if ( ! this.state.isDraggingOverDocument ) { dropzonesToUpdate = this.dropzones; - } else if ( hoveredDropZone !== this.state.hoveredDropZone ) { + } else if ( hoveredDropZoneIndex !== this.state.hoveredDropZone ) { if ( this.state.hoveredDropZone !== -1 ) { dropzonesToUpdate.push( this.dropzones[ this.state.hoveredDropZone ] ); } - if ( hoveredDropZone !== -1 ) { - dropzonesToUpdate.push( this.dropzones[ hoveredDropZone ] ); + if ( hoveredDropZone ) { + dropzonesToUpdate.push( hoveredDropZone ); } } else if ( - hoveredDropZone !== -1 && - hoveredDropZone === this.state.hoveredDropZone && + hoveredDropZone && + hoveredDropZoneIndex === this.state.hoveredDropZone && ! isEqual( position, this.state.position ) ) { - dropzonesToUpdate.push( this.dropzones[ hoveredDropZone ] ); + dropzonesToUpdate.push( hoveredDropZone ); } // Notifying the dropzones dropzonesToUpdate.map( ( dropzone ) => { const index = this.dropzones.indexOf( dropzone ); + const isDraggingOverDropZone = index === hoveredDropZoneIndex; dropzone.updateState( { - isDraggingOverElement: index === hoveredDropZone, - position: index === hoveredDropZone ? position : null, - isDraggingOverDocument: true, + isDraggingOverElement: isDraggingOverDropZone, + position: isDraggingOverDropZone ? position : null, + isDraggingOverDocument: this.doesDropzoneSupportType( dropzone, dragEventType ), + type: isDraggingOverDropZone ? dragEventType : null, } ); } ); - this.setState( { + const newState = { isDraggingOverDocument: true, - hoveredDropZone, + hoveredDropZone: hoveredDropZoneIndex, position, - } ); - - event.preventDefault(); + }; + if ( ! isShallowEqual( newState, this.state ) ) { + this.setState( newState ); + } } isWithinZoneBounds( dropzone, x, y ) { @@ -158,8 +198,7 @@ class DropZoneProvider extends Component { ); }; - const childZones = without( dropzone.parentElement.querySelectorAll( '.components-drop-zone' ), dropzone ); - return ! some( childZones, isWithinElement ) && isWithinElement( dropzone ); + return isWithinElement( dropzone ); } onDrop( event ) { @@ -168,22 +207,21 @@ class DropZoneProvider extends Component { event.dataTransfer && event.dataTransfer.files.length; // eslint-disable-line no-unused-expressions const { position, hoveredDropZone } = this.state; - const dropzone = hoveredDropZone !== -1 ? this.dropzones[ hoveredDropZone ] : null; - + const dragEventType = this.getDragEventType( event ); + const dropzone = this.dropzones[ hoveredDropZone ]; + const isValidDropzone = !! dropzone && this.container.contains( event.target ); this.resetDragState(); - if ( !! dropzone && !! dropzone.onDrop ) { - dropzone.onDrop( event, position ); - } - - if ( event.dataTransfer && !! dropzone ) { - const files = event.dataTransfer.files; - const HTML = event.dataTransfer.getData( 'text/html' ); - - if ( files.length && dropzone.onFilesDrop ) { - dropzone.onFilesDrop( [ ...event.dataTransfer.files ], position ); - } else if ( HTML && dropzone.onHTMLDrop ) { - dropzone.onHTMLDrop( HTML, position ); + if ( isValidDropzone ) { + switch ( dragEventType ) { + case 'file': + dropzone.onFilesDrop( [ ...event.dataTransfer.files ], position ); + break; + case 'html': + dropzone.onHTMLDrop( event.dataTransfer.getData( 'text/html' ), position ); + break; + case 'default': + dropzone.onDrop( event, position ); } } diff --git a/components/dropdown-menu/style.scss b/components/dropdown-menu/style.scss index 941925d1286acc..6b9a6874f109d8 100644 --- a/components/dropdown-menu/style.scss +++ b/components/dropdown-menu/style.scss @@ -4,7 +4,7 @@ .components-dropdown-menu__toggle { width: auto; - margin: 0px; + margin: 0; padding: 4px; border: 1px solid transparent; border-radius: 0; @@ -47,7 +47,7 @@ .components-dropdown-menu__menu { // note that left is set by react in a style attribute width: 100%; - padding: 3px 3px 0 3px; + padding: 3px 3px 0; font-family: $default-font; font-size: $default-font-size; line-height: $default-line-height; diff --git a/components/dropdown/README.md b/components/dropdown/README.md index aaa1483bd0348d..1d231f901a3c8b 100644 --- a/components/dropdown/README.md +++ b/components/dropdown/README.md @@ -85,3 +85,11 @@ Opt-in prop to show popovers fullscreen on mobile, pass `false` in this prop to - Type: `Boolean` - Required: No - Default: `false` + + ## headerTitle + + Set this to customize the text that is shown in the dropdown's header when + it is fullscreen on mobile. + + - Type: `String` + - Required: No diff --git a/components/dropdown/index.js b/components/dropdown/index.js index 6f46edf58ba742..dd92406246523f 100644 --- a/components/dropdown/index.js +++ b/components/dropdown/index.js @@ -65,6 +65,7 @@ class Dropdown extends Component { className, contentClassName, expandOnMobile, + headerTitle, } = this.props; const args = { isOpen, onToggle: this.toggle, onClose: this.close }; @@ -84,6 +85,7 @@ class Dropdown extends Component { onClose={ this.close } onClickOutside={ this.clickOutside } expandOnMobile={ expandOnMobile } + headerTitle={ headerTitle } > { renderContent( args ) } </Popover> diff --git a/components/form-file-upload/style.scss b/components/form-file-upload/style.scss index 68e2ea80c02b84..9ea118168002ab 100644 --- a/components/form-file-upload/style.scss +++ b/components/form-file-upload/style.scss @@ -1,14 +1,9 @@ .wp-core-ui .components-form-file-upload { .button.button-large { - font-size: 0; - padding: 6px 0 3px; + padding: 6px 12px 2px 3px; @include break-medium() { - padding: 0 12px 2px; - } - - @include break-large() { - font-size: inherit; + padding: 0 12px 2px 3px; } } } diff --git a/components/form-toggle/index.js b/components/form-toggle/index.js index 3baba9c6fc03d9..8ccb4f10b64be2 100644 --- a/components/form-toggle/index.js +++ b/components/form-toggle/index.js @@ -4,21 +4,16 @@ import classnames from 'classnames'; import { noop } from 'lodash'; -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; - /** * Internal dependencies */ import './style.scss'; -function FormToggle( { className, checked, id, onChange = noop, showHint = true, ...props } ) { +function FormToggle( { className, checked, id, onChange = noop, ...props } ) { const wrapperClasses = classnames( 'components-form-toggle', className, - { 'is-checked': checked } + { 'is-checked': checked }, ); return ( @@ -33,10 +28,8 @@ function FormToggle( { className, checked, id, onChange = noop, showHint = true, /> <span className="components-form-toggle__track"></span> <span className="components-form-toggle__thumb"></span> - { showHint && - <span className="components-form-toggle__hint" aria-hidden> - { checked ? __( 'On' ) : __( 'Off' ) } - </span> + { checked ? + <svg className="components-form-toggle__on" width="2" height="6" aria-hidden="true" role="img" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2 6"><path d="M0 0h2v6H0z" /></svg> : <svg className="components-form-toggle__off" width="6" height="6" aria-hidden="true" role="img" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 6 6"><path d="M3 1.5c.8 0 1.5.7 1.5 1.5S3.8 4.5 3 4.5 1.5 3.8 1.5 3 2.2 1.5 3 1.5M3 0C1.3 0 0 1.3 0 3s1.3 3 3 3 3-1.3 3-3-1.3-3-3-3z" /></svg> } </span> ); diff --git a/components/form-toggle/style.scss b/components/form-toggle/style.scss index c0284a6d4a8326..2e713b6634e75f 100644 --- a/components/form-toggle/style.scss +++ b/components/form-toggle/style.scss @@ -1,32 +1,44 @@ -$toggle-width: 32px; +$toggle-width: 36px; $toggle-height: 18px; $toggle-border-width: 2px; .components-form-toggle { position: relative; + // On/Off icon indicators + .components-form-toggle__on, + .components-form-toggle__off { + position: absolute; + top: $toggle-border-width * 3; + } + + .components-form-toggle__off { + color: $dark-gray-300; + fill: currentColor; + right: $toggle-border-width * 3; + } + + .components-form-toggle__on { + left: $toggle-border-width * 3 + 2px; // indent 2px extra because icon is thinner + filter: invert( 100% ) contrast( 500% ); // this makes the icon white, and it makes it dark blue in Windows High Contrast Mode + outline: 1px solid transparent; // this makes the dark blue all black in Windows High Contrast Mode + outline-offset: -1px; + } + + // unchecked state .components-form-toggle__track { content: ''; display: inline-block; vertical-align: top; box-sizing: border-box; background-color: $white; - border: $toggle-border-width solid $dark-gray-500; + border: $toggle-border-width solid $dark-gray-300; width: $toggle-width; height: $toggle-height; border-radius: $toggle-height / 2; transition: 0.2s background ease; } - &:hover .components-form-toggle__track { - background-color: $light-gray-500; - } - - &.is-checked .components-form-toggle__track { - background-color: $blue-medium-400; - border: $toggle-border-width solid $blue-medium-400; - } - .components-form-toggle__thumb { display: block; position: absolute; @@ -35,25 +47,41 @@ $toggle-border-width: 2px; width: $toggle-height - ( $toggle-border-width * 4 ); height: $toggle-height - ( $toggle-border-width * 4 ); border-radius: 50%; - border: 2px solid $dark-gray-500; - background: $white; transition: 0.1s transform ease; + background-color: $dark-gray-300; + border: 5px solid $dark-gray-300; // has explicit border to act as a fill in Windows High Contrast Mode } - &__input:focus { - & + .components-form-toggle__track { - box-shadow: 0 0 0 1px $white, 0 0 0 2px $blue-medium-400, 0 0 2px 2px $blue-medium-400; + &:hover { + .components-form-toggle__track { + border: $toggle-border-width solid $dark-gray-500; + } + + .components-form-toggle__thumb { + background-color: $dark-gray-500; + border: 5px solid $dark-gray-300; // has explicit border to act as a fill in Windows High Contrast Mode + } - & + .components-form-toggle__thumb { - border-width: 5px; - } + .components-form-toggle__off { + color: $dark-gray-500; } } + // checked state + &.is-checked .components-form-toggle__track { + background-color: $blue-medium-400; + border: $toggle-border-width solid $blue-medium-400; + border: #{ $toggle-height / 2 } solid transparent; // expand the border to fake a solid in Windows High Contrast Mode + } + + &__input:focus + .components-form-toggle__track { + @include switch-style__focus-active(); + } + &.is-checked { .components-form-toggle__thumb { - border: 2px solid $white; - background-color: $blue-medium-500; + background-color: $white; + border-width: 0; // zero out the border color to make the thumb invisible in Windows High Contrast Mode transform: translateX( $toggle-width - ( $toggle-border-width * 4 ) - ( $toggle-height - ( $toggle-border-width * 4 ) ) ); } @@ -75,10 +103,3 @@ $toggle-border-width: 2px; padding: 0; z-index: z-index( '.components-form-toggle__input' ); } - -.components-form-toggle__hint { - display: inline-block; - min-width: 24px; // This prevents a position jog when the control is right aligned, and the width of the label changes - margin-left: 10px; - font-weight: 500; -} diff --git a/components/form-toggle/test/index.js b/components/form-toggle/test/index.js index 14da785a33eaf2..4e2471a16c5519 100644 --- a/components/form-toggle/test/index.js +++ b/components/form-toggle/test/index.js @@ -16,16 +16,12 @@ describe( 'FormToggle', () => { expect( formToggle.hasClass( 'components-form-toggle' ) ).toBe( true ); expect( formToggle.hasClass( 'is-checked' ) ).toBe( false ); expect( formToggle.type() ).toBe( 'span' ); - expect( formToggle.find( '.components-form-toggle__input' ).prop( 'checked' ) ).toBeUndefined(); - expect( formToggle.find( '.components-form-toggle__hint' ).text() ).toBe( 'Off' ); - expect( formToggle.find( '.components-form-toggle__hint' ).prop( 'aria-hidden' ) ).toBe( true ); } ); it( 'should render a checked checkbox and change the accessibility text to On when providing checked prop', () => { const formToggle = shallow( <FormToggle checked /> ); expect( formToggle.hasClass( 'is-checked' ) ).toBe( true ); expect( formToggle.find( '.components-form-toggle__input' ).prop( 'checked' ) ).toBe( true ); - expect( formToggle.find( '.components-form-toggle__hint' ).text() ).toBe( 'On' ); } ); it( 'should render with an additional className', () => { @@ -50,12 +46,5 @@ describe( 'FormToggle', () => { const checkBox = formToggle.prop( 'children' ).find( child => 'input' === child.type && 'checkbox' === child.props.type ); expect( checkBox.props.onChange ).toBe( testFunction ); } ); - - it( 'should not render the hint when showHint is set to false', () => { - const formToggle = shallow( <FormToggle showHint={ false } /> ); - - // When showHint is not provided this element is not rendered. - expect( formToggle.find( '.components-form-toggle__hint' ).exists() ).toBe( false ); - } ); } ); } ); diff --git a/components/form-token-field/index.js b/components/form-token-field/index.js index 6e77a55f6540b8..8561fad5727456 100644 --- a/components/form-token-field/index.js +++ b/components/form-token-field/index.js @@ -504,7 +504,7 @@ class FormTokenField extends Component { render() { const { disabled, - placeholder = _( 'Add item.' ), + placeholder = __( 'Add item.' ), instanceId, } = this.props; const classes = classnames( 'components-form-token-field', { diff --git a/components/form-token-field/style.scss b/components/form-token-field/style.scss index a89ab6760f1bd5..5d21fa0b4c9e32 100644 --- a/components/form-token-field/style.scss +++ b/components/form-token-field/style.scss @@ -1,18 +1,13 @@ .components-form-token-field { - box-sizing: border-box; width: 100%; margin: 0; padding: 0; background-color: $white; - border-radius: 4px; - border: 1px solid $light-gray-500; + border: 1px solid $light-gray-700; color: $dark-gray-700; cursor: text; - transition: all .15s ease-in-out; - &:hover { - border-color: $light-gray-700; - } + @include input-style__neutral(); &.is-disabled { background: $light-gray-500; @@ -20,8 +15,7 @@ } &.is-active { - border-color: $blue-wordpress; - box-shadow: 0 0 0 2px $blue-medium-200; + @include input-style__focus(); } } @@ -30,26 +24,26 @@ flex-wrap: wrap; align-items: flex-start; padding: 4px; -} - -// Token input -input[type="text"].components-form-token-field__input { - display: inline-block; - width: auto; - max-width: 100%; - margin: 2px 0 2px 8px; - padding: 0; - line-height: 24px; - background: inherit; - border: 0; - outline: none; - font-family: inherit; - font-size: $default-font-size; - color: $dark-gray-800; - box-shadow: none; - &:focus { + // Token input + input[type="text"].components-form-token-field__input { + display: inline-block; + width: auto; + max-width: 100%; + margin: 2px 0 2px 8px; + padding: 0; + line-height: 24px; + background: inherit; + border: 0; + font-size: $default-font-size; + color: $dark-gray-800; box-shadow: none; + + &:focus, + .components-form-token-field.is-active & { + outline: none; + box-shadow: none; + } } } diff --git a/components/higher-order/with-api-data/index.js b/components/higher-order/with-api-data/index.js index 19c4e4910c19f4..29a24791ad854a 100644 --- a/components/higher-order/with-api-data/index.js +++ b/components/higher-order/with-api-data/index.js @@ -6,7 +6,7 @@ import { mapValues, reduce, forEach, noop } from 'lodash'; /** * WordPress dependencies */ -import { Component, getWrapperDisplayName } from '@wordpress/element'; +import { Component, createHigherOrderComponent } from '@wordpress/element'; /** * Internal dependencies @@ -14,7 +14,7 @@ import { Component, getWrapperDisplayName } from '@wordpress/element'; import request, { getCachedResponse } from './request'; import { getRoute } from './routes'; -export default ( mapPropsToData ) => ( WrappedComponent ) => { +export default ( mapPropsToData ) => createHigherOrderComponent( ( WrappedComponent ) => { class APIDataComponent extends Component { constructor( props, context ) { super( ...arguments ); @@ -221,8 +221,6 @@ export default ( mapPropsToData ) => ( WrappedComponent ) => { } } - APIDataComponent.displayName = getWrapperDisplayName( WrappedComponent, 'apiData' ); - APIDataComponent.contextTypes = { getAPISchema: noop, getAPIPostTypeRestBaseMapping: noop, @@ -230,4 +228,4 @@ export default ( mapPropsToData ) => ( WrappedComponent ) => { }; return APIDataComponent; -}; +}, 'withAPIData' ); diff --git a/components/higher-order/with-filters/README.md b/components/higher-order/with-filters/README.md index bbe7bea3a2af3e..5fc12a0ca0246b 100644 --- a/components/higher-order/with-filters/README.md +++ b/components/higher-order/with-filters/README.md @@ -24,4 +24,4 @@ function MyCustomElement() { export default withFilters( 'MyCustomElement' )( MyCustomElement ); ``` -`withFilters` expects a string argument which provides a hook name. It returns a function which can then be used in composing your component. The hook name allows plugin developers to customize or completely override the component passed to this higher-order component using `wp.utils.addFilter` method. +`withFilters` expects a string argument which provides a hook name. It returns a function which can then be used in composing your component. The hook name allows plugin developers to customize or completely override the component passed to this higher-order component using `wp.hooks.addFilter` method. diff --git a/components/higher-order/with-filters/index.js b/components/higher-order/with-filters/index.js index 522e7bb358a20c..278bb325c76abf 100644 --- a/components/higher-order/with-filters/index.js +++ b/components/higher-order/with-filters/index.js @@ -6,7 +6,7 @@ import { debounce, uniqueId } from 'lodash'; /** * WordPress dependencies */ -import { Component, getWrapperDisplayName } from '@wordpress/element'; +import { Component, createHigherOrderComponent } from '@wordpress/element'; import { addAction, applyFilters, removeAction } from '@wordpress/hooks'; const ANIMATION_FRAME_PERIOD = 16; @@ -22,8 +22,8 @@ const ANIMATION_FRAME_PERIOD = 16; * @return {Function} Higher-order component factory. */ export default function withFilters( hookName ) { - return ( OriginalComponent ) => { - class FilteredComponent extends Component { + return createHigherOrderComponent( ( OriginalComponent ) => { + return class FilteredComponent extends Component { /** @inheritdoc */ constructor( props ) { super( props ); @@ -62,9 +62,6 @@ export default function withFilters( hookName ) { render() { return <this.Component { ...this.props } />; } - } - FilteredComponent.displayName = getWrapperDisplayName( OriginalComponent, 'filters' ); - - return FilteredComponent; - }; + }; + }, 'withFilters' ); } diff --git a/components/higher-order/with-filters/test/index.js b/components/higher-order/with-filters/test/index.js index 2c01d0ecf9883e..22909643a62a6b 100644 --- a/components/higher-order/with-filters/test/index.js +++ b/components/higher-order/with-filters/test/index.js @@ -65,6 +65,31 @@ describe( 'withFilters', () => { expect( wrapper.html() ).toBe( '<div><div>My component</div><div>Composed component</div></div>' ); } ); + it( 'should not re-render component when new filter added before component was mounted', () => { + const spy = jest.fn(); + const SpiedComponent = () => { + spy(); + return <div>Spied component</div>; + }; + addFilter( + hookName, + 'test/enhanced-component-spy-1', + FilteredComponent => () => ( + <blockquote> + <FilteredComponent /> + </blockquote> + ), + ); + const EnhancedComponent = withFilters( hookName )( SpiedComponent ); + + wrapper = mount( <EnhancedComponent /> ); + + jest.runAllTimers(); + + expect( spy ).toHaveBeenCalledTimes( 1 ); + expect( wrapper.html() ).toBe( '<blockquote><div>Spied component</div></blockquote>' ); + } ); + it( 'should re-render component once when new filter added after component was mounted', () => { const spy = jest.fn(); const SpiedComponent = () => { diff --git a/components/higher-order/with-focus-outside/index.js b/components/higher-order/with-focus-outside/index.js index 79dd61eef19157..f5686079bb595d 100644 --- a/components/higher-order/with-focus-outside/index.js +++ b/components/higher-order/with-focus-outside/index.js @@ -1,8 +1,47 @@ +/** + * External dependencies + */ +import { includes } from 'lodash'; + /** * WordPress dependencies */ import { Component } from '@wordpress/element'; +/** + * Input types which are classified as button types, for use in considering + * whether element is a (focus-normalized) button. + * + * @type {string[]} + */ +const INPUT_BUTTON_TYPES = [ + 'button', + 'submit', +]; + +/** + * Returns true if the given element is a button element subject to focus + * normalization, or false otherwise. + * + * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#Clicking_and_focus + * + * @param {Element} element Element to test. + * + * @return {boolean} Whether element is a button. + */ +function isFocusNormalizedButton( element ) { + switch ( element.nodeName ) { + case 'A': + case 'BUTTON': + return true; + + case 'INPUT': + return includes( INPUT_BUTTON_TYPES, element.type ); + } + + return false; +} + function withFocusOutside( WrappedComponent ) { return class extends Component { constructor() { @@ -11,6 +50,7 @@ function withFocusOutside( WrappedComponent ) { this.bindNode = this.bindNode.bind( this ); this.cancelBlurCheck = this.cancelBlurCheck.bind( this ); this.queueBlurCheck = this.queueBlurCheck.bind( this ); + this.normalizeButtonFocus = this.normalizeButtonFocus.bind( this ); } componentWillUnmount() { @@ -31,6 +71,11 @@ function withFocusOutside( WrappedComponent ) { // due to recycling behavior, except when explicitly persisted. event.persist(); + // Skip blur check if clicking button. See `normalizeButtonFocus`. + if ( this.preventBlurCheck ) { + return; + } + this.blurCheckTimeout = setTimeout( () => { if ( 'function' === typeof this.node.handleFocusOutside ) { this.node.handleFocusOutside( event ); @@ -42,10 +87,41 @@ function withFocusOutside( WrappedComponent ) { clearTimeout( this.blurCheckTimeout ); } + /** + * Handles a mousedown or mouseup event to respectively assign and + * unassign a flag for preventing blur check on button elements. Some + * browsers, namely Firefox and Safari, do not emit a focus event on + * button elements when clicked, while others do. The logic here + * intends to normalize this as treating click on buttons as focus. + * + * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#Clicking_and_focus + * + * @param {MouseEvent} event Event for mousedown or mouseup. + */ + normalizeButtonFocus( event ) { + const { type, target } = event; + + const isInteractionEnd = includes( [ 'mouseup', 'touchend' ], type ); + + if ( isInteractionEnd ) { + this.preventBlurCheck = false; + } else if ( isFocusNormalizedButton( target ) ) { + this.preventBlurCheck = true; + } + } + render() { + // Disable reason: See `normalizeButtonFocus` for browser-specific + // focus event normalization. + + /* eslint-disable jsx-a11y/no-static-element-interactions */ return ( <div onFocus={ this.cancelBlurCheck } + onMouseDown={ this.normalizeButtonFocus } + onMouseUp={ this.normalizeButtonFocus } + onTouchStart={ this.normalizeButtonFocus } + onTouchEnd={ this.normalizeButtonFocus } onBlur={ this.queueBlurCheck } > <WrappedComponent @@ -53,6 +129,7 @@ function withFocusOutside( WrappedComponent ) { { ...this.props } /> </div> ); + /* eslint-enable jsx-a11y/no-static-element-interactions */ } }; } diff --git a/components/higher-order/with-focus-outside/test/index.js b/components/higher-order/with-focus-outside/test/index.js index 960ad4bf616f06..815e082b2390d0 100644 --- a/components/higher-order/with-focus-outside/test/index.js +++ b/components/higher-order/with-focus-outside/test/index.js @@ -26,7 +26,7 @@ describe( 'withFocusOutside', () => { return ( <div> <input /> - <input /> + <input type="button" /> </div> ); } @@ -46,6 +46,23 @@ describe( 'withFocusOutside', () => { expect( callback ).not.toHaveBeenCalled(); } ); + it( 'should not call handler if focus transitions via click to button', () => { + const callback = jest.fn(); + const wrapper = mount( <EnhancedComponent onFocusOutside={ callback } /> ); + + wrapper.find( 'input' ).at( 0 ).simulate( 'focus' ); + wrapper.find( 'input' ).at( 1 ).simulate( 'mousedown' ); + wrapper.find( 'input' ).at( 0 ).simulate( 'blur' ); + // In most browsers, the input at index 1 would receive a focus event + // at this point, but this is not guaranteed, which is the intention of + // the normalization behavior tested here. + wrapper.find( 'input' ).at( 1 ).simulate( 'mouseup' ); + + jest.runAllTimers(); + + expect( callback ).not.toHaveBeenCalled(); + } ); + it( 'should call handler if focus doesn\'t shift to element within component', () => { const callback = jest.fn(); const wrapper = mount( <EnhancedComponent onFocusOutside={ callback } /> ); diff --git a/components/higher-order/with-state/index.js b/components/higher-order/with-state/index.js index b2c47819ed259f..f44ac94de7ecf8 100644 --- a/components/higher-order/with-state/index.js +++ b/components/higher-order/with-state/index.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { Component, getWrapperDisplayName } from '@wordpress/element'; +import { Component, createHigherOrderComponent } from '@wordpress/element'; /** * A Higher Order Component used to provide and manage internal component state @@ -11,9 +11,9 @@ import { Component, getWrapperDisplayName } from '@wordpress/element'; * * @return {Component} Wrapped component. */ -function withState( initialState = {} ) { - return ( OriginalComponent ) => { - class WrappedComponent extends Component { +export default function withState( initialState = {} ) { + return createHigherOrderComponent( ( OriginalComponent ) => { + return class WrappedComponent extends Component { constructor() { super( ...arguments ); @@ -31,12 +31,6 @@ function withState( initialState = {} ) { /> ); } - } - - WrappedComponent.displayName = getWrapperDisplayName( WrappedComponent, 'state' ); - - return WrappedComponent; - }; + }; + }, 'withState' ); } - -export default withState; diff --git a/components/icon-button/style.scss b/components/icon-button/style.scss index 66c13a02c53d4f..a86ed188bf7b61 100644 --- a/components/icon-button/style.scss +++ b/components/icon-button/style.scss @@ -7,16 +7,18 @@ background: none; color: $dark-gray-500; position: relative; - width: $icon-button-size; // show only icon on small breakpoints overflow: hidden; + text-indent: 4px; .dashicon { display: inline-block; flex: 0 0 auto; } - @include break-medium() { - width: auto; + // Ensure that even SVG icons that don't include the .dashicon class are colored + svg { + fill: currentColor; + outline: none; } &:not( :disabled ):hover { diff --git a/components/index.js b/components/index.js index d56bdf3e1d53fe..38731f65401287 100644 --- a/components/index.js +++ b/components/index.js @@ -3,23 +3,29 @@ export { default as APIProvider } from './higher-order/with-api-data/provider'; export { default as Autocomplete } from './autocomplete'; export { default as BaseControl } from './base-control'; export { default as Button } from './button'; +export { default as ButtonGroup } from './button-group'; export { default as CheckboxControl } from './checkbox-control'; export { default as ClipboardButton } from './clipboard-button'; +export { default as CodeEditor } from './code-editor'; export { default as Dashicon } from './dashicon'; export { DateTimePicker, DatePicker, TimePicker } from './date-time'; +export { default as Disabled } from './disabled'; +export { default as Draggable } from './draggable'; export { default as DropZone } from './drop-zone'; export { default as DropZoneProvider } from './drop-zone/provider'; export { default as Dropdown } from './dropdown'; export { default as DropdownMenu } from './dropdown-menu'; export { default as ExternalLink } from './external-link'; +export { default as FocusableIframe } from './focusable-iframe'; export { default as FormFileUpload } from './form-file-upload'; export { default as FormToggle } from './form-toggle'; export { default as FormTokenField } from './form-token-field'; export { default as IconButton } from './icon-button'; export { default as KeyboardShortcuts } from './keyboard-shortcuts'; -export { default as MenuItemsChoice } from './menu-items/menu-items-choice'; -export { default as MenuItemsGroup } from './menu-items/menu-items-group'; -export { default as MenuItemsToggle } from './menu-items/menu-items-toggle'; +export { default as MenuGroup } from './menu-group'; +export { default as MenuItem } from './menu-item'; +export { default as MenuItemsChoice } from './menu-items-choice'; +export { default as ScrollLock } from './scroll-lock'; export { NavigableMenu, TabbableContainer } from './navigable-container'; export { default as Notice } from './notice'; export { default as NoticeList } from './notice/list'; @@ -30,6 +36,7 @@ export { default as PanelHeader } from './panel/header'; export { default as PanelRow } from './panel/row'; export { default as Placeholder } from './placeholder'; export { default as Popover } from './popover'; +export { default as QueryControls } from './query-controls'; export { default as RadioControl } from './radio-control'; export { default as RangeControl } from './range-control'; export { default as ResponsiveWrapper } from './responsive-wrapper'; @@ -43,9 +50,10 @@ export { default as ToggleControl } from './toggle-control'; export { default as Toolbar } from './toolbar'; export { default as Tooltip } from './tooltip'; export { default as TreeSelect } from './tree-select'; -export { Slot, Fill, Provider as SlotFillProvider } from './slot-fill'; +export { createSlotFill, Slot, Fill, Provider as SlotFillProvider } from './slot-fill'; // Higher-Order Components +export { default as ifCondition } from './higher-order/if-condition'; export { default as navigateRegions } from './higher-order/navigate-regions'; export { default as withAPIData } from './higher-order/with-api-data'; export { default as withContext } from './higher-order/with-context'; @@ -53,6 +61,7 @@ export { default as withFallbackStyles } from './higher-order/with-fallback-styl export { default as withFilters } from './higher-order/with-filters'; export { default as withFocusOutside } from './higher-order/with-focus-outside'; export { default as withFocusReturn } from './higher-order/with-focus-return'; +export { default as withGlobalEvents } from './higher-order/with-global-events'; export { default as withInstanceId } from './higher-order/with-instance-id'; export { default as withSafeTimeout } from './higher-order/with-safe-timeout'; export { default as withSpokenMessages } from './higher-order/with-spoken-messages'; diff --git a/components/keyboard-shortcuts/README.md b/components/keyboard-shortcuts/README.md index f7a023c3a0340f..af414974e4cb9b 100644 --- a/components/keyboard-shortcuts/README.md +++ b/components/keyboard-shortcuts/README.md @@ -1,7 +1,9 @@ Keyboard Shortcuts ================== -`<KeyboardShortcuts />` is a component which renders no children of its own, but instead handles keyboard sequences during the lifetime of the rendering element. +`<KeyboardShortcuts />` is a component which handles keyboard sequences during the lifetime of the rendering element. + +When passed children, it will capture key events which occur on or within the children. If no children are passed, events are captured on the document. It uses the [Mousetrap](https://craig.is/killing/mice) library to implement keyboard sequence bindings. @@ -40,6 +42,13 @@ class SelectAllDetection extends Component { The component accepts the following props: +### children + +Elements to render, upon whom key events are to be monitored. + +- Type: `Element` | `Element[]` +- Required: No + ### shortcuts An object of shortcut bindings, where each key is a keyboard combination, the value of which is the callback to be invoked when the key combination is pressed. diff --git a/components/keyboard-shortcuts/index.js b/components/keyboard-shortcuts/index.js index 24b1b48dbe3018..c086d922f348c9 100644 --- a/components/keyboard-shortcuts/index.js +++ b/components/keyboard-shortcuts/index.js @@ -8,11 +8,19 @@ import { forEach } from 'lodash'; /** * WordPress dependencies */ -import { Component } from '@wordpress/element'; +import { Component, Children } from '@wordpress/element'; class KeyboardShortcuts extends Component { - componentWillMount() { - this.mousetrap = new Mousetrap; + constructor() { + super( ...arguments ); + + this.bindKeyTarget = this.bindKeyTarget.bind( this ); + } + + componentDidMount() { + const { keyTarget = document } = this; + + this.mousetrap = new Mousetrap( keyTarget ); forEach( this.props.shortcuts, ( callback, key ) => { const { bindGlobal, eventName } = this.props; const bindFn = bindGlobal ? 'bindGlobal' : 'bind'; @@ -24,8 +32,25 @@ class KeyboardShortcuts extends Component { this.mousetrap.reset(); } + /** + * When rendering with children, binds the wrapper node on which events + * will be bound. + * + * @param {Element} node Key event target. + */ + bindKeyTarget( node ) { + this.keyTarget = node; + } + render() { - return null; + // Render as non-visual if there are no children pressed. Keyboard + // events will be bound to the document instead. + const { children } = this.props; + if ( ! Children.count( children ) ) { + return null; + } + + return <div ref={ this.bindKeyTarget }>{ children }</div>; } } diff --git a/components/keyboard-shortcuts/test/index.js b/components/keyboard-shortcuts/test/index.js index f27cccfff33c4c..caaa459780b0aa 100644 --- a/components/keyboard-shortcuts/test/index.js +++ b/components/keyboard-shortcuts/test/index.js @@ -82,4 +82,34 @@ describe( 'KeyboardShortcuts', () => { expect( spy ).toHaveBeenCalled(); expect( spy.mock.calls[ 0 ][ 0 ].type ).toBe( 'keyup' ); } ); + + it( 'should capture key events on children', () => { + const spy = jest.fn(); + const attachNode = document.createElement( 'div' ); + document.body.appendChild( attachNode ); + + const wrapper = mount( + <div> + <KeyboardShortcuts + shortcuts={ { + d: spy, + } } + > + <textarea></textarea> + </KeyboardShortcuts> + <textarea></textarea> + </div>, + { attachTo: attachNode } + ); + + const textareas = wrapper.find( 'textarea' ); + + // Outside scope + keyPress( 68, textareas.at( 1 ).getDOMNode() ); + expect( spy ).not.toHaveBeenCalled(); + + // Inside scope + keyPress( 68, textareas.at( 0 ).getDOMNode() ); + expect( spy ).toHaveBeenCalled(); + } ); } ); diff --git a/components/menu-items/menu-items-choice.js b/components/menu-items/menu-items-choice.js deleted file mode 100644 index acc292e6d911be..00000000000000 --- a/components/menu-items/menu-items-choice.js +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Internal dependencies - */ -import './style.scss'; -import MenuItemsToggle from './menu-items-toggle'; - -export default function MenuItemsChoice( { - choices = [], - onSelect, - value, -} ) { - return choices.map( ( item ) => { - const isSelected = value === item.value; - return ( - <MenuItemsToggle - key={ item.value } - label={ item.label } - isSelected={ isSelected } - shortcut={ item.shortcut } - onClick={ () => { - if ( ! isSelected ) { - onSelect( item.value ); - } - } } - /> - ); - } ); -} diff --git a/components/menu-items/menu-items-group.js b/components/menu-items/menu-items-group.js deleted file mode 100644 index 9f31813dc3b3b2..00000000000000 --- a/components/menu-items/menu-items-group.js +++ /dev/null @@ -1,50 +0,0 @@ -/** - * External dependencies - */ -import classnames from 'classnames'; - -/** - * WordPress dependencies - */ -import { Children } from '@wordpress/element'; -import { applyFilters } from '@wordpress/hooks'; - -/** - * Internal dependencies - */ -import './style.scss'; -import { NavigableMenu } from '../navigable-container'; -import withInstanceId from '../higher-order/with-instance-id'; - -export function MenuItemsGroup( { - children, - className = '', - filterName, - instanceId, - label, -} ) { - const childrenArray = Children.toArray( children ); - const menuItems = filterName ? - applyFilters( filterName, childrenArray ) : - childrenArray; - - if ( ! Array.isArray( menuItems ) || ! menuItems.length ) { - return null; - } - - const labelId = `components-menu-items-group-label-${ instanceId }`; - const classNames = classnames( className, 'components-menu-items__group' ); - - return ( - <div className={ classNames }> - { label && - <div className="components-menu-items__group-label" id={ labelId }>{ label }</div> - } - <NavigableMenu orientation="vertical" aria-labelledby={ labelId }> - { menuItems } - </NavigableMenu> - </div> - ); -} - -export default withInstanceId( MenuItemsGroup ); diff --git a/components/menu-items/menu-items-shortcut.js b/components/menu-items/menu-items-shortcut.js deleted file mode 100644 index 436802abb48ba6..00000000000000 --- a/components/menu-items/menu-items-shortcut.js +++ /dev/null @@ -1,10 +0,0 @@ -function MenuItemsShortcut( { shortcut } ) { - if ( ! shortcut ) { - return null; - } - return ( - <span style={ { float: 'right', opacity: .5 } }>{ shortcut }</span> - ); -} - -export default MenuItemsShortcut; diff --git a/components/menu-items/test/menu-items-group.js b/components/menu-items/test/menu-items-group.js deleted file mode 100644 index c0a13fc070eeae..00000000000000 --- a/components/menu-items/test/menu-items-group.js +++ /dev/null @@ -1,30 +0,0 @@ -/** - * External dependencies - */ -import { shallow } from 'enzyme'; - -/** - * Internal dependencies - */ -import { MenuItemsGroup } from '../menu-items-group'; - -describe( 'MenuItemsGroup', () => { - test( 'should render null when no children provided', () => { - const wrapper = shallow( <MenuItemsGroup /> ); - - expect( wrapper.html() ).toBe( null ); - } ); - - test( 'should match snapshot', () => { - const wrapper = shallow( - <MenuItemsGroup - label="My group" - instanceId="1" - > - <p>My item</p> - </MenuItemsGroup> - ); - - expect( wrapper ).toMatchSnapshot(); - } ); -} ); diff --git a/components/navigable-container/index.js b/components/navigable-container/index.js index 52baba1b093422..073ba090b5bd37 100644 --- a/components/navigable-container/index.js +++ b/components/navigable-container/index.js @@ -1,7 +1,7 @@ /** * External Dependencies */ -import { omit, noop } from 'lodash'; +import { omit, noop, includes } from 'lodash'; /** * WordPress Dependencies @@ -74,7 +74,13 @@ class NavigableContainer extends Component { if ( offset !== undefined && stopNavigationEvents ) { // Prevents arrow key handlers bound to the document directly interfering event.nativeEvent.stopImmediatePropagation(); - event.preventDefault(); + + // When navigating a collection of items, prevent scroll containers + // from scrolling. + if ( event.target.getAttribute( 'role' ) === 'menuitem' ) { + event.preventDefault(); + } + event.stopPropagation(); } @@ -138,9 +144,9 @@ export class NavigableMenu extends Component { previous = [ LEFT, UP ]; } - if ( next.includes( keyCode ) ) { + if ( includes( next, keyCode ) ) { return 1; - } else if ( previous.includes( keyCode ) ) { + } else if ( includes( previous, keyCode ) ) { return -1; } }; diff --git a/components/notice/index.js b/components/notice/index.js index c2d1a8273400b6..e8fa2ca7482a14 100644 --- a/components/notice/index.js +++ b/components/notice/index.js @@ -14,13 +14,13 @@ import { __ } from '@wordpress/i18n'; */ import './style.scss'; -function Notice( { status, content, onRemove = noop, isDismissible = true } ) { - const className = classnames( 'notice notice-alt notice-' + status, { +function Notice( { className, status, children, onRemove = noop, isDismissible = true } ) { + const classNames = classnames( className, 'notice notice-alt notice-' + status, { 'is-dismissible': isDismissible, } ); return ( - <div className={ className }> - { isString( content ) ? <p>{ content }</p> : content } + <div className={ classNames }> + { isString( children ) ? <p>{ children }</p> : children } { isDismissible && ( <button className="notice-dismiss" type="button" onClick={ onRemove }> <span className="screen-reader-text">{ __( 'Dismiss this notice' ) }</span> diff --git a/components/notice/list.js b/components/notice/list.js index bc9059a5f4579a..e2e5ec8b1c5576 100644 --- a/components/notice/list.js +++ b/components/notice/list.js @@ -1,20 +1,23 @@ /** * External depednencies */ -import { noop } from 'lodash'; +import { noop, omit } from 'lodash'; /** * Internal dependencies */ import Notice from './'; -function NoticeList( { notices, onRemove = noop } ) { +function NoticeList( { notices, onRemove = noop, children } ) { const removeNotice = ( id ) => () => onRemove( id ); return ( <div className="components-notice-list"> + { children } { [ ...notices ].reverse().map( ( notice ) => ( - <Notice { ...notice } key={ notice.id } onRemove={ removeNotice( notice.id ) } /> + <Notice { ...omit( notice, 'content' ) } key={ notice.id } onRemove={ removeNotice( notice.id ) }> + { notice.content } + </Notice> ) ) } </div> ); diff --git a/components/panel/body.js b/components/panel/body.js index 09dbb3f7224d4d..90a9af5c33c400 100644 --- a/components/panel/body.js +++ b/components/panel/body.js @@ -39,7 +39,7 @@ class PanelBody extends Component { render() { const { title, children, opened, className } = this.props; const isOpened = opened === undefined ? this.state.opened : opened; - const icon = `arrow-${ isOpened ? 'down' : 'right' }`; + const icon = `arrow-${ isOpened ? 'up' : 'down' }`; const classes = classnames( 'components-panel__body', className, { 'is-opened': isOpened } ); return ( diff --git a/components/panel/style.scss b/components/panel/style.scss index f7109b72b970e4..78168f5bc736ed 100644 --- a/components/panel/style.scss +++ b/components/panel/style.scss @@ -22,7 +22,7 @@ border-bottom: 1px solid $light-gray-500; h3 { - margin: 0 0 .5em 0; + margin: 0 0 .5em; } &.is-opened { @@ -63,7 +63,7 @@ } .components-panel__body.is-opened > .components-panel__body-title { margin: -1 * $panel-padding; - margin-bottom: 0; + margin-bottom: 5px; } .components-panel__body-toggle.components-button { @@ -73,6 +73,7 @@ width: 100%; font-weight: 600; text-align: left; + color: $dark-gray-900; @include menu-style__neutral; &:focus { @@ -131,7 +132,8 @@ max-width: 75%; } - &:empty { + &:empty, + &:first-of-type { margin-top: 0; } } diff --git a/components/panel/test/body.js b/components/panel/test/body.js index d9e09ac44573e1..ae9ee855793cbe 100644 --- a/components/panel/test/body.js +++ b/components/panel/test/body.js @@ -23,7 +23,7 @@ describe( 'PanelBody', () => { expect( button.shallow().hasClass( 'components-panel__body-toggle' ) ).toBe( true ); expect( panelBody.state( 'opened' ) ).toBe( true ); expect( button.prop( 'onClick' ) ).toBe( panelBody.instance().toggle ); - expect( icon.prop( 'icon' ) ).toBe( 'arrow-down' ); + expect( icon.prop( 'icon' ) ).toBe( 'arrow-up' ); expect( button.childAt( 0 ).name() ).toBe( 'Dashicon' ); expect( button.childAt( 1 ).text() ).toBe( 'Some Text' ); } ); @@ -32,14 +32,14 @@ describe( 'PanelBody', () => { const panelBody = shallow( <PanelBody title="Some Text" initialOpen={ false } /> ); expect( panelBody.state( 'opened' ) ).toBe( false ); const icon = panelBody.find( 'Dashicon' ); - expect( icon.prop( 'icon' ) ).toBe( 'arrow-right' ); + expect( icon.prop( 'icon' ) ).toBe( 'arrow-down' ); } ); it( 'should use the "opened" prop instead of state if provided', () => { const panelBody = shallow( <PanelBody title="Some Text" opened={ true } initialOpen={ false } /> ); expect( panelBody.state( 'opened' ) ).toBe( false ); const icon = panelBody.find( 'Dashicon' ); - expect( icon.prop( 'icon' ) ).toBe( 'arrow-down' ); + expect( icon.prop( 'icon' ) ).toBe( 'arrow-up' ); } ); it( 'should render child elements within PanelBody element', () => { diff --git a/components/placeholder/index.js b/components/placeholder/index.js index be362bdf7866ac..884c2e241b4be4 100644 --- a/components/placeholder/index.js +++ b/components/placeholder/index.js @@ -2,6 +2,7 @@ * External dependencies */ import classnames from 'classnames'; +import { isString } from 'lodash'; /** * Internal dependencies @@ -15,7 +16,7 @@ function Placeholder( { icon, children, label, instructions, className, ...addit return ( <div { ...additionalProps } className={ classes }> <div className="components-placeholder__label"> - { !! icon && <Dashicon icon={ icon } /> } + { isString( icon ) ? <Dashicon icon={ icon } /> : icon } { label } </div> { !! instructions && <div className="components-placeholder__instructions">{ instructions }</div> } diff --git a/components/placeholder/style.scss b/components/placeholder/style.scss index adb75f48a03278..555fa529d489be 100644 --- a/components/placeholder/style.scss +++ b/components/placeholder/style.scss @@ -5,6 +5,7 @@ justify-content: center; padding: 1em; min-height: 200px; + width: 100%; text-align: center; font-family: $default-font; font-size: $default-font-size; @@ -14,7 +15,7 @@ .components-placeholder__label { display: flex; justify-content: center; - font-weight: bold; + font-weight: 600; margin-bottom: 1em; .dashicon { @@ -30,6 +31,7 @@ width: 100%; max-width: 280px; flex-wrap: wrap; + z-index: z-index( '.components-placeholder__fieldset' ); p { font-family: $default-font; diff --git a/components/popover/README.md b/components/popover/README.md index 54577251f30bbd..158c36f5eb7ede 100644 --- a/components/popover/README.md +++ b/components/popover/README.md @@ -29,7 +29,7 @@ function ToggleButton( { isVisible, toggleVisible } ) { If a Popover is returned by your component, it will be shown. To hide the popover, simply omit it from your component's render value. -If you want Popover elementss to render to a specific location on the page to allow style cascade to take effect, you must render a `Popover.Slot` further up the element tree: +If you want Popover elements to render to a specific location on the page to allow style cascade to take effect, you must render a `Popover.Slot` further up the element tree: ```jsx import { render } from '@wordpress/element'; @@ -81,24 +81,31 @@ An optional additional class name to apply to the rendered popover. - Type: `String` - Required: No -## onClose +### onClose A callback invoked when the popover should be closed. - Type: `Function` - Required: No -## onClickOutside +### onClickOutside A callback invoked when the user clicks outside the opened popover, passing the click event. The popover should be closed in response to this interaction. Defaults to `onClose`. - Type: `Function` - Required: No -## expandOnMobile +### expandOnMobile Opt-in prop to show popovers fullscreen on mobile, pass `false` in this prop to avoid this behavior. - Type: `Boolean` - Required: No - Default: `false` + + ## headerTitle + + Set this to customize the text that is shown in popover's header when it is fullscreen on mobile. + + - Type: `String` + - Required: No diff --git a/components/popover/index.js b/components/popover/index.js index f10e52f2d60ab7..62f7c6712a6217 100644 --- a/components/popover/index.js +++ b/components/popover/index.js @@ -17,6 +17,7 @@ import './style.scss'; import withFocusReturn from '../higher-order/with-focus-return'; import PopoverDetectOutside from './detect-outside'; import IconButton from '../icon-button'; +import ScrollLock from '../scroll-lock'; import { Slot, Fill } from '../slot-fill'; const FocusManaged = withFocusReturn( ( { children } ) => children ); @@ -239,6 +240,7 @@ class Popover extends Component { render() { const { + headerTitle, onClose, children, className, @@ -280,6 +282,9 @@ class Popover extends Component { > { this.state.isMobile && ( <div className="components-popover__header"> + <span className="components-popover__header-title"> + { headerTitle } + </span> <IconButton className="components-popover__close" icon="no-alt" onClick={ onClose } /> </div> ) } @@ -308,7 +313,10 @@ class Popover extends Component { content = <Fill name={ SLOT_NAME }>{ content }</Fill>; } - return <span ref={ this.bindNode( 'anchor' ) }>{ content }</span>; + return <span ref={ this.bindNode( 'anchor' ) }> + { content } + { this.state.isMobile && expandOnMobile && <ScrollLock /> } + </span>; } } diff --git a/components/popover/style.scss b/components/popover/style.scss index 32587cb8bdb62b..5de9e77578d54b 100644 --- a/components/popover/style.scss +++ b/components/popover/style.scss @@ -48,6 +48,7 @@ &.is-bottom { top: 100%; margin-top: 8px; + z-index: z-index( ".components-popover.is-bottom" ); &:before { top: -8px; @@ -111,16 +112,22 @@ } .components-popover__header { - height: $panel-header-height; - background: $light-gray-300; - padding: 0 $panel-padding; + align-items: center; + background: $white; border: 1px solid $light-gray-500; - position: relative; + display: flex; + height: $panel-header-height; + justify-content: space-between; + padding: 0 8px 0 $panel-padding; +} + +.components-popover__header-title { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + width: 100%; } .components-popover__close.components-icon-button { - position: absolute; - top: 6px; - right: 6px; z-index: z-index( '.components-popover__close' ); } diff --git a/components/popover/test/index.js b/components/popover/test/index.js index 8fa4b179a2ed7e..7fb17499a924e4 100644 --- a/components/popover/test/index.js +++ b/components/popover/test/index.js @@ -25,6 +25,14 @@ describe( 'Popover', () => { afterEach( () => { jest.restoreAllMocks(); + + // Resetting keyboard state is deferred, so ensure that timers are + // consumed to avoid leaking into other tests. + jest.runAllTimers(); + + if ( document.activeElement ) { + document.activeElement.blur(); + } } ); it( 'should add window events', () => { @@ -59,7 +67,12 @@ describe( 'Popover', () => { expect( Popover.prototype.setForcedPositions ).not.toHaveBeenCalled(); } ); - it( 'should focus when opening', () => { + it( 'should focus when opening in response to keyboard event', () => { + // As in the real world, these occur in sequence before the popover + // has been mounted. Keyup's resetting is deferred. + document.dispatchEvent( new window.KeyboardEvent( 'keydown' ) ); + document.dispatchEvent( new window.KeyboardEvent( 'keyup' ) ); + // An ideal test here would mount with an input child and focus the // child, but in context of JSDOM the inputs are not visible and // are therefore skipped as tabbable, defaulting to popover. diff --git a/components/radio-control/index.js b/components/radio-control/index.js index 7b864ee4b2ead6..b665c32122b21b 100644 --- a/components/radio-control/index.js +++ b/components/radio-control/index.js @@ -15,23 +15,23 @@ function RadioControl( { label, selected, help, instanceId, onChange, options = const onChangeValue = ( event ) => onChange( event.target.value ); return ! isEmpty( options ) && ( - <BaseControl label={ label } id={ id } help={ help } className="blocks-radio-control"> + <BaseControl label={ label } id={ id } help={ help } className="components-radio-control"> { options.map( ( option, index ) => <div - key={ ( id + '-' + index ) } - className="blocks-radio-control__option" + key={ `${ id }-${ index }` } + className="components-radio-control__option" > <input - id={ ( id + '-' + index ) } - className="blocks-radio-control__input" + id={ `${ id }-${ index }` } + className="components-radio-control__input" type="radio" name={ id } value={ option.value } onChange={ onChangeValue } checked={ option.value === selected } - aria-describedby={ !! help ? id + '__help' : undefined } + aria-describedby={ !! help ? `${ id }__help` : undefined } /> - <label key={ option.value } htmlFor={ ( id + '-' + index ) }> + <label htmlFor={ `${ id }-${ index }` }> { option.label } </label> </div> diff --git a/components/radio-control/style.scss b/components/radio-control/style.scss index 9f11fd1c76f015..0f79124d3285b5 100644 --- a/components/radio-control/style.scss +++ b/components/radio-control/style.scss @@ -1,13 +1,13 @@ -.blocks-radio-control { +.components-radio-control { display: flex; flex-direction: column; } -.blocks-radio-control__option:not(:last-child) { +.components-radio-control__option:not(:last-child) { margin-bottom: 4px; } -.blocks-radio-control__input[type=radio] { +.components-radio-control__input[type=radio] { margin-top: 0; margin-right: 6px; } \ No newline at end of file diff --git a/components/range-control/README.md b/components/range-control/README.md index c8f2ac8ab19bf0..98f52096d9e259 100644 --- a/components/range-control/README.md +++ b/components/range-control/README.md @@ -58,6 +58,13 @@ If this property is true, a button to reset the the slider is rendered. - Type: `Boolean` - Required: No +### initialPosition + +In no value exists this prop contains the slider starting position. + +- Type: `Number` +- Required: No + ### value The current value of the range slider. diff --git a/components/range-control/index.js b/components/range-control/index.js index eb672ad6e01daf..a443b91adbe7c0 100644 --- a/components/range-control/index.js +++ b/components/range-control/index.js @@ -1,6 +1,7 @@ /** * WordPress dependencies */ +import classnames from 'classnames'; import { __ } from '@wordpress/i18n'; /** @@ -10,24 +11,42 @@ import { BaseControl, Button, Dashicon } from '../'; import withInstanceId from '../higher-order/with-instance-id'; import './style.scss'; -function RangeControl( { label, value, instanceId, onChange, beforeIcon, afterIcon, help, allowReset, ...props } ) { +function RangeControl( { + className, + label, + value, + instanceId, + onChange, + beforeIcon, + afterIcon, + help, + allowReset, + initialPosition, + ...props +} ) { const id = `inspector-range-control-${ instanceId }`; const onChangeValue = ( event ) => onChange( Number( event.target.value ) ); + const initialSliderValue = value || initialPosition || ''; return ( - <BaseControl label={ label } id={ id } help={ help } className="blocks-range-control"> - { beforeIcon && <Dashicon icon={ beforeIcon } size={ 20 } /> } + <BaseControl + label={ label } + id={ id } + help={ help } + className={ classnames( 'components-range-control', className ) } + > + { beforeIcon && <Dashicon icon={ beforeIcon } /> } <input - className="blocks-range-control__slider" + className="components-range-control__slider" id={ id } type="range" - value={ value } + value={ initialSliderValue } onChange={ onChangeValue } aria-describedby={ !! help ? id + '__help' : undefined } { ...props } /> { afterIcon && <Dashicon icon={ afterIcon } /> } <input - className="blocks-range-control__number" + className="components-range-control__number" type="number" onChange={ onChangeValue } aria-label={ label } diff --git a/components/range-control/style.scss b/components/range-control/style.scss index de5fc0e2bdb799..4592bfbe2b33b2 100644 --- a/components/range-control/style.scss +++ b/components/range-control/style.scss @@ -1,19 +1,22 @@ -.blocks-range-control { - display: flex; - justify-content: center; - flex-wrap: wrap; - align-items: center; + +.components-range-control { + .components-base-control__field { + display: flex; + justify-content: center; + flex-wrap: wrap; + align-items: center; + } .dashicon { flex-shrink: 0; margin-right: 10px; } - .blocks-base-control__label { + .components-base-control__label { width: 100%; } - .blocks-range-control__slider { + .components-range-control__slider { margin-left: 0; flex: 1; } @@ -38,7 +41,7 @@ border-radius: 1.5px; } -.blocks-range-control__slider { +.components-range-control__slider { width: 100%; margin-left: $item-spacing; padding: 0; @@ -114,7 +117,7 @@ } } -.blocks-range-control__number { +.components-range-control__number { display: inline-block; margin-left: $item-spacing; font-weight: 500; diff --git a/components/select-control/README.md b/components/select-control/README.md index 19d3c372884ada..09ec4a041c0bf5 100644 --- a/components/select-control/README.md +++ b/components/select-control/README.md @@ -19,10 +19,28 @@ Render a user interface to select the size of an image. /> ``` +Render a user interface to select multiple users from a list. +```jsx + <SelectControl + multiple + label={ __( 'Select some users:' ) } + value={ this.state.users } // e.g: value = [ 'a', 'c' ] + onChange={ ( users ) => { this.setState( { users } ) } } + options={ [ + { value: 'a', label: 'User A' }, + { value: 'b', label: 'User B' }, + { value: 'c', label: 'User c' }, + ] } + /> +``` + ## Props The set of props accepted by the component will be specified below. Props not included in this set will be applied to the select element. +One important prop to refer is value, if multiple is true, +value should be an array with the values of the selected options. +If multiple is false value should be equal to the value of the selected option. ### label @@ -38,6 +56,13 @@ If this property is added, a help text will be generated using help property as - Type: `String` - Required: No +### multiple + +If this property is added, multiple values can be selected. The value passed should be an array. + +- Type: `Boolean` +- Required: No + ### options An array of objects containing the following properties: @@ -50,6 +75,8 @@ An array of objects containing the following properties: ### onChange A function that receives the value of the new option that is being selected as input. +If multiple is true the value received is an array of the selected value. +If multiple is false the value received is a single value with the new selected value. - Type: `function` - Required: Yes diff --git a/components/select-control/index.js b/components/select-control/index.js index 32d8de96806a73..391069a68fe835 100644 --- a/components/select-control/index.js +++ b/components/select-control/index.js @@ -10,9 +10,25 @@ import BaseControl from '../base-control'; import withInstanceId from '../higher-order/with-instance-id'; import './style.scss'; -function SelectControl( { label, help, instanceId, onChange, options = [], ...props } ) { +function SelectControl( { + help, + instanceId, + label, + multiple = false, + onChange, + options = [], + ...props +} ) { const id = `inspector-select-control-${ instanceId }`; - const onChangeValue = ( event ) => onChange( event.target.value ); + const onChangeValue = ( event ) => { + if ( multiple ) { + const selectedOptions = [ ...event.target.options ].filter( ( { selected } ) => selected ); + const newValues = selectedOptions.map( ( { value } ) => value ); + onChange( newValues ); + return; + } + onChange( event.target.value ); + }; // Disable reason: A select with an onchange throws a warning @@ -21,14 +37,15 @@ function SelectControl( { label, help, instanceId, onChange, options = [], ...pr <BaseControl label={ label } id={ id } help={ help }> <select id={ id } - className="blocks-select-control__input" + className="components-select-control__input" onChange={ onChangeValue } - aria-describedby={ !! help ? id + '__help' : undefined } + aria-describedby={ !! help ? `${ id }__help` : undefined } + multiple={ multiple } { ...props } > - { options.map( ( option ) => + { options.map( ( option, index ) => <option - key={ option.value } + key={ `${ option.label }-${ option.value }-${ index }` } value={ option.value } > { option.label } diff --git a/components/select-control/style.scss b/components/select-control/style.scss index fbfa090c76f202..a0c73a4e8efd8f 100644 --- a/components/select-control/style.scss +++ b/components/select-control/style.scss @@ -1,3 +1,3 @@ -.blocks-select-control__input { +.components-select-control__input { width: 100%; } \ No newline at end of file diff --git a/components/slot-fill/README.md b/components/slot-fill/README.md index 9584b47d493d6b..45d4bd24491f88 100644 --- a/components/slot-fill/README.md +++ b/components/slot-fill/README.md @@ -42,13 +42,48 @@ Any Fill will automatically occupy this Slot space, even if rendered elsewhere i You can either use the Fill component directly, or a wrapper component type as in the above example to abstract the slot name from consumer awareness. +There is also `createSlotFill` helper method which was created to simplify the process of matching the corresponding `Slot` and `Fill` components: + +```jsx +const Toolbar = createSlotFill( 'Toolbar' ); + +const MyToolbarItem = () => ( + <Toolbar> + My item + </Toolbar> +); + +const MyToolbar = () => ( + <div className="toolbar"> + <Toolbar.Slot /> + </div> +); +``` + ## Props The `SlotFillProvider` component does not accept any props. Both `Slot` and `Fill` accept a `name` string prop, where a `Slot` with a given `name` will render the `children` of any associated `Fill`s. -`Slot` also accepts a `bubblesVirtually` prop which changes the event bubbling behaviour: +`Slot` accepts a `bubblesVirtually` prop which changes the event bubbling behaviour: - By default, events will bubble to their parents on the DOM hierarchy (native event bubbling) - If `bubblesVirtually` is set to true, events will bubble to their virtual parent in the React elements hierarchy instead. + +`Slot` also accepts optional `children` function prop, which takes `fills` as a param. It allows to perform additional processing and wrap `fills` conditionally. + +_Example_: +```jsx +const Toolbar = ( { isMobile } ) => ( + <div className="toolbar"> + <Slot name="Toolbar"> + { ( fills ) => { + return isMobile && fills.length > 3 ? + <div className="toolbar__mobile-long">{ fills }</div> : + fills; + } } + </Slot> + </div> +); +``` diff --git a/components/slot-fill/fill.js b/components/slot-fill/fill.js index dd32649554ec9b..9675fc215882f2 100644 --- a/components/slot-fill/fill.js +++ b/components/slot-fill/fill.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { noop } from 'lodash'; +import { noop, isFunction } from 'lodash'; /** * WordPress dependencies @@ -57,12 +57,19 @@ class Fill extends Component { render() { const { getSlot = noop } = this.context; - const { name, children } = this.props; + const { name } = this.props; + let { children } = this.props; const slot = getSlot( name ); + if ( ! slot || ! slot.props.bubblesVirtually ) { return null; } + // If a function is passed as a child, provide it with the fillProps. + if ( isFunction( children ) ) { + children = children( slot.props.fillProps ); + } + return createPortal( children, slot.node ); } } diff --git a/components/slot-fill/index.js b/components/slot-fill/index.js index 6f4e9f2141e5be..c81d29eafd2e3a 100644 --- a/components/slot-fill/index.js +++ b/components/slot-fill/index.js @@ -9,4 +9,19 @@ export { Slot }; export { Fill }; export { Provider }; -export default { Slot, Fill, Provider }; +export function createSlotFill( name ) { + const Component = ( { children, ...props } ) => ( + <Fill name={ name } { ...props }> + { children } + </Fill> + ); + Component.displayName = name; + + Component.Slot = ( { children, ...props } ) => ( + <Slot name={ name } { ...props }> + { children } + </Slot> + ); + + return Component; +} diff --git a/components/slot-fill/slot.js b/components/slot-fill/slot.js index 8dc373509a1d16..4785e6e0837d1a 100644 --- a/components/slot-fill/slot.js +++ b/components/slot-fill/slot.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { noop, map, isString } from 'lodash'; +import { noop, map, isString, isFunction } from 'lodash'; /** * WordPress dependencies @@ -45,25 +45,34 @@ class Slot extends Component { } render() { - const { name, bubblesVirtually = false } = this.props; + const { children, name, bubblesVirtually = false, fillProps = {} } = this.props; const { getFills = noop } = this.context; if ( bubblesVirtually ) { return <div ref={ this.bindNode } />; } + const fills = map( getFills( name ), ( fill ) => { + const fillKey = fill.occurrence; + + // If a function is passed as a child, render it with the fillProps. + if ( isFunction( fill.props.children ) ) { + return cloneElement( fill.props.children( fillProps ), { key: fillKey } ); + } + + return Children.map( fill.props.children, ( child, childIndex ) => { + if ( ! child || isString( child ) ) { + return child; + } + + const childKey = `${ fillKey }---${ child.key || childIndex }`; + return cloneElement( child, { key: childKey } ); + } ); + } ); + return ( <div ref={ this.bindNode }> - { map( getFills( name ), ( fill ) => { - const fillKey = fill.occurrence; - return Children.map( fill.props.children, ( child, childIndex ) => { - if ( ! child || isString( child ) ) { - return child; - } - const childKey = `${ fillKey }---${ child.key || childIndex }`; - return cloneElement( child, { key: childKey } ); - } ); - } ) } + { isFunction( children ) ? children( fills.filter( Boolean ) ) : fills } </div> ); } diff --git a/components/slot-fill/test/slot.js b/components/slot-fill/test/slot.js index 95b60233758360..3d84e9d5f0289b 100644 --- a/components/slot-fill/test/slot.js +++ b/components/slot-fill/test/slot.js @@ -2,6 +2,7 @@ * External dependencies */ import { mount } from 'enzyme'; +import { isEmpty } from 'lodash'; /** * Internal dependencies @@ -82,6 +83,63 @@ describe( 'Slot', () => { expect( element.find( 'Slot > div' ).html() ).toBe( '<div><span></span><div></div>text</div>' ); } ); + it( 'calls the functions passed as the Slot\'s fillProps in the Fill', () => { + const onClose = jest.fn(); + + const element = mount( + <Provider> + <Slot name="chicken" fillProps={ { onClose } } /> + <Fill name="chicken"> + { ( props ) => { + return ( + <button onClick={ props.onClose }>Click me</button> + ); + } } + </Fill> + </Provider> + ); + + element.find( 'button' ).simulate( 'click' ); + + expect( onClose ).toHaveBeenCalledTimes( 1 ); + } ); + + it( 'should render empty Fills without HTML wrapper when render props used', () => { + const element = mount( + <Provider> + <Slot name="chicken"> + { ( fills ) => ( ! isEmpty( fills ) && ( + <blockquote> + { fills } + </blockquote> + ) ) } + </Slot> + <Fill name="chicken" /> + </Provider> + ); + + expect( element.find( 'Slot > div' ).html() ).toBe( '<div></div>' ); + } ); + + it( 'should render a string Fill with HTML wrapper when render props used', () => { + const element = mount( + <Provider> + <Slot name="chicken"> + { ( fills ) => ( fills && ( + <blockquote> + { fills } + </blockquote> + ) ) } + </Slot> + <Fill name="chicken"> + content + </Fill> + </Provider> + ); + + expect( element.find( 'Slot > div' ).html() ).toBe( '<div><blockquote>content</blockquote></div>' ); + } ); + it( 'should re-render Slot when not bubbling virtually', () => { const element = mount( <Provider> diff --git a/components/tab-panel/index.js b/components/tab-panel/index.js index d3a3221e7cff42..96f02f55a34da3 100644 --- a/components/tab-panel/index.js +++ b/components/tab-panel/index.js @@ -88,6 +88,7 @@ class TabPanel extends Component { role="tabpanel" id={ selectedId + '-view' } className="components-tab-panel__tab-content" + tabIndex="0" > { this.props.children( selectedTab.name ) } </div> diff --git a/components/text-control/index.js b/components/text-control/index.js index 04c384fb79f8c1..d0a801f7b72827 100644 --- a/components/text-control/index.js +++ b/components/text-control/index.js @@ -11,7 +11,7 @@ function TextControl( { label, value, help, instanceId, onChange, type = 'text', return ( <BaseControl label={ label } id={ id } help={ help }> - <input className="blocks-text-control__input" + <input className="components-text-control__input" type={ type } id={ id } value={ value } diff --git a/components/text-control/style.scss b/components/text-control/style.scss index f363338fada1b6..273ee929fbb62f 100644 --- a/components/text-control/style.scss +++ b/components/text-control/style.scss @@ -1,4 +1,4 @@ -.blocks-text-control__input { +.components-text-control__input { width: 100%; padding: 6px 8px; } diff --git a/components/textarea-control/index.js b/components/textarea-control/index.js index 2ec2d7d027b442..bff8d0346ca47b 100644 --- a/components/textarea-control/index.js +++ b/components/textarea-control/index.js @@ -12,7 +12,7 @@ function TextareaControl( { label, value, help, instanceId, onChange, rows = 4, return ( <BaseControl label={ label } id={ id } help={ help }> <textarea - className="blocks-textarea-control__input" + className="components-textarea-control__input" id={ id } rows={ rows } onChange={ onChangeValue } diff --git a/components/textarea-control/style.scss b/components/textarea-control/style.scss index 8a646f39b6dc05..e942a5e8f16550 100644 --- a/components/textarea-control/style.scss +++ b/components/textarea-control/style.scss @@ -1,4 +1,4 @@ -.blocks-textarea-control__input { +.components-textarea-control__input { width: 100%; padding: 6px 8px; } diff --git a/components/toggle-control/README.md b/components/toggle-control/README.md index 555be4f530c702..96caf8a17d1ca3 100644 --- a/components/toggle-control/README.md +++ b/components/toggle-control/README.md @@ -11,6 +11,7 @@ Render a user interface to change fixed background setting. <ToggleControl label={ __( 'Fixed Background' ) } checked={ !! hasParallax } + help={ ( checked ) => checked ? __( 'Has fixed background.' ) : __( 'No fixed background.' ) } onChange={ toggleParallax } /> ``` @@ -30,7 +31,7 @@ If this property is added, a label will be generated using label property as the If this property is added, a help text will be generated using help property as the content. -- Type: `String` +- Type: `String` | `Function` - Required: No ### checked diff --git a/components/toggle-control/index.js b/components/toggle-control/index.js index 1ce56d659f3a31..da692b8bd6b11d 100644 --- a/components/toggle-control/index.js +++ b/components/toggle-control/index.js @@ -1,3 +1,8 @@ +/** + * External dependencies + */ +import { isFunction } from 'lodash'; + /** * WordPress dependencies */ @@ -28,17 +33,18 @@ class ToggleControl extends Component { const { label, checked, help, instanceId } = this.props; const id = `inspector-toggle-control-${ instanceId }`; - let describedBy; + let describedBy, helpLabel; if ( help ) { describedBy = id + '__help'; + helpLabel = isFunction( help ) ? help( checked ) : help; } return ( <BaseControl label={ label } id={ id } - help={ help } - className="blocks-toggle-control" + help={ helpLabel } + className="components-toggle-control" > <FormToggle id={ id } diff --git a/components/toggle-control/style.scss b/components/toggle-control/style.scss index 56823361911d8f..b425b7b5713920 100644 --- a/components/toggle-control/style.scss +++ b/components/toggle-control/style.scss @@ -1,4 +1,4 @@ -.blocks-toggle-control { +.components-toggle-control .components-base-control__field { display: flex; justify-content: space-between; -} \ No newline at end of file +} diff --git a/components/toolbar/style.scss b/components/toolbar/style.scss index 8101e5b6def2ba..ed669e6d2e4eb0 100644 --- a/components/toolbar/style.scss +++ b/components/toolbar/style.scss @@ -68,14 +68,14 @@ div.components-toolbar { // subscript for numbered icon buttons, like headings &[data-subscript] svg { - padding: 4px 8px 4px 0px; + padding: 4px 8px 4px 0; } &[data-subscript]:after { content: attr( data-subscript ); font-family: $default-font; font-size: $default-font-size; - font-weight: bold; + font-weight: 600; position: absolute; right: 8px; bottom: 8px; @@ -92,6 +92,10 @@ div.components-toolbar { @include formatting-button-style__active; } + &:not(:disabled).is-active[data-subscript]:after { + color: $white; + } + // focus style &:not(:disabled):focus > svg { @include formatting-button-style__focus; diff --git a/components/tooltip/index.js b/components/tooltip/index.js index a4a444d6ca1ca1..4c4fd64c842776 100644 --- a/components/tooltip/index.js +++ b/components/tooltip/index.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { debounce, includes, upperFirst, toLower } from 'lodash'; +import { debounce, includes } from 'lodash'; /** * WordPress dependencies @@ -178,7 +178,7 @@ class Tooltip extends Component { className="components-tooltip" aria-hidden="true" > - { upperFirst( toLower( text ) ) } + { text } </Popover> ), ), diff --git a/components/tooltip/test/index.js b/components/tooltip/test/index.js index 57e9b1a674eee4..04be4b06fdde56 100644 --- a/components/tooltip/test/index.js +++ b/components/tooltip/test/index.js @@ -21,7 +21,7 @@ describe( 'Tooltip', () => { it( 'should render children', () => { const wrapper = shallow( - <Tooltip position="bottom right" text="Help Text"> + <Tooltip position="bottom right" text="Help text"> <button>Hover Me!</button> </Tooltip> ); @@ -34,7 +34,7 @@ describe( 'Tooltip', () => { it( 'should render children with additional popover when over', () => { const wrapper = shallow( - <Tooltip position="bottom right" text="Help Text"> + <Tooltip position="bottom right" text="Help text"> <button>Hover Me!</button> </Tooltip> ); @@ -56,7 +56,7 @@ describe( 'Tooltip', () => { const originalFocus = jest.fn(); const event = { type: 'focus', currentTarget: {} }; const wrapper = shallow( - <Tooltip text="Help Text"> + <Tooltip text="Help text"> <button onMouseEnter={ originalFocus } onFocus={ originalFocus } @@ -82,7 +82,7 @@ describe( 'Tooltip', () => { // rendered components: https://github.com/airbnb/enzyme/issues/450 const originalMouseEnter = jest.fn(); const wrapper = mount( - <Tooltip text="Help Text"> + <Tooltip text="Help text"> <button onMouseEnter={ originalMouseEnter } onFocus={ originalMouseEnter } @@ -115,7 +115,7 @@ describe( 'Tooltip', () => { // rendered components: https://github.com/airbnb/enzyme/issues/450 const originalMouseEnter = jest.fn(); const wrapper = mount( - <Tooltip text="Help Text"> + <Tooltip text="Help text"> <button onMouseEnter={ originalMouseEnter } onFocus={ originalMouseEnter } @@ -146,7 +146,7 @@ describe( 'Tooltip', () => { // rendered components: https://github.com/airbnb/enzyme/issues/450 const originalMouseEnter = jest.fn(); const wrapper = mount( - <Tooltip text="Help Text"> + <Tooltip text="Help text"> <button onMouseEnter={ originalMouseEnter } onFocus={ originalMouseEnter } diff --git a/cypress.json b/cypress.json deleted file mode 100644 index 87ff4b29a8f833..00000000000000 --- a/cypress.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "baseUrl": "http://localhost:8888", - "env": { - "username": "admin", - "password": "password" - }, - "integrationFolder": "test/e2e/integration", - "supportFile": "test/e2e/support", - "videoRecording": false, - "chromeWebSecurity": false, - "pluginsFile": "test/e2e/plugins" -} diff --git a/data/README.md b/data/README.md index 134483cd61ca59..d1b44f20fc050c 100644 --- a/data/README.md +++ b/data/README.md @@ -1,132 +1,251 @@ -Data -==== +# Data -The more WordPress UI moves to the client, the more there's a need for a centralized data module allowing data management and sharing between several WordPress modules and plugins. +WordPress' data module serves as a hub to manage application state for both plugins and WordPress itself, providing tools to manage data within and between distinct modules. It is designed as a modular pattern for organizing and sharing data: simple enough to satisfy the needs of a small plugin, while scalable to serve the requirements of a complex single-page application. -This module holds a global state variable and exposes a "Redux-like" API containing the following methods: +The data module is built upon and shares many of the same core principles of [Redux](https://redux.js.org/), but shouldn't be mistaken as merely _Redux for WordPress_, as it includes a few of its own [distinguishing characteristics](#comparison-with-redux). As you read through this guide, you may find it useful to reference the Redux documentation — particularly [its glossary](https://redux.js.org/glossary) — for more detail on core concepts. +## Registering a Store -### `wp.data.registerReducer( key: string, reducer: function )` +Use the `registerStore` function to add your own store to the centralized data registry. This function accepts two arguments: a name to identify the module, and an object with values describing how your state is represented, modified, and accessed. At a minimum, you must provide a reducer function describing the shape of your state and how it changes in response to actions dispatched to the store. -If your module or plugin needs to store and manipulate client-side data, you'll have to register a "reducer" to do so. A reducer is a function taking the previous `state` and `action` and returns an update `state`. You can learn more about reducers on the [Redux Documentation](https://redux.js.org/docs/basics/Reducers.html) +```js +const { data, apiRequest } = wp; +const { registerStore, dispatch } = data; + +const DEFAULT_STATE = { + prices: {}, + discountPercent: 0, +}; + +registerStore( 'my-shop', { + reducer( state = DEFAULT_STATE, action ) { + switch ( action.type ) { + case 'SET_PRICE': + return { + ...state, + prices: { + ...state.prices, + [ action.item ]: action.price, + }, + }; + + case 'START_SALE': + return { + ...state, + discountPercent: action.discountPercent, + }; + } + + return state; + }, -This function takes two arguments: a `key` to identify the module (example: `myAwesomePlugin`) and the reducer function. It returns a [Redux-like store object](https://redux.js.org/docs/basics/Store.html) with the following methods: + actions: { + setPrice( item, price ) { + return { + type: 'SET_PRICE', + item, + price, + }; + }, + startSale( discountPercent ) { + return { + type: 'START_SALE', + discountPercent, + }; + }, + }, -#### `store.getState()` + selectors: { + getPrice( state, item ) { + const { prices, discountPercent } = state; + const price = prices[ item ]; -Returns the [state object](https://redux.js.org/docs/Glossary.html#state) of the registered reducer. See: https://redux.js.org/docs/api/Store.html#getState + return price * ( 1 - ( 0.01 * discountPercent ) ); + }, + }, -#### `store.subscribe( listener: function )` + resolvers: { + async getPrice( state, item ) { + const price = await apiRequest( { path: '/wp/v2/prices/' + item } ); + dispatch( 'my-shop' ).setPrice( item, price ); + }, + }, +} ); +``` + +A [**reducer**](https://redux.js.org/docs/basics/Reducers.html) is a function accepting the previous `state` and `action` as arguments and returns an updated `state` value. + +The **`actions`** object should describe all [action creators](https://redux.js.org/glossary#action-creator) available for your store. An action creator is a function that optionally accepts arguments and returns an action object to dispatch to the registered reducer. _Dispatching actions is the primary mechanism for making changes to your state._ -Registers a [`listener`](https://redux.js.org/docs/api/Store.html#subscribe) function called everytime the state is updated. +The **`selectors`** object includes a set of functions for accessing and deriving state values. A selector is a function which accepts state and optional arguments and returns some value from state. _Calling selectors is the primary mechanism for retrieving data from your state_, and serve as a useful abstraction over the raw data which is typically more susceptible to change and less readily usable as a [normalized object](https://redux.js.org/recipes/structuring-reducers/normalizing-state-shape#designing-a-normalized-state). -#### `store.dispatch( action: object )` +The return value of `registerStore` is a [Redux-like store object](https://redux.js.org/docs/basics/Store.html) with the following methods: -The dispatch function should be called to trigger the registered reducers function and update the state. An [`action`](https://redux.js.org/docs/api/Store.html#dispatch) object should be passed to this function. This action is passed to the registered reducers in addition to the previous state. +- `store.getState()`: Returns the state value of the registered reducer + - _Redux parallel:_ [`getState`](https://redux.js.org/api-reference/store#getState) +- `store.subscribe( listener: Function )`: Registers a function called any time the value of state changes. + - _Redux parallel:_ [`subscribe`](https://redux.js.org/api-reference/store#subscribe(listener)) +- `store.dispatch( action: Object )`: Given an action object, calls the registered reducer and updates the state value. + - _Redux parallel:_ [`dispatch`](https://redux.js.org/api-reference/store#dispatch(action)) +## Data Access and Manipulation -### `wp.data.registerSelectors( reducerKey: string, newSelectors: object )` +It is very rare that you should access store methods directly. Instead, the following suite of functions and higher-order components is provided for the most common data access and manipulation needs. -If your module or plugin needs to expose its state to other modules and plugins, you'll have to register state selectors. +### Data API -A selector is a function that takes the current state value as a first argument and extra arguments if needed and returns any data extracted from the state. +The top-level API of `@wordpress/data` includes a number of functions which allow immediate access to select from and dispatch to a registered store. These are most useful in low-level code where a selector or action dispatch is called a single time or at known intervals. For displaying data in a user interface, you should use [higher-order components](#higher-order-components) instead. -#### Example: +#### `select( storeName: string ): Object` -Let's say the state of our plugin (registered with the key `myPlugin`) has the following shape: `{ title: 'My post title' }`. We can register a `getTitle` selector to make this state value available like so: +Given the name of a registered store, returns an object of the store's selectors. The selector functions are been pre-bound to pass the current state automatically. As a consumer, you need only pass arguments of the selector, if applicable. + +_Example:_ ```js -wp.data.registerSelectors( 'myPlugin', { getTitle: ( state ) => state.title } ); -``` +const { select } = wp.data; -### `wp.data.registerActions( reducerKey: string, newActions: object )` +select( 'my-shop' ).getPrice( 'hammer' ); +``` -If your module or plugin needs to expose its actions to other modules and plugins, you'll have to register action creators. +#### `dispatch( storeName: string ): Object` -An action creator is a function that takes arguments and returns an action object dispatch to the registered reducer to update the state. +Given the name of a registered store, returns an object of the store's action creators. Calling an action creator will cause it to be dispatched, updating the state value accordingly. -#### Example: +_Example:_ ```js -wp.data.registerActions( 'myPlugin', { - setTitle( newTitle ) { - return { - type: 'SET_TITLE', - title: newTitle, - }; - }, -} ); +const { dispatch } = wp.data; + +dispatch( 'my-shop' ).setPrice( 'hammer', 9.75 ); ``` -### `wp.data.select( key: string )` +#### `subscribe(): Function` -This function allows calling any registered selector. Given a module's key, this function returns an object of all selector functions registered for the module. +Given a listener function, the function will be called any time the state value of one of the registered stores has changed. This function returns a `unsubscribe` function used to stop the subscription. -#### Example: +_Example:_ ```js -wp.data.select( 'myPlugin' ).getTitle(); // Returns "My post title" +const { subscribe } = wp.data; + +const unsubscribe = subscribe( () => { + // You could use this opportunity to test whether the derived result of a + // selector has subsequently changed as the result of a state update. +} ); + +// Later, if necessary... +unsubscribe(); ``` -### `wp.data.dispatch( key: string )` +### Helpers -This function allows calling any registered action. Given a module's key, this function returns an object of all action creators functions registered for the module. +#### `combineReducers( reducers: Object ): Function` -#### Example: +As your app grows more complex, you'll want to split your reducing function into separate functions, each managing independent parts of the state. The `combineReducers` helper function turns an object whose values are different reducing functions into a single reducing function you can pass to `registerStore`. + +_Example:_ ```js -wp.data.dispatch( 'myPlugin' ).setTitle( 'new Title' ); // Dispatches the setTitle action to the reducer +const { combineReducers, registerStore } = wp.data; + +const prices = ( state = {}, action ) => { + return action.type === 'SET_PRICE' ? + { + ...state, + [ action.item ]: action.price, + } : + state; +}; + +const discountPercent = ( state = 0, action ) => { + return action.type === 'START_SALE' ? + action.discountPercent : + state; +}; + +registerStore( 'my-shop', { + reducer: combineReducers( { + prices, + discountPercent, + } ), +} ); ``` -### `wp.data.subscribe( listener: function )` +### Higher-Order Components + +A higher-order component is a function which accepts a [component](https://github.com/WordPress/gutenberg/tree/master/element) and returns a new, enhanced component. A stateful user interface should respond to changes in the underlying state and updates its displayed element accordingly. WordPress uses higher-order components both as a means to separate the purely visual aspects of an interface from its data backing, and to ensure that the data is kept in-sync with the stores. + +#### `withSelect( mapSelectToProps: Function ): Function` -Function used to subscribe to data changes. The listener function is called each time a change is made to any of the registered reducers. This function returns a `unsubscribe` function used to abort the subscription. +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. + +_Example:_ ```js -// Subscribe. -const unsubscribe = wp.data.subscribe( () => { - const data = { - slug: wp.data.select( 'core/editor' ).getEditedPostSlug(), - }; +function PriceDisplay( { price, currency } ) { + return new Intl.NumberFormat( 'en-US', { + style: 'currency', + currency, + } ).format( price ); +} - console.log( 'data changed', data ); -} ); +const { withSelect } = wp.data; -// Unsubcribe. -unsubscribe(); +const HammerPriceDisplay = withSelect( ( select, ownProps ) => { + const { getPrice } = select( 'my-shop' ); + const { currency } = ownProps; + + return { + price: getPrice( 'hammer', currency ), + }; +} )( PriceDisplay ); + +// Rendered in the application: +// +// <HammerPriceDisplay currency="USD" /> ``` -### `wp.data.withSelect( mapStateToProps: Object|Function )( WrappedComponent: Component )` +In the above example, when `HammerPriceDisplay` is rendered into an application, it will pass the price into the underlying `PriceDisplay` component and update automatically if the price of a hammer ever changes in the store. + +#### `withDispatch( mapDispatchToProps: Function ): Function` -To inject state-derived props into a WordPress Element Component, use the `withSelect` higher-order 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) and the props passed to the original component. ```jsx -const Component = ( { title } ) => <div>{ title }</div>; +function Button( { onClick, children } ) { + return <button type="button" onClick={ onClick }>{ children }</button>; +} + +const { withDispatch } = wp.data; + +const SaleButton = withDispatch( ( dispatch, ownProps ) => { + const { startSale } = dispatch( 'my-shop' ); + const { discountPercent = 20 } = ownProps; -const EnhancedComponent = wp.data.withSelect( ( select ) => { return { - title: select( 'myPlugin' ).getTitle, + onClick() { + startSale( discountPercent ); + }, }; -} )( Component ); +} )( Button ); + +// Rendered in the application: +// +// <SaleButton>Start Sale!</SaleButton> ``` -### `wp.data.withDispatch( propsToDispatchers: Object )( WrappedComponent: Component )` +## Comparison with Redux -To manipulate store data, you can pass dispatching actions into your component as props using the `withDispatch` higher-order component: +The data module shares many of the same [core principles](https://redux.js.org/introduction/three-principles) and [API method naming](https://redux.js.org/api-reference) of [Redux](https://redux.js.org/). In fact, it is implemented atop Redux. Where it differs is in establishing a modularization pattern for creating separate but interdependent stores, and in codifying conventions such as selector functions as the primary entry point for data access. -```jsx -const Component = ( { title, updateTitle } ) => <input value={ title } onChange={ updateTitle } />; +The [higher-order components](#higher-order-components) were created to complement this distinction. The intention with splitting `withSelect` and `withDispatch` — where in React Redux they are combined under `connect` as `mapStateToProps` and `mapDispatchToProps` arguments — is to more accurately reflect that dispatch is not dependent upon a subscription to state changes, and to allow for state-derived values to be used in `withDispatch` (via [higher-order component composition](https://github.com/WordPress/gutenberg/tree/master/element#compose)). -const EnhancedComponent = wp.element.compose( [ - wp.data.withSelect( ( select ) => { - return { - title: select( 'myPlugin' ).getTitle(), - }; - } ), - wp.data.withDispatch( ( dispatch ) => { - return { - updateTitle: dispatch( 'myPlugin' ).setTitle, - }; - } ), -] )( Component ); -``` +Specific implementation differences from Redux and React Redux: + +- In Redux, a `subscribe` listener is called on every dispatch, regardless of whether the value of state has changed. + - In `@wordpress/data`, a subscriber is only called when state has changed. +- In React Redux, a `mapStateToProps` function must return an object. + - In `@wordpress/data`, a `withSelect` mapping function can return `undefined` if it has no props to inject. +- In React Redux, the `mapDispatchToProps` argument can be defined as an object or a function. + - In `@wordpress/data`, the `withDispatch` higher-order component creator must be passed a function. diff --git a/data/index.js b/data/index.js index 93fa8fa80c09a5..691033b38d1f2d 100644 --- a/data/index.js +++ b/data/index.js @@ -1,15 +1,15 @@ /** * External dependencies */ -import isEqualShallow from 'is-equal-shallow'; -import { createStore } from 'redux'; +import isShallowEqual from 'shallowequal'; +import { combineReducers, createStore } from 'redux'; import { flowRight, without, mapValues } from 'lodash'; +import memoize from 'memize'; /** * WordPress dependencies */ -import { deprecated } from '@wordpress/utils'; -import { Component, getWrapperDisplayName } from '@wordpress/element'; +import { Component, createHigherOrderComponent } from '@wordpress/element'; /** * Internal dependencies @@ -28,7 +28,37 @@ let listeners = []; * Global listener called for each store's update. */ export function globalListener() { - listeners.forEach( listener => listener() ); + listeners.forEach( ( listener ) => listener() ); +} + +/** + * Convenience for registering reducer with actions and selectors. + * + * @param {string} reducerKey Reducer key. + * @param {Object} options Store description (reducer, actions, selectors, resolvers). + * + * @return {Object} Registered store object. + */ +export function registerStore( reducerKey, options ) { + if ( ! options.reducer ) { + throw new TypeError( 'Must specify store reducer' ); + } + + const store = registerReducer( reducerKey, options.reducer ); + + if ( options.actions ) { + registerActions( reducerKey, options.actions ); + } + + if ( options.selectors ) { + registerSelectors( reducerKey, options.selectors ); + } + + if ( options.resolvers ) { + registerResolvers( reducerKey, options.resolvers ); + } + + return store; } /** @@ -46,11 +76,35 @@ export function registerReducer( reducerKey, reducer ) { } const store = createStore( reducer, flowRight( enhancers ) ); stores[ reducerKey ] = store; - store.subscribe( globalListener ); + + // Customize subscribe behavior to call listeners only on effective change, + // not on every dispatch. + let lastState = store.getState(); + store.subscribe( () => { + const state = store.getState(); + const hasChanged = state !== lastState; + lastState = state; + + if ( hasChanged ) { + globalListener(); + } + } ); return store; } +/** + * The combineReducers helper function turns an object whose values are different + * reducing functions into a single reducing function you can pass to registerReducer. + * + * @param {Object} reducers An object whose values correspond to different reducing + * functions that need to be combined into one. + * + * @return {Function} A reducer that invokes every reducer inside the reducers + * object, and constructs a state object with the same shape. + */ +export { combineReducers }; + /** * Registers selectors for external usage. * @@ -66,6 +120,65 @@ export function registerSelectors( reducerKey, newSelectors ) { selectors[ reducerKey ] = mapValues( newSelectors, createStateSelector ); } +/** + * Registers resolvers for a given reducer key. Resolvers are side effects + * invoked once per argument set of a given selector call, used in ensuring + * that the data needs for the selector are satisfied. + * + * @param {string} reducerKey Part of the state shape to register the + * resolvers for. + * @param {Object} newResolvers Resolvers to register. + */ +export function registerResolvers( reducerKey, newResolvers ) { + const createResolver = ( selector, key ) => { + // Don't modify selector behavior if no resolver exists. + if ( ! newResolvers.hasOwnProperty( key ) ) { + return selector; + } + + const store = stores[ reducerKey ]; + const resolver = newResolvers[ key ]; + + const rawFulfill = async ( ...args ) => { + // At this point, selectors have already been pre-bound to inject + // state, it would not be otherwise provided to fulfill. + const state = store.getState(); + + const fulfill = resolver.fulfill ? resolver.fulfill : resolver; + let fulfillment = fulfill( state, ...args ); + + // Attempt to normalize fulfillment as async iterable. + fulfillment = toAsyncIterable( fulfillment ); + if ( ! isAsyncIterable( fulfillment ) ) { + return; + } + + for await ( const maybeAction of fulfillment ) { + // Dispatch if it quacks like an action. + if ( isActionLike( maybeAction ) ) { + store.dispatch( maybeAction ); + } + } + }; + + // Ensure single invocation per argument set via memoization + // or via isFulfilled call if provided. + const fulfill = resolver.isFulfilled ? ( ...args ) => { + const state = store.getState(); + if ( ! resolver.isFulfilled( state, ...args ) ) { + rawFulfill( ...args ); + } + } : memoize( rawFulfill ); + + return ( ...args ) => { + fulfill( ...args ); + return selector( ...args ); + }; + }; + + selectors[ reducerKey ] = mapValues( selectors[ reducerKey ], createResolver ); +} + /** * Registers actions for external usage. * @@ -103,16 +216,6 @@ export const subscribe = ( listener ) => { * @return {*} The selector's returned value. */ export function select( reducerKey ) { - if ( arguments.length > 1 ) { - deprecated( 'Calling select with multiple arguments', { - version: '2.4', - plugin: 'Gutenberg', - } ); - - const [ , selectorKey, ...args ] = arguments; - return select( reducerKey )[ selectorKey ]( ...args ); - } - return selectors[ reducerKey ]; } @@ -138,8 +241,8 @@ export function dispatch( reducerKey ) { * * @return {Component} Enhanced component with merged state data props. */ -export const withSelect = ( mapStateToProps ) => ( WrappedComponent ) => { - class ComponentWithSelect extends Component { +export const withSelect = ( mapStateToProps ) => createHigherOrderComponent( ( WrappedComponent ) => { + return class ComponentWithSelect extends Component { constructor() { super( ...arguments ); @@ -148,6 +251,10 @@ export const withSelect = ( mapStateToProps ) => ( WrappedComponent ) => { this.state = {}; } + shouldComponentUpdate( nextProps, nextState ) { + return ! isShallowEqual( nextProps, this.props ) || ! isShallowEqual( nextState, this.state ); + } + componentWillMount() { this.subscribe(); @@ -156,13 +263,19 @@ export const withSelect = ( mapStateToProps ) => ( WrappedComponent ) => { } componentWillReceiveProps( nextProps ) { - if ( ! isEqualShallow( nextProps, this.props ) ) { + if ( ! isShallowEqual( nextProps, this.props ) ) { this.runSelection( nextProps ); } } componentWillUnmount() { this.unsubscribe(); + + // While above unsubscribe avoids future listener calls, callbacks + // are snapshotted before being invoked, so if unmounting occurs + // during a previous callback, we need to explicitly track and + // avoid the `runSelection` that is scheduled to occur. + this.isUnmounting = true; } subscribe() { @@ -170,21 +283,25 @@ export const withSelect = ( mapStateToProps ) => ( WrappedComponent ) => { } runSelection( props = this.props ) { - const newState = mapStateToProps( select, props ); - if ( ! isEqualShallow( newState, this.state ) ) { - this.setState( newState ); + if ( this.isUnmounting ) { + return; + } + + const { mergeProps } = this.state; + const nextMergeProps = mapStateToProps( select, props ) || {}; + + if ( ! isShallowEqual( nextMergeProps, mergeProps ) ) { + this.setState( { + mergeProps: nextMergeProps, + } ); } } render() { - return <WrappedComponent { ...this.props } { ...this.state } />; + return <WrappedComponent { ...this.props } { ...this.state.mergeProps } />; } - } - - ComponentWithSelect.displayName = getWrapperDisplayName( WrappedComponent, 'select' ); - - return ComponentWithSelect; -}; + }; +}, 'withSelect' ); /** * Higher-order component used to add dispatch props using registered action @@ -198,8 +315,8 @@ export const withSelect = ( mapStateToProps ) => ( WrappedComponent ) => { * * @return {Component} Enhanced component with merged dispatcher props. */ -export const withDispatch = ( mapDispatchToProps ) => ( WrappedComponent ) => { - class ComponentWithDispatch extends Component { +export const withDispatch = ( mapDispatchToProps ) => createHigherOrderComponent( ( WrappedComponent ) => { + return class ComponentWithDispatch extends Component { constructor() { super( ...arguments ); @@ -238,21 +355,77 @@ export const withDispatch = ( mapDispatchToProps ) => ( WrappedComponent ) => { render() { return <WrappedComponent { ...this.props } { ...this.proxyProps } />; } - } + }; +}, 'withDispatch' ); - ComponentWithDispatch.displayName = getWrapperDisplayName( WrappedComponent, 'dispatch' ); +/** + * Returns true if the given argument appears to be a dispatchable action. + * + * @param {*} action Object to test. + * + * @return {boolean} Whether object is action-like. + */ +export function isActionLike( action ) { + return ( + !! action && + typeof action.type === 'string' + ); +} - return ComponentWithDispatch; -}; +/** + * Returns true if the given object is an async iterable, or false otherwise. + * + * @param {*} object Object to test. + * + * @return {boolean} Whether object is an async iterable. + */ +export function isAsyncIterable( object ) { + return ( + !! object && + typeof object[ Symbol.asyncIterator ] === 'function' + ); +} -export const query = ( mapSelectToProps ) => { - deprecated( 'wp.data.query', { - version: '2.5', - alternative: 'wp.data.withSelect', - plugin: 'Gutenberg', - } ); +/** + * Returns true if the given object is iterable, or false otherwise. + * + * @param {*} object Object to test. + * + * @return {boolean} Whether object is iterable. + */ +export function isIterable( object ) { + return ( + !! object && + typeof object[ Symbol.iterator ] === 'function' + ); +} - return withSelect( ( props ) => { - return mapSelectToProps( select, props ); - } ); -}; +/** + * Normalizes the given object argument to an async iterable, asynchronously + * yielding on a singular or array of generator yields or promise resolution. + * + * @param {*} object Object to normalize. + * + * @return {AsyncGenerator} Async iterable actions. + */ +export function toAsyncIterable( object ) { + if ( isAsyncIterable( object ) ) { + return object; + } + + return ( async function* () { + // Normalize as iterable... + if ( ! isIterable( object ) ) { + object = [ object ]; + } + + for ( let maybeAction of object ) { + // ...of Promises. + if ( ! ( maybeAction instanceof Promise ) ) { + maybeAction = Promise.resolve( maybeAction ); + } + + yield await maybeAction; + } + }() ); +} diff --git a/data/test/index.js b/data/test/index.js index 8b63e41f4e7161..672f30c4c78606 100644 --- a/data/test/index.js +++ b/data/test/index.js @@ -12,17 +12,60 @@ import { compose } from '@wordpress/element'; * Internal dependencies */ import { + registerStore, registerReducer, registerSelectors, + registerResolvers, registerActions, dispatch, select, withSelect, withDispatch, subscribe, + isActionLike, + isAsyncIterable, + isIterable, + toAsyncIterable, } from '../'; -describe( 'store', () => { +jest.mock( '@wordpress/utils', () => ( { + deprecated: jest.fn(), +} ) ); + +describe( 'registerStore', () => { + it( 'should be shorthand for reducer, actions, selectors registration', () => { + const store = registerStore( 'butcher', { + reducer( state = { ribs: 6, chicken: 4 }, action ) { + switch ( action.type ) { + case 'sale': + return { + ...state, + [ action.meat ]: state[ action.meat ] / 2, + }; + } + + return state; + }, + selectors: { + getPrice: ( state, meat ) => state[ meat ], + }, + actions: { + startSale: ( meat ) => ( { type: 'sale', meat } ), + }, + } ); + + expect( store.getState() ).toEqual( { ribs: 6, chicken: 4 } ); + expect( dispatch( 'butcher' ) ).toHaveProperty( 'startSale' ); + expect( select( 'butcher' ) ).toHaveProperty( 'getPrice' ); + expect( select( 'butcher' ).getPrice( 'chicken' ) ).toBe( 4 ); + expect( select( 'butcher' ).getPrice( 'ribs' ) ).toBe( 6 ); + dispatch( 'butcher' ).startSale( 'chicken' ); + expect( select( 'butcher' ).getPrice( 'chicken' ) ).toBe( 2 ); + expect( select( 'butcher' ).getPrice( 'ribs' ) ).toBe( 6 ); + } ); +} ); + +describe( 'registerReducer', () => { it( 'Should append reducers to the state', () => { const reducer1 = () => 'chicken'; const reducer2 = () => 'ribs'; @@ -35,6 +78,256 @@ describe( 'store', () => { } ); } ); +describe( 'registerResolvers', () => { + const unsubscribes = []; + afterEach( () => { + let unsubscribe; + while ( ( unsubscribe = unsubscribes.shift() ) ) { + unsubscribe(); + } + } ); + + function subscribeWithUnsubscribe( ...args ) { + const unsubscribe = subscribe( ...args ); + unsubscribes.push( unsubscribe ); + return unsubscribe; + } + + it( 'should not do anything for selectors which do not have resolvers', () => { + registerReducer( 'demo', ( state = 'OK' ) => state ); + registerSelectors( 'demo', { + getValue: ( state ) => state, + } ); + registerResolvers( 'demo', {} ); + + expect( select( 'demo' ).getValue() ).toBe( 'OK' ); + } ); + + it( 'should behave as a side effect for the given selector, with arguments', () => { + const resolver = jest.fn(); + + registerReducer( 'demo', ( state = 'OK' ) => state ); + registerSelectors( 'demo', { + getValue: ( state ) => state, + } ); + registerResolvers( 'demo', { + getValue: resolver, + } ); + + const value = select( 'demo' ).getValue( 'arg1', 'arg2' ); + expect( value ).toBe( 'OK' ); + expect( resolver ).toHaveBeenCalledWith( 'OK', 'arg1', 'arg2' ); + select( 'demo' ).getValue( 'arg1', 'arg2' ); + expect( resolver ).toHaveBeenCalledTimes( 1 ); + select( 'demo' ).getValue( 'arg3', 'arg4' ); + expect( resolver ).toHaveBeenCalledTimes( 2 ); + } ); + + it( 'should support the object resolver definition', () => { + const resolver = jest.fn(); + + registerReducer( 'demo', ( state = 'OK' ) => state ); + registerSelectors( 'demo', { + getValue: ( state ) => state, + } ); + registerResolvers( 'demo', { + getValue: { fulfill: resolver }, + } ); + + const value = select( 'demo' ).getValue( 'arg1', 'arg2' ); + expect( value ).toBe( 'OK' ); + } ); + + it( 'should use isFulfilled definition before calling the side effect', () => { + const resolver = jest.fn(); + let count = 0; + + registerReducer( 'demo', ( state = 'OK' ) => state ); + registerSelectors( 'demo', { + getValue: ( state ) => state, + } ); + registerResolvers( 'demo', { + getValue: { + fulfill: ( ...args ) => { + count++; + resolver( ...args ); + }, + isFulfilled: () => count > 1, + }, + } ); + + for ( let i = 0; i < 4; i++ ) { + select( 'demo' ).getValue( 'arg1', 'arg2' ); + } + expect( resolver ).toHaveBeenCalledTimes( 2 ); + } ); + + it( 'should resolve action to dispatch', ( done ) => { + registerReducer( 'demo', ( state = 'NOTOK', action ) => { + return action.type === 'SET_OK' ? 'OK' : state; + } ); + registerSelectors( 'demo', { + getValue: ( state ) => state, + } ); + registerResolvers( 'demo', { + getValue: () => ( { type: 'SET_OK' } ), + } ); + + subscribeWithUnsubscribe( () => { + try { + expect( select( 'demo' ).getValue() ).toBe( 'OK' ); + done(); + } catch ( error ) { + done( error ); + } + } ); + + select( 'demo' ).getValue(); + } ); + + it( 'should resolve mixed type action array to dispatch', ( done ) => { + registerReducer( 'counter', ( state = 0, action ) => { + return action.type === 'INCREMENT' ? state + 1 : state; + } ); + registerSelectors( 'counter', { + getCount: ( state ) => state, + } ); + registerResolvers( 'counter', { + getCount: () => [ + { type: 'INCREMENT' }, + Promise.resolve( { type: 'INCREMENT' } ), + ], + } ); + + subscribeWithUnsubscribe( () => { + if ( select( 'counter' ).getCount() === 2 ) { + done(); + } + } ); + + select( 'counter' ).getCount(); + } ); + + it( 'should resolve generator action to dispatch', ( done ) => { + registerReducer( 'demo', ( state = 'NOTOK', action ) => { + return action.type === 'SET_OK' ? 'OK' : state; + } ); + registerSelectors( 'demo', { + getValue: ( state ) => state, + } ); + registerResolvers( 'demo', { + * getValue() { + yield { type: 'SET_OK' }; + }, + } ); + + subscribeWithUnsubscribe( () => { + try { + expect( select( 'demo' ).getValue() ).toBe( 'OK' ); + done(); + } catch ( error ) { + done( error ); + } + } ); + + select( 'demo' ).getValue(); + } ); + + it( 'should resolve promise action to dispatch', ( done ) => { + registerReducer( 'demo', ( state = 'NOTOK', action ) => { + return action.type === 'SET_OK' ? 'OK' : state; + } ); + registerSelectors( 'demo', { + getValue: ( state ) => state, + } ); + registerResolvers( 'demo', { + getValue: () => Promise.resolve( { type: 'SET_OK' } ), + } ); + + subscribeWithUnsubscribe( () => { + try { + expect( select( 'demo' ).getValue() ).toBe( 'OK' ); + done(); + } catch ( error ) { + done( error ); + } + } ); + + select( 'demo' ).getValue(); + } ); + + it( 'should resolve promise non-action to dispatch', ( done ) => { + let shouldThrow = false; + registerReducer( 'demo', ( state = 'OK' ) => { + if ( shouldThrow ) { + throw 'Should not have dispatched'; + } + + return state; + } ); + shouldThrow = true; + registerSelectors( 'demo', { + getValue: ( state ) => state, + } ); + registerResolvers( 'demo', { + getValue: () => Promise.resolve(), + } ); + + select( 'demo' ).getValue(); + + process.nextTick( () => { + done(); + } ); + } ); + + it( 'should resolve async iterator action to dispatch', ( done ) => { + registerReducer( 'counter', ( state = 0, action ) => { + return action.type === 'INCREMENT' ? state + 1 : state; + } ); + registerSelectors( 'counter', { + getCount: ( state ) => state, + } ); + registerResolvers( 'counter', { + getCount: async function* () { + yield { type: 'INCREMENT' }; + yield await Promise.resolve( { type: 'INCREMENT' } ); + }, + } ); + + subscribeWithUnsubscribe( () => { + if ( select( 'counter' ).getCount() === 2 ) { + done(); + } + } ); + + select( 'counter' ).getCount(); + } ); + + it( 'should not dispatch resolved promise action on subsequent selector calls', ( done ) => { + registerReducer( 'demo', ( state = 'NOTOK', action ) => { + return action.type === 'SET_OK' && state === 'NOTOK' ? 'OK' : 'NOTOK'; + } ); + registerSelectors( 'demo', { + getValue: ( state ) => state, + } ); + registerResolvers( 'demo', { + getValue: () => Promise.resolve( { type: 'SET_OK' } ), + } ); + + subscribeWithUnsubscribe( () => { + try { + expect( select( 'demo' ).getValue() ).toBe( 'OK' ); + done(); + } catch ( error ) { + done( error ); + } + } ); + + select( 'demo' ).getValue(); + select( 'demo' ).getValue(); + } ); +} ); + describe( 'select', () => { it( 'registers multiple selectors to the public API', () => { const store = registerReducer( 'reducer1', () => 'state1' ); @@ -52,20 +345,30 @@ describe( 'select', () => { expect( select( 'reducer1' ).selector2() ).toEqual( 'result2' ); expect( selector2 ).toBeCalledWith( store.getState() ); } ); - - it( 'provides upgrade path for deprecated usage', () => { - const store = registerReducer( 'reducer', () => 'state' ); - const selector = jest.fn( () => 'result' ); - - registerSelectors( 'reducer', { selector } ); - - expect( select( 'reducer', 'selector', 'arg' ) ).toEqual( 'result' ); - expect( selector ).toBeCalledWith( store.getState(), 'arg' ); - expect( console ).toHaveWarned(); - } ); } ); describe( 'withSelect', () => { + let wrapper; + + const unsubscribes = []; + afterEach( () => { + let unsubscribe; + while ( ( unsubscribe = unsubscribes.shift() ) ) { + unsubscribe(); + } + + if ( wrapper ) { + wrapper.unmount(); + wrapper = null; + } + } ); + + function subscribeWithUnsubscribe( ...args ) { + const unsubscribe = subscribe( ...args ); + unsubscribes.push( unsubscribe ); + return unsubscribe; + } + it( 'passes the relevant data to the component', () => { registerReducer( 'reactReducer', () => ( { reactKey: 'reactState' } ) ); registerSelectors( 'reactReducer', { @@ -83,7 +386,7 @@ describe( 'withSelect', () => { data: _select( 'reactReducer' ).reactSelector( ownProps.keyName ), } ) )( ( props ) => <div>{ props.data }</div> ); - const wrapper = mount( <Component keyName="reactKey" /> ); + wrapper = mount( <Component keyName="reactKey" /> ); // Wrapper is the enhanced component. Find props on the rendered child. const child = wrapper.childAt( 0 ); @@ -92,8 +395,6 @@ describe( 'withSelect', () => { data: 'reactState', } ); expect( wrapper.text() ).toBe( 'reactState' ); - - wrapper.unmount(); } ); it( 'should rerun selection on state changes', () => { @@ -126,15 +427,13 @@ describe( 'withSelect', () => { </button> ) ); - const wrapper = mount( <Component /> ); + wrapper = mount( <Component /> ); const button = wrapper.find( 'button' ); button.simulate( 'click' ); expect( button.text() ).toBe( '1' ); - - wrapper.unmount(); } ); it( 'should rerun selection on props changes', () => { @@ -154,17 +453,121 @@ describe( 'withSelect', () => { count: _select( 'counter' ).getCount( ownProps.offset ), } ) )( ( props ) => <div>{ props.count }</div> ); - const wrapper = mount( <Component offset={ 0 } /> ); + wrapper = mount( <Component offset={ 0 } /> ); wrapper.setProps( { offset: 10 } ); expect( wrapper.childAt( 0 ).text() ).toBe( '10' ); + } ); - wrapper.unmount(); + it( 'ensures component is still mounted before setting state', () => { + // This test verifies that even though unsubscribe doesn't take effect + // until after the current listener stack is called, we don't attempt + // to setState on an unmounting `withSelect` component. It will fail if + // an attempt is made to `setState` on an unmounted component. + const store = registerReducer( 'counter', ( state = 0, action ) => { + if ( action.type === 'increment' ) { + return state + 1; + } + + return state; + } ); + + registerSelectors( 'counter', { + getCount: ( state, offset ) => state + offset, + } ); + + subscribeWithUnsubscribe( () => { + wrapper.unmount(); + } ); + + const Component = withSelect( ( _select, ownProps ) => ( { + count: _select( 'counter' ).getCount( ownProps.offset ), + } ) )( ( props ) => <div>{ props.count }</div> ); + + wrapper = mount( <Component offset={ 0 } /> ); + + store.dispatch( { type: 'increment' } ); + } ); + + it( 'should not rerun selection on unchanging state', () => { + const store = registerReducer( 'unchanging', ( state = {} ) => state ); + + registerSelectors( 'unchanging', { + getState: ( state ) => state, + } ); + + const mapSelectToProps = jest.fn(); + + const Component = compose( [ + withSelect( mapSelectToProps ), + ] )( () => <div /> ); + + wrapper = mount( <Component /> ); + + store.dispatch( { type: 'dummy' } ); + + expect( mapSelectToProps ).toHaveBeenCalledTimes( 1 ); + } ); + + it( 'omits props which are not returned on subsequent mappings', () => { + registerReducer( 'demo', ( state = 'OK' ) => state ); + registerSelectors( 'demo', { + getValue: ( state ) => state, + } ); + + const Component = withSelect( ( _select, ownProps ) => { + return { + [ ownProps.propName ]: _select( 'demo' ).getValue(), + }; + } )( () => <div /> ); + + wrapper = mount( <Component propName="foo" /> ); + + expect( wrapper.childAt( 0 ).props() ).toEqual( { foo: 'OK', propName: 'foo' } ); + + wrapper.setProps( { propName: 'bar' } ); + + expect( wrapper.childAt( 0 ).props() ).toEqual( { bar: 'OK', propName: 'bar' } ); + } ); + + it( 'allows undefined return from mapSelectToProps', () => { + registerReducer( 'demo', ( state = 'OK' ) => state ); + registerSelectors( 'demo', { + getValue: ( state ) => state, + } ); + + const Component = withSelect( ( _select, ownProps ) => { + if ( ownProps.pass ) { + return { + count: _select( 'demo' ).getValue(), + }; + } + } )( ( props ) => <div>{ props.count || 'Unknown' }</div> ); + + wrapper = mount( <Component pass={ false } /> ); + + expect( wrapper.childAt( 0 ).text() ).toBe( 'Unknown' ); + + wrapper.setProps( { pass: true } ); + + expect( wrapper.childAt( 0 ).text() ).toBe( 'OK' ); + + wrapper.setProps( { pass: false } ); + + expect( wrapper.childAt( 0 ).text() ).toBe( 'Unknown' ); } ); } ); describe( 'withDispatch', () => { + let wrapper; + afterEach( () => { + if ( wrapper ) { + wrapper.unmount(); + wrapper = null; + } + } ); + it( 'passes the relevant data to the component', () => { const store = registerReducer( 'counter', ( state = 0, action ) => { if ( action.type === 'increment' ) { @@ -186,7 +589,7 @@ describe( 'withDispatch', () => { }; } )( ( props ) => <button onClick={ props.increment } /> ); - const wrapper = mount( <Component count={ 0 } /> ); + wrapper = mount( <Component count={ 0 } /> ); // Wrapper is the enhanced component. Find props on the rendered child. const child = wrapper.childAt( 0 ); @@ -203,12 +606,24 @@ describe( 'withDispatch', () => { wrapper.find( 'button' ).simulate( 'click' ); expect( store.getState() ).toBe( 2 ); - - wrapper.unmount(); } ); } ); describe( 'subscribe', () => { + const unsubscribes = []; + afterEach( () => { + let unsubscribe; + while ( ( unsubscribe = unsubscribes.shift() ) ) { + unsubscribe(); + } + } ); + + function subscribeWithUnsubscribe( ...args ) { + const unsubscribe = subscribe( ...args ); + unsubscribes.push( unsubscribe ); + return unsubscribe; + } + it( 'registers multiple selectors to the public API', () => { let incrementedValue = null; const store = registerReducer( 'myAwesomeReducer', ( state = 0 ) => state + 1 ); @@ -233,6 +648,45 @@ describe( 'subscribe', () => { expect( incrementedValue ).toBe( 3 ); } ); + + it( 'snapshots listeners on change, avoiding a later listener if subscribed during earlier callback', () => { + const store = registerReducer( 'myAwesomeReducer', ( state = 0 ) => state + 1 ); + const secondListener = jest.fn(); + const firstListener = jest.fn( () => { + subscribeWithUnsubscribe( secondListener ); + } ); + + subscribeWithUnsubscribe( firstListener ); + + store.dispatch( { type: 'dummy' } ); + + expect( secondListener ).not.toHaveBeenCalled(); + } ); + + it( 'snapshots listeners on change, calling a later listener even if unsubscribed during earlier callback', () => { + const store = registerReducer( 'myAwesomeReducer', ( state = 0 ) => state + 1 ); + const firstListener = jest.fn( () => { + secondUnsubscribe(); + } ); + const secondListener = jest.fn(); + + subscribeWithUnsubscribe( firstListener ); + const secondUnsubscribe = subscribeWithUnsubscribe( secondListener ); + + store.dispatch( { type: 'dummy' } ); + + expect( secondListener ).toHaveBeenCalled(); + } ); + + it( 'does not call listeners if state has not changed', () => { + const store = registerReducer( 'unchanging', ( state = {} ) => state ); + const listener = jest.fn(); + subscribeWithUnsubscribe( listener ); + + store.dispatch( { type: 'dummy' } ); + + expect( listener ).not.toHaveBeenCalled(); + } ); } ); describe( 'dispatch', () => { @@ -253,3 +707,114 @@ describe( 'dispatch', () => { expect( store.getState() ).toBe( 5 ); } ); } ); + +describe( 'isActionLike', () => { + it( 'returns false if non-action-like', () => { + expect( isActionLike( undefined ) ).toBe( false ); + expect( isActionLike( null ) ).toBe( false ); + expect( isActionLike( [] ) ).toBe( false ); + expect( isActionLike( {} ) ).toBe( false ); + expect( isActionLike( 1 ) ).toBe( false ); + expect( isActionLike( 0 ) ).toBe( false ); + expect( isActionLike( Infinity ) ).toBe( false ); + expect( isActionLike( { type: null } ) ).toBe( false ); + } ); + + it( 'returns true if action-like', () => { + expect( isActionLike( { type: 'POW' } ) ).toBe( true ); + } ); +} ); + +describe( 'isAsyncIterable', () => { + it( 'returns false if not async iterable', () => { + expect( isAsyncIterable( undefined ) ).toBe( false ); + expect( isAsyncIterable( null ) ).toBe( false ); + expect( isAsyncIterable( [] ) ).toBe( false ); + expect( isAsyncIterable( {} ) ).toBe( false ); + } ); + + it( 'returns true if async iterable', async () => { + async function* getAsyncIterable() { + yield new Promise( ( resolve ) => process.nextTick( resolve ) ); + } + + const result = getAsyncIterable(); + + expect( isAsyncIterable( result ) ).toBe( true ); + + await result; + } ); +} ); + +describe( 'isIterable', () => { + it( 'returns false if not iterable', () => { + expect( isIterable( undefined ) ).toBe( false ); + expect( isIterable( null ) ).toBe( false ); + expect( isIterable( {} ) ).toBe( false ); + expect( isIterable( Promise.resolve( {} ) ) ).toBe( false ); + } ); + + it( 'returns true if iterable', () => { + function* getIterable() { + yield 'foo'; + } + + const result = getIterable(); + + expect( isIterable( result ) ).toBe( true ); + expect( isIterable( [] ) ).toBe( true ); + } ); +} ); + +describe( 'toAsyncIterable', () => { + it( 'normalizes async iterable', async () => { + async function* getAsyncIterable() { + yield await Promise.resolve( { ok: true } ); + } + + const object = getAsyncIterable(); + const normalized = toAsyncIterable( object ); + + expect( ( await normalized.next() ).value ).toEqual( { ok: true } ); + } ); + + it( 'normalizes promise', async () => { + const object = Promise.resolve( { ok: true } ); + const normalized = toAsyncIterable( object ); + + expect( ( await normalized.next() ).value ).toEqual( { ok: true } ); + } ); + + it( 'normalizes object', async () => { + const object = { ok: true }; + const normalized = toAsyncIterable( object ); + + expect( ( await normalized.next() ).value ).toEqual( { ok: true } ); + } ); + + it( 'normalizes array of promise', async () => { + const object = [ Promise.resolve( { ok: true } ) ]; + const normalized = toAsyncIterable( object ); + + expect( ( await normalized.next() ).value ).toEqual( { ok: true } ); + } ); + + it( 'normalizes mixed array', async () => { + const object = [ { foo: 'bar' }, Promise.resolve( { ok: true } ) ]; + const normalized = toAsyncIterable( object ); + + expect( ( await normalized.next() ).value ).toEqual( { foo: 'bar' } ); + expect( ( await normalized.next() ).value ).toEqual( { ok: true } ); + } ); + + it( 'normalizes generator', async () => { + function* getIterable() { + yield Promise.resolve( { ok: true } ); + } + + const object = getIterable(); + const normalized = toAsyncIterable( object ); + + expect( ( await normalized.next() ).value ).toEqual( { ok: true } ); + } ); +} ); diff --git a/date/index.js b/date/index.js index 6a84fdb3e5620d..3c8cb2bc37c7e6 100644 --- a/date/index.js +++ b/date/index.js @@ -1,4 +1,23 @@ -import moment from 'moment'; +import momentLib from 'moment'; +import 'moment-timezone'; +import 'moment-timezone/moment-timezone-utils'; + +export const settings = window._wpDateSettings; + +// Create WP timezone based off dateSettings. +momentLib.tz.add( momentLib.tz.pack( { + name: 'WP', + abbrs: [ 'WP' ], + untils: [ null ], + offsets: [ -settings.timezone.offset * 60 || 0 ], +} ) ); + +// Create a new moment object which mirrors moment but includes +// the attached timezone, instead of setting a default timezone on +// the global moment object. +export const moment = ( ...args ) => { + return momentLib.tz( ...args, 'WP' ); +}; // Date constants. /** @@ -112,7 +131,7 @@ const formatMap = { * @return {string} Formatted date. */ B( momentDate ) { - const timezoned = moment( momentDate ).utcOffset( 60 ); + const timezoned = momentLib( momentDate ).utcOffset( 60 ); const seconds = parseInt( timezoned.format( 's' ), 10 ), minutes = parseInt( timezoned.format( 'm' ), 10 ), hours = parseInt( timezoned.format( 'H' ), 10 ); @@ -171,37 +190,37 @@ const formatMap = { /** * Adds a locale to moment, using the format supplied by `wp_localize_script()`. * - * @param {Object} settings Settings, including locale data. + * @param {Object} localSettings Settings, including locale data. */ -function setupLocale( settings ) { +function setupLocale( localSettings ) { // Backup and restore current locale. - const currentLocale = moment.locale(); - moment.updateLocale( settings.l10n.locale, { + const currentLocale = momentLib.locale(); + momentLib.updateLocale( localSettings.l10n.locale, { // Inherit anything missing from the default locale. parentLocale: currentLocale, - months: settings.l10n.months, - monthsShort: settings.l10n.monthsShort, - weekdays: settings.l10n.weekdays, - weekdaysShort: settings.l10n.weekdaysShort, + months: localSettings.l10n.months, + monthsShort: localSettings.l10n.monthsShort, + weekdays: localSettings.l10n.weekdays, + weekdaysShort: localSettings.l10n.weekdaysShort, meridiem( hour, minute, isLowercase ) { if ( hour < 12 ) { - return isLowercase ? settings.l10n.meridiem.am : settings.l10n.meridiem.AM; + return isLowercase ? localSettings.l10n.meridiem.am : localSettings.l10n.meridiem.AM; } - return isLowercase ? settings.l10n.meridiem.pm : settings.l10n.meridiem.PM; + return isLowercase ? localSettings.l10n.meridiem.pm : localSettings.l10n.meridiem.PM; }, longDateFormat: { - LT: settings.formats.time, + LT: localSettings.formats.time, LTS: null, L: null, - LL: settings.formats.date, - LLL: settings.formats.datetime, + LL: localSettings.formats.date, + LLL: localSettings.formats.datetime, LLLL: null, }, // From human_time_diff? // Set to `(number, withoutSuffix, key, isFuture) => {}` instead. relativeTime: { - future: settings.l10n.relative.future, - past: settings.l10n.relative.past, + future: localSettings.l10n.relative.future, + past: localSettings.l10n.relative.past, s: 'seconds', m: 'a minute', mm: '%d minutes', @@ -215,7 +234,7 @@ function setupLocale( settings ) { yy: '%d years', }, } ); - moment.locale( currentLocale ); + momentLib.locale( currentLocale ); } /** @@ -231,7 +250,7 @@ function setupLocale( settings ) { export function format( dateFormat, dateValue = new Date() ) { let i, char; let newFormat = []; - const momentDate = moment( dateValue ); + const momentDate = momentLib( dateValue ); for ( i = 0; i < dateFormat.length; i++ ) { char = dateFormat[ i ]; // Is this an escape? @@ -271,7 +290,7 @@ export function format( dateFormat, dateValue = new Date() ) { */ export function date( dateFormat, dateValue = new Date() ) { const offset = window._wpDateSettings.timezone.offset * HOUR_IN_MINUTES; - const dateMoment = moment( dateValue ).utcOffset( offset, true ); + const dateMoment = momentLib( dateValue ).utcOffset( offset, true ); return format( dateFormat, dateMoment ); } @@ -286,7 +305,7 @@ export function date( dateFormat, dateValue = new Date() ) { * @return {string} Formatted date. */ export function gmdate( dateFormat, dateValue = new Date() ) { - const dateMoment = moment( dateValue ).utc(); + const dateMoment = momentLib( dateValue ).utc(); return format( dateFormat, dateMoment ); } @@ -306,7 +325,7 @@ export function dateI18n( dateFormat, dateValue = new Date(), gmt = false ) { // Defaults. const offset = gmt ? 0 : window._wpDateSettings.timezone.offset * HOUR_IN_MINUTES; // Convert to moment object. - const dateMoment = moment( dateValue ).utcOffset( offset, true ); + const dateMoment = momentLib( dateValue ).utcOffset( offset, true ); // Set the locale. dateMoment.locale( window._wpDateSettings.l10n.locale ); @@ -314,7 +333,5 @@ export function dateI18n( dateFormat, dateValue = new Date(), gmt = false ) { return format( dateFormat, dateMoment ); } -export const settings = window._wpDateSettings; - // Initialize. setupLocale( window._wpDateSettings ); diff --git a/docker-compose.yml b/docker-compose.yml index 22ff627e88bf23..ff07ac52bd180d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,6 +12,7 @@ services: volumes: - wordpress:/var/www/html - .:/var/www/html/wp-content/plugins/gutenberg + - ./test/e2e/test-plugins:/var/www/html/wp-content/plugins/gutenberg-test-plugins cli: image: wordpress:cli diff --git a/docs/articles.md b/docs/articles.md index 34bdf6cec8d999..558f0de43014fd 100644 --- a/docs/articles.md +++ b/docs/articles.md @@ -23,6 +23,7 @@ This includes useful articles for those wanting to run a meetup or promote Guten ## Article Compilations +- [Curated Collection of Gutenberg Articles, Plugins, Blocks, Tutorials, etc](http://gutenberghub.com/), By Munir Kamal - [Articles about Gutenberg](https://github.com/WordPress/gutenberg/issues/1419) (Github Issue thread with links) - [Gutenberg articles on ManageWP.org](https://managewp.org/search?q=gutenberg) -- [Storify stream](https://storify.com/bph/wordpress-gutenberg-early-comments) +- [Gutenberg Times](https://gutenbergtimes.com/category/updates/) diff --git a/docs/attributes.md b/docs/attributes.md index 87df1607b958ee..5262c6d0204e47 100644 --- a/docs/attributes.md +++ b/docs/attributes.md @@ -173,6 +173,8 @@ function gutenberg_my_block_init() { add_action( 'init', 'gutenberg_my_block_init' ); ``` +If you'd like to use an object or an array in an attribute, you can register a `string` attribute type and use JSON as the intermediary. Serialize the structured data to JSON prior to saving, and then deserialize the JSON string on the server. Keep in mind that you're responsible for the integrity of the data; make sure to properly sanitize, accommodate missing data, etc. + Lastly, make sure that you respect the data's type when setting attributes, as the framework does not automatically perform type casting of meta. Incorrect typing in block attributes will result in a post remaining dirty even after saving (_cf._ `isEditedPostDirty`, `hasEditedAttributes`). For instance, if `authorCount` is an integer, remember that event handlers may pass a different kind of data, thus the value should be cast explicitly: ```js diff --git a/docs/block-api.md b/docs/block-api.md index 26254182edb11d..34bb7596f111b8 100644 --- a/docs/block-api.md +++ b/docs/block-api.md @@ -250,6 +250,7 @@ transforms: { ``` {% end %} +To control the priority with which a transform is applied, define a `priority` numeric property on your transform object, where a lower value will take precedence over higher values. This behaves much like a [WordPress hook](https://codex.wordpress.org/Plugin_API#Hook_to_WordPress). Like hooks, the default priority is `10` when not otherwise set. #### useOnce (optional) @@ -279,14 +280,14 @@ anchor: true, - `customClassName` (default `true`): This property adds a field to define a custom className for the block's wrapper. ```js -// Remove the support for a the custom className . +// Remove the support for the custom className. customClassName: false, ``` - `className` (default `true`): By default, Gutenberg adds a class with the form `.wp-block-your-block-name` to the root element of your saved markup. This helps having a consistent mechanism for styling blocks that themes and plugins can rely on. If for whatever reason a class is not desired on the markup, this functionality can be disabled. ```js -// Remove the support for a the generated className . +// Remove the support for the generated className. className: false, ``` diff --git a/docs/coding-guidelines.md b/docs/coding-guidelines.md index 53c811f5d27e8e..dabdf0c316affc 100644 --- a/docs/coding-guidelines.md +++ b/docs/coding-guidelines.md @@ -62,7 +62,7 @@ import TinyMCE from 'tinymce'; #### WordPress Dependencies -To encourage reusability between features, our JavaScript is split into domain-specific modules which [`export`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export) one or more functions or objects. In the Gutenberg project, we've distinguished these modules under top-level directories `blocks`, `components`, `editor`, `edit-post`, `element`, `data` and `i18n`. These each serve an independent purpose, and often code is shared between them. For example, in order to localize its text, editor code will need to include functions from the `i18n` module. +To encourage reusability between features, our JavaScript is split into domain-specific modules which [`export`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export) one or more functions or objects. In the Gutenberg project, we've distinguished these modules under top-level directories. Each module serve an independent purpose, and often code is shared between them. For example, in order to localize its text, editor code will need to include functions from the `i18n` module. Example: @@ -86,6 +86,18 @@ Example: import VisualEditor from '../visual-editor'; ``` +### Experimental APIs + +Exposed APIs that are still being tested, discussed and are subject to change should be prefixed with `__experimental`, until they are finalized. This is meant to discourage developers from relying on the API, because it might be removed or changed in the (near) future. + +Example: + +```js +export { + internalApi as __experimentalExposedApi +} from './internalApi.js'; +``` + ## PHP We use diff --git a/docs/deprecated-blocks.md b/docs/deprecated-blocks.md index 345c398a734b56..3b952088e717df 100644 --- a/docs/deprecated-blocks.md +++ b/docs/deprecated-blocks.md @@ -173,3 +173,96 @@ registerBlockType( 'gutenberg/block-with-deprecated-version', { {% end %} In the example above we updated the markup of the block to use a `div` instead of `p` and rename the `text` attribute to `content`. + + +## Changing the innerBlocks + +Situations may exist where when migrating the block we may need to add or remove innerBlocks. +E.g: a block wants to migrate a title attribute to a paragraph innerBlock. + +### Example: + +{% codetabs %} +{% ES5 %} +```js +var el = wp.element.createElement, + registerBlockType = wp.blocks.registerBlockType; + +registerBlockType( 'gutenberg/block-with-deprecated-version', { + + // ... block properties go here + + deprecated: [ + { + attributes: { + title: { + type: 'array', + source: 'children', + selector: 'p', + }, + }, + + migrate: function( attributes, innerBlocks ) { + return [ + omit( attributes, 'title' ), + [ + createBlock( 'core/paragraph', { + content: attributes.title, + fontSize: 'large', + } ), + ].concat( innerBlocks ), + ]; + }, + + save: function( props ) { + return el( 'p', {}, props.attributes.title ); + }, + } + ] +} ); +``` +{% ESNext %} +```js +const { registerBlockType } = wp.blocks; + +registerBlockType( 'gutenberg/block-with-deprecated-version', { + + // ... block properties go here + + save( props ) { + return <p>{ props.attributes.title }</div>; + }, + + deprecated: [ + { + attributes: { + title: { + type: 'array', + source: 'children', + selector: 'p', + }, + }, + + migrate( attributes, innerBlocks ) { + return [ + omit( attributes, 'title' ), + [ + createBlock( 'core/paragraph', { + content: attributes.title, + fontSize: 'large', + } ), + ...innerBlocks, + ], + ]; + }, + + save( props ) { + return <p>{ props.attributes.title }</div>; + }, + } + ] +} ); +``` +{% end %} + +In the example above we updated the block to use an inner paragraph block with a title instead of a title attribute. diff --git a/docs/design.md b/docs/design.md index b46ac07d3c9afe..1915883a7da9da 100644 --- a/docs/design.md +++ b/docs/design.md @@ -216,8 +216,8 @@ This concept is speculative, but it's one direction Gutenberg could go in the fu ## More resources -If you'd like to contribute, you can download a Sketch file of the Gutenberg mockups. Note that those are still mockups, and not 1:1 accurate: **<a href="https://cloudup.com/ccnN8GCsXwC">Download Sketch file</a>**. +If you'd like to contribute, you can download a Sketch file of the Gutenberg mockups. Note that those are still mockups, and not 1:1 accurate. It is also possibole that the Sketch files aren't up-to-date with the latest Gutenberg itself, as development sometimes moves faster than our updating of the Sketch files! -A pattern file is here: **<a href="https://cloudup.com/c0Q8RQDHByq">Download Pattern Sketch file</a>** +**<a href="https://cloudup.com/c8Rbgsgg3nq">Download Sketch mockups & patterns files</a>**. Be sure to also read <a href="https://wordpress.org/gutenberg/handbook/reference/faq/">the FAQ</a>, and <a href="https://wordpress.org/gutenberg/handbook/">how to build blocks</a>. diff --git a/docs/extensibility.md b/docs/extensibility.md index 7913499cdce651..ee1931cecbcacf 100644 --- a/docs/extensibility.md +++ b/docs/extensibility.md @@ -1,11 +1,11 @@ # Extensibility -Extensibility is key for WordPress and like the rest of WordPress components, Gutenberg is highly extensible. +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 static blocks, dynamic blocks rendering on the server and also blocks saving data to Post Meta for more structured content. +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): @@ -25,101 +25,32 @@ wp.blocks.registerBlockType( 'mytheme/red-block', { } ); ``` -If you want to learn more about block creation, The [Blocks Tutorial](./blocks) is the best place to start. +If you want to learn more about block creation, the [Blocks Tutorial](./blocks.md) is the best place to start. +## Extending Blocks -## Removing Blocks +It is also possible to modify the behavior of existing blocks or even remove them completely using filters. -### Using a blacklist +Learn more in the [Extending Blocks](./extensibility/extending-blocks.md) section. -Adding blocks is easy enough, removing them is as easy. Plugin or theme authors have the possibility to "unregister" blocks. +## Extending the Editor UI -```js -// myplugin.js - -wp.blocks.unregisterBlockType( 'core/verse' ); -``` - -and load this script in the Editor - -```php -<?php -// myplugin.php - -function myplugin_blacklist_blocks() { - wp_enqueue_script( - 'myplugin-blacklist-blocks', - plugins_url( 'myplugin.js', __FILE__ ), - array( 'wp-blocks' ) - ); -} -add_action( 'enqueue_block_editor_assets', 'myplugin_blacklist_blocks' ); -``` - - -### Using a whitelist - -If you want to disable all blocks except a whitelisted list, you can adapt the script above like so: - -```js -// myplugin.js -var allowedBlocks = [ - 'core/paragraph', - 'core/image', - 'core/html', - 'core/freeform' -]; - -wp.blocks.getBlockTypes().forEach( function( blockType ) { - if ( allowedBlocks.indexOf( blockType.name ) === -1 ) { - wp.blocks.unregisterBlockType( blockType.name ); - } -} ); -``` - -## Hiding blocks from the inserter - -On the server, you can filter the list of blocks shown in the inserter using the `allowed_block_types` filter. you can return either true (all block types supported), false (no block types supported), or an array of block type names to allow. - -```php -add_filter( 'allowed_block_types', function() { - return [ 'core/paragraph' ]; -} ); -``` +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/plugins/README.md) and [Edit Post](https://github.com/WordPress/gutenberg/blob/master/edit-post/README.md) section for more information. -## Modifying Blocks (Experimental) +## Meta Boxes -To modify the behaviour of existing blocks, Gutenberg exposes a list of filters: +**Porting PHP meta boxes to blocks is highly encouraged!** -- `blocks.registerBlockType`: Used to filter the block settings. It receives the block settings and the name of the block the registered block as arguments. +Discover how [Meta Box](./extensibility/meta-box.md) support works in Gutenberg. -- `blocks.getSaveElement`: A filter that applies to the result of a block's `save` function. This filter is used to replace or extend the element, for example using `wp.element.cloneElement` to modify the element's props or replace its children, or returning an entirely new element. +## Theme Support -- `blocks.getSaveContent.extraProps`: A filter that applies to all blocks returning a WP Element in the `save` function. This filter is used to add extra props to the root element of the `save` function. For example: to add a className, an id, or any valid prop for this element. It receives the current props of the `save` element, the block Type and the block attributes as arguments. - -- `blocks.BlockEdit`: Used to modify the block's `edit` component. It receives the original block `edit` component and returns a new wrapped component. - -**Example** - -Adding a background by default to all blocks. - -```js -// Our filter function -function addBackgroundProp( props ) { - return Object.assign( props, { style: { backgroundColor: 'red' } } ); -} - -// Adding the filter -wp.hooks.addFilter( - 'blocks.getSaveContent.extraProps', - 'myplugin/add-background', - addBackgroundProp -); -``` +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. -_Note:_ This filter must always be run on every page load, and not in your browser's developer tools console. Otherwise, a [block validation](https://wordpress.org/gutenberg/handbook/block-api/block-edit-save/#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. +There are some advanced block features which require opt-in support in the theme. See [theme support](./extensibility/theme-support.md). -## Extending the editor's UI (Slot and Fill) +## Autocomplete -Coming soon. +Autocompleters within blocks may be extended and overridden. See [autocomplete](./extensibility/autocomplete.md). diff --git a/docs/manifest.json b/docs/manifest.json index 22aa553fa583f4..f1630e0aec0e60 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -53,16 +53,28 @@ "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": "Meta Boxes", "slug": "meta-box", - "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/meta-box.md", + "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\/themes.md", + "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" }, { @@ -155,6 +167,12 @@ "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/release.md", "parent": "reference" }, + { + "title": "Deprecated Features", + "slug": "deprecated", + "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/deprecated.md", + "parent": "reference" + }, { "title": "Repository Management", "slug": "repository-management", diff --git a/docs/meetups.md b/docs/meetups.md index 11d394e12f186f..273db729910069 100644 --- a/docs/meetups.md +++ b/docs/meetups.md @@ -12,3 +12,5 @@ A list of meetups about Gutenberg so far: - [What's New In WordPress 4.9 and Gutenberg 1.5](https://www.meetup.com/Tuscaloosa-WordPress-Meetup/events/244584939/), Tuscaloosa, Alabama, USA - [WordPress & JavaScript: Let's talk Gutenberg!](https://www.meetup.com/WordPress-Lahore/events/246446478/), Lahore, PK - [The state of Gutenberg](https://www.meetup.com/WP-Porto/events/245585131/), Porto, Portugal +- [Discuss and learn about the new WordPress Editor : Gutenberg](https://www.meetup.com/Pune-WordPress-Knowledge-Exchange/events/248496830/), Pune, India +- [WordPress 5.0 - Gutenberg is upon us](https://www.meetup.com/WordPress-Perth/events/249490075/), Perth, Australia diff --git a/docs/meta-box.md b/docs/meta-box.md deleted file mode 100644 index 2d2f153cd9e38c..00000000000000 --- a/docs/meta-box.md +++ /dev/null @@ -1,85 +0,0 @@ -# Meta Boxes - -This is a brief document detailing how meta box support works in Gutenberg. With the superior developer and user experience of blocks however, especially once block templates are available, **converting PHP meta boxes to blocks is highly encouraged!** - -### Testing, Converting, and Maintaining Existing Meta Boxes - -Before converting meta boxes to blocks, it may be easier to test if a meta box works with Gutenberg, and explicitly mark it as such. - -If a meta box *doesn't* work with in Gutenberg, and updating it to work correctly is not an option, the next step is to add the `__block_editor_compatible_meta_box` argument to the meta box declaration: - -```php -add_meta_box( 'my-meta-box', 'My Meta Box', 'my_meta_box_callback', - null, 'normal', 'high', - array( - '__block_editor_compatible_meta_box' => false, - ) -); -``` - -This will cause WordPress to fall back to the Classic editor, where the meta box will continue working as before. - -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, of course). - -After a meta box is converted to a block, it can be declared as existing for backwards compatibility: - -```php -add_meta_box( 'my-meta-box', 'My Meta Box', 'my_meta_box_callback', - null, 'normal', 'high', - array( - '__back_compat_meta_box' => false, - ) -); -``` - -When Gutenberg is run, 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 be displayed correctly in the Classic editor, should some other meta box cause a fallback. - -### Meta Box Data Collection - -On each Gutenberg page load, we register an action that collects the meta box data to determine if an area is empty. The original global state is reset upon collection of meta box data. - -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. - -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`. - -Ideally, this could be done at instantiation of the editor and help simplify this flow. However, it is not possible to know the meta box state before `admin_enqueue_scripts`, where we are calling `initializeEditor()`. This will have to do, unless we want to move `initializeEditor()` to fire in the footer or at some point after `admin_head`. With recent changes to editor bootstrapping this might now be possible. Test with ACF to make sure. - -### Redux and React Meta Box Management - -When rendering the Gutenberg Page, the metaboxes are rendered to a hidden div `#metaboxes`. - -*The Redux store by default will hold all meta boxes as inactive*. When -`INITIALIZE_META_BOX_STATE` comes in, the store will update any active meta box areas by setting the `isActive` flag to `true`. Once this happens React will check for the new props sent in by Redux on the `MetaBox` component. If that `MetaBox` is now active, instead of rendering null, a `MetaBoxArea` component will be rendered. The `MetaBox` component is the container component that mediates between the `MetaBoxArea` and the Redux Store. *If no meta boxes are active, nothing happens. This will be the default behavior, as all core meta boxes have been stripped.* - -#### MetaBoxArea Component - -When the component renders it will store a ref to the metaboxes container, retrieve the metaboxes HTML from the prefetch location. - -When the post is updated, only meta boxes areas that are active will be submitted. This removes any unnecessary requests being made. No extra revisions, are created either by the meta box submissions. A Redux action will trigger on `REQUEST_POST_UPDATE` for any active meta box. See `editor/effects.js`. The `REQUEST_META_BOX_UPDATES` action will set that meta boxes' state to `isUpdating`, the `isUpdating` prop will be sent into the `MetaBoxArea` and cause a form submission. - -If the metabox area is saving, we display an updating overlay, to prevent users from changing the form values while the meta box is submitting. - -When the new block editor was made into the default editor it is now required to provide the classic-editor flag to access the metabox partial page. - -`gutenberg_meta_box_save()` is used to save the meta boxes changes. A `meta_box` request parameter should be present and should match one of `'advanced'`, `'normal'`, or `'side'`. This value will determine which meta box area is served. - -So an example url would look like: - -`mysite.com/wp-admin/post.php?post=1&action=edit&meta_box=$location&classic-editor` - -This url is automatically passed into React via a `_wpMetaBoxUrl` global variable. - -Thus page page mimics the `post.php` post form, so when it is submitted it will normally fire all of the necessary hooks and actions, and have the proper global state to correctly fire any PHP meta box mumbo jumbo without needing to modify any existing code. On successful submission, React will signal a `handleMetaBoxReload` to remove the updating overlay, and set the store to no longer be updating the meta box area. - - -### Common Compatibility Issues - -Most PHP meta boxes should continue to work in Gutenberg, however some meta boxes that include advanced functionality could break. The following list describes some of the most common reasons why meta boxes might not work as expected in Gutenberg: - -- 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". diff --git a/docs/readme.md b/docs/readme.md index 44c365be4c1928..ee27d55debfddd 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -15,3 +15,7 @@ Gutenberg is being developed on [GitHub](https://github.com/WordPress/gutenberg) ## Logo Released under GPL license, made by [Cristel Rossignol](https://twitter.com/cristelrossi). [Gutenberg logo](https://github.com/WordPress/gutenberg/blob/master/docs/final-g-wapuu-black.svg). + +## Mockups + +Mockup Sketch files are available in <a href="https://wordpress.org/gutenberg/handbook/reference/design-principles/#more-resources">the Design section</a>. diff --git a/docs/release.md b/docs/release.md index 4e371fdffbd0f2..71d1e3b009b571 100644 --- a/docs/release.md +++ b/docs/release.md @@ -4,7 +4,7 @@ This document is a checklist for building and releasing a new version of Gutenbe ## Writing the Release Post and Changelog -* Open the [recently updated PRs view](https://github.com/WordPress/gutenberg/pulls?page=2&q=is%3Apr+is%3Aclosed+sort%3Aupdated-desc), and find the PR where the last version bump occurred. +* Open the [recently updated PRs view](https://github.com/WordPress/gutenberg/pulls?q=is%3Apr+is%3Aclosed+sort%3Aupdated-desc), and find the PR where the last version bump occurred. * Read through each PR since then, to determine if it needs to be included in the Release Post and/or changelog. * Choose a feature or two to highlight in the release post - record an animation of them in action. * Save the draft post on [make.wordpress.org/core](https://make.wordpress.org/core/), for publishing after the release. @@ -31,12 +31,13 @@ Note: The `1.x.0` notation `git` and `svn` commands should be replaced with the * Have a checkout of https://wordpress.org/plugins/gutenberg/. First time: `svn checkout https://plugins.svn.wordpress.org/gutenberg` Subsequent times: `svn up` -* Delete the contents of `trunk` except for the `readme.txt` file (this file doesn’t exist in github, only on svn). +* Delete the contents of `trunk` except for the `readme.txt` and `changelog.txt` files (these files don’t exist in github, only on svn). * Copy all the contents of the zip file to `trunk`. -* Edit `readme.txt` to include the changelog for the current release. +* Edit `readme.txt` to include the changelog for the current release, replacing the previous release's. +* Add the changelog for the current release to `changelog.txt`. * Add new files to the SVN repo, and remove old files, in the `trunk` directory: - Add new files: `svn st | grep '^\?' | awk '{print $2}' | svn add` - Delete old files: `svn st | grep '^!' | awk '{print $2}' | svn rm` + Add new files: `svn st | grep '^\?' | awk '{print $2}' | xargs svn add` + Delete old files: `svn st | grep '^!' | awk '{print $2}' | xargs svn rm` * Commit the new version to `trunk`: `svn ci -m "Committing version 1.x.0"` * Tag the new version. Change to the parent directory, and run: diff --git a/docs/repository-management.md b/docs/repository-management.md index caf6ced90c4cb4..d1787e868a4f90 100644 --- a/docs/repository-management.md +++ b/docs/repository-management.md @@ -115,6 +115,8 @@ A pull request can generally be merged once it is: The final pull request merge decision is made by the **@wordpress/gutenberg-core** team. +Please make sure to assign your merged pull request to its release milestone. Doing so creates the historical legacy of what code landed when, and makes it possible for all project contributors (even non-technical ones) to access this information. + ### Closing Pull Requests Sometimes, a pull request may not be mergeable, no matter how much additional effort is applied to it (e.g. out of scope). In these cases, it’s best to communicate with the contributor graciously while describing why the pull request was closed, this encourages productive future involvement. diff --git a/docs/templates.md b/docs/templates.md index d136f21bedadae..86879f6446f989 100644 --- a/docs/templates.md +++ b/docs/templates.md @@ -34,7 +34,7 @@ const template = [ A custom post type can register its own template during registration: ```php -function register_post_type() { +function myplugin_register_book_post_type() { $args = array( 'public' => true, 'label' => 'Books', @@ -53,7 +53,7 @@ function register_post_type() { ); register_post_type( 'book', $args ); } -add_action( 'init', 'register_post_type' ); +add_action( 'init', 'myplugin_register_book_post_type' ); ``` ### Locking @@ -85,3 +85,22 @@ function my_add_template_to_posts() { } add_action( 'init', 'my_add_template_to_posts' ); ``` + +## Nested Templates + +Container blocks like the columns blocks also support templates. This is achieved by assigned a nested template to the block. + +```php +$template = array( + array( 'core/paragraph', array( + 'placeholder' => 'Add a root-level paragraph', + ) ), + array( 'core/columns', array(), array( + array( 'core/image', array( 'layout' => 'column-1' ) ), + array( 'core/paragraph', array( + 'placeholder' => 'Add a inner paragraph', + 'layout' => 'column-2' + ) ), + ) ) +); +``` \ No newline at end of file diff --git a/docs/testing-overview.md b/docs/testing-overview.md index b4242382137a59..ed301b3a567c78 100644 --- a/docs/testing-overview.md +++ b/docs/testing-overview.md @@ -4,9 +4,9 @@ Gutenberg contains both PHP and JavaScript code, and encourages testing and code ## Why test? -Aside from the joy testing will bring to your life, tests are important not only because they help to ensure that our application behaves as it should, but also because they provide concise examples of how to use a piece of code. +Aside from the joy testing will bring to your life, tests are important not only because they help to ensure that our application behaves as it should, but also because they provide concise examples of how to use a piece of code. -Tests are also part of our code base, which means we apply to them the same standards we apply to all our application code. +Tests are also part of our code base, which means we apply to them the same standards we apply to all our application code. As with all code, tests have to be maintained. Writing tests for the sake of having a test isn't the goal – rather we should try to strike the right balance between covering expected and unexpected behaviours, speedy execution and code maintenance. @@ -44,7 +44,7 @@ Keep your tests in a `test` folder in your working directory. The test file shou Only test files (with at least one test case) should live directly under `/test`. If you need to add external mocks or fixtures, place them in a sub folder, for example: * `test/mocks/[file-name.js` -* `test/fixtures/[file-name].js` +* `test/fixtures/[file-name].js` ### Importing tests @@ -90,7 +90,7 @@ describe( 'CheckboxWithLabel', () => { The Jest API includes some nifty [setup and teardown methods](https://facebook.github.io/jest/docs/en/setup-teardown.html) that allow you to perform tasks *before* and *after* each or all of your tests, or tests within a specific `describe` block. -These methods can handle asynchronous code to allow setup that you normally cannot do inline. As with [individual test cases](https://facebook.github.io/jest/docs/en/asynchronous.html#promises), you can return a Promise and Jest will wait for it to resolve: +These methods can handle asynchronous code to allow setup that you normally cannot do inline. As with [individual test cases](https://facebook.github.io/jest/docs/en/asynchronous.html#promises), you can return a Promise and Jest will wait for it to resolve: ```javascript // one-time setup for *all* tests @@ -105,7 +105,7 @@ afterAll( () => { ``` `afterEach` and `afterAll` provide a perfect (and preferred) way to 'clean up' after our tests, for example, by resetting state data. - + Avoid placing clean up code after assertions since, if any of those tests fail, the clean up won't take place and may cause failures in unrelated tests. ### Mocking dependencies @@ -152,9 +152,9 @@ Because we're passing the list as an argument, we can pass mock `validValuesLis #### Imported dependencies -Often our code will use methods and properties from imported external and internal libraries in multiple places, which makes passing around arguments messy and impracticable. For these cases `jest.mock` offers a neat way to stub these dependencies. +Often our code will use methods and properties from imported external and internal libraries in multiple places, which makes passing around arguments messy and impracticable. For these cases `jest.mock` offers a neat way to stub these dependencies. -For instance, lets assume we have `config` module to control a great deal of functionality via feature flags. +For instance, lets assume we have `config` module to control a great deal of functionality via feature flags. ```javascript // bilbo.js @@ -361,10 +361,10 @@ or interactively npm run test-e2e:watch ``` -If you're using another local environment setup, you can still run the e2e tests by overriding the base URL and the default WP username/password used in the tests like so: +If you're using a different setup, you can provide the base URL, username and password like this: ```bash -cypress_base_url=http://my-custom-basee-url cypress_username=myusername cypress_password=mypassword npm run test-e2e +WP_BASE_URL=http://localhost:8888 WP_USERNAME=admin WP_PASSWORD=password npm run test-e2e ``` ## PHP Testing diff --git a/docs/themes.md b/docs/themes.md deleted file mode 100644 index b5b4fb7cc08d79..00000000000000 --- a/docs/themes.md +++ /dev/null @@ -1,55 +0,0 @@ -# Blocks support by themes - -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 theme provides. - -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. - -To opt-in for one of these features, call `add_theme_support` in the `functions.php` file of the theme. For example: - -```php -function mytheme_setup_theme_supported_features() { - add_theme_support( 'editor-color-palette', - '#a156b4', - '#d0a5db', - '#eee', - '#444' - ); -} - -add_action( 'after_setup_theme', 'mytheme_setup_theme_supported_features' ); -``` - -## Opt-in features - -### Wide Alignment: - -Some blocks such as the image block have the possibility to define a "wide" or "full" alignment by adding the corresponding classname to the block's wrapper ( `alignwide` or `alignfull` ). A theme can opt-in for this feature by calling: - -```php -add_theme_support( 'align-wide' ); -``` - -### Block Color Palettes: - -Different blocks have the possibility of customizing colors. Gutenberg provides a default palette, but a theme can overwrite it and provide its own: - -```php -add_theme_support( 'editor-color-palette', - '#a156b4', - '#d0a5db', - '#eee', - '#444' -); -``` - -The colors will be shown in order on the palette, and there's no limit to how many can be specified. - -### Disabling custom colors in block Color Palettes - -By default, the color palette offered to blocks, allows the user to select a custom color different from the editor or theme default colors. -Themes can disable this feature using: -```php -add_theme_support( 'disable-custom-colors' ); -``` - -This flag will make sure users are only able to choose colors from the `editor-color-palette` the theme provided or from the editor default colors if the theme did not provide one. diff --git a/edit-post/api/index.js b/edit-post/api/index.js deleted file mode 100644 index 1252f8010ec3f4..00000000000000 --- a/edit-post/api/index.js +++ /dev/null @@ -1,4 +0,0 @@ -export { - registerSidebar, - activateSidebar, -} from './sidebar'; diff --git a/edit-post/api/sidebar.js b/edit-post/api/sidebar.js deleted file mode 100644 index b2d52152d421e2..00000000000000 --- a/edit-post/api/sidebar.js +++ /dev/null @@ -1,98 +0,0 @@ -/* eslint no-console: [ 'error', { allow: [ 'error' ] } ] */ - -/* External dependencies */ -import { isFunction } from 'lodash'; - -/* Internal dependencies */ -import store from '../store'; -import { setGeneralSidebarActivePanel, openGeneralSidebar } from '../store/actions'; -import { applyFilters } from '@wordpress/hooks'; - -const sidebars = {}; - -/** - * Registers a sidebar to the editor. - * - * A button will be shown in the settings menu to open the sidebar. The sidebar - * can be manually opened by calling the `activateSidebar` function. - * - * @param {string} name The name of the sidebar. Should be in - * `[plugin]/[sidebar]` format. - * @param {Object} settings The settings for this sidebar. - * @param {string} settings.title The name to show in the settings menu. - * @param {Function} settings.render The function that renders the sidebar. - * - * @return {Object} The final sidebar settings object. - */ -export function registerSidebar( name, settings ) { - settings = { - name, - ...settings, - }; - - if ( typeof name !== 'string' ) { - console.error( - 'Sidebar names must be strings.' - ); - return null; - } - if ( ! /^[a-z][a-z0-9-]*\/[a-z][a-z0-9-]*$/.test( name ) ) { - console.error( - 'Sidebar names must contain a namespace prefix, include only lowercase alphanumeric characters or dashes, and start with a letter. Example: my-plugin/my-custom-sidebar.' - ); - return null; - } - if ( ! settings || ! isFunction( settings.render ) ) { - console.error( - 'The "render" property must be specified and must be a valid function.' - ); - return null; - } - if ( sidebars[ name ] ) { - console.error( - `Sidebar ${ name } is already registered.` - ); - } - - if ( ! settings.title ) { - console.error( - `The sidebar ${ name } must have a title.` - ); - return null; - } - if ( typeof settings.title !== 'string' ) { - console.error( - 'Sidebar titles must be strings.' - ); - return null; - } - - settings = applyFilters( 'editor.registerSidebar', settings, name ); - - return sidebars[ name ] = settings; -} - -/** - * Retrieves the sidebar settings object. - * - * @param {string} name The name of the sidebar to retrieve the settings for. - * - * @return {Object} The settings object of the sidebar. Or null if the - * sidebar doesn't exist. - */ -export function getSidebarSettings( name ) { - if ( ! sidebars.hasOwnProperty( name ) ) { - return null; - } - return sidebars[ name ]; -} -/** - * Activates the given sidebar. - * - * @param {string} name The name of the sidebar to activate. - * @return {void} - */ -export function activateSidebar( name ) { - store.dispatch( openGeneralSidebar( 'plugin' ) ); - store.dispatch( setGeneralSidebarActivePanel( 'plugin', name ) ); -} diff --git a/edit-post/assets/stylesheets/_admin-schemes.scss b/edit-post/assets/stylesheets/_admin-schemes.scss index b6c4cbe8dc9b72..c58e2f261dfc2f 100644 --- a/edit-post/assets/stylesheets/_admin-schemes.scss +++ b/edit-post/assets/stylesheets/_admin-schemes.scss @@ -32,7 +32,7 @@ $scheme-sunrise__spot-color: #de823f; // Tab indicators .edit-post-sidebar__panel-tab.is-active, .editor-inserter__tab.is-active { - border-bottom-color: $spot-color; + border-bottom: 3px solid $spot-color; } // Switch @@ -42,13 +42,8 @@ $scheme-sunrise__spot-color: #de823f; border-color: $spot-color; } - .components-form-toggle__thumb { - background-color: $spot-color; - } - &:before { background-color: $spot-color; - border-color: $spot-color; } } @@ -64,5 +59,10 @@ $scheme-sunrise__spot-color: #de823f; background-image: repeating-linear-gradient( -45deg, darken( $spot-color, 20 ), darken( $spot-color, 20 ) 11px, darken( $spot-color, 10 ) 10px, darken( $spot-color, 10 ) 20px ); } } + + // Datepicker + .react-datepicker__day--selected { + background-color: $spot-color; + } } } diff --git a/edit-post/assets/stylesheets/_animations.scss b/edit-post/assets/stylesheets/_animations.scss index dad82172b52768..2b65713d45f1f4 100644 --- a/edit-post/assets/stylesheets/_animations.scss +++ b/edit-post/assets/stylesheets/_animations.scss @@ -17,4 +17,7 @@ animation: slide_in_right 0.1s forwards; } - +@mixin fade_in { + animation: fade-in 0.3s ease-out; + animation-fill-mode: forwards; +} diff --git a/edit-post/assets/stylesheets/_mixins.scss b/edit-post/assets/stylesheets/_mixins.scss index bac1daff4c66b3..06c44bd6a68c7f 100644 --- a/edit-post/assets/stylesheets/_mixins.scss +++ b/edit-post/assets/stylesheets/_mixins.scss @@ -1,3 +1,7 @@ +/** + * Breakpoint mixins + */ + @mixin break-huge() { @media ( min-width: #{ ( $break-huge ) } ) { @content; @@ -34,6 +38,7 @@ } } + /** * Long content fade mixin * @@ -108,8 +113,8 @@ } } -$visual-editor-max-width-padding: $visual-editor-max-width + $block-mover-padding-visible + $block-mover-padding-visible; -$float-margin: calc( 50% - #{ $visual-editor-max-width-padding / 2 } ); +$content-width-padding: $content-width + $block-side-ui-padding + $block-side-ui-padding; +$float-margin: calc( 50% - #{ $content-width-padding / 2 } ); /** * Button states and focus styles @@ -141,6 +146,15 @@ $float-margin: calc( 50% - #{ $visual-editor-max-width-padding / 2 } ); outline-offset: -2px; } +// Switch +@mixin switch-style__focus-active() { + box-shadow: 0 0 0 2px $white, 0 0 0 3px $dark-gray-300; + + // Windows High Contrast mode will show this outline, but not the box-shadow + outline: 2px solid transparent; + outline-offset: 2px; +} + // Formatting Buttons @mixin formatting-button-style__hover { color: $dark-gray-500; @@ -163,13 +177,27 @@ $float-margin: calc( 50% - #{ $visual-editor-max-width-padding / 2 } ); } // Tabs, Inputs, Square buttons +@mixin input-style__neutral() { + outline-offset: -1px; + box-shadow: 0 0 0 transparent; + transition: box-shadow .05s linear; +} + +@mixin input-style__focus() { + color: $dark-gray-900; + outline: 1px solid $blue-medium-600; + box-shadow: 0 0 0 2px $blue-medium-200; +} + +// Square buttons @mixin square-style__neutral() { outline-offset: -1px; } -@mixin square-style__focus-active() { +@mixin square-style__focus() { color: $dark-gray-900; outline: 1px solid $dark-gray-300; + box-shadow: none; } // Menu items @@ -179,26 +207,13 @@ $float-margin: calc( 50% - #{ $visual-editor-max-width-padding / 2 } ); } @mixin menu-style__focus() { - color: $black; + color: $dark-gray-900; border: none; box-shadow: none; outline-offset: -2px; - color: $dark-gray-900; outline: 1px dotted $dark-gray-500; } -// Old -@mixin tab-style__focus-active() { - outline: none; - color: $dark-gray-900; - box-shadow: inset 0 0 0 1px $dark-gray-300; -} - -@mixin input-style__focus-active() { - outline: none; - color: $dark-gray-900; - box-shadow: 0 0 0 1px $dark-gray-300; -} /** * Applies editor left position to the selector passed as argument diff --git a/edit-post/assets/stylesheets/_variables.scss b/edit-post/assets/stylesheets/_variables.scss index 65e46440578f5e..acc3fc6dfa9c3b 100644 --- a/edit-post/assets/stylesheets/_variables.scss +++ b/edit-post/assets/stylesheets/_variables.scss @@ -26,26 +26,27 @@ $admin-sidebar-width-big: 190px; $admin-sidebar-width-collapsed: 36px; // Visuals -$shadow-popover: 0px 3px 20px rgba( $dark-gray-900, .1 ), 0px 1px 3px rgba( $dark-gray-900, .1 ); -$shadow-toolbar: 0px 2px 10px rgba( $dark-gray-900, .1 ), 0px 0px 2px rgba( $dark-gray-900, .1 ); +$shadow-popover: 0 3px 20px rgba( $dark-gray-900, .1 ), 0 1px 3px rgba( $dark-gray-900, .1 ); +$shadow-toolbar: 0 2px 10px rgba( $dark-gray-900, .1 ), 0 0 2px rgba( $dark-gray-900, .1 ); -// Editor -$text-editor-max-width: 760px; -$visual-editor-max-width: 636px; +// Editor Widths +$sidebar-width: 280px; +$text-editor-max-width: 760px; // @todo: merge with variable below +$content-width: 636px; // @todo: leverage theme $content_width variable + +// Block UI $block-controls-height: 36px; $icon-button-size: 36px; $icon-button-size-small: 24px; $inserter-tabs-height: 36px; $block-toolbar-height: 37px; -$sidebar-width: 280px; // Blocks $block-padding: 14px; $block-mover-margin: 18px; $block-spacing: 4px; -// old $block-mover-padding-visible: 32px; -$block-mover-padding-visible: $icon-button-size-small + $icon-button-size-small; - +$block-side-ui-padding: 36px; +$block-side-ui-width: 28px; // The side UI max height matches a single line of text, 56px. 28px is half, allowing 2 mover arrows // Buttons & UI Widgets $button-style__radius-roundrect: 4px; diff --git a/edit-post/assets/stylesheets/_z-index.scss b/edit-post/assets/stylesheets/_z-index.scss index 5ce5a9b728468a..aad13647ad017c 100644 --- a/edit-post/assets/stylesheets/_z-index.scss +++ b/edit-post/assets/stylesheets/_z-index.scss @@ -4,7 +4,7 @@ $z-layers: ( '.editor-block-switcher__arrow': 1, - '.editor-block-list__block:before': -1, + '.editor-block-list__block-edit:before': -1, '.editor-block-list__block .wp-block-more:before': -1, '.editor-block-list__block {core/image aligned left or right}': 20, '.editor-block-list__block {core/image aligned wide or fullwide}': 20, @@ -16,26 +16,45 @@ $z-layers: ( '.editor-inserter__tab.is-active': 1, '.components-panel__header': 1, '.edit-post-meta-boxes-area.is-loading:before': 1, - '.edit-post-meta-boxes-area .spinner': 2, - '.blocks-format-toolbar__link-modal': 2, + '.edit-post-meta-boxes-area .spinner': 5, '.editor-block-contextual-toolbar': 21, - '.editor-block-switcher__menu': 2, - '.components-popover__close': 2, - '.editor-block-mover': 1, + '.editor-block-switcher__menu': 5, + '.components-popover__close': 5, + '.editor-block-list__insertion-point': 5, + '.blocks-format-toolbar__link-modal': 6, '.blocks-gallery-item__inline-menu': 20, '.editor-block-settings-menu__popover': 20, // Below the header + '.blocks-url-input__suggestions': 30, '.edit-post-header': 30, '.wp-block-image__resize-handlers': 1, // Resize handlers above sibling inserter + // Should have lower index than anything else positioned inside the block containers + '.editor-block-list__block-draggable': 0, + + // The draggable element should show up above the entire UI + '.components-draggable__clone': 1000000000, + + // Should have higher index than the inset/underlay used for dragging + '.components-placeholder__fieldset': 1, + '.editor-block-list__block-edit .shared-block-edit-panel *': 1, + // Show drop zone above most standard content, but below any overlays '.components-drop-zone': 100, '.components-drop-zone__content': 110, + // Block controls, particularly in nested contexts, floats aside block and + // should overlap most block content. + '.editor-block-list__block.is-{selected,hovered} .editor-block-{settings-menu,mover}': 80, + // Show sidebar above wp-admin navigation bar for mobile viewports: // #wpadminbar { z-index: 99999 } '.edit-post-sidebar': 100000, '.edit-post-layout .edit-post-post-publish-panel': 100001, + // Show sidebar in greater than small viewports above editor related elements + // but bellow #adminmenuback { z-index: 100 } + '.edit-post-sidebar {greater than small}': 90, + // Show notices below expanded wp-admin submenus: // #adminmenuwrap { z-index: 9990 } '.components-notice-list': 9989, @@ -43,8 +62,14 @@ $z-layers: ( // Show popovers above wp-admin menus and submenus and sidebar: // #adminmenuwrap { z-index: 9990 } '.components-popover': 1000000, + + // Shows adminbar quicklink submenu above bottom popover: + // #wpadminbar ul li {z-index: 99999;} + '.components-popover.is-bottom': 99990, + '.components-autocomplete__results': 1000000, - '.blocks-url-input__suggestions': 9999, + + '.skip-to-selected-block': 100000, ); @function z-index( $key ) { diff --git a/edit-post/assets/stylesheets/main.scss b/edit-post/assets/stylesheets/main.scss index a9d5f41a0157b8..4e85ea9d25cc49 100644 --- a/edit-post/assets/stylesheets/main.scss +++ b/edit-post/assets/stylesheets/main.scss @@ -70,11 +70,6 @@ body.gutenberg-editor-page { top: -1px; } - svg { - fill: currentColor; - outline: none; - } - ul#adminmenu a.wp-has-current-submenu:after, ul#adminmenu>li.current>a.current:after { border-right-color: $white; @@ -86,20 +81,6 @@ body.gutenberg-editor-page { box-sizing: border-box; } - ul:not(.wp-block-gallery) { - list-style-type: disc; - } - - ol:not(.wp-block-gallery) { - list-style-type: decimal; - } - - ul, - ol { - margin: 0; - padding: 0; - } - select { font-size: $default-font-size; color: $dark-gray-500; @@ -137,16 +118,47 @@ body.gutenberg-editor-page { } } -.editor-post-title, +// Override core input styles to provide ones consistent with Gutenberg +// @todo submit as upstream patch as well +.edit-post-sidebar, +.editor-post-publish-panel, .editor-block-list__block { - input, + .input-control, // upstream name is .regular-text + input[type=text], + input[type=search], + input[type=radio], + input[type=tel], + input[type=time], + input[type=url], + input[type=week], + input[type=password], + input[type=checkbox], + input[type=color], + input[type=date], + input[type=datetime], + input[type=datetime-local], + input[type=email], + input[type=month], + input[type=number], + select, textarea { - border-radius: 4px; - border-color: $light-gray-500; + border: 1px solid $light-gray-700; font-family: $default-font; font-size: $default-font-size; - padding: 6px 10px; + padding: 6px 8px; + @include input-style__neutral(); + + &:focus { + @include input-style__focus(); + } + } +} +// Placeholder colors +.editor-post-title, +.editor-block-list__block { + input, + textarea { &::-webkit-input-placeholder { color: $dark-gray-300; } @@ -161,3 +173,12 @@ body.gutenberg-editor-page { } } } + +@keyframes fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} diff --git a/edit-post/components/header/fixed-toolbar-toggle/index.js b/edit-post/components/header/fixed-toolbar-toggle/index.js index 703fba930a8195..5561dc7eb99d82 100644 --- a/edit-post/components/header/fixed-toolbar-toggle/index.js +++ b/edit-post/components/header/fixed-toolbar-toggle/index.js @@ -1,49 +1,43 @@ /** - * External Dependencies + * WordPress Dependencies */ -import { connect } from 'react-redux'; +import { withSelect, withDispatch } from '@wordpress/data'; /** * WordPress Dependencies */ import { __ } from '@wordpress/i18n'; -import { MenuItemsGroup, MenuItemsToggle, withInstanceId } from '@wordpress/components'; - -/** - * Internal Dependencies - */ -import { hasFixedToolbar, isMobile } from '../../../store/selectors'; -import { toggleFeature } from '../../../store/actions'; +import { compose } from '@wordpress/element'; +import { MenuGroup, MenuItem, withInstanceId } from '@wordpress/components'; +import { ifViewportMatches } from '@wordpress/viewport'; -function FeatureToggle( { onToggle, active, onMobile } ) { - if ( onMobile ) { - return null; - } +function FeatureToggle( { onToggle, isActive } ) { return ( - <MenuItemsGroup + <MenuGroup label={ __( 'Settings' ) } filterName="editPost.MoreMenu.settings" > - <MenuItemsToggle - label={ __( 'Fix Toolbar to Top' ) } - isSelected={ active } + <MenuItem + icon={ isActive && 'yes' } + isSelected={ isActive } onClick={ onToggle } - /> - </MenuItemsGroup> + > + { __( 'Fix Toolbar to Top' ) } + </MenuItem> + </MenuGroup> ); } -export default connect( - ( state ) => ( { - active: hasFixedToolbar( state ), - onMobile: isMobile( state ), - } ), - ( dispatch, ownProps ) => ( { +export default compose( [ + withSelect( ( select ) => ( { + isActive: select( 'core/edit-post' ).isFeatureActive( 'fixedToolbar' ), + } ) ), + withDispatch( ( dispatch, ownProps ) => ( { onToggle() { - dispatch( toggleFeature( 'fixedToolbar' ) ); + dispatch( 'core/edit-post' ).toggleFeature( 'fixedToolbar' ); ownProps.onToggle(); }, - } ), - undefined, - { storeKey: 'edit-post' } -)( withInstanceId( FeatureToggle ) ); + } ) ), + ifViewportMatches( 'medium' ), + withInstanceId, +] )( FeatureToggle ); diff --git a/edit-post/components/header/header-toolbar/index.js b/edit-post/components/header/header-toolbar/index.js index 13333bab5c71d2..452656debf3623 100644 --- a/edit-post/components/header/header-toolbar/index.js +++ b/edit-post/components/header/header-toolbar/index.js @@ -1,7 +1,9 @@ /** - * External dependencies + * WordPress dependencies */ -import { connect } from 'react-redux'; +import { compose } from '@wordpress/element'; +import { withSelect } from '@wordpress/data'; +import { withViewportMatch } from '@wordpress/viewport'; /** * WordPress dependencies @@ -21,9 +23,8 @@ import { * Internal dependencies */ import './style.scss'; -import { hasFixedToolbar } from '../../../store/selectors'; -function HeaderToolbar( { fixedToolbarActive } ) { +function HeaderToolbar( { hasFixedToolbar, isLargeViewport } ) { return ( <NavigableToolbar className="edit-post-header-toolbar" @@ -34,7 +35,7 @@ function HeaderToolbar( { fixedToolbarActive } ) { <EditorHistoryRedo /> <TableOfContents /> <MultiBlocksSwitcher /> - { fixedToolbarActive && ( + { hasFixedToolbar && isLargeViewport && ( <div className="edit-post-header-toolbar__block-toolbar"> <BlockToolbar /> </div> @@ -43,11 +44,9 @@ function HeaderToolbar( { fixedToolbarActive } ) { ); } -export default connect( - ( state ) => ( { - fixedToolbarActive: hasFixedToolbar( state ), - } ), - undefined, - undefined, - { storeKey: 'edit-post' } -)( HeaderToolbar ); +export default compose( [ + withSelect( ( select ) => ( { + hasFixedToolbar: select( 'core/edit-post' ).isFeatureActive( 'fixedToolbar' ), + } ) ), + withViewportMatch( { isLargeViewport: 'medium' } ), +] )( HeaderToolbar ); diff --git a/edit-post/components/header/index.js b/edit-post/components/header/index.js index 19f13f24598fca..850642d2c7ad94 100644 --- a/edit-post/components/header/index.js +++ b/edit-post/components/header/index.js @@ -1,8 +1,3 @@ -/** - * External dependencies - */ -import { connect } from 'react-redux'; - /** * WordPress dependencies */ @@ -13,6 +8,8 @@ import { PostSavedState, PostPublishPanelToggle, } from '@wordpress/editor'; +import { withDispatch, withSelect } from '@wordpress/data'; +import { compose } from '@wordpress/element'; /** * Internal dependencies @@ -20,28 +17,17 @@ import { import './style.scss'; import MoreMenu from './more-menu'; import HeaderToolbar from './header-toolbar'; -import { - getOpenedGeneralSidebar, - isPublishSidebarOpened, - hasMetaBoxes, - isSavingMetaBoxes, -} from '../../store/selectors'; -import { + +function Header( { + isEditorSidebarOpened, openGeneralSidebar, closeGeneralSidebar, + isPublishSidebarOpened, togglePublishSidebar, -} from '../../store/actions'; - -function Header( { - isGeneralSidebarEditorOpen, - onOpenGeneralSidebar, - onCloseGeneralSidebar, - isPublishSidebarOpen, - onTogglePublishSidebar, hasActiveMetaboxes, isSaving, } ) { - const toggleGeneralSidebar = isGeneralSidebarEditorOpen ? onCloseGeneralSidebar : onOpenGeneralSidebar; + const toggleGeneralSidebar = isEditorSidebarOpened ? closeGeneralSidebar : openGeneralSidebar; return ( <div @@ -51,7 +37,7 @@ function Header( { tabIndex="-1" > <HeaderToolbar /> - { ! isPublishSidebarOpen && ( + { ! isPublishSidebarOpened && ( <div className="edit-post-header__settings"> <PostSavedState forceIsDirty={ hasActiveMetaboxes } @@ -59,15 +45,17 @@ function Header( { /> <PostPreviewButton /> <PostPublishPanelToggle - isOpen={ isPublishSidebarOpen } - onToggle={ onTogglePublishSidebar } + isOpen={ isPublishSidebarOpened } + onToggle={ togglePublishSidebar } + forceIsDirty={ hasActiveMetaboxes } + forceIsSaving={ isSaving } /> <IconButton icon="admin-generic" onClick={ toggleGeneralSidebar } - isToggled={ isGeneralSidebarEditorOpen } + isToggled={ isEditorSidebarOpened } label={ __( 'Settings' ) } - aria-expanded={ isGeneralSidebarEditorOpen } + aria-expanded={ isEditorSidebarOpened } /> <MoreMenu key="more-menu" /> </div> @@ -76,18 +64,16 @@ function Header( { ); } -export default connect( - ( state ) => ( { - isGeneralSidebarEditorOpen: getOpenedGeneralSidebar( state ) === 'editor', - isPublishSidebarOpen: isPublishSidebarOpened( state ), - hasActiveMetaboxes: hasMetaBoxes( state ), - isSaving: isSavingMetaBoxes( state ), - } ), - { - onOpenGeneralSidebar: () => openGeneralSidebar( 'editor' ), - onCloseGeneralSidebar: closeGeneralSidebar, - onTogglePublishSidebar: togglePublishSidebar, - }, - undefined, - { storeKey: 'edit-post' } +export default compose( + withSelect( ( select ) => ( { + isEditorSidebarOpened: select( 'core/edit-post' ).isEditorSidebarOpened(), + isPublishSidebarOpened: select( 'core/edit-post' ).isPublishSidebarOpened(), + hasActiveMetaboxes: select( 'core/edit-post' ).hasMetaBoxes(), + isSaving: select( 'core/edit-post' ).isSavingMetaBoxes(), + } ) ), + withDispatch( ( dispatch ) => ( { + openGeneralSidebar: () => dispatch( 'core/edit-post' ).openGeneralSidebar( 'edit-post/document' ), + closeGeneralSidebar: dispatch( 'core/edit-post' ).closeGeneralSidebar, + togglePublishSidebar: dispatch( 'core/edit-post' ).togglePublishSidebar, + } ) ), )( Header ); diff --git a/edit-post/components/header/mode-switcher/index.js b/edit-post/components/header/mode-switcher/index.js index 9af936bb24a4d4..750a5a623facce 100644 --- a/edit-post/components/header/mode-switcher/index.js +++ b/edit-post/components/header/mode-switcher/index.js @@ -1,21 +1,15 @@ -/** - * External dependencies - */ -import { connect } from 'react-redux'; - /** * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { MenuItemsChoice, MenuItemsGroup } from '@wordpress/components'; +import { MenuItemsChoice, MenuGroup } from '@wordpress/components'; +import { compose } from '@wordpress/element'; +import { withSelect, withDispatch } from '@wordpress/data'; /** * Internal dependencies */ import shortcuts from '../../../keyboard-shortcuts'; -import { getEditorMode } from '../../../store/selectors'; -import { switchEditorMode } from '../../../store/actions'; - /** * Set of available mode options. * @@ -41,7 +35,7 @@ function ModeSwitcher( { onSwitch, mode } ) { } ); return ( - <MenuItemsGroup + <MenuGroup label={ __( 'Editor' ) } filterName="editPost.MoreMenu.editor" > @@ -50,20 +44,18 @@ function ModeSwitcher( { onSwitch, mode } ) { value={ mode } onSelect={ onSwitch } /> - </MenuItemsGroup> + </MenuGroup> ); } -export default connect( - ( state ) => ( { - mode: getEditorMode( state ), - } ), - ( dispatch, ownProps ) => ( { +export default compose( [ + withSelect( ( select ) => ( { + mode: select( 'core/edit-post' ).getEditorMode(), + } ) ), + withDispatch( ( dispatch, ownProps ) => ( { onSwitch( mode ) { - dispatch( switchEditorMode( mode ) ); + dispatch( 'core/edit-post' ).switchEditorMode( mode ); ownProps.onSelect( mode ); }, - } ), - undefined, - { storeKey: 'edit-post' } -)( ModeSwitcher ); + } ) ), +] )( ModeSwitcher ); diff --git a/edit-post/components/header/more-menu/index.js b/edit-post/components/header/more-menu/index.js index 64a4d108f126f4..62b76cef629a71 100644 --- a/edit-post/components/header/more-menu/index.js +++ b/edit-post/components/header/more-menu/index.js @@ -2,7 +2,7 @@ * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { IconButton, Dropdown, MenuItemsGroup } from '@wordpress/components'; +import { IconButton, Dropdown, MenuGroup } from '@wordpress/components'; /** * Internal dependencies @@ -10,6 +10,7 @@ import { IconButton, Dropdown, MenuItemsGroup } from '@wordpress/components'; import './style.scss'; import ModeSwitcher from '../mode-switcher'; import FixedToolbarToggle from '../fixed-toolbar-toggle'; +import PluginMoreMenuGroup from '../../plugin-more-menu-group'; const MoreMenu = () => ( <Dropdown @@ -27,7 +28,8 @@ const MoreMenu = () => ( <div className="edit-post-more-menu__content"> <ModeSwitcher onSelect={ onClose } /> <FixedToolbarToggle onToggle={ onClose } /> - <MenuItemsGroup + <PluginMoreMenuGroup fillProps={ { onClose } } /> + <MenuGroup label={ __( 'Tools' ) } filterName="editPost.MoreMenu.tools" /> diff --git a/edit-post/components/header/more-menu/style.scss b/edit-post/components/header/more-menu/style.scss index d253e131b8f121..3af5cec41b2084 100644 --- a/edit-post/components/header/more-menu/style.scss +++ b/edit-post/components/header/more-menu/style.scss @@ -1,12 +1,18 @@ .edit-post-more-menu { + margin-left: -4px; + // the padding and margin of the more menu is intentionally non-standard + .components-icon-button { + width: auto; + padding: 8px 2px; + } + @include break-small() { margin-left: 4px; - } - .components-icon-button { - padding: 8px 4px; - width: auto; + .components-icon-button { + padding: 8px 4px; + } } .components-button svg { @@ -15,7 +21,7 @@ } .edit-post-more-menu__content { - .components-menu-items__group:not(:last-child) { + .components-menu-group:not(:last-child) { border-bottom: 1px solid $light-gray-500; } } diff --git a/edit-post/components/header/style.scss b/edit-post/components/header/style.scss index b9a68fe95a866e..33992c96bf8b66 100644 --- a/edit-post/components/header/style.scss +++ b/edit-post/components/header/style.scss @@ -1,6 +1,6 @@ .edit-post-header { height: $header-height; - padding: $item-spacing; + padding: $item-spacing 2px; border-bottom: 1px solid $light-gray-500; background: $white; display: flex; @@ -19,12 +19,22 @@ // otherwise you can invoke the overscroll bounce on the non-scrolling container, causing (ノಠ益ಠ)ノ彡┻━┻ @include break-small { position: fixed; + padding: $item-spacing; top: $admin-bar-height-big; } @include break-medium() { top: $admin-bar-height; } + + .editor-post-switch-to-draft + .editor-post-preview, + .editor-post-switch-to-draft + .editor-post-preview + .editor-post-publish-button { + display: none; + + @include break-small { + display: inline-block; + } + } } @include editor-left('.edit-post-header'); @@ -43,7 +53,7 @@ } // put the gray background on a separate layer, so as to match the size of the publish button (34px) - &.is-toggled::before { + &.is-toggled:before { content: ""; border-radius: $button-style__radius-roundrect; position: absolute; @@ -57,22 +67,34 @@ &.is-toggled:hover, &.is-toggled:focus { - outline: none; box-shadow: 0 0 0 1px $dark-gray-500, inset 0 0 0 1px $white; color: $white; background: $dark-gray-500; } - &.editor-post-publish-button, &.editor-post-publish-panel__toggle { + &.editor-post-switch-to-draft, + &.editor-post-preview, + &.editor-post-publish-button, + &.editor-post-publish-panel__toggle { margin: 2px; height: 33px; line-height: 32px; - padding: 0 12px 2px; + padding: 0 5px 2px; + font-size: $default-font-size; + + @include break-small() { + padding: 0 12px 2px; + } } @include break-medium() { - &.editor-post-publish-button, &.editor-post-publish-panel__toggle { - margin: 0 10px; + &.editor-post-preview { + margin: 0 3px 0 12px; + } + + &.editor-post-publish-button, + &.editor-post-publish-panel__toggle { + margin: 0 12px 0 3px; } } } diff --git a/edit-post/components/layout/index.js b/edit-post/components/layout/index.js index 5ec118509484be..cef9eacf5e7a28 100644 --- a/edit-post/components/layout/index.js +++ b/edit-post/components/layout/index.js @@ -1,14 +1,13 @@ /** * External dependencies */ -import { connect } from 'react-redux'; import classnames from 'classnames'; import { some } from 'lodash'; /** * WordPress dependencies */ -import { Popover, navigateRegions } from '@wordpress/components'; +import { Popover, ScrollLock, navigateRegions } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { AutosaveMonitor, @@ -16,57 +15,43 @@ import { EditorNotices, PostPublishPanel, DocumentTitle, + PreserveScrollInReorder, } from '@wordpress/editor'; +import { withDispatch, withSelect } from '@wordpress/data'; +import { compose } from '@wordpress/element'; +import { PluginArea } from '@wordpress/plugins'; +import { withViewportMatch } from '@wordpress/viewport'; /** * Internal dependencies */ import './style.scss'; +import BlockSidebar from '../sidebar/block-sidebar'; +import DocumentSidebar from '../sidebar/document-sidebar'; import Header from '../header'; -import Sidebar from '../sidebar'; -import TextEditor from '../modes/text-editor'; -import VisualEditor from '../modes/visual-editor'; -import EditorModeKeyboardShortcuts from '../modes/keyboard-shortcuts'; +import TextEditor from '../text-editor'; +import VisualEditor from '../visual-editor'; +import EditorModeKeyboardShortcuts from '../keyboard-shortcuts'; import MetaBoxes from '../meta-boxes'; import { getMetaBoxContainer } from '../../utils/meta-boxes'; -import { - getEditorMode, - hasOpenSidebar, - isFeatureActive, - getOpenedGeneralSidebar, - isPublishSidebarOpened, - getActivePlugin, - getMetaBoxes, -} from '../../store/selectors'; -import { closePublishSidebar } from '../../store/actions'; -import PluginsPanel from '../../components/plugins-panel/index.js'; -import { getSidebarSettings } from '../../api/sidebar'; - -function GeneralSidebar( { openedGeneralSidebar } ) { - switch ( openedGeneralSidebar ) { - case 'editor': - return <Sidebar />; - case 'plugin': - return <PluginsPanel />; - default: - } - return null; -} +import Sidebar from '../sidebar'; function Layout( { mode, - layoutHasOpenSidebar, - publishSidebarOpen, - openedGeneralSidebar, + editorSidebarOpened, + pluginSidebarOpened, + publishSidebarOpened, hasFixedToolbar, - onClosePublishSidebar, - plugin, + closePublishSidebar, metaBoxes, + hasActiveMetaboxes, + isSaving, + isMobileViewport, } ) { - const isSidebarOpened = layoutHasOpenSidebar && - ( openedGeneralSidebar !== 'plugin' || getSidebarSettings( plugin ) ); + const sidebarIsOpened = editorSidebarOpened || pluginSidebarOpened || publishSidebarOpened; + const className = classnames( 'edit-post-layout', { - 'is-sidebar-opened': isSidebarOpened, + 'is-sidebar-opened': sidebarIsOpened, 'has-fixed-toolbar': hasFixedToolbar, } ); @@ -83,11 +68,10 @@ function Layout( { <Header /> <div className="edit-post-layout__content" role="region" aria-label={ __( 'Editor content' ) } tabIndex="-1"> <EditorNotices /> - <div className="edit-post-layout__editor"> - <EditorModeKeyboardShortcuts /> - { mode === 'text' && <TextEditor /> } - { mode === 'visual' && <VisualEditor /> } - </div> + <PreserveScrollInReorder /> + <EditorModeKeyboardShortcuts /> + { mode === 'text' && <TextEditor /> } + { mode === 'visual' && <VisualEditor /> } <div className="edit-post-layout__metaboxes"> <MetaBoxes location="normal" /> </div> @@ -95,29 +79,39 @@ function Layout( { <MetaBoxes location="advanced" /> </div> </div> - { publishSidebarOpen && <PostPublishPanel onClose={ onClosePublishSidebar } /> } + { publishSidebarOpened && ( + <PostPublishPanel + onClose={ closePublishSidebar } + forceIsDirty={ hasActiveMetaboxes } + forceIsSaving={ isSaving } + /> + ) } + <DocumentSidebar /> + <BlockSidebar /> + <Sidebar.Slot /> { - openedGeneralSidebar !== null && <GeneralSidebar - openedGeneralSidebar={ openedGeneralSidebar } /> + isMobileViewport && sidebarIsOpened && <ScrollLock /> } <Popover.Slot /> + <PluginArea /> </div> ); } -export default connect( - ( state ) => ( { - mode: getEditorMode( state ), - layoutHasOpenSidebar: hasOpenSidebar( state ), - openedGeneralSidebar: getOpenedGeneralSidebar( state ), - publishSidebarOpen: isPublishSidebarOpened( state ), - hasFixedToolbar: isFeatureActive( state, 'fixedToolbar' ), - plugin: getActivePlugin( state ), - metaBoxes: getMetaBoxes( state ), - } ), - { - onClosePublishSidebar: closePublishSidebar, - }, - undefined, - { storeKey: 'edit-post' } -)( navigateRegions( Layout ) ); +export default compose( + withSelect( ( select ) => ( { + mode: select( 'core/edit-post' ).getEditorMode(), + editorSidebarOpened: select( 'core/edit-post' ).isEditorSidebarOpened(), + pluginSidebarOpened: select( 'core/edit-post' ).isPluginSidebarOpened(), + publishSidebarOpened: select( 'core/edit-post' ).isPublishSidebarOpened(), + hasFixedToolbar: select( 'core/edit-post' ).isFeatureActive( 'fixedToolbar' ), + metaBoxes: select( 'core/edit-post' ).getMetaBoxes(), + hasActiveMetaboxes: select( 'core/edit-post' ).hasMetaBoxes(), + isSaving: select( 'core/edit-post' ).isSavingMetaBoxes(), + } ) ), + withDispatch( ( dispatch ) => ( { + closePublishSidebar: dispatch( 'core/edit-post' ).closePublishSidebar, + } ) ), + navigateRegions, + withViewportMatch( { isMobileViewport: '< small' } ), +)( Layout ); diff --git a/edit-post/components/layout/style.scss b/edit-post/components/layout/style.scss index 733827d2b8419f..148295b1d46931 100644 --- a/edit-post/components/layout/style.scss +++ b/edit-post/components/layout/style.scss @@ -63,19 +63,34 @@ clear: both; .edit-post-meta-boxes-area { - max-width: $visual-editor-max-width; - margin: auto; + margin: auto 20px; } } .edit-post-layout__content { position: relative; + display: flex; + min-height: 100%; + flex-direction: column; - // on mobile the main content area has to scroll - // otherwise you can invoke the overscroll bounce on the non-scrolling container, causing (ノಠ益ಠ)ノ彡┻━┻ + // Pad the scroll box so content on the bottom can be scrolled up. + padding-bottom: 50vh; @include break-small { - overflow-y: auto; - -webkit-overflow-scrolling: touch; + padding-bottom: 0; + } + + // On mobile the main content area has to scroll otherwise you can invoke + // the overscroll bounce on the non-scrolling container, causing + // (ノಠ益ಠ)ノ彡┻━┻ + overflow-y: auto; + -webkit-overflow-scrolling: touch; + + .edit-post-visual-editor { + flex-basis: 100%; + } + + .edit-post-layout__metaboxes { + flex-shrink: 0; } } diff --git a/edit-post/components/meta-boxes/index.js b/edit-post/components/meta-boxes/index.js index 7e611e3a95b5cd..8c048c5172abdd 100644 --- a/edit-post/components/meta-boxes/index.js +++ b/edit-post/components/meta-boxes/index.js @@ -1,14 +1,13 @@ /** - * External dependencies + * WordPress dependencies */ -import { connect } from 'react-redux'; +import { withSelect } from '@wordpress/data'; /** * Internal dependencies */ import MetaBoxesArea from './meta-boxes-area'; import MetaBoxesPanel from './meta-boxes-panel'; -import { getMetaBox } from '../../store/selectors'; function MetaBoxes( { location, isActive, usePanel = false } ) { if ( ! isActive ) { @@ -28,11 +27,8 @@ function MetaBoxes( { location, isActive, usePanel = false } ) { ); } -export default connect( - ( state, ownProps ) => ( { - isActive: getMetaBox( state, ownProps.location ).isActive, +export default withSelect( + ( select, ownProps ) => ( { + isActive: select( 'core/edit-post' ).getMetaBox( ownProps.location ).isActive, } ), - undefined, - undefined, - { storeKey: 'edit-post' } )( MetaBoxes ); diff --git a/edit-post/components/meta-boxes/meta-boxes-area/index.js b/edit-post/components/meta-boxes/meta-boxes-area/index.js index fe6692a0c28053..213972394c1e65 100644 --- a/edit-post/components/meta-boxes/meta-boxes-area/index.js +++ b/edit-post/components/meta-boxes/meta-boxes-area/index.js @@ -2,19 +2,18 @@ * External dependencies */ import classnames from 'classnames'; -import { connect } from 'react-redux'; /** * WordPress dependencies */ import { Component } from '@wordpress/element'; import { Spinner } from '@wordpress/components'; +import { withSelect } from '@wordpress/data'; /** * Internal dependencies */ import './style.scss'; -import { isSavingMetaBoxes } from '../../../store/selectors'; class MetaBoxesArea extends Component { /** @@ -77,18 +76,8 @@ class MetaBoxesArea extends Component { } } -/** - * @inheritdoc - */ -function mapStateToProps( state ) { +export default withSelect( ( select ) => { return { - isSaving: isSavingMetaBoxes( state ), + isSaving: select( 'core/edit-post' ).isSavingMetaBoxes(), }; -} - -export default connect( - mapStateToProps, - undefined, - undefined, - { storeKey: 'edit-post' } -)( MetaBoxesArea ); +} )( MetaBoxesArea ); diff --git a/edit-post/components/meta-boxes/meta-boxes-area/style.scss b/edit-post/components/meta-boxes/meta-boxes-area/style.scss index 2e00937b43e9f7..b25f0c693f2903 100644 --- a/edit-post/components/meta-boxes/meta-boxes-area/style.scss +++ b/edit-post/components/meta-boxes/meta-boxes-area/style.scss @@ -2,25 +2,35 @@ .edit-post-meta-boxes-area { position: relative; - /* Match width and positioning of the meta boxes. Override default styles. */ + /** + * The wordpress default for most meta-box elements is content-box. Some + * elements such as textarea and input are set to border-box in forms.css. + * These elements therefore specifically set back to border-box here, while + * other elements (such as .button) are unaffected by Gutenberg's style + * because of their higher specificity. + */ + * { + box-sizing: content-box; + } + + textarea, input { + box-sizing: border-box; + } + + /* Match width and positioning of the meta boxes. Override default styles. */ #poststuff { margin: 0 auto; padding-top: 0; min-width: auto; } - #post { - margin: 0; - } - /* Override Default meta box stylings */ - #poststuff h3.hndle, #poststuff .stuffbox > h3, #poststuff h2.hndle { /* WordPress selectors yolo */ border-bottom: 1px solid $light-gray-500; box-sizing: border-box; - color: $dark-gray-500; + color: inherit; font-weight: 600; outline: none; padding: 15px; @@ -30,43 +40,17 @@ .postbox { border: 0; - color: $dark-gray-500; + color: inherit; margin-bottom: 0; } .postbox > .inside { border-bottom: 1px solid $light-gray-500; - color: $dark-gray-500; - padding: 15px; + color: inherit; + padding: 0 $block-padding $block-padding; margin: 0; } - input { - max-width: 300px; - } - - input, - select, - textarea { - background: inherit; - border: 1px solid $light-gray-500; - border-radius: 4px; - box-shadow: none; - color: $dark-gray-800; - display: inline-block; - font-family: inherit; - font-size: 13px; - line-height: 24px; - outline: none; - padding: 4px; - } - - input:hover, - select:hover, - textarea:hover { - border: 1px solid $light-gray-700; - } - .postbox .handlediv { height: 44px; width: 44px; diff --git a/edit-post/components/meta-boxes/meta-boxes-panel/style.scss b/edit-post/components/meta-boxes/meta-boxes-panel/style.scss index a9dceb9e533261..0db5d1bc568fb2 100644 --- a/edit-post/components/meta-boxes/meta-boxes-panel/style.scss +++ b/edit-post/components/meta-boxes/meta-boxes-panel/style.scss @@ -6,6 +6,10 @@ .is-sidebar-opened & { display: block; } + + .edit-post-meta-boxes-panel__toggle .components-panel__header { + position: inherit; + } } .edit-post-meta-boxes-panel__body { diff --git a/edit-post/components/modes/keyboard-shortcuts/index.js b/edit-post/components/modes/keyboard-shortcuts/index.js deleted file mode 100644 index 86e5a17682621c..00000000000000 --- a/edit-post/components/modes/keyboard-shortcuts/index.js +++ /dev/null @@ -1,55 +0,0 @@ -/** - * External dependencies - */ -import { connect } from 'react-redux'; - -/** - * WordPress dependencies - */ -import { Component } from '@wordpress/element'; -import { KeyboardShortcuts } from '@wordpress/components'; - -/** - * Internal dependencies - */ -import shortcuts from '../../../keyboard-shortcuts'; -import { getEditorMode } from '../../../store/selectors'; -import { switchEditorMode } from '../../../store/actions'; - -class EditorModeKeyboardShortcuts extends Component { - constructor() { - super( ...arguments ); - - this.toggleMode = this.toggleMode.bind( this ); - } - - toggleMode() { - const { mode, switchMode } = this.props; - switchMode( mode === 'visual' ? 'text' : 'visual' ); - } - - render() { - return ( - <KeyboardShortcuts shortcuts={ { - [ shortcuts.toggleEditorMode.value ]: this.toggleMode, - } } /> - ); - } -} - -export default connect( - ( state ) => { - return { - mode: getEditorMode( state ), - }; - }, - ( dispatch ) => { - return { - switchMode: ( mode ) => { - dispatch( switchEditorMode( mode ) ); - }, - }; - }, - undefined, - { storeKey: 'edit-post' } -)( EditorModeKeyboardShortcuts ); diff --git a/edit-post/components/modes/text-editor/index.js b/edit-post/components/modes/text-editor/index.js deleted file mode 100644 index 3a3e69b2176ef8..00000000000000 --- a/edit-post/components/modes/text-editor/index.js +++ /dev/null @@ -1,22 +0,0 @@ -/** - * WordPress dependencies - */ -import { PostTextEditor, PostTitle } from '@wordpress/editor'; - -/** - * Internal dependencies - */ -import './style.scss'; - -function TextEditor() { - return ( - <div className="edit-post-text-editor"> - <div className="edit-post-text-editor__body"> - <PostTitle /> - <PostTextEditor /> - </div> - </div> - ); -} - -export default TextEditor; diff --git a/edit-post/components/modes/text-editor/style.scss b/edit-post/components/modes/text-editor/style.scss deleted file mode 100644 index 39e7c8cb1943b6..00000000000000 --- a/edit-post/components/modes/text-editor/style.scss +++ /dev/null @@ -1,40 +0,0 @@ -.edit-post-text-editor__body { - padding-top: 40px; - - @include break-small() { - padding-top: 40px + $admin-bar-height-big; - } - - @include break-medium() { - padding-top: 40px + $admin-bar-height; - } -} - -// Use padding to center text in the textarea, this allows you to click anywhere to focus it -.edit-post-text-editor { - padding-left: 20px; - padding-right: 20px; - - @include break-large() { - padding-left: calc( 50% - #{ $text-editor-max-width / 2 } ); - padding-right: calc( 50% - #{ $text-editor-max-width / 2 } ); - } - - .edit-post-post-text-editor__toolbar { - width: 100%; - max-width: $text-editor-max-width; - margin: 0 auto; - } - - // Always show outlines in code editor - .editor-post-title div, - .editor-post-text-editor { - border: 1px solid $light-gray-500; - } - - .editor-post-text-editor { - padding: $block-padding; - min-height: 200px; - line-height: 1.8; - } -} diff --git a/edit-post/components/modes/visual-editor/block-inspector-button.js b/edit-post/components/modes/visual-editor/block-inspector-button.js deleted file mode 100644 index dd53a77622644e..00000000000000 --- a/edit-post/components/modes/visual-editor/block-inspector-button.js +++ /dev/null @@ -1,61 +0,0 @@ -/** - * External dependencies - */ -import { connect } from 'react-redux'; -import { flow, noop } from 'lodash'; - -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; -import { IconButton, withSpokenMessages } from '@wordpress/components'; - -/** - * Internal dependencies - */ -import { getActiveEditorPanel, isGeneralSidebarPanelOpened } from '../../../store/selectors'; -import { openGeneralSidebar } from '../../../store/actions'; - -export function BlockInspectorButton( { - isGeneralSidebarEditorOpened, - onOpenGeneralSidebarEditor, - panel, - onClick = noop, - small = false, - speak, -} ) { - const speakMessage = () => { - if ( ! isGeneralSidebarEditorOpened || ( isGeneralSidebarEditorOpened && panel !== 'block' ) ) { - speak( __( 'Additional settings are now available in the Editor advanced settings sidebar' ) ); - } else { - speak( __( 'Advanced settings closed' ) ); - } - }; - - const label = ( isGeneralSidebarEditorOpened && panel === 'block' ) ? __( 'Hide Advanced Settings' ) : __( 'Show Advanced Settings' ); - - return ( - <IconButton - className="editor-block-settings-menu__control" - onClick={ flow( onOpenGeneralSidebarEditor, speakMessage, onClick ) } - icon="admin-generic" - label={ small ? label : undefined } - > - { ! small && label } - </IconButton> - ); -} - -export default connect( - ( state ) => ( { - isGeneralSidebarEditorOpened: isGeneralSidebarPanelOpened( state, 'editor' ), - panel: getActiveEditorPanel( state ), - } ), - ( dispatch ) => ( { - onOpenGeneralSidebarEditor() { - dispatch( openGeneralSidebar( 'editor', 'block' ) ); - }, - } ), - undefined, - { storeKey: 'edit-post' } -)( withSpokenMessages( BlockInspectorButton ) ); diff --git a/edit-post/components/modes/visual-editor/index.js b/edit-post/components/modes/visual-editor/index.js deleted file mode 100644 index fbf990e16e5e8a..00000000000000 --- a/edit-post/components/modes/visual-editor/index.js +++ /dev/null @@ -1,58 +0,0 @@ -/** - * External dependencies - */ -import { connect } from 'react-redux'; - -/** - * WordPress dependencies - */ -import { - BlockList, - CopyHandler, - PostTitle, - WritingFlow, - EditorGlobalKeyboardShortcuts, - BlockSelectionClearer, - MultiSelectScrollIntoView, -} from '@wordpress/editor'; -import { Fragment } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import './style.scss'; -import BlockInspectorButton from './block-inspector-button'; -import { hasFixedToolbar } from '../../../store/selectors'; - -function VisualEditor( props ) { - return ( - <BlockSelectionClearer className="edit-post-visual-editor"> - <EditorGlobalKeyboardShortcuts /> - <CopyHandler /> - <MultiSelectScrollIntoView /> - <WritingFlow> - <PostTitle /> - <BlockList - showContextualToolbar={ ! props.hasFixedToolbar } - renderBlockMenu={ ( { children, onClose } ) => ( - <Fragment> - <BlockInspectorButton onClick={ onClose } /> - { children } - </Fragment> - ) } - /> - </WritingFlow> - </BlockSelectionClearer> - ); -} - -export default connect( - ( state ) => { - return { - hasFixedToolbar: hasFixedToolbar( state ), - }; - }, - undefined, - undefined, - { storeKey: 'edit-post' } -)( VisualEditor ); diff --git a/edit-post/components/modes/visual-editor/style.scss b/edit-post/components/modes/visual-editor/style.scss deleted file mode 100644 index 75784393aff389..00000000000000 --- a/edit-post/components/modes/visual-editor/style.scss +++ /dev/null @@ -1,90 +0,0 @@ -.edit-post-visual-editor { - position: relative; - height: 100%; - margin: 0 auto; - padding: 50px 0; - - &, - & p { - font-family: $editor-font; - font-size: $editor-font-size; - line-height: $editor-line-height; - } -} - -.edit-post-visual-editor .editor-block-list__block { - margin-left: auto; - margin-right: auto; - max-width: $visual-editor-max-width + ( 2 * $block-mover-padding-visible ); - - &[data-align="wide"] { - max-width: 1100px; - } - - &[data-align="full"] { - max-width: 100%; - } - - &[data-align="full"], - &[data-align="wide"] { - .editor-block-contextual-toolbar { - @include break-small() { - width: $visual-editor-max-width + 2; // 1px border left and right - } - margin-left: auto; - margin-right: auto; - } - } -} - -// This is a focus style shown for blocks that need an indicator even when in an isEditing state -// like for example an image block that receives arrowkey focus. -.edit-post-visual-editor .editor-block-list__block:not( .is-selected ) .editor-block-list__block-edit { - box-shadow: 0 0 0 0 $white, 0 0 0 0 $dark-gray-900; - transition: .1s box-shadow .05s; - - &:focus { - box-shadow: 0 0 0 1px $white, 0 0 0 3px $dark-gray-900; - } -} - -.edit-post-visual-editor .editor-post-title { - margin-left: auto; - margin-right: auto; - max-width: $visual-editor-max-width + ( 2 * $block-mover-padding-visible ); - - .editor-post-permalink { - left: $block-padding; - right: $block-padding; - } - - @include break-small() { - padding: 5px #{ $block-mover-padding-visible - 1px }; // subtract 1px border, because this is an outline - - .editor-post-permalink { - left: $block-mover-padding-visible; - right: $block-mover-padding-visible; - } - } -} - -.edit-post-visual-editor .editor-default-block-appender { - max-width: $visual-editor-max-width + ( 2 * $block-mover-padding-visible ); - clear: left; - margin-left: auto; - margin-right: auto; - position: relative; - - @include break-small() { - padding: 0 $block-mover-padding-visible; // don't subtract 1px border because it's a border not an outline - - .components-drop-zone { - left: $block-mover-padding-visible; - right: $block-mover-padding-visible; - } - - .editor-default-block-appender__content { - padding: 0 $block-padding; - } - } -} diff --git a/edit-post/components/plugins-panel/index.js b/edit-post/components/plugins-panel/index.js deleted file mode 100644 index 57be1622457e96..00000000000000 --- a/edit-post/components/plugins-panel/index.js +++ /dev/null @@ -1,63 +0,0 @@ -/** - * External dependencies - */ -import { connect } from 'react-redux'; - -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; -import { IconButton, withFocusReturn } from '@wordpress/components'; - -/** - * Internal Dependencies - */ -import './style.scss'; -import { getSidebarSettings } from '../../api/sidebar'; -import { getActivePlugin } from '../../store/selectors'; -import { closeGeneralSidebar } from '../../store/actions'; - -function PluginsPanel( { onClose, plugin } ) { - const pluginSidebar = getSidebarSettings( plugin ); - - if ( ! pluginSidebar ) { - return null; - } - - const { - title, - render, - } = pluginSidebar; - - return ( - <div - className="edit-post-sidebar edit-post-plugins-panel" - role="region" - aria-label={ __( 'Editor plugins' ) } - tabIndex="-1"> - <div className="edit-post-plugins-panel__header"> - <h3>{ title }</h3> - <IconButton - onClick={ onClose } - icon="no-alt" - label={ __( 'Close settings' ) } - /> - </div> - <div className="edit-post-plugins-panel__content"> - { render() } - </div> - </div> - ); -} - -export default connect( - ( state ) => { - return { - plugin: getActivePlugin( state ), - }; - }, { - onClose: closeGeneralSidebar, - }, - undefined, - { storeKey: 'edit-post' } -)( withFocusReturn( PluginsPanel ) ); diff --git a/edit-post/components/plugins-panel/style.scss b/edit-post/components/plugins-panel/style.scss deleted file mode 100644 index 9a1e1dfb5a4f80..00000000000000 --- a/edit-post/components/plugins-panel/style.scss +++ /dev/null @@ -1,21 +0,0 @@ -.edit-post-layout { - .edit-post-plugins-panel__header { - padding: 0 8px 0 16px; - height: $panel-header-height; - border-bottom: 1px solid $light-gray-500; - display: flex; - align-items: center; - - .components-icon-button { - margin-left: auto; - } - - h3 { - margin: 0; - font-weight: 400; - color: inherit; - } - } -} - - diff --git a/edit-post/components/sidebar/block-inspector-panel/index.js b/edit-post/components/sidebar/block-inspector-panel/index.js deleted file mode 100644 index d46c22d4c117b1..00000000000000 --- a/edit-post/components/sidebar/block-inspector-panel/index.js +++ /dev/null @@ -1,22 +0,0 @@ -/** - * WordPress dependencies - */ -import { Panel, PanelBody } from '@wordpress/components'; -import { BlockInspector } from '@wordpress/editor'; - -/** - * Internal Dependencies - */ -import './style.scss'; - -function BlockInspectorPanel() { - return ( - <Panel> - <PanelBody className="edit-post-block-inspector-panel"> - <BlockInspector /> - </PanelBody> - </Panel> - ); -} - -export default BlockInspectorPanel; diff --git a/edit-post/components/sidebar/block-inspector-panel/style.scss b/edit-post/components/sidebar/block-inspector-panel/style.scss deleted file mode 100644 index 96e897f0e1811c..00000000000000 --- a/edit-post/components/sidebar/block-inspector-panel/style.scss +++ /dev/null @@ -1,16 +0,0 @@ -.edit-post-block-inspector-panel .components-panel__body { - border: none; - margin: 0 -16px; - - .blocks-base-control { - margin: 0 0 1em 0; - } - - &.is-opened > .components-panel__body-title { - margin-bottom: 5px; - } - - .components-panel__body-toggle { - color: $dark-gray-500; - } -} diff --git a/edit-post/components/sidebar/discussion-panel/index.js b/edit-post/components/sidebar/discussion-panel/index.js index 44a05155c8f763..6dde6cd7597b70 100644 --- a/edit-post/components/sidebar/discussion-panel/index.js +++ b/edit-post/components/sidebar/discussion-panel/index.js @@ -1,20 +1,11 @@ -/** - * External dependencies - */ -import { connect } from 'react-redux'; - /** * WordPress dependencies */ import { __ } from '@wordpress/i18n'; import { PanelBody, PanelRow } from '@wordpress/components'; import { PostComments, PostPingbacks, PostTypeSupportCheck } from '@wordpress/editor'; - -/** - * Internal Dependencies - */ -import { isEditorSidebarPanelOpened } from '../../../store/selectors'; -import { toggleGeneralSidebarEditorPanel } from '../../../store/actions'; +import { compose } from '@wordpress/element'; +import { withSelect, withDispatch } from '@wordpress/data'; /** * Module Constants @@ -41,18 +32,16 @@ function DiscussionPanel( { isOpened, onTogglePanel } ) { ); } -export default connect( - ( state ) => { +export default compose( [ + withSelect( ( select ) => { return { - isOpened: isEditorSidebarPanelOpened( state, PANEL_NAME ), + isOpened: select( 'core/edit-post' ).isEditorSidebarPanelOpened( PANEL_NAME ), }; - }, - { + } ), + withDispatch( ( dispatch ) => ( { onTogglePanel() { - return toggleGeneralSidebarEditorPanel( PANEL_NAME ); + return dispatch( 'core/edit-post' ).toggleGeneralSidebarEditorPanel( PANEL_NAME ); }, - }, - undefined, - { storeKey: 'edit-post' } -)( DiscussionPanel ); + } ) ), +] )( DiscussionPanel ); diff --git a/edit-post/components/sidebar/document-outline-panel/index.js b/edit-post/components/sidebar/document-outline-panel/index.js index c99be78806ba58..19d97c3a9b79c6 100644 --- a/edit-post/components/sidebar/document-outline-panel/index.js +++ b/edit-post/components/sidebar/document-outline-panel/index.js @@ -1,20 +1,11 @@ -/** - * External dependencies - */ -import { connect } from 'react-redux'; - /** * WordPress dependencies */ import { __ } from '@wordpress/i18n'; import { PanelBody } from '@wordpress/components'; import { DocumentOutline, DocumentOutlineCheck } from '@wordpress/editor'; - -/** - * Internal dependencies - */ -import { isEditorSidebarPanelOpened } from '../../../store/selectors'; -import { toggleGeneralSidebarEditorPanel } from '../../../store/actions'; +import { compose } from '@wordpress/element'; +import { withSelect, withDispatch } from '@wordpress/data'; /** * Module constants @@ -31,17 +22,15 @@ function DocumentOutlinePanel( { isOpened, onTogglePanel } ) { ); } -export default connect( - ( state ) => { +export default compose( [ + withSelect( ( select ) => { return { - isOpened: isEditorSidebarPanelOpened( state, PANEL_NAME ), + isOpened: select( 'core/edit-post' ).isEditorSidebarPanelOpened( PANEL_NAME ), }; - }, - { + } ), + withDispatch( ( dispatch ) => ( { onTogglePanel() { - return toggleGeneralSidebarEditorPanel( PANEL_NAME ); + return dispatch( 'core/edit-post' ).toggleGeneralSidebarEditorPanel( PANEL_NAME ); }, - }, - undefined, - { storeKey: 'edit-post' } -)( DocumentOutlinePanel ); + } ) ), +] )( DocumentOutlinePanel ); diff --git a/edit-post/components/sidebar/featured-image/index.js b/edit-post/components/sidebar/featured-image/index.js index f0c5dc6029cd7f..9d979dbf6affde 100644 --- a/edit-post/components/sidebar/featured-image/index.js +++ b/edit-post/components/sidebar/featured-image/index.js @@ -1,23 +1,16 @@ /** * External dependencies */ -import { connect } from 'react-redux'; -import { get } from 'lodash'; +import { get, partial } from 'lodash'; /** * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { PanelBody, withAPIData } from '@wordpress/components'; +import { PanelBody } from '@wordpress/components'; import { PostFeaturedImage, PostFeaturedImageCheck } from '@wordpress/editor'; import { compose } from '@wordpress/element'; -import { withSelect } from '@wordpress/data'; - -/** - * Internal dependencies - */ -import { isEditorSidebarPanelOpened } from '../../../store/selectors'; -import { toggleGeneralSidebarEditorPanel } from '../../../store/actions'; +import { withSelect, withDispatch } from '@wordpress/data'; /** * Module Constants @@ -30,7 +23,7 @@ function FeaturedImage( { isOpened, postType, onTogglePanel } ) { <PanelBody title={ get( postType, - [ 'data', 'labels', 'featured_image' ], + [ 'labels', 'featured_image' ], __( 'Featured Image' ) ) } opened={ isOpened } @@ -42,34 +35,26 @@ function FeaturedImage( { isOpened, postType, onTogglePanel } ) { ); } -const applyWithSelect = withSelect( ( select ) => ( { - postTypeSlug: select( 'core/editor' ).getEditedPostAttribute( 'type' ), -} ) ); +const applyWithSelect = withSelect( ( select ) => { + const { getEditedPostAttribute } = select( 'core/editor' ); + const { getPostType } = select( 'core' ); + const { isEditorSidebarPanelOpened } = select( 'core/edit-post' ); + + return { + postType: getPostType( getEditedPostAttribute( 'type' ) ), + isOpened: isEditorSidebarPanelOpened( PANEL_NAME ), + }; +} ); -const applyConnect = connect( - ( state ) => { - return { - isOpened: isEditorSidebarPanelOpened( state, PANEL_NAME ), - }; - }, - { - onTogglePanel() { - return toggleGeneralSidebarEditorPanel( PANEL_NAME ); - }, - }, - undefined, - { storeKey: 'edit-post' } -); +const applyWithDispatch = withDispatch( ( dispatch ) => { + const { toggleGeneralSidebarEditorPanel } = dispatch( 'core/edit-post' ); -const applyWithAPIData = withAPIData( ( props ) => { - const { postTypeSlug } = props; return { - postType: `/wp/v2/types/${ postTypeSlug }?context=edit`, + onTogglePanel: partial( toggleGeneralSidebarEditorPanel, PANEL_NAME ), }; } ); export default compose( applyWithSelect, - applyConnect, - applyWithAPIData, + applyWithDispatch, )( FeaturedImage ); diff --git a/edit-post/components/sidebar/header.js b/edit-post/components/sidebar/header.js deleted file mode 100644 index 3e0901371b45ab..00000000000000 --- a/edit-post/components/sidebar/header.js +++ /dev/null @@ -1,64 +0,0 @@ -/** - * External dependencies - */ -import { connect } from 'react-redux'; - -/** - * WordPress dependencies - */ -import { compose } from '@wordpress/element'; -import { __, _n, sprintf } from '@wordpress/i18n'; -import { IconButton } from '@wordpress/components'; -import { withSelect } from '@wordpress/data'; - -/** - * Internal Dependencies - */ -import { getActiveEditorPanel } from '../../store/selectors'; -import { closeGeneralSidebar, setGeneralSidebarActivePanel } from '../../store/actions'; - -const SidebarHeader = ( { panel, onSetPanel, onCloseSidebar, count } ) => { - // Do not display "0 Blocks". - count = count === 0 ? 1 : count; - - return ( - <div className="components-panel__header edit-post-sidebar__panel-tabs"> - <button - onClick={ () => onSetPanel( 'document' ) } - className={ `edit-post-sidebar__panel-tab ${ panel === 'document' ? 'is-active' : '' }` } - aria-label={ __( 'Document settings' ) } - > - { __( 'Document' ) } - </button> - <button - onClick={ () => onSetPanel( 'block' ) } - className={ `edit-post-sidebar__panel-tab ${ panel === 'block' ? 'is-active' : '' }` } - aria-label={ __( 'Block settings' ) } - > - { sprintf( _n( 'Block', '%d Blocks', count ), count ) } - </button> - <IconButton - onClick={ onCloseSidebar } - icon="no-alt" - label={ __( 'Close settings' ) } - /> - </div> - ); -}; - -export default compose( - withSelect( ( select ) => ( { - count: select( 'core/editor' ).getSelectedBlockCount(), - } ) ), - connect( - ( state ) => ( { - panel: getActiveEditorPanel( state ), - } ), - { - onSetPanel: setGeneralSidebarActivePanel.bind( null, 'editor' ), - onCloseSidebar: closeGeneralSidebar, - }, - undefined, - { storeKey: 'edit-post' } - ) -)( SidebarHeader ); diff --git a/edit-post/components/sidebar/index.js b/edit-post/components/sidebar/index.js index 6a696b443b5fde..118b1ca64f8f17 100644 --- a/edit-post/components/sidebar/index.js +++ b/edit-post/components/sidebar/index.js @@ -1,75 +1,45 @@ -/** - * External dependencies - */ -import { connect } from 'react-redux'; - /** * WordPress Dependencies */ -import { __ } from '@wordpress/i18n'; -import { withFocusReturn } from '@wordpress/components'; +import { createSlotFill, ifCondition, withFocusReturn } from '@wordpress/components'; +import { withSelect } from '@wordpress/data'; +import { compose } from '@wordpress/element'; /** * Internal Dependencies */ import './style.scss'; -import PostSettings from './post-settings'; -import BlockInspectorPanel from './block-inspector-panel'; -import Header from './header'; -import { getActiveEditorPanel } from '../../store/selectors'; -/** - * Returns the panel that should be rendered in the sidebar. - * - * @param {string} panel The currently active panel. - * - * @return {Object} The React element to render as a panel. - */ -function getPanel( panel ) { - switch ( panel ) { - case 'document': - return PostSettings; - case 'block': - return BlockInspectorPanel; - default: - return PostSettings; - } -} +const SidebarFill = createSlotFill( 'editPost.Sidebar' ); /** - * Renders a sidebar with the relevant panel. - * - * @param {string} panel The currently active panel. + * Renders a sidebar with its content. * * @return {Object} The rendered sidebar. */ -const Sidebar = ( { panel } ) => { - const ActivePanel = getPanel( panel ); - - const props = { - panel, - }; - +const Sidebar = ( { children, label } ) => { return ( - <div - className="edit-post-sidebar" - role="region" - aria-label={ __( 'Editor advanced settings' ) } - tabIndex="-1" - > - <Header /> - <ActivePanel { ...props } /> - </div> + <SidebarFill> + <div + className="edit-post-sidebar" + role="region" + aria-label={ label } + tabIndex="-1" + > + { children } + </div> + </SidebarFill> ); }; -export default connect( - ( state ) => { - return { - panel: getActiveEditorPanel( state ), - }; - }, - undefined, - undefined, - { storeKey: 'edit-post' } -)( withFocusReturn( Sidebar ) ); +const WrappedSidebar = compose( + withSelect( ( select, { name } ) => ( { + isActive: select( 'core/edit-post' ).getActiveGeneralSidebarName() === name, + } ) ), + ifCondition( ( { isActive } ) => isActive ), + withFocusReturn, +)( Sidebar ); + +WrappedSidebar.Slot = SidebarFill.Slot; + +export default WrappedSidebar; diff --git a/edit-post/components/sidebar/page-attributes/index.js b/edit-post/components/sidebar/page-attributes/index.js index 8dd4a7f37759b5..a2ee57264ab7d0 100644 --- a/edit-post/components/sidebar/page-attributes/index.js +++ b/edit-post/components/sidebar/page-attributes/index.js @@ -1,23 +1,16 @@ /** * External dependencies */ -import { connect } from 'react-redux'; -import { get } from 'lodash'; +import { get, partial } from 'lodash'; /** * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { PanelBody, PanelRow, withAPIData } from '@wordpress/components'; +import { PanelBody, PanelRow } from '@wordpress/components'; import { compose } from '@wordpress/element'; import { PageAttributesCheck, PageAttributesOrder, PageAttributesParent, PageTemplate } from '@wordpress/editor'; -import { withSelect } from '@wordpress/data'; - -/** - * Internal dependencies - */ -import { toggleGeneralSidebarEditorPanel } from '../../../store/actions'; -import { isEditorSidebarPanelOpened } from '../../../store/selectors'; +import { withSelect, withDispatch } from '@wordpress/data'; /** * Module Constants @@ -25,13 +18,13 @@ import { isEditorSidebarPanelOpened } from '../../../store/selectors'; const PANEL_NAME = 'page-attributes'; export function PageAttributes( { isOpened, onTogglePanel, postType } ) { - if ( ! postType.data ) { + if ( ! postType ) { return null; } return ( <PageAttributesCheck> <PanelBody - title={ get( postType, 'data.labels.attributes', __( 'Page Attributes' ) ) } + title={ get( postType, 'labels.attributes', __( 'Page Attributes' ) ) } opened={ isOpened } onToggle={ onTogglePanel } > @@ -45,34 +38,25 @@ export function PageAttributes( { isOpened, onTogglePanel, postType } ) { ); } -const applyWithSelect = withSelect( ( select ) => ( { - postTypeSlug: select( 'core/editor' ).getEditedPostAttribute( 'type' ), -} ) ); +const applyWithSelect = withSelect( ( select ) => { + const { getEditedPostAttribute } = select( 'core/editor' ); + const { isEditorSidebarPanelOpened } = select( 'core/edit-post' ); + const { getPostType } = select( 'core' ); + return { + isOpened: isEditorSidebarPanelOpened( PANEL_NAME ), + postType: getPostType( getEditedPostAttribute( 'type' ) ), + }; +} ); -const applyConnect = connect( - ( state ) => { - return { - isOpened: isEditorSidebarPanelOpened( state, PANEL_NAME ), - }; - }, - { - onTogglePanel() { - return toggleGeneralSidebarEditorPanel( PANEL_NAME ); - }, - }, - undefined, - { storeKey: 'edit-post' } -); +const applyWithDispatch = withDispatch( ( dispatch ) => { + const { toggleGeneralSidebarEditorPanel } = dispatch( 'core/edit-post' ); -const applyWithAPIData = withAPIData( ( props ) => { - const { postTypeSlug } = props; return { - postType: `/wp/v2/types/${ postTypeSlug }?context=edit`, + onTogglePanel: partial( toggleGeneralSidebarEditorPanel, PANEL_NAME ), }; } ); export default compose( applyWithSelect, - applyConnect, - applyWithAPIData, + applyWithDispatch, )( PageAttributes ); diff --git a/edit-post/components/sidebar/post-excerpt/index.js b/edit-post/components/sidebar/post-excerpt/index.js index cf46d944e2c54d..994532ba40d258 100644 --- a/edit-post/components/sidebar/post-excerpt/index.js +++ b/edit-post/components/sidebar/post-excerpt/index.js @@ -1,20 +1,11 @@ -/** - * External dependencies - */ -import { connect } from 'react-redux'; - /** * WordPress dependencies */ import { __ } from '@wordpress/i18n'; import { PanelBody } from '@wordpress/components'; import { PostExcerpt as PostExcerptForm, PostExcerptCheck } from '@wordpress/editor'; - -/** - * Internal Dependencies - */ -import { isEditorSidebarPanelOpened } from '../../../store/selectors'; -import { toggleGeneralSidebarEditorPanel } from '../../../store/actions'; +import { compose } from '@wordpress/element'; +import { withSelect, withDispatch } from '@wordpress/data'; /** * Module Constants @@ -31,18 +22,16 @@ function PostExcerpt( { isOpened, onTogglePanel } ) { ); } -export default connect( - ( state ) => { +export default compose( [ + withSelect( ( select ) => { return { - isOpened: isEditorSidebarPanelOpened( state, PANEL_NAME ), + isOpened: select( 'core/edit-post' ).isEditorSidebarPanelOpened( PANEL_NAME ), }; - }, - { + } ), + withDispatch( ( dispatch ) => ( { onTogglePanel() { - return toggleGeneralSidebarEditorPanel( PANEL_NAME ); + return dispatch( 'core/edit-post' ).toggleGeneralSidebarEditorPanel( PANEL_NAME ); }, - }, - undefined, - { storeKey: 'edit-post' } -)( PostExcerpt ); + } ) ), +] )( PostExcerpt ); diff --git a/edit-post/components/sidebar/post-settings/index.js b/edit-post/components/sidebar/post-settings/index.js deleted file mode 100644 index 71a6aad75406e2..00000000000000 --- a/edit-post/components/sidebar/post-settings/index.js +++ /dev/null @@ -1,34 +0,0 @@ -/** - * WordPress dependencies - */ -import { Panel } from '@wordpress/components'; - -/** - * Internal Dependencies - */ -import './style.scss'; -import PostStatus from '../post-status'; -import PostExcerpt from '../post-excerpt'; -import PostTaxonomies from '../post-taxonomies'; -import FeaturedImage from '../featured-image'; -import DiscussionPanel from '../discussion-panel'; -import LastRevision from '../last-revision'; -import PageAttributes from '../page-attributes'; -import DocumentOutlinePanel from '../document-outline-panel'; -import MetaBoxes from '../../meta-boxes'; - -const panel = ( - <Panel> - <PostStatus /> - <LastRevision /> - <PostTaxonomies /> - <FeaturedImage /> - <PostExcerpt /> - <DiscussionPanel /> - <PageAttributes /> - <DocumentOutlinePanel /> - <MetaBoxes location="side" usePanel /> - </Panel> -); - -export default () => panel; diff --git a/edit-post/components/sidebar/post-settings/style.scss b/edit-post/components/sidebar/post-settings/style.scss deleted file mode 100644 index 5d1268114f02e1..00000000000000 --- a/edit-post/components/sidebar/post-settings/style.scss +++ /dev/null @@ -1,4 +0,0 @@ -.edit-post-sidebar-post-settings__icons { - display: inline-flex; - margin-right: -5px; -} diff --git a/edit-post/components/sidebar/post-status/index.js b/edit-post/components/sidebar/post-status/index.js index dbdf0a7056e13a..0143013fa0172c 100644 --- a/edit-post/components/sidebar/post-status/index.js +++ b/edit-post/components/sidebar/post-status/index.js @@ -1,13 +1,10 @@ -/** - * External dependencies - */ -import { connect } from 'react-redux'; - /** * WordPress dependencies */ import { __ } from '@wordpress/i18n'; import { PanelBody } from '@wordpress/components'; +import { compose } from '@wordpress/element'; +import { withSelect, withDispatch } from '@wordpress/data'; /** * Internal Dependencies @@ -20,10 +17,6 @@ import PostSticky from '../post-sticky'; import PostAuthor from '../post-author'; import PostFormat from '../post-format'; import PostPendingStatus from '../post-pending-status'; -import { - isEditorSidebarPanelOpened, -} from '../../../store/selectors'; -import { toggleGeneralSidebarEditorPanel } from '../../../store/actions'; /** * Module Constants @@ -44,16 +37,14 @@ function PostStatus( { isOpened, onTogglePanel } ) { ); } -export default connect( - ( state ) => ( { - isOpened: isEditorSidebarPanelOpened( state, PANEL_NAME ), - } ), - { +export default compose( [ + withSelect( ( select ) => ( { + isOpened: select( 'core/edit-post' ).isEditorSidebarPanelOpened( PANEL_NAME ), + } ) ), + withDispatch( ( dispatch ) => ( { onTogglePanel() { - return toggleGeneralSidebarEditorPanel( PANEL_NAME ); + return dispatch( 'core/edit-post' ).toggleGeneralSidebarEditorPanel( PANEL_NAME ); }, - }, - undefined, - { storeKey: 'edit-post' } -)( PostStatus ); + } ) ), +] )( PostStatus ); diff --git a/edit-post/components/sidebar/post-taxonomies/index.js b/edit-post/components/sidebar/post-taxonomies/index.js index aebae97031f711..a7a63b211f9e79 100644 --- a/edit-post/components/sidebar/post-taxonomies/index.js +++ b/edit-post/components/sidebar/post-taxonomies/index.js @@ -1,52 +1,46 @@ -/** - * External Dependencies - */ -import { connect } from 'react-redux'; - /** * WordPress dependencies */ -import { __ } from '@wordpress/i18n'; -import { PanelBody } from '@wordpress/components'; import { PostTaxonomies as PostTaxonomiesForm, PostTaxonomiesCheck } from '@wordpress/editor'; +import { compose } from '@wordpress/element'; +import { withSelect, withDispatch } from '@wordpress/data'; /** * Internal dependencies */ -import { isEditorSidebarPanelOpened } from '../../../store/selectors'; -import { toggleGeneralSidebarEditorPanel } from '../../../store/actions'; +import TaxonomyPanel from './taxonomy-panel'; /** * Module Constants */ const PANEL_NAME = 'post-taxonomies'; -function PostTaxonomies( { isOpened, onTogglePanel } ) { +function PostTaxonomies() { return ( <PostTaxonomiesCheck> - <PanelBody - title={ __( 'Categories & Tags' ) } - opened={ isOpened } - onToggle={ onTogglePanel } - > - <PostTaxonomiesForm /> - </PanelBody> + <PostTaxonomiesForm + taxonomyWrapper={ ( content, taxonomy ) => { + return ( + <TaxonomyPanel taxonomy={ taxonomy }> + { content } + </TaxonomyPanel> + ); + } } + /> </PostTaxonomiesCheck> ); } -export default connect( - ( state ) => { +export default compose( [ + withSelect( ( select ) => { return { - isOpened: isEditorSidebarPanelOpened( state, PANEL_NAME ), + isOpened: select( 'core/edit-post' ).isEditorSidebarPanelOpened( PANEL_NAME ), }; - }, - { + } ), + withDispatch( ( dispatch ) => ( { onTogglePanel() { - return toggleGeneralSidebarEditorPanel( PANEL_NAME ); + return dispatch( 'core/edit-post' ).toggleGeneralSidebarEditorPanel( PANEL_NAME ); }, - }, - undefined, - { storeKey: 'edit-post' } -)( PostTaxonomies ); + } ) ), +] )( PostTaxonomies ); diff --git a/edit-post/components/sidebar/style.scss b/edit-post/components/sidebar/style.scss index 8c85fd91d5ef3a..a3d5b5f8487135 100644 --- a/edit-post/components/sidebar/style.scss +++ b/edit-post/components/sidebar/style.scss @@ -13,7 +13,7 @@ @include break-small() { top: $admin-bar-height-big + $header-height; - z-index: auto; + z-index: z-index( '.edit-post-sidebar {greater than small}' ); height: auto; overflow: auto; -webkit-overflow-scrolling: touch; @@ -28,13 +28,15 @@ border-right: none; overflow: auto; -webkit-overflow-scrolling: touch; - height: 100%; + height: auto; + max-height: calc( 100vh - #{ $admin-bar-height-big + $panel-header-height } ); margin-top: -1px; margin-bottom: -1px; @include break-small() { overflow: inherit; height: auto; + max-height: none; } } @@ -79,6 +81,13 @@ p + div.components-toolbar { margin-top: -1em; } + + .editor-skip-to-selected-block:focus { + top: auto; + right: 10px; + bottom: 10px; + left: auto; + } } /* Visual and Text editor both */ @@ -90,7 +99,7 @@ .edit-post-layout.is-sidebar-opened { .edit-post-sidebar, - .edit-post-plugins-panel { + .edit-post-plugin-sidebar__sidebar-layout { /* Sidebar covers screen on mobile */ width: 100%; @@ -102,36 +111,57 @@ } /* Text Editor specific */ +.components-panel__header.edit-post-sidebar__header { + background: $white; + padding-right: $panel-padding / 2; + + .edit-post-sidebar__title { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + width: 100%; + } + + @include break-medium() { + display: none; + } +} + .components-panel__header.edit-post-sidebar__panel-tabs { justify-content: flex-start; padding-left: 0; padding-right: $panel-padding / 2; border-top: 0; + margin-top: 0; .components-icon-button { + display: none; margin-left: auto; + + @include break-medium() { + display: flex; + } } } .edit-post-sidebar__panel-tab { background: transparent; border: none; - border-bottom: 3px solid transparent; border-radius: 0; cursor: pointer; height: 50px; - line-height: 50px; - padding: 0 20px; + padding: 3px 15px; // Use padding to offset the is-active border, this benefits Windows High Contrast mode margin-left: 0; font-weight: 400; @include square-style__neutral; &.is-active { - border-bottom-color: $blue-medium-500; + padding-bottom: 0; + border-bottom: 3px solid $blue-medium-500; font-weight: 600; } &:focus { - @include square-style__focus-active; + @include square-style__focus; } } diff --git a/edit-post/hooks/index.js b/edit-post/hooks/index.js index f32f0019a1b24e..4025e1ac47617e 100644 --- a/edit-post/hooks/index.js +++ b/edit-post/hooks/index.js @@ -1,5 +1,6 @@ /** * Internal dependencies */ +import './blocks'; import './more-menu'; import './validate-use-once'; diff --git a/edit-post/hooks/more-menu/copy-content-menu-item/index.js b/edit-post/hooks/more-menu/copy-content-menu-item/index.js index 42596cdd8c5d1a..f10e9ba386ed73 100644 --- a/edit-post/hooks/more-menu/copy-content-menu-item/index.js +++ b/edit-post/hooks/more-menu/copy-content-menu-item/index.js @@ -10,7 +10,7 @@ function CopyContentMenuItem( { editedPostContent, hasCopied, setState } ) { return ( <ClipboardButton text={ editedPostContent } - className="components-menu-items__button" + className="components-menu-item__button" onCopy={ () => setState( { hasCopied: true } ) } onFinishCopy={ () => setState( { hasCopied: false } ) } > diff --git a/edit-post/hooks/validate-use-once/index.js b/edit-post/hooks/validate-use-once/index.js index 1f8c8a187630a0..6317a28971aff9 100644 --- a/edit-post/hooks/validate-use-once/index.js +++ b/edit-post/hooks/validate-use-once/index.js @@ -6,11 +6,11 @@ import { find } from 'lodash'; /** * WordPress dependencies */ -import { createBlock, getBlockType } from '@wordpress/blocks'; +import { createBlock, getBlockType, findTransform, getBlockTransforms } from '@wordpress/blocks'; import { Button } from '@wordpress/components'; import { withSelect, withDispatch } from '@wordpress/data'; import { Warning } from '@wordpress/editor'; -import { compose, getWrapperDisplayName } from '@wordpress/element'; +import { compose, createHigherOrderComponent } from '@wordpress/element'; import { addFilter } from '@wordpress/hooks'; import { __ } from '@wordpress/i18n'; @@ -49,8 +49,8 @@ const enhance = compose( } ) ), ); -function withUseOnceValidation( BlockEdit ) { - const WrappedBlockEdit = ( { +const withUseOnceValidation = createHigherOrderComponent( ( BlockEdit ) => { + return enhance( ( { originalBlockUid, selectFirst, ...props @@ -60,53 +60,55 @@ function withUseOnceValidation( BlockEdit ) { } const blockType = getBlockType( props.name ); - const outboundType = getOutboundType( blockType ); + const outboundType = getOutboundType( props.name ); return [ <div key="invalid-preview" style={ { minHeight: '100px' } }> <BlockEdit key="block-edit" { ...props } /> </div>, - <Warning key="use-once-warning"> - <p> - <strong>{ blockType.title }: </strong> - { __( 'This block may not be used more than once.' ) }</p> - <p> - <Button isLarge onClick={ - selectFirst - }>{ __( 'Find original' ) }</Button> - <Button isLarge onClick={ - () => props.onReplace( [] ) - }>{ __( 'Remove' ) }</Button> - { outboundType && - <Button isLarge onClick={ () => props.onReplace( - createBlock( outboundType.name, props.attributes ) - ) }> + <Warning + key="use-once-warning" + actions={ [ + <Button key="find-original" isLarge onClick={ selectFirst }> + { __( 'Find original' ) } + </Button>, + <Button key="remove" isLarge onClick={ () => props.onReplace( [] ) }> + { __( 'Remove' ) } + </Button>, + outboundType && ( + <Button + key="transform" + isLarge + onClick={ () => props.onReplace( + createBlock( outboundType.name, props.attributes ) + ) } + > { __( 'Transform into:' ) }{ ' ' } { outboundType.title } </Button> - } - </p> + ), + ] } + > + <strong>{ blockType.title }: </strong> + { __( 'This block may not be used more than once.' ) } </Warning>, ]; - }; - - WrappedBlockEdit.displayName = getWrapperDisplayName( BlockEdit, 'useOnceValidation' ); - - return enhance( WrappedBlockEdit ); -} + } ); +}, 'withUseOnceValidation' ); /** - * Given a base block type, returns the default block type to which to offer + * Given a base block name, returns the default block type to which to offer * transforms. * - * @param {Object} blockType Base block type. - * @return {?Object} The chosen default block type. + * @param {string} blockName Base block name. + * + * @return {?Object} The chosen default block type. */ -function getOutboundType( blockType ) { +function getOutboundType( blockName ) { // Grab the first outbound transform - const { to = [] } = blockType.transforms || {}; - const transform = find( to, ( { type, blocks } ) => - type === 'block' && blocks.length === 1 // What about when .length > 1? + const transform = findTransform( + getBlockTransforms( 'to', blockName ), + ( { type, blocks } ) => type === 'block' && blocks.length === 1 // What about when .length > 1? ); if ( ! transform ) { diff --git a/edit-post/index.js b/edit-post/index.js index ef9bf85f6463f6..76a4fc7de7a389 100644 --- a/edit-post/index.js +++ b/edit-post/index.js @@ -1,43 +1,17 @@ -/** - * External dependencies - */ -import moment from 'moment-timezone'; -import 'moment-timezone/moment-timezone-utils'; -import { createProvider } from 'react-redux'; - /** * WordPress dependencies */ import { render, unmountComponentAtNode } from '@wordpress/element'; -import { settings as dateSettings } from '@wordpress/date'; -import { EditorProvider, ErrorBoundary } from '@wordpress/editor'; /** * Internal dependencies */ import './assets/stylesheets/main.scss'; import './hooks'; -import Layout from './components/layout'; import store from './store'; import { initializeMetaBoxState } from './store/actions'; - -export * from './api'; - -// Configure moment globally -moment.locale( dateSettings.l10n.locale ); -if ( dateSettings.timezone.string ) { - moment.tz.setDefault( dateSettings.timezone.string ); -} else { - const momentTimezone = { - name: 'WP', - abbrs: [ 'WP' ], - untils: [ null ], - offsets: [ -dateSettings.timezone.offset * 60 ], - }; - const unpackedTimezone = moment.tz.pack( momentTimezone ); - moment.tz.add( unpackedTimezone ); - moment.tz.setDefault( 'WP' ); -} +import Editor from './editor'; +import PluginMoreMenuItem from './components/plugin-more-menu-item'; /** * Configure heartbeat to refresh the wp-api nonce, keeping the editor @@ -59,18 +33,10 @@ window.jQuery( document ).on( 'heartbeat-tick', ( event, response ) => { */ export function reinitializeEditor( target, settings ) { unmountComponentAtNode( target ); - const reboot = reinitializeEditor.bind( null, target, settings ); - const ReduxProvider = createProvider( 'edit-post' ); render( - <EditorProvider settings={ settings } recovery> - <ErrorBoundary onError={ reboot }> - <ReduxProvider store={ store }> - <Layout /> - </ReduxProvider> - </ErrorBoundary> - </EditorProvider>, + <Editor settings={ settings } onError={ reboot } recovery />, target ); } @@ -90,16 +56,9 @@ export function reinitializeEditor( target, settings ) { export function initializeEditor( id, post, settings ) { const target = document.getElementById( id ); const reboot = reinitializeEditor.bind( null, target, settings ); - const ReduxProvider = createProvider( 'edit-post' ); render( - <EditorProvider settings={ settings } post={ post }> - <ErrorBoundary onError={ reboot }> - <ReduxProvider store={ store }> - <Layout /> - </ReduxProvider> - </ErrorBoundary> - </EditorProvider>, + <Editor settings={ settings } onError={ reboot } post={ post } />, target ); @@ -109,3 +68,9 @@ export function initializeEditor( id, post, settings ) { }, }; } + +export const __experimental = { + PluginMoreMenuItem, +}; + +export { default as PluginSidebar } from './components/sidebar/plugin-sidebar'; diff --git a/edit-post/store/actions.js b/edit-post/store/actions.js index c635ab8f76ebe6..44dfa42034f506 100644 --- a/edit-post/store/actions.js +++ b/edit-post/store/actions.js @@ -1,32 +1,13 @@ - -/** - * Returns an action object used in signalling that the user switched the active - * sidebar tab panel. - * - * @param {string} sidebar Sidebar name - * @param {string} panel Panel name - * @return {Object} Action object - */ -export function setGeneralSidebarActivePanel( sidebar, panel ) { - return { - type: 'SET_GENERAL_SIDEBAR_ACTIVE_PANEL', - sidebar, - panel, - }; -} - /** - * Returns an action object used in signalling that the user opened a sidebar. + * Returns an action object used in signalling that the user opened an editor sidebar. * - * @param {string} sidebar Sidebar to open. - * @param {string} [panel = null] Panel to open in the sidebar. Null if unchanged. - * @return {Object} Action object. + * @param {string} name Sidebar name to be opened. + * @return {Object} Action object. */ -export function openGeneralSidebar( sidebar, panel = null ) { +export function openGeneralSidebar( name ) { return { type: 'OPEN_GENERAL_SIDEBAR', - sidebar, - panel, + name, }; } @@ -89,19 +70,6 @@ export function toggleGeneralSidebarEditorPanel( panel ) { }; } -/** - * Returns an action object used in signalling that the viewport type preference should be set. - * - * @param {string} viewportType The viewport type (desktop or mobile). - * @return {Object} Action object. - */ -export function setViewportType( viewportType ) { - return { - type: 'SET_VIEWPORT_TYPE', - viewportType, - }; -} - /** * Returns an action object used to toggle a feature flag. * diff --git a/edit-post/store/constants.js b/edit-post/store/constants.js deleted file mode 100644 index d15bc20dc69715..00000000000000 --- a/edit-post/store/constants.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Internal dependencies - */ -import breakpointsScssVariables from '!!sass-variables-loader!../assets/stylesheets/_breakpoints.scss'; - -export const BREAK_HUGE = parseInt( breakpointsScssVariables.breakHuge ); -export const BREAK_WIDE = parseInt( breakpointsScssVariables.breakWide ); -export const BREAK_LARGE = parseInt( breakpointsScssVariables.breakLarge ); -export const BREAK_MEDIUM = parseInt( breakpointsScssVariables.breakMedium ); -export const BREAK_SMALL = parseInt( breakpointsScssVariables.breakSmall ); -export const BREAK_MOBILE = parseInt( breakpointsScssVariables.breakMobile ); diff --git a/edit-post/store/defaults.js b/edit-post/store/defaults.js index bb5277c4efb797..5b20d0ec189817 100644 --- a/edit-post/store/defaults.js +++ b/edit-post/store/defaults.js @@ -1,11 +1,6 @@ export const PREFERENCES_DEFAULTS = { editorMode: 'visual', - viewportType: 'desktop', // 'desktop' | 'mobile' - activeGeneralSidebar: 'editor', // null | 'editor' | 'plugin' - activeSidebarPanel: { // The keys in this object should match activeSidebarPanel values - editor: null, // 'document' | 'block' - plugin: null, // pluginId - }, + activeGeneralSidebar: 'edit-post/document', // null | 'edit-post/block' | 'edit-post/document' | 'plugin/*' panels: { 'post-status': true }, features: { fixedToolbar: false, diff --git a/edit-post/store/effects.js b/edit-post/store/effects.js index 5a275aa94c89ca..221e2bc62a3d7b 100644 --- a/edit-post/store/effects.js +++ b/edit-post/store/effects.js @@ -1,12 +1,14 @@ /** * External dependencies */ -import { reduce, values, some } from 'lodash'; +import { reduce, some } from 'lodash'; /** * WordPress dependencies */ import { select, subscribe } from '@wordpress/data'; +import { speak } from '@wordpress/a11y'; +import { __ } from '@wordpress/i18n'; /** * Internal dependencies @@ -15,18 +17,31 @@ import { metaBoxUpdatesSuccess, setMetaBoxSavedData, requestMetaBoxUpdates, + openGeneralSidebar, + closeGeneralSidebar, } from './actions'; -import { getMetaBoxes } from './selectors'; +import { getMetaBoxes, getActiveGeneralSidebarName } from './selectors'; import { getMetaBoxContainer } from '../utils/meta-boxes'; +import { onChangeListener } from './utils'; const effects = { INITIALIZE_META_BOX_STATE( action, store ) { const hasActiveMetaBoxes = some( action.metaBoxes ); + if ( ! hasActiveMetaBoxes ) { + return; + } // Allow toggling metaboxes panels - if ( hasActiveMetaBoxes ) { - window.postboxes.add_postbox_toggles( 'post' ); - } + // We need to wait for all scripts to load + // If the meta box loads the post script, it will already trigger this. + // After merge in Core, make sure to drop the timeout and update the postboxes script + // to avoid the double binding. + setTimeout( () => { + const postType = select( 'core/editor' ).getCurrentPostType(); + if ( window.postboxes.page !== postType ) { + window.postboxes.add_postbox_toggles( postType ); + } + } ); // Initialize metaboxes state const dataPerLocation = reduce( action.metaBoxes, ( memo, isActive, location ) => { @@ -38,15 +53,11 @@ const effects = { store.dispatch( setMetaBoxSavedData( dataPerLocation ) ); // Saving metaboxes when saving posts - let previousIsSaving = select( 'core/editor' ).isSavingPost(); - subscribe( () => { - const isSavingPost = select( 'core/editor' ).isSavingPost(); - const shouldTriggerSaving = ! isSavingPost && previousIsSaving; - previousIsSaving = isSavingPost; - if ( shouldTriggerSaving ) { + subscribe( onChangeListener( select( 'core/editor' ).isSavingPost, ( isSavingPost ) => { + if ( ! isSavingPost ) { store.dispatch( requestMetaBoxUpdates() ); } - } ); + } ) ); }, REQUEST_META_BOX_UPDATES( action, store ) { const state = store.getState(); @@ -62,27 +73,81 @@ const effects = { // If we do not provide this data the post will be overriden with the default values. const post = select( 'core/editor' ).getCurrentPost( state ); const additionalData = [ - post.comment_status && `comment_status=${ post.comment_status }`, - post.ping_status && `ping_status=${ post.ping_status }`, + post.comment_status ? [ 'comment_status', post.comment_status ] : false, + post.ping_status ? [ 'ping_status', post.ping_status ] : false, + post.sticky ? [ 'sticky', post.sticky ] : false, + [ 'post_author', post.author ], ].filter( Boolean ); - // To save the metaboxes, we serialize each one of the location forms and combine them - // We also add the "common" hidden fields from the base .metabox-base-form - const formData = values( dataPerLocation ) - .concat( jQuery( '.metabox-base-form' ).serialize() ) - .concat( additionalData ) - .join( '&' ); - const fetchOptions = { - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: formData, - credentials: 'include', - }; + // We gather all the metaboxes locations data and the base form data + const baseFormData = new window.FormData( document.querySelector( '.metabox-base-form' ) ); + const formDataToMerge = reduce( getMetaBoxes( state ), ( memo, metabox, location ) => { + if ( metabox.isActive ) { + memo.push( new window.FormData( getMetaBoxContainer( location ) ) ); + } + return memo; + }, [ baseFormData ] ); + + // Merge all form data objects into a single one. + const formData = reduce( formDataToMerge, ( memo, currentFormData ) => { + for ( const [ key, value ] of currentFormData ) { + memo.append( key, value ); + } + return memo; + }, new window.FormData() ); + additionalData.forEach( ( [ key, value ] ) => formData.append( key, value ) ); // Save the metaboxes - window.fetch( window._wpMetaBoxUrl, fetchOptions ) + wp.apiRequest( { + url: window._wpMetaBoxUrl, + method: 'POST', + processData: false, + contentType: false, + data: formData, + } ) .then( () => store.dispatch( metaBoxUpdatesSuccess() ) ); }, + SWITCH_MODE( action ) { + const message = action.mode === 'visual' ? __( 'Visual editor selected' ) : __( 'Code editor selected' ); + speak( message, 'assertive' ); + }, + INIT( _, store ) { + // Select the block settings tab when the selected block changes + subscribe( onChangeListener( + () => !! select( 'core/editor' ).getBlockSelectionStart(), + ( hasBlockSelection ) => { + if ( ! select( 'core/edit-post' ).isEditorSidebarOpened() ) { + return; + } + if ( hasBlockSelection ) { + store.dispatch( openGeneralSidebar( 'edit-post/block' ) ); + } else { + store.dispatch( openGeneralSidebar( 'edit-post/document' ) ); + } + } ) + ); + + // Collapse sidebar when viewport shrinks. + subscribe( onChangeListener( + () => select( 'core/viewport' ).isViewportMatch( '< medium' ), + ( () => { + // contains the sidebar we close when going to viewport sizes lower than medium. + // This allows to reopen it when going again to viewport sizes greater than medium. + let sidebarToReOpenOnExpand = null; + return ( isSmall ) => { + if ( isSmall ) { + sidebarToReOpenOnExpand = getActiveGeneralSidebarName( store.getState() ); + if ( sidebarToReOpenOnExpand ) { + store.dispatch( closeGeneralSidebar() ); + } + } else if ( sidebarToReOpenOnExpand && ! getActiveGeneralSidebarName( store.getState() ) ) { + store.dispatch( openGeneralSidebar( sidebarToReOpenOnExpand ) ); + } + }; + } )() + ) ); + }, + }; export default effects; diff --git a/edit-post/store/index.js b/edit-post/store/index.js index d96869e558bbc7..7b276eb0373eda 100644 --- a/edit-post/store/index.js +++ b/edit-post/store/index.js @@ -1,27 +1,33 @@ /** * WordPress Dependencies */ -import { registerReducer, withRehydratation, loadAndPersist } from '@wordpress/data'; +import { + registerStore, + withRehydratation, + loadAndPersist, +} from '@wordpress/data'; /** * Internal dependencies */ import reducer from './reducer'; -import enhanceWithBrowserSize from './mobile'; -import { BREAK_MEDIUM } from './constants'; import applyMiddlewares from './middlewares'; +import * as actions from './actions'; +import * as selectors from './selectors'; /** * Module Constants */ const STORAGE_KEY = `WP_EDIT_POST_PREFERENCES_${ window.userSettings.uid }`; -const MODULE_KEY = 'core/edit-post'; -const store = applyMiddlewares( - registerReducer( MODULE_KEY, withRehydratation( reducer, 'preferences', STORAGE_KEY ) ) -); +const store = registerStore( 'core/edit-post', { + reducer: withRehydratation( reducer, 'preferences', STORAGE_KEY ), + actions, + selectors, +} ); +applyMiddlewares( store ); loadAndPersist( store, reducer, 'preferences', STORAGE_KEY ); -enhanceWithBrowserSize( store, BREAK_MEDIUM ); +store.dispatch( { type: 'INIT' } ); export default store; diff --git a/edit-post/store/middlewares.js b/edit-post/store/middlewares.js index 790b6e548786e9..33c748dc9551d4 100644 --- a/edit-post/store/middlewares.js +++ b/edit-post/store/middlewares.js @@ -36,10 +36,8 @@ function applyMiddlewares( store ) { chain = middlewares.map( middleware => middleware( middlewareAPI ) ); enhancedDispatch = flowRight( ...chain )( store.dispatch ); - return { - ...store, - dispatch: enhancedDispatch, - }; + store.dispatch = enhancedDispatch; + return store; } export default applyMiddlewares; diff --git a/edit-post/store/mobile.js b/edit-post/store/mobile.js deleted file mode 100644 index 797ce3a3a96dfb..00000000000000 --- a/edit-post/store/mobile.js +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Enhance a redux store with the browser size. - * - * @param {Object} store Redux Store. - * @param {number} mobileBreakpoint The mobile breakpoint. - */ -function enhanceWithBrowserSize( store, mobileBreakpoint ) { - const updateSize = () => { - store.dispatch( { - type: 'UPDATE_MOBILE_STATE', - isMobile: window.innerWidth < mobileBreakpoint, - } ); - }; - - const mediaQueryList = window.matchMedia( `(min-width: ${ mobileBreakpoint }px)` ); - mediaQueryList.addListener( updateSize ); - window.addEventListener( 'orientationchange', updateSize ); - updateSize(); -} - -export default enhanceWithBrowserSize; diff --git a/edit-post/store/reducer.js b/edit-post/store/reducer.js index 1fa763747c6883..695b8208405bcb 100644 --- a/edit-post/store/reducer.js +++ b/edit-post/store/reducer.js @@ -1,8 +1,7 @@ /** - * External dependencies + * WordPress dependencies */ -import { combineReducers } from 'redux'; -import { get, omit } from 'lodash'; +import { combineReducers } from '@wordpress/data'; /** * Internal dependencies @@ -20,75 +19,46 @@ import { PREFERENCES_DEFAULTS } from './defaults'; * * @return {string} Updated state. */ -export function preferences( state = PREFERENCES_DEFAULTS, action ) { - switch ( action.type ) { - case 'OPEN_GENERAL_SIDEBAR': - const activeSidebarPanel = action.panel ? action.panel : state.activeSidebarPanel[ action.sidebar ]; - return { - ...state, - activeGeneralSidebar: action.sidebar, - activeSidebarPanel: { - ...state.activeSidebarPanel, - [ action.sidebar ]: activeSidebarPanel, - }, - }; - case 'SET_GENERAL_SIDEBAR_ACTIVE_PANEL': - return { - ...state, - activeSidebarPanel: { - ...state.activeSidebarPanel, - [ action.sidebar ]: action.panel, - }, - }; - case 'CLOSE_GENERAL_SIDEBAR': - return { - ...state, - activeGeneralSidebar: null, - }; - case 'TOGGLE_GENERAL_SIDEBAR_EDITOR_PANEL': - return { - ...state, - panels: { - ...state.panels, - [ action.panel ]: ! get( state, [ 'panels', action.panel ], false ), - }, - }; - case 'SET_VIEWPORT_TYPE': - return { - ...state, - viewportType: action.viewportType, - }; - case 'UPDATE_MOBILE_STATE': - if ( action.isMobile ) { - return { - ...state, - viewportType: 'mobile', - activeGeneralSidebar: null, - }; - } - return { - ...state, - viewportType: 'desktop', - }; - case 'SWITCH_MODE': +export const preferences = combineReducers( { + activeGeneralSidebar( state = PREFERENCES_DEFAULTS.activeGeneralSidebar, action ) { + switch ( action.type ) { + case 'OPEN_GENERAL_SIDEBAR': + return action.name; + + case 'CLOSE_GENERAL_SIDEBAR': + return null; + } + + return state; + }, + panels( state = PREFERENCES_DEFAULTS.panels, action ) { + if ( action.type === 'TOGGLE_GENERAL_SIDEBAR_EDITOR_PANEL' ) { return { ...state, - editorMode: action.mode, + [ action.panel ]: ! state[ action.panel ], }; - case 'TOGGLE_FEATURE': + } + + return state; + }, + features( state = PREFERENCES_DEFAULTS.features, action ) { + if ( action.type === 'TOGGLE_FEATURE' ) { return { ...state, - features: { - ...state.features, - [ action.feature ]: ! state.features[ action.feature ], - }, + [ action.feature ]: ! state[ action.feature ], }; - case 'SERIALIZE': - return omit( state, [ 'sidebars.mobile', 'sidebars.publish' ] ); - } + } - return state; -} + return state; + }, + editorMode( state = PREFERENCES_DEFAULTS.editorMode, action ) { + if ( action.type === 'SWITCH_MODE' ) { + return action.mode; + } + + return state; + }, +} ); export function panel( state = 'document', action ) { switch ( action.type ) { @@ -111,13 +81,6 @@ export function publishSidebarActive( state = false, action ) { return state; } -export function mobile( state = false, action ) { - if ( action.type === 'UPDATE_MOBILE_STATE' ) { - return action.isMobile; - } - return state; -} - const locations = [ 'normal', 'side', @@ -191,7 +154,6 @@ export default combineReducers( { preferences, panel, publishSidebarActive, - mobile, metaBoxes, isSavingMetaBoxes, } ); diff --git a/edit-post/store/selectors.js b/edit-post/store/selectors.js index bbf325d6351931..f34c0df9e5a2e4 100644 --- a/edit-post/store/selectors.js +++ b/edit-post/store/selectors.js @@ -2,7 +2,7 @@ * External dependencies */ import createSelector from 'rememo'; -import { some } from 'lodash'; +import { includes, some } from 'lodash'; /** * Returns the current editing mode. @@ -16,24 +16,37 @@ export function getEditorMode( state ) { } /** - * Returns the current active panel for the sidebar. + * Returns true if the editor sidebar is opened. * - * @param {Object} state Global application state. + * @param {Object} state Global application state + * @return {boolean} Whether the editor sidebar is opened. + */ +export function isEditorSidebarOpened( state ) { + const activeGeneralSidebar = getPreference( state, 'activeGeneralSidebar', null ); + + return includes( [ 'edit-post/document', 'edit-post/block' ], activeGeneralSidebar ); +} + +/** + * Returns true if the plugin sidebar is opened. * - * @return {string} Active sidebar panel. + * @param {Object} state Global application state + * @return {boolean} Whether the plugin sidebar is opened. */ -export function getActiveEditorPanel( state ) { - return getPreference( state, 'activeSidebarPanel', {} ).editor; +export function isPluginSidebarOpened( state ) { + const activeGeneralSidebar = getActiveGeneralSidebarName( state ); + return !! activeGeneralSidebar && ! isEditorSidebarOpened( state ); } /** - * Returns the current active plugin for the plugin sidebar. + * Returns the current active general sidebar name. * - * @param {Object} state Global application state - * @return {string} Active plugin sidebar plugin + * @param {Object} state Global application state. + * + * @return {?string} Active general sidebar name. */ -export function getActivePlugin( state ) { - return getPreference( state, 'activeSidebarPanel', {} ).plugin; +export function getActiveGeneralSidebarName( state ) { + return getPreference( state, 'activeGeneralSidebar', null ); } /** @@ -61,30 +74,6 @@ export function getPreference( state, preferenceKey, defaultValue ) { return value === undefined ? defaultValue : value; } -/** - * Returns the opened general sidebar and null if the sidebar is closed. - * - * @param {Object} state Global application state. - * @return {string} The opened general sidebar panel. - */ -export function getOpenedGeneralSidebar( state ) { - return getPreference( state, 'activeGeneralSidebar' ); -} - -/** - * Returns true if the panel is open in the currently opened sidebar. - * - * @param {Object} state Global application state - * @param {string} sidebar Sidebar name (leave undefined for the default sidebar) - * @param {string} panel Sidebar panel name (leave undefined for the default panel) - * @return {boolean} Whether the given general sidebar panel is open - */ -export function isGeneralSidebarPanelOpened( state, sidebar, panel ) { - const activeGeneralSidebar = getPreference( state, 'activeGeneralSidebar' ); - const activeSidebarPanel = getPreference( state, 'activeSidebarPanel' ); - return activeGeneralSidebar === sidebar && activeSidebarPanel === panel; -} - /** * Returns true if the publish sidebar is opened. * @@ -95,20 +84,6 @@ export function isPublishSidebarOpened( state ) { return state.publishSidebarActive; } -/** - * Returns true if there's any open sidebar (mobile, desktop or publish). - * - * @param {Object} state Global application state. - * - * @return {boolean} Whether sidebar is open. - */ -export function hasOpenSidebar( state ) { - const generalSidebarOpen = getPreference( state, 'activeGeneralSidebar' ) !== null; - const publishSidebarOpen = state.publishSidebarActive; - - return generalSidebarOpen || publishSidebarOpen; -} - /** * Returns true if the editor sidebar panel is open, or false otherwise. * @@ -121,29 +96,6 @@ export function isEditorSidebarPanelOpened( state, panel ) { return panels ? !! panels[ panel ] : false; } -/** - * Returns true if the current window size corresponds to mobile resolutions (<= medium breakpoint). - * - * @param {Object} state Global application state. - * - * @return {boolean} Whether current window size corresponds to - * mobile resolutions. - */ -export function isMobile( state ) { - return state.mobile; -} - -/** - * Returns whether the toolbar should be fixed or not. - * - * @param {Object} state Global application state. - * - * @return {boolean} True if toolbar is fixed. - */ -export function hasFixedToolbar( state ) { - return ! isMobile( state ) && isFeatureActive( state, 'fixedToolbar' ); -} - /** * Returns whether the given feature is enabled or not. * diff --git a/edit-post/store/test/actions.js b/edit-post/store/test/actions.js index 7c1ff16c09d15d..5da68294a71cd9 100644 --- a/edit-post/store/test/actions.js +++ b/edit-post/store/test/actions.js @@ -2,38 +2,24 @@ * Internal dependencies */ import { - setGeneralSidebarActivePanel, toggleGeneralSidebarEditorPanel, openGeneralSidebar, closeGeneralSidebar, openPublishSidebar, closePublishSidebar, togglePublishSidebar, - setViewportType, toggleFeature, requestMetaBoxUpdates, initializeMetaBoxState, } from '../actions'; describe( 'actions', () => { - describe( 'setGeneralSidebarActivePanel', () => { - it( 'should return SET_GENERAL_SIDEBAR_ACTIVE_PANEL action', () => { - expect( setGeneralSidebarActivePanel( 'editor', 'document' ) ).toEqual( { - type: 'SET_GENERAL_SIDEBAR_ACTIVE_PANEL', - sidebar: 'editor', - panel: 'document', - } ); - } ); - } ); - describe( 'openGeneralSidebar', () => { it( 'should return OPEN_GENERAL_SIDEBAR action', () => { - const sidebar = 'sidebarName'; - const panel = 'panelName'; - expect( openGeneralSidebar( sidebar, panel ) ).toEqual( { + const name = 'plugin/my-name'; + expect( openGeneralSidebar( name ) ).toEqual( { type: 'OPEN_GENERAL_SIDEBAR', - sidebar, - panel, + name, } ); } ); } ); @@ -80,16 +66,6 @@ describe( 'actions', () => { } ); } ); - describe( 'setViewportType', () => { - it( 'should return SET_VIEWPORT_TYPE action', () => { - const viewportType = 'mobile'; - expect( setViewportType( viewportType ) ).toEqual( { - type: 'SET_VIEWPORT_TYPE', - viewportType, - } ); - } ); - } ); - describe( 'toggleFeature', () => { it( 'should return TOGGLE_FEATURE action', () => { const feature = 'name'; diff --git a/edit-post/store/test/reducer.js b/edit-post/store/test/reducer.js index 4f6a1407155427..5ead70e8b2c9e1 100644 --- a/edit-post/store/test/reducer.js +++ b/edit-post/store/test/reducer.js @@ -18,46 +18,55 @@ describe( 'state', () => { const state = preferences( undefined, {} ); expect( state ).toEqual( { - activeGeneralSidebar: 'editor', - activeSidebarPanel: { - editor: null, - plugin: null, - }, + activeGeneralSidebar: 'edit-post/document', editorMode: 'visual', panels: { 'post-status': true }, features: { fixedToolbar: false }, - viewportType: 'desktop', } ); } ); - it( 'should set the general sidebar active panel', () => { - const state = preferences( deepFreeze( { - activeGeneralSidebar: 'editor', - activeSidebarPanel: { - editor: null, - plugin: null, - }, - } ), { - type: 'SET_GENERAL_SIDEBAR_ACTIVE_PANEL', - sidebar: 'editor', - panel: 'document', + it( 'should set the general sidebar', () => { + const original = deepFreeze( preferences( undefined, {} ) ); + const state = preferences( original, { + type: 'OPEN_GENERAL_SIDEBAR', + name: 'edit-post/document', } ); - expect( state ).toEqual( { - activeGeneralSidebar: 'editor', - activeSidebarPanel: { - editor: 'document', - plugin: null, - }, + + expect( state.activeGeneralSidebar ).toBe( 'edit-post/document' ); + } ); + + it( 'should does not update if sidebar is already set to value', () => { + const original = deepFreeze( preferences( undefined, { + type: 'OPEN_GENERAL_SIDEBAR', + name: 'edit-post/document', + } ) ); + const state = preferences( original, { + type: 'OPEN_GENERAL_SIDEBAR', + name: 'edit-post/document', } ); + + expect( original ).toBe( state ); + } ); + + it( 'should unset the general sidebar', () => { + const original = deepFreeze( preferences( undefined, { + type: 'OPEN_GENERAL_SIDEBAR', + name: 'edit-post/document', + } ) ); + const state = preferences( original, { + type: 'CLOSE_GENERAL_SIDEBAR', + } ); + + expect( state.activeGeneralSidebar ).toBe( null ); } ); it( 'should set the sidebar panel open flag to true if unset', () => { - const state = preferences( deepFreeze( {} ), { + const state = preferences( deepFreeze( { panels: {} } ), { type: 'TOGGLE_GENERAL_SIDEBAR_EDITOR_PANEL', panel: 'post-taxonomies', } ); - expect( state ).toEqual( { panels: { 'post-taxonomies': true } } ); + expect( state.panels ).toEqual( { 'post-taxonomies': true } ); } ); it( 'should toggle the sidebar panel open flag', () => { @@ -66,16 +75,16 @@ describe( 'state', () => { panel: 'post-taxonomies', } ); - expect( state ).toEqual( { panels: { 'post-taxonomies': false } } ); + expect( state.panels ).toEqual( { 'post-taxonomies': false } ); } ); it( 'should return switched mode', () => { - const state = preferences( deepFreeze( {} ), { + const state = preferences( deepFreeze( { editorMode: 'visual' } ), { type: 'SWITCH_MODE', mode: 'text', } ); - expect( state ).toEqual( { editorMode: 'text' } ); + expect( state.editorMode ).toBe( 'text' ); } ); it( 'should toggle a feature flag', () => { @@ -83,7 +92,8 @@ describe( 'state', () => { type: 'TOGGLE_FEATURE', feature: 'chicken', } ); - expect( state ).toEqual( { features: { chicken: false } } ); + + expect( state.features ).toEqual( { chicken: false } ); } ); } ); diff --git a/edit-post/store/test/selectors.js b/edit-post/store/test/selectors.js index 689d30afdf6e44..6c2dbbd87e8ebb 100644 --- a/edit-post/store/test/selectors.js +++ b/edit-post/store/test/selectors.js @@ -4,22 +4,16 @@ import { getEditorMode, getPreference, - isGeneralSidebarPanelOpened, - hasOpenSidebar, + isEditorSidebarOpened, isEditorSidebarPanelOpened, - isMobile, - hasFixedToolbar, isFeatureActive, + isPluginSidebarOpened, getMetaBoxes, hasMetaBoxes, isSavingMetaBoxes, getMetaBox, } from '../selectors'; -jest.mock( '../constants', () => ( { - BREAK_MEDIUM: 500, -} ) ); - describe( 'selectors', () => { describe( 'getEditorMode', () => { it( 'should return the selected editor mode', () => { @@ -65,70 +59,68 @@ describe( 'selectors', () => { } ); } ); - describe( 'isGeneralSidebarPanelOpened', () => { - it( 'should return true when specified the sidebar panel is opened', () => { + describe( 'isEditorSidebarOpened', () => { + it( 'should return false when the editor sidebar is not opened', () => { const state = { preferences: { - activeGeneralSidebar: 'editor', - viewportType: 'desktop', - activeSidebarPanel: 'document', + activeGeneralSidebar: null, }, }; - const panel = 'document'; - const sidebar = 'editor'; - expect( isGeneralSidebarPanelOpened( state, sidebar, panel ) ).toBe( true ); + expect( isEditorSidebarOpened( state ) ).toBe( false ); } ); - it( 'should return false when another panel than the specified sidebar panel is opened', () => { + it( 'should return false when the plugin sidebar is opened', () => { const state = { preferences: { - activeGeneralSidebar: 'editor', - viewportType: 'desktop', - activeSidebarPanel: 'blocks', + activeGeneralSidebar: 'my-plugin/my-sidebar', }, }; - const panel = 'document'; - const sidebar = 'editor'; - expect( isGeneralSidebarPanelOpened( state, sidebar, panel ) ).toBe( false ); + expect( isEditorSidebarOpened( state ) ).toBe( false ); } ); - it( 'should return false when no sidebar panel is opened', () => { + it( 'should return true when the editor sidebar is opened', () => { const state = { preferences: { - activeGeneralSidebar: null, - viewportType: 'desktop', - activeSidebarPanel: null, + activeGeneralSidebar: 'edit-post/document', }, }; - const panel = 'blocks'; - const sidebar = 'editor'; - expect( isGeneralSidebarPanelOpened( state, sidebar, panel ) ).toBe( false ); + expect( isEditorSidebarOpened( state ) ).toBe( true ); } ); } ); - describe( 'hasOpenSidebar', () => { - it( 'should return true if at least one sidebar is open', () => { + describe( 'isPluginSidebarOpened', () => { + it( 'should return false when the plugin sidebar is not opened', () => { const state = { preferences: { - activeSidebarPanel: null, + activeGeneralSidebar: null, }, }; - expect( hasOpenSidebar( state ) ).toBe( true ); + expect( isPluginSidebarOpened( state ) ).toBe( false ); } ); - it( 'should return false if no sidebar is open', () => { + it( 'should return false when the editor sidebar is opened', () => { const state = { - publishSidebarActive: false, preferences: { - activeGeneralSidebar: null, + activeGeneralSidebar: 'edit-post/document', }, }; - expect( hasOpenSidebar( state ) ).toBe( false ); + expect( isPluginSidebarOpened( state ) ).toBe( false ); + } ); + + it( 'should return true when the plugin sidebar is opened', () => { + const name = 'plugin-sidebar/my-plugin/my-sidebar'; + const state = { + preferences: { + activeGeneralSidebar: name, + }, + }; + + expect( isPluginSidebarOpened( state ) ).toBe( true ); } ); } ); @@ -158,78 +150,6 @@ describe( 'selectors', () => { } ); } ); - describe( 'isMobile', () => { - it( 'should return true if resolution is equal or less than medium breakpoint', () => { - const state = { - mobile: true, - }; - - expect( isMobile( state ) ).toBe( true ); - } ); - - it( 'should return true if resolution is greater than medium breakpoint', () => { - const state = { - mobile: false, - }; - - expect( isMobile( state ) ).toBe( false ); - } ); - } ); - - describe( 'hasFixedToolbar', () => { - it( 'should return true if fixedToolbar is active and is not mobile screen size', () => { - const state = { - mobile: false, - preferences: { - features: { - fixedToolbar: true, - }, - }, - }; - - expect( hasFixedToolbar( state ) ).toBe( true ); - } ); - - it( 'should return false if fixedToolbar is active and is mobile screen size', () => { - const state = { - mobile: true, - preferences: { - features: { - fixedToolbar: true, - }, - }, - }; - - expect( hasFixedToolbar( state ) ).toBe( false ); - } ); - - it( 'should return false if fixedToolbar is disable and is not mobile screen size', () => { - const state = { - mobile: false, - preferences: { - features: { - fixedToolbar: false, - }, - }, - }; - - expect( hasFixedToolbar( state ) ).toBe( false ); - } ); - - it( 'should return false if fixedToolbar is disable and is mobile screen size', () => { - const state = { - mobile: true, - preferences: { - features: { - fixedToolbar: false, - }, - }, - }; - - expect( hasFixedToolbar( state ) ).toBe( false ); - } ); - } ); - describe( 'isFeatureActive', () => { it( 'should return true if feature is active', () => { const state = { diff --git a/editor/components/autosave-monitor/index.js b/editor/components/autosave-monitor/index.js index f47f126ab054d3..462230dac7da86 100644 --- a/editor/components/autosave-monitor/index.js +++ b/editor/components/autosave-monitor/index.js @@ -1,21 +1,8 @@ -/** - * External dependencies - */ -import { connect } from 'react-redux'; - /** * WordPress dependencies */ -import { Component } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import { autosave } from '../../store/actions'; -import { - isEditedPostDirty, - isEditedPostSaveable, -} from '../../store/selectors'; +import { Component, compose } from '@wordpress/element'; +import { withSelect, withDispatch } from '@wordpress/data'; export class AutosaveMonitor extends Component { componentDidUpdate( prevProps ) { @@ -46,12 +33,15 @@ export class AutosaveMonitor extends Component { } } -export default connect( - ( state ) => { +export default compose( [ + withSelect( ( select ) => { + const { isEditedPostDirty, isEditedPostSaveable } = select( 'core/editor' ); return { - isDirty: isEditedPostDirty( state ), - isSaveable: isEditedPostSaveable( state ), + isDirty: isEditedPostDirty(), + isSaveable: isEditedPostSaveable(), }; - }, - { autosave } -)( AutosaveMonitor ); + } ), + withDispatch( ( dispatch ) => ( { + autosave: dispatch( 'core/editor' ).autosave, + } ) ), +] )( AutosaveMonitor ); diff --git a/editor/components/block-drop-zone/index.js b/editor/components/block-drop-zone/index.js index 1654048f20436f..3f05aef9348182 100644 --- a/editor/components/block-drop-zone/index.js +++ b/editor/components/block-drop-zone/index.js @@ -1,93 +1,144 @@ /** * External Dependencies */ -import { connect } from 'react-redux'; -import { reduce, get, find, castArray } from 'lodash'; +import { castArray } from 'lodash'; +import classnames from 'classnames'; /** * WordPress dependencies */ -import { DropZone, withContext } from '@wordpress/components'; -import { getBlockTypes, rawHandler, cloneBlock } from '@wordpress/blocks'; -import { compose } from '@wordpress/element'; +import { DropZone } from '@wordpress/components'; +import { + rawHandler, + cloneBlock, + getBlockTransforms, + findTransform, + withEditorSettings, +} from '@wordpress/blocks'; +import { compose, Component } from '@wordpress/element'; +import { withDispatch } from '@wordpress/data'; /** * Internal dependencies */ -import { insertBlocks, updateBlockAttributes } from '../../store/actions'; +import './style.scss'; -function BlockDropZone( { index, isLocked, ...props } ) { - if ( isLocked ) { - return null; +class BlockDropZone extends Component { + constructor() { + super( ...arguments ); + + this.onFilesDrop = this.onFilesDrop.bind( this ); + this.onHTMLDrop = this.onHTMLDrop.bind( this ); + this.onDrop = this.onDrop.bind( this ); } - const getInsertIndex = ( position ) => { + getInsertIndex( position ) { + const { index } = this.props; if ( index !== undefined ) { return position.y === 'top' ? index : index + 1; } - }; - - const onDropFiles = ( files, position ) => { - const transformation = reduce( getBlockTypes(), ( ret, blockType ) => { - if ( ret ) { - return ret; - } + } - return find( get( blockType, 'transforms.from', [] ), ( transform ) => ( - transform.type === 'files' && transform.isMatch( files ) - ) ); - }, false ); + onFilesDrop( files, position ) { + const transformation = findTransform( + getBlockTransforms( 'from' ), + ( transform ) => transform.type === 'files' && transform.isMatch( files ) + ); if ( transformation ) { - const insertIndex = getInsertIndex( position ); - const blocks = transformation.transform( files, props.updateBlockAttributes ); - props.insertBlocks( blocks, insertIndex ); + const insertIndex = this.getInsertIndex( position ); + const blocks = transformation.transform( files, this.props.updateBlockAttributes ); + this.props.insertBlocks( blocks, insertIndex ); } - }; + } - const onHTMLDrop = ( HTML, position ) => { + onHTMLDrop( HTML, position ) { const blocks = rawHandler( { HTML, mode: 'BLOCKS' } ); if ( blocks.length ) { - props.insertBlocks( blocks, getInsertIndex( position ) ); + this.props.insertBlocks( blocks, this.getInsertIndex( position ) ); + } + } + + onDrop( event, position ) { + if ( ! event.dataTransfer ) { + return; + } + + let uid, type, rootUID, fromIndex; + + try { + ( { uid, type, rootUID, fromIndex } = JSON.parse( event.dataTransfer.getData( 'text' ) ) ); + } catch ( err ) { + return; + } + + if ( type !== 'block' ) { + return; } - }; - - return ( - <DropZone - onFilesDrop={ onDropFiles } - onHTMLDrop={ onHTMLDrop } - /> - ); + const { index } = this.props; + const positionIndex = this.getInsertIndex( position ); + + // If the block is kept at the same level and moved downwards, subtract + // to account for blocks shifting upward to occupy its old position. + const insertIndex = index && fromIndex < index && rootUID === this.props.rootUID ? positionIndex - 1 : positionIndex; + this.props.moveBlockToPosition( uid, rootUID, insertIndex ); + } + + render() { + const { isLocked, index } = this.props; + if ( isLocked ) { + return null; + } + const isAppender = index === undefined; + + return ( + <DropZone + className={ classnames( 'editor-block-drop-zone', { + 'is-appender': isAppender, + } ) } + onFilesDrop={ this.onFilesDrop } + onHTMLDrop={ this.onHTMLDrop } + onDrop={ this.onDrop } + /> + ); + } } export default compose( - connect( - undefined, - ( dispatch, ownProps ) => { - return { - insertBlocks( blocks, insertIndex ) { - const { rootUID, layout } = ownProps; - - if ( layout ) { - // A block's transform function may return a single - // transformed block or an array of blocks, so ensure - // to first coerce to an array before mapping to inject - // the layout attribute. - blocks = castArray( blocks ).map( ( block ) => ( - cloneBlock( block, { layout } ) - ) ); - } - - dispatch( insertBlocks( blocks, insertIndex, rootUID ) ); - }, - updateBlockAttributes( ...args ) { - dispatch( updateBlockAttributes( ...args ) ); - }, - }; - } - ), - withContext( 'editor' )( ( settings ) => { + withDispatch( ( dispatch, ownProps ) => { + const { + insertBlocks, + updateBlockAttributes, + moveBlockToPosition, + } = dispatch( 'core/editor' ); + + return { + insertBlocks( blocks, insertIndex ) { + const { rootUID, layout } = ownProps; + + if ( layout ) { + // A block's transform function may return a single + // transformed block or an array of blocks, so ensure + // to first coerce to an array before mapping to inject + // the layout attribute. + blocks = castArray( blocks ).map( ( block ) => ( + cloneBlock( block, { layout } ) + ) ); + } + + insertBlocks( blocks, insertIndex, rootUID ); + }, + updateBlockAttributes( ...args ) { + updateBlockAttributes( ...args ); + }, + moveBlockToPosition( uid, fromRootUID, index ) { + const { rootUID, layout } = ownProps; + moveBlockToPosition( uid, fromRootUID, rootUID, layout, index ); + }, + }; + } ), + withEditorSettings( ( settings ) => { const { templateLock } = settings; return { diff --git a/editor/components/block-inspector/index.js b/editor/components/block-inspector/index.js index 6d9b0545430776..928b980905d152 100644 --- a/editor/components/block-inspector/index.js +++ b/editor/components/block-inspector/index.js @@ -1,20 +1,26 @@ /** * External dependencies */ -import { connect } from 'react-redux'; +import { isEmpty } from 'lodash'; /** * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { Slot } from '@wordpress/components'; -import { getBlockType, BlockIcon } from '@wordpress/blocks'; +import { + BlockIcon, + getBlockType, + InspectorControls, + InspectorAdvancedControls, +} from '@wordpress/blocks'; +import { PanelBody } from '@wordpress/components'; +import { withSelect } from '@wordpress/data'; /** * Internal Dependencies */ import './style.scss'; -import { getSelectedBlock, getSelectedBlockCount } from '../../store/selectors'; +import SkipToSelectedBlock from '../skip-to-selected-block'; const BlockInspector = ( { selectedBlock, count } ) => { if ( count > 1 ) { @@ -37,15 +43,28 @@ const BlockInspector = ( { selectedBlock, count } ) => { <div className="editor-block-inspector__card-description">{ blockType.description }</div> </div> </div>, - <Slot name="Inspector.Controls" key="inspector-controls" />, + <InspectorControls.Slot key="inspector-controls" />, + <InspectorAdvancedControls.Slot key="inspector-advanced-controls"> + { ( fills ) => ! isEmpty( fills ) && ( + <PanelBody + className="editor-block-inspector__advanced" + title={ __( 'Advanced' ) } + initialOpen={ false } + > + { fills } + </PanelBody> + ) } + </InspectorAdvancedControls.Slot>, + <SkipToSelectedBlock key="back" />, ]; }; -export default connect( - ( state ) => { +export default withSelect( + ( select ) => { + const { getSelectedBlock, getSelectedBlockCount } = select( 'core/editor' ); return { - selectedBlock: getSelectedBlock( state ), - count: getSelectedBlockCount( state ), + selectedBlock: getSelectedBlock(), + count: getSelectedBlockCount(), }; } )( BlockInspector ); diff --git a/editor/components/block-inspector/style.scss b/editor/components/block-inspector/style.scss index 5b582584e5444d..a6d93da8dc5c54 100644 --- a/editor/components/block-inspector/style.scss +++ b/editor/components/block-inspector/style.scss @@ -4,16 +4,17 @@ font-size: $default-font-size; background: $white; padding: ( $panel-padding * 2 ) $panel-padding; - border-bottom: 1px solid $light-gray-500; text-align: center; } +.editor-block-inspector__multi-blocks { + border-bottom: 1px solid $light-gray-500; +} .editor-block-inspector__card { display: flex; align-items: flex-start; - border-bottom: 1px solid $light-gray-500; - margin: -16px -16px 16px -16px; + margin: -16px; padding: 16px; } diff --git a/editor/components/block-list/block-crash-warning.js b/editor/components/block-list/block-crash-warning.js index f712961a4c4dd4..626785255fbbe7 100644 --- a/editor/components/block-list/block-crash-warning.js +++ b/editor/components/block-list/block-crash-warning.js @@ -10,9 +10,7 @@ import Warning from '../warning'; const warning = ( <Warning> - <p>{ __( - 'This block has encountered an error and cannot be previewed.' - ) }</p> + { __( 'This block has encountered an error and cannot be previewed.' ) } </Warning> ); diff --git a/editor/components/block-list/block-html.js b/editor/components/block-list/block-html.js index 56ac18c1b17ccb..43413558971f50 100644 --- a/editor/components/block-list/block-html.js +++ b/editor/components/block-list/block-html.js @@ -1,21 +1,16 @@ -/** - * WordPress Dependencies - */ -import { isEqual } from 'lodash'; -import { Component } from '@wordpress/element'; -import { getBlockAttributes, getBlockContent, getBlockType, isValidBlock } from '@wordpress/blocks'; /** * External Dependencies */ -import { connect } from 'react-redux'; import TextareaAutosize from 'react-autosize-textarea'; +import { isEqual } from 'lodash'; /** - * Internal Dependencies + * WordPress Dependencies */ -import { updateBlock } from '../../store/actions'; -import { getBlock } from '../../store/selectors'; +import { Component, compose } from '@wordpress/element'; +import { getBlockAttributes, getBlockContent, getBlockType, isValidBlock } from '@wordpress/blocks'; +import { withSelect, withDispatch } from '@wordpress/data'; class BlockHTML extends Component { constructor( props ) { @@ -59,13 +54,13 @@ class BlockHTML extends Component { } } -export default connect( - ( state, ownProps ) => ( { - block: getBlock( state, ownProps.uid ), - } ), - { +export default compose( [ + withSelect( ( select, ownProps ) => ( { + block: select( 'core/editor' ).getBlock( ownProps.uid ), + } ) ), + withDispatch( dispatch => ( { onChange( uid, attributes, originalContent, isValid ) { - return updateBlock( uid, { attributes, originalContent, isValid } ); + dispatch( 'core/editor' ).updateBlock( uid, { attributes, originalContent, isValid } ); }, - } -)( BlockHTML ); + } ) ), +] )( BlockHTML ); diff --git a/editor/components/block-list/block-mobile-toolbar.js b/editor/components/block-list/block-mobile-toolbar.js index d80157c3512d94..a2b19e98e8c9b0 100644 --- a/editor/components/block-list/block-mobile-toolbar.js +++ b/editor/components/block-list/block-mobile-toolbar.js @@ -1,3 +1,8 @@ +/** + * WordPress dependencies + */ +import { ifViewportMatches } from '@wordpress/viewport'; + /** * Internal dependencies */ @@ -6,15 +11,15 @@ import BlockRemoveButton from '../block-settings-menu/block-remove-button'; import BlockSettingsMenu from '../block-settings-menu'; import VisualEditorInserter from '../inserter'; -function BlockMobileToolbar( { uid, renderBlockMenu } ) { +function BlockMobileToolbar( { rootUID, uid, renderBlockMenu } ) { return ( <div className="editor-block-list__block-mobile-toolbar"> <VisualEditorInserter /> <BlockMover uids={ [ uid ] } /> <BlockRemoveButton uids={ [ uid ] } small /> - <BlockSettingsMenu uids={ [ uid ] } renderBlockMenu={ renderBlockMenu } /> + <BlockSettingsMenu rootUID={ rootUID } uids={ [ uid ] } renderBlockMenu={ renderBlockMenu } /> </div> ); } -export default BlockMobileToolbar; +export default ifViewportMatches( '< small' )( BlockMobileToolbar ); diff --git a/editor/components/block-list/block.js b/editor/components/block-list/block.js index 6f00b36419e0d1..7d5da4e893d4b3 100644 --- a/editor/components/block-list/block.js +++ b/editor/components/block-list/block.js @@ -1,19 +1,18 @@ /** * External dependencies */ -import { connect } from 'react-redux'; import classnames from 'classnames'; -import { get, reduce, size, castArray, noop, first, last } from 'lodash'; +import { get, reduce, size, castArray, first, last, noop } from 'lodash'; import tinymce from 'tinymce'; /** * WordPress dependencies */ -import { Component, findDOMNode, compose } from '@wordpress/element'; +import { Component, findDOMNode, Fragment, compose } from '@wordpress/element'; import { keycodes, focus, - getScrollContainer, + isTextField, placeCaretAtHorizontalEdge, placeCaretAtVerticalEdge, } from '@wordpress/utils'; @@ -23,11 +22,14 @@ import { cloneBlock, getBlockType, getSaveElement, - isReusableBlock, + isSharedBlock, isUnmodifiedDefaultBlock, + withEditorSettings, } from '@wordpress/blocks'; -import { withFilters, withContext, withAPIData } from '@wordpress/components'; +import { withFilters } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; +import { withDispatch, withSelect } from '@wordpress/data'; +import { withViewportMatch } from '@wordpress/viewport'; /** * Internal dependencies @@ -39,44 +41,19 @@ import InvalidBlockWarning from './invalid-block-warning'; import BlockCrashWarning from './block-crash-warning'; import BlockCrashBoundary from './block-crash-boundary'; import BlockHtml from './block-html'; +import BlockBreadcrumb from './breadcrumb'; import BlockContextualToolbar from './block-contextual-toolbar'; import BlockMultiControls from './multi-controls'; import BlockMobileToolbar from './block-mobile-toolbar'; import BlockInsertionPoint from './insertion-point'; +import BlockDraggable from './block-draggable'; import IgnoreNestedEvents from './ignore-nested-events'; import InserterWithShortcuts from '../inserter-with-shortcuts'; -import { createInnerBlockList } from './utils'; -import { - clearSelectedBlock, - editPost, - insertBlocks, - mergeBlocks, - removeBlock, - replaceBlocks, - selectBlock, - startTyping, - stopTyping, - updateBlockAttributes, - toggleSelection, -} from '../../store/actions'; -import { - getBlock, - isMultiSelecting, - getBlockIndex, - getEditedPostAttribute, - getNextBlockUid, - getPreviousBlockUid, - isBlockMultiSelected, - isBlockSelected, - isFirstMultiSelectedBlock, - isSelectionEnabled, - isTyping, - getBlockMode, - getCurrentPostType, - getSelectedBlocksInitialCaretPosition, -} from '../../store/selectors'; - -const { BACKSPACE, ESCAPE, DELETE, ENTER, UP, RIGHT, DOWN, LEFT } = keycodes; +import Inserter from '../inserter'; +import withHoverAreas from './with-hover-areas'; +import { createInnerBlockList } from '../../utils/block-list'; + +const { BACKSPACE, DELETE, ENTER } = keycodes; export class BlockListBlock extends Component { constructor() { @@ -87,107 +64,63 @@ export class BlockListBlock extends Component { this.setAttributes = this.setAttributes.bind( this ); this.maybeHover = this.maybeHover.bind( this ); this.hideHoverEffects = this.hideHoverEffects.bind( this ); - this.maybeStartTyping = this.maybeStartTyping.bind( this ); - this.stopTypingOnMouseMove = this.stopTypingOnMouseMove.bind( this ); this.mergeBlocks = this.mergeBlocks.bind( this ); + this.insertBlocksAfter = this.insertBlocksAfter.bind( this ); this.onFocus = this.onFocus.bind( this ); this.preventDrag = this.preventDrag.bind( this ); this.onPointerDown = this.onPointerDown.bind( this ); - this.onKeyDown = this.onKeyDown.bind( this ); + this.deleteOrInsertAfterWrapper = this.deleteOrInsertAfterWrapper.bind( this ); this.onBlockError = this.onBlockError.bind( this ); - this.insertBlocksAfter = this.insertBlocksAfter.bind( this ); this.onTouchStart = this.onTouchStart.bind( this ); this.onClick = this.onClick.bind( this ); + this.onDragStart = this.onDragStart.bind( this ); + this.onDragEnd = this.onDragEnd.bind( this ); this.selectOnOpen = this.selectOnOpen.bind( this ); - this.onSelectionChange = this.onSelectionChange.bind( this ); - - this.previousOffset = null; this.hadTouchStart = false; this.state = { error: null, + dragging: false, isHovered: false, - isSelectionCollapsed: true, }; } + /** + * Provides context for descendent components for use in block rendering. + * + * @return {Object} Child context. + */ getChildContext() { - const { - uid, - renderBlockMenu, - showContextualToolbar, - } = this.props; - + // Blocks may render their own BlockEdit, in which case we must provide + // a mechanism for them to create their own InnerBlockList. BlockEdit + // is defined in `@wordpress/blocks`, so to avoid a circular dependency + // we inject this function via context. return { - BlockList: createInnerBlockList( - uid, - renderBlockMenu, - showContextualToolbar - ), - canUserUseUnfilteredHTML: get( this.props.user, [ 'data', 'capabilities', 'unfiltered_html' ], false ), + createInnerBlockList: ( uid ) => { + const { renderBlockMenu } = this.props; + return createInnerBlockList( uid, renderBlockMenu ); + }, }; } componentDidMount() { - if ( this.props.isTyping ) { - document.addEventListener( 'mousemove', this.stopTypingOnMouseMove ); - } - document.addEventListener( 'selectionchange', this.onSelectionChange ); - if ( this.props.isSelected ) { this.focusTabbable(); } } componentWillReceiveProps( newProps ) { - if ( - this.props.order !== newProps.order && - ( newProps.isSelected || newProps.isFirstMultiSelected ) - ) { - this.previousOffset = this.node.getBoundingClientRect().top; - } - - if ( newProps.isTyping || newProps.isSelected ) { + if ( newProps.isTypingWithinBlock || newProps.isSelected ) { this.hideHoverEffects(); } } componentDidUpdate( prevProps ) { - // Preserve scroll prosition when block rearranged - if ( this.previousOffset ) { - const scrollContainer = getScrollContainer( this.node ); - if ( scrollContainer ) { - scrollContainer.scrollTop = scrollContainer.scrollTop + - this.node.getBoundingClientRect().top - - this.previousOffset; - } - - this.previousOffset = null; - } - - // Bind or unbind mousemove from page when user starts or stops typing - if ( this.props.isTyping !== prevProps.isTyping ) { - if ( this.props.isTyping ) { - document.addEventListener( 'mousemove', this.stopTypingOnMouseMove ); - } else { - this.removeStopTypingListener(); - } - } - if ( this.props.isSelected && ! prevProps.isSelected ) { this.focusTabbable(); } } - componentWillUnmount() { - this.removeStopTypingListener(); - document.removeEventListener( 'selectionchange', this.onSelectionChange ); - } - - removeStopTypingListener() { - document.removeEventListener( 'mousemove', this.stopTypingOnMouseMove ); - } - setBlockListRef( node ) { // Disable reason: The root return element uses a component to manage // event nesting, but the parent block list layout needs the raw DOM @@ -196,13 +129,14 @@ export class BlockListBlock extends Component { // eslint-disable-next-line react/no-find-dom-node node = findDOMNode( node ); + this.wrapperNode = node; + this.props.blockRef( node, this.props.uid ); } bindBlockNode( node ) { // Disable reason: The block element uses a component to manage event - // nesting, but we rely on a raw DOM node for focusing and preserving - // scroll offset on move. + // nesting, but we rely on a raw DOM node for focusing. // // eslint-disable-next-line react/no-find-dom-node this.node = findDOMNode( node ); @@ -214,20 +148,24 @@ export class BlockListBlock extends Component { focusTabbable() { const { initialPosition } = this.props; - if ( this.node.contains( document.activeElement ) ) { + // Focus is captured by the wrapper node, so while focus transition + // should only consider tabbables within editable display, since it + // may be the wrapper itself or a side control which triggered the + // focus event, don't unnecessary transition to an inner tabbable. + if ( this.wrapperNode.contains( document.activeElement ) ) { return; } // Find all tabbables within node. - const tabbables = focus.tabbable.find( this.node ) - .filter( ( node ) => node !== this.node ); + const textInputs = focus.tabbable.find( this.node ).filter( isTextField ); // If reversed (e.g. merge via backspace), use the last in the set of // tabbables. const isReverse = -1 === initialPosition; - const target = ( isReverse ? last : first )( tabbables ); + const target = ( isReverse ? last : first )( textInputs ); if ( ! target ) { + this.wrapperNode.focus(); return; } @@ -314,31 +252,6 @@ export class BlockListBlock extends Component { } } - maybeStartTyping() { - // We do not want to dispatch start typing if state value already reflects - // that we're typing (dispatch noise) - if ( ! this.props.isTyping ) { - this.props.onStartTyping(); - } - } - - stopTypingOnMouseMove( { clientX, clientY } ) { - const { lastClientX, lastClientY } = this; - - // We need to check that the mouse really moved - // Because Safari trigger mousemove event when we press shift, ctrl... - if ( - lastClientX && - lastClientY && - ( lastClientX !== clientX || lastClientY !== clientY ) - ) { - this.props.onStopTyping(); - } - - this.lastClientX = clientX; - this.lastClientY = clientY; - } - mergeBlocks( forward = false ) { const { block, previousBlockUid, nextBlockUid, onMerge } = this.props; @@ -355,10 +268,6 @@ export class BlockListBlock extends Component { } else { onMerge( previousBlockUid, block.uid ); } - - // Manually trigger typing mode, since merging will remove this block and - // cause onKeyDown to not fire - this.maybeStartTyping(); } insertBlocksAfter( blocks ) { @@ -370,20 +279,10 @@ export class BlockListBlock extends Component { * specifically handles the case where block does not set focus on its own * (via `setFocus`), typically if there is no focusable input in the block. * - * @param {FocusEvent} event A focus event - * * @return {void} */ - onFocus( event ) { - // Firefox-specific: Firefox will redirect focus of an already-focused - // node to its parent, but assign a property before doing so. If that - // property exists, ensure that it is the node, or abort. - const { explicitOriginalTarget } = event.nativeEvent; - if ( explicitOriginalTarget && explicitOriginalTarget !== this.node ) { - return; - } - - if ( event.target === this.node && ! this.props.isSelected ) { + onFocus() { + if ( ! this.props.isSelected && ! this.props.isMultiSelected ) { this.props.onSelect(); } } @@ -422,62 +321,52 @@ export class BlockListBlock extends Component { } else { this.props.onSelectionStart( this.props.uid ); - if ( ! this.props.isSelected ) { + // Allow user to escape out of a multi-selection to a singular + // selection of a block via click. This is handled here since + // onFocus excludes blocks involved in a multiselection, as + // focus can be incurred by starting a multiselection (focus + // moved to first block's multi-controls). + if ( this.props.isMultiSelected ) { this.props.onSelect(); } } } - onKeyDown( event ) { + /** + * Interprets keydown event intent to remove or insert after block if key + * event occurs on wrapper node. This can occur when the block has no text + * fields of its own, particularly after initial insertion, to allow for + * easy deletion and continuous writing flow to add additional content. + * + * @param {KeyboardEvent} event Keydown event. + */ + deleteOrInsertAfterWrapper( event ) { const { keyCode, target } = event; + if ( target !== this.wrapperNode || this.props.isLocked ) { + return; + } + switch ( keyCode ) { case ENTER: // Insert default block after current block if enter and event // not already handled by descendant. - if ( target === this.node && ! this.props.isLocked ) { - event.preventDefault(); - - this.props.onInsertBlocks( [ - createBlock( 'core/paragraph' ), - ], this.props.order + 1 ); - } - - // Pressing enter should trigger typing mode after the content has split - this.maybeStartTyping(); - break; - - case UP: - case RIGHT: - case DOWN: - case LEFT: - // Arrow keys do not fire keypress event, but should still - // trigger typing mode. - this.maybeStartTyping(); + this.props.onInsertBlocks( [ + createBlock( 'core/paragraph' ), + ], this.props.order + 1 ); + event.preventDefault(); break; case BACKSPACE: case DELETE: // Remove block on backspace. - if ( target === this.node ) { - const { uid, onRemove, isLocked, previousBlock, onSelect } = this.props; - event.preventDefault(); - if ( ! isLocked ) { - onRemove( uid ); - - if ( previousBlock ) { - onSelect( previousBlock.uid, -1 ); - } - } - } - - // Pressing backspace should trigger typing mode - this.maybeStartTyping(); - break; + const { uid, onRemove, previousBlockUid, onSelect } = this.props; + onRemove( uid ); - case ESCAPE: - // Deselect on escape. - this.props.onDeselect(); + if ( previousBlockUid ) { + onSelect( previousBlockUid, -1 ); + } + event.preventDefault(); break; } } @@ -486,22 +375,17 @@ export class BlockListBlock extends Component { this.setState( { error } ); } - selectOnOpen( open ) { - if ( open && ! this.props.isSelected ) { - this.props.onSelect(); - } + onDragStart() { + this.setState( { dragging: true } ); } - onSelectionChange() { - if ( ! this.props.isSelected ) { - return; - } + onDragEnd() { + this.setState( { dragging: false } ); + } - const selection = window.getSelection(); - const isCollapsed = selection.rangeCount > 0 && selection.getRangeAt( 0 ).collapsed; - // We only keep track of the collapsed selection for selected blocks. - if ( isCollapsed !== this.state.isSelectionCollapsed && this.props.isSelected ) { - this.setState( { isSelectionCollapsed: isCollapsed } ); + selectOnOpen( open ) { + if ( open && ! this.props.isSelected ) { + this.props.onSelect(); } } @@ -510,10 +394,11 @@ export class BlockListBlock extends Component { block, order, mode, - showContextualToolbar, + hasFixedToolbar, isLocked, isFirst, isLast, + uid, rootUID, layout, renderBlockMenu, @@ -521,8 +406,12 @@ export class BlockListBlock extends Component { isMultiSelected, isFirstMultiSelected, isLastInSelection, + isTypingWithinBlock, + isMultiSelecting, + hoverArea, + isLargeViewport, } = this.props; - const isHovered = this.state.isHovered && ! this.props.isMultiSelecting; + const isHovered = this.state.isHovered && ! isMultiSelecting; const { name: blockName, isValid } = block; const blockType = getBlockType( blockName ); // translators: %s: Type of block (i.e. Text, Image etc) @@ -530,22 +419,28 @@ export class BlockListBlock extends Component { // The block as rendered in the editor is composed of general block UI // (mover, toolbar, wrapper) and the display of the block content. - // If the block is selected and we're typing the block should not appear as selected unless the selection is not collapsed. + // If the block is selected and we're typing the block should not appear. // Empty paragraph blocks should always show up as unselected. const isEmptyDefaultBlock = isUnmodifiedDefaultBlock( block ); + const isSelectedNotTyping = isSelected && ! isTypingWithinBlock; const showSideInserter = ( isSelected || isHovered ) && isEmptyDefaultBlock; - const isSelectedNotTyping = isSelected && ( ! this.props.isTyping || ! this.state.isSelectionCollapsed ); const shouldAppearSelected = ! showSideInserter && isSelectedNotTyping; - const shouldShowMovers = shouldAppearSelected || isHovered || ( isEmptyDefaultBlock && isSelectedNotTyping ); - const shouldShowSettingsMenu = shouldShowMovers; - const shouldShowContextualToolbar = shouldAppearSelected && isValid && showContextualToolbar; + // We render block movers and block settings to keep them tabbale even if hidden + const shouldRenderMovers = ( isSelected || hoverArea === 'left' ) && ! showSideInserter && ! isMultiSelecting && ! isMultiSelected; + const shouldRenderBlockSettings = ( isSelected || hoverArea === 'right' ) && ! showSideInserter && ! isMultiSelecting && ! isMultiSelected; + const shouldShowBreadcrumb = isHovered; + const shouldShowContextualToolbar = shouldAppearSelected && isValid && ( ! hasFixedToolbar || ! isLargeViewport ); const shouldShowMobileToolbar = shouldAppearSelected; - const { error } = this.state; + const { error, dragging } = this.state; // Insertion point can only be made visible when the side inserter is // not present, and either the block is at the extent of a selection or - // is the last block in the top-level list rendering. - const shouldShowInsertionPoint = ! showSideInserter && ( isLastInSelection || ( isLast && ! rootUID ) ); + // is the first block in the top-level list rendering. + const shouldShowInsertionPoint = ( + ( ! isMultiSelected && ! isFirst ) || + ( isMultiSelected && isLastInSelection ) || + ( isFirst && ! rootUID && ! isEmptyDefaultBlock ) + ); // Generate the wrapper class names handling the different states of the block. const wrapperClassName = classnames( 'editor-block-list__block', { @@ -553,7 +448,9 @@ export class BlockListBlock extends Component { 'is-selected': shouldAppearSelected, 'is-multi-selected': isMultiSelected, 'is-hovered': isHovered, - 'is-reusable': isReusableBlock( blockType ), + 'is-shared': isSharedBlock( blockType ), + 'is-hidden': dragging, + 'is-typing': isTypingWithinBlock, } ); const { onReplace } = this.props; @@ -566,6 +463,7 @@ export class BlockListBlock extends Component { ...blockType.getEditWrapperProps( block.attributes ), }; } + const blockElementId = `block-${ uid }`; // Disable reasons: // @@ -578,56 +476,78 @@ export class BlockListBlock extends Component { /* eslint-disable jsx-a11y/mouse-events-have-key-events, jsx-a11y/no-static-element-interactions, jsx-a11y/onclick-has-role, jsx-a11y/click-events-have-key-events */ return ( <IgnoreNestedEvents + id={ blockElementId } ref={ this.setBlockListRef } onMouseOver={ this.maybeHover } + onMouseOverHandled={ this.hideHoverEffects } onMouseLeave={ this.hideHoverEffects } className={ wrapperClassName } data-type={ block.name } onTouchStart={ this.onTouchStart } + onFocus={ this.onFocus } onClick={ this.onClick } + onKeyDown={ this.deleteOrInsertAfterWrapper } + tabIndex="0" childHandledEvents={ [ - 'onKeyPress', 'onDragStart', 'onMouseDown', - 'onKeyDown', - 'onFocus', ] } { ...wrapperProps } > + { ! isMultiSelected && ( + <BlockDraggable + rootUID={ rootUID } + index={ order } + uid={ uid } + layout={ layout } + onDragStart={ this.onDragStart } + onDragEnd={ this.onDragEnd } + isDragging={ dragging } + elementId={ blockElementId } + /> + ) } + { shouldShowInsertionPoint && ( + <BlockInsertionPoint + uid={ uid } + rootUID={ rootUID } + layout={ layout } + /> + ) } <BlockDropZone index={ order } rootUID={ rootUID } layout={ layout } /> - { shouldShowMovers && ( + { shouldRenderMovers && ( <BlockMover - uids={ [ block.uid ] } + uids={ [ uid ] } rootUID={ rootUID } layout={ layout } isFirst={ isFirst } isLast={ isLast } + isHidden={ ! ( isHovered || isSelected ) || hoverArea !== 'left' } /> ) } - { shouldShowSettingsMenu && ( + { shouldRenderBlockSettings && ( <BlockSettingsMenu - uids={ [ block.uid ] } + uids={ [ uid ] } + rootUID={ rootUID } renderBlockMenu={ renderBlockMenu } + isHidden={ ! ( isHovered || isSelected ) || hoverArea !== 'right' } /> ) } + { shouldShowBreadcrumb && <BlockBreadcrumb uid={ uid } isHidden={ ! ( isHovered || isSelected ) || hoverArea !== 'left' } /> } { shouldShowContextualToolbar && <BlockContextualToolbar /> } { isFirstMultiSelected && <BlockMultiControls rootUID={ rootUID } /> } <IgnoreNestedEvents ref={ this.bindBlockNode } - onKeyPress={ this.maybeStartTyping } onDragStart={ this.preventDrag } onMouseDown={ this.onPointerDown } - onKeyDown={ this.onKeyDown } - onFocus={ this.onFocus } - className={ BlockListBlock.className } - tabIndex="0" + className="editor-block-list__block-edit" aria-label={ blockLabel } - data-block={ block.uid } + data-block={ uid } > + <BlockCrashBoundary onError={ this.onBlockError }> { isValid && mode === 'visual' && ( <BlockEdit @@ -638,13 +558,13 @@ export class BlockListBlock extends Component { insertBlocksAfter={ isLocked ? undefined : this.insertBlocksAfter } onReplace={ isLocked ? undefined : onReplace } mergeBlocks={ isLocked ? undefined : this.mergeBlocks } - id={ block.uid } + id={ uid } isSelectionEnabled={ this.props.isSelectionEnabled } toggleSelection={ this.props.toggleSelection } /> ) } { isValid && mode === 'html' && ( - <BlockHtml uid={ block.uid } /> + <BlockHtml uid={ uid } /> ) } { ! isValid && [ <div key="invalid-preview"> @@ -656,19 +576,27 @@ export class BlockListBlock extends Component { />, ] } </BlockCrashBoundary> - { shouldShowMobileToolbar && <BlockMobileToolbar uid={ block.uid } renderBlockMenu={ renderBlockMenu } /> } + { shouldShowMobileToolbar && ( + <BlockMobileToolbar + rootUID={ rootUID } + uid={ uid } + renderBlockMenu={ renderBlockMenu } + /> + ) } </IgnoreNestedEvents> { !! error && <BlockCrashWarning /> } - { shouldShowInsertionPoint && ( - <BlockInsertionPoint - uid={ block.uid } - rootUID={ rootUID } - /> - ) } { showSideInserter && ( - <div className="editor-block-list__side-inserter"> - <InserterWithShortcuts uid={ block.uid } layout={ layout } onToggle={ this.selectOnOpen } /> - </div> + <Fragment> + <div className="editor-block-list__side-inserter"> + <InserterWithShortcuts uid={ uid } layout={ layout } onToggle={ this.selectOnOpen } /> + </div> + <div className="editor-block-list__empty-block-inserter"> + <Inserter + position="top right" + onToggle={ this.selectOnOpen } + /> + </div> + </Fragment> ) } </IgnoreNestedEvents> ); @@ -676,103 +604,106 @@ export class BlockListBlock extends Component { } } -const mapStateToProps = ( state, { uid, rootUID } ) => { - const isSelected = isBlockSelected( state, uid ); +const applyWithSelect = withSelect( ( select, { uid, rootUID } ) => { + const { + isBlockSelected, + getPreviousBlockUid, + getNextBlockUid, + getBlock, + isBlockMultiSelected, + isFirstMultiSelectedBlock, + isMultiSelecting, + isTyping, + getBlockIndex, + getEditedPostAttribute, + getBlockMode, + isSelectionEnabled, + getSelectedBlocksInitialCaretPosition, + getBlockSelectionEnd, + } = select( 'core/editor' ); + const isSelected = isBlockSelected( uid ); return { - previousBlockUid: getPreviousBlockUid( state, uid ), - nextBlockUid: getNextBlockUid( state, uid ), - block: getBlock( state, uid ), - isMultiSelected: isBlockMultiSelected( state, uid ), - isFirstMultiSelected: isFirstMultiSelectedBlock( state, uid ), - isMultiSelecting: isMultiSelecting( state ), - isLastInSelection: state.blockSelection.end === uid, + previousBlockUid: getPreviousBlockUid( uid ), + nextBlockUid: getNextBlockUid( uid ), + block: getBlock( uid ), + isMultiSelected: isBlockMultiSelected( uid ), + isFirstMultiSelected: isFirstMultiSelectedBlock( uid ), + isMultiSelecting: isMultiSelecting(), + isLastInSelection: getBlockSelectionEnd() === uid, // 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. - isTyping: isSelected && isTyping( state ), - order: getBlockIndex( state, uid, rootUID ), - meta: getEditedPostAttribute( state, 'meta' ), - mode: getBlockMode( state, uid ), - isSelectionEnabled: isSelectionEnabled( state ), - postType: getCurrentPostType( state ), - initialPosition: getSelectedBlocksInitialCaretPosition( state ), + isTypingWithinBlock: isSelected && isTyping(), + order: getBlockIndex( uid, rootUID ), + meta: getEditedPostAttribute( 'meta' ), + mode: getBlockMode( uid ), + isSelectionEnabled: isSelectionEnabled(), + initialPosition: getSelectedBlocksInitialCaretPosition(), isSelected, }; -}; - -const mapDispatchToProps = ( dispatch, ownProps ) => ( { - onChange( uid, attributes ) { - dispatch( updateBlockAttributes( uid, attributes ) ); - }, - - onSelect( uid = ownProps.uid, initialPosition ) { - dispatch( selectBlock( uid, initialPosition ) ); - }, - - onDeselect() { - dispatch( clearSelectedBlock() ); - }, - - onStartTyping() { - dispatch( startTyping() ); - }, - - onStopTyping() { - dispatch( stopTyping() ); - }, - - onInsertBlocks( blocks, index ) { - const { rootUID, layout } = ownProps; - - blocks = blocks.map( ( block ) => cloneBlock( block, { layout } ) ); - - dispatch( insertBlocks( blocks, index, rootUID ) ); - }, - - onRemove( uid ) { - dispatch( removeBlock( uid ) ); - }, - - onMerge( ...args ) { - dispatch( mergeBlocks( ...args ) ); - }, - - onReplace( blocks ) { - const { layout } = ownProps; - - blocks = castArray( blocks ).map( ( block ) => ( - cloneBlock( block, { layout } ) - ) ); - - dispatch( replaceBlocks( [ ownProps.uid ], blocks ) ); - }, +} ); - onMetaChange( meta ) { - dispatch( editPost( { meta } ) ); - }, +const applyWithDispatch = withDispatch( ( dispatch, ownProps ) => { + const { + updateBlockAttributes, + selectBlock, + insertBlocks, + removeBlock, + mergeBlocks, + replaceBlocks, + editPost, + toggleSelection, + } = dispatch( 'core/editor' ); - toggleSelection( selectionEnabled ) { - dispatch( toggleSelection( selectionEnabled ) ); - }, + return { + onChange( uid, attributes ) { + updateBlockAttributes( uid, attributes ); + }, + onSelect( uid = ownProps.uid, initialPosition ) { + selectBlock( uid, initialPosition ); + }, + onInsertBlocks( blocks, index ) { + const { rootUID, layout } = ownProps; + blocks = blocks.map( ( block ) => cloneBlock( block, { layout } ) ); + insertBlocks( blocks, index, rootUID ); + }, + onRemove( uid ) { + removeBlock( uid ); + }, + onMerge( ...args ) { + mergeBlocks( ...args ); + }, + onReplace( blocks ) { + const { layout } = ownProps; + blocks = castArray( blocks ).map( ( block ) => ( + cloneBlock( block, { layout } ) + ) ); + replaceBlocks( [ ownProps.uid ], blocks ); + }, + onMetaChange( meta ) { + editPost( { meta } ); + }, + toggleSelection( selectionEnabled ) { + toggleSelection( selectionEnabled ); + }, + }; } ); -BlockListBlock.className = 'editor-block-list__block-edit'; - BlockListBlock.childContextTypes = { - BlockList: noop, - canUserUseUnfilteredHTML: noop, + createInnerBlockList: noop, }; export default compose( - connect( mapStateToProps, mapDispatchToProps ), - withContext( 'editor' )( ( settings ) => { + applyWithSelect, + applyWithDispatch, + withViewportMatch( { isLargeViewport: 'medium' } ), + withEditorSettings( ( settings ) => { const { templateLock } = settings; return { isLocked: !! templateLock, + hasFixedToolbar: settings.hasFixedToolbar, }; } ), withFilters( 'editor.BlockListBlock' ), - withAPIData( ( { postType } ) => ( { - user: `/wp/v2/users/me?post_type=${ postType }&context=edit`, - } ) ), + withHoverAreas )( BlockListBlock ); diff --git a/editor/components/block-list/ignore-nested-events.js b/editor/components/block-list/ignore-nested-events.js index fe13f37df13b86..7cdc45063d4a94 100644 --- a/editor/components/block-list/ignore-nested-events.js +++ b/editor/components/block-list/ignore-nested-events.js @@ -43,10 +43,7 @@ class IgnoreNestedEvents extends Component { * @return {void} */ proxyEvent( event ) { - // Skip if already handled (i.e. assume nested block) - if ( event.nativeEvent._blockHandled ) { - return; - } + const isHandled = !! event.nativeEvent._blockHandled; // Assign into the native event, since React will reuse their synthetic // event objects and this property assignment could otherwise leak. @@ -55,7 +52,14 @@ class IgnoreNestedEvents extends Component { event.nativeEvent._blockHandled = true; // Invoke original prop handler - const propKey = this.eventMap[ event.type ]; + let propKey = this.eventMap[ event.type ]; + + // If already handled (i.e. assume nested block), only invoke a + // corresponding "Handled"-suffixed prop callback. + if ( isHandled ) { + propKey += 'Handled'; + } + if ( this.props[ propKey ] ) { this.props[ propKey ]( event ); } @@ -69,16 +73,24 @@ class IgnoreNestedEvents extends Component { ...Object.keys( props ), ], ( result, key ) => { // Try to match prop key as event handler - const match = key.match( /^on([A-Z][a-zA-Z]+)$/ ); + const match = key.match( /^on([A-Z][a-zA-Z]+?)(Handled)?$/ ); if ( match ) { + const isHandledProp = !! match[ 2 ]; + if ( isHandledProp ) { + // Avoid assigning through the invalid prop key. This + // assumes mutation of shallow clone by above spread. + delete props[ key ]; + } + // Re-map the prop to the local proxy handler to check whether // the event has already been handled. - result[ key ] = this.proxyEvent; + const proxiedPropName = 'on' + match[ 1 ]; + result[ proxiedPropName ] = this.proxyEvent; // Assign event -> propName into an instance variable, so as to // avoid re-renders which could be incurred either by setState // or in mapping values to a newly created function. - this.eventMap[ match[ 1 ].toLowerCase() ] = key; + this.eventMap[ match[ 1 ].toLowerCase() ] = proxiedPropName; } return result; diff --git a/editor/components/block-list/index.js b/editor/components/block-list/index.js index e7afa032ff8fc7..89edaab7a80c28 100644 --- a/editor/components/block-list/index.js +++ b/editor/components/block-list/index.js @@ -1,7 +1,6 @@ /** * External dependencies */ -import { connect } from 'react-redux'; import { reduce, get, @@ -12,23 +11,23 @@ import { * WordPress dependencies */ import { createElement } from '@wordpress/element'; +import { withSelect } from '@wordpress/data'; /** * Internal dependencies */ import './style.scss'; import BlockListLayout from './layout'; -import { getBlocks, getBlockOrder } from '../../store/selectors'; -const UngroupedLayoutBlockList = connect( - ( state, ownProps ) => ( { - blockUIDs: getBlockOrder( state, ownProps.rootUID ), +const UngroupedLayoutBlockList = withSelect( + ( select, ownProps ) => ( { + blockUIDs: select( 'core/editor' ).getBlockOrder( ownProps.rootUID ), } ) )( BlockListLayout ); -const GroupedLayoutBlockList = connect( - ( state, ownProps ) => ( { - blocks: getBlocks( state, ownProps.rootUID ), +const GroupedLayoutBlockList = withSelect( + ( select, ownProps ) => ( { + blocks: select( 'core/editor' ).getBlocks( ownProps.rootUID ), } ), )( ( { blocks, diff --git a/editor/components/block-list/insertion-point.js b/editor/components/block-list/insertion-point.js index e109cad8bb53a7..19386f0cedb54c 100644 --- a/editor/components/block-list/insertion-point.js +++ b/editor/components/block-list/insertion-point.js @@ -1,38 +1,74 @@ /** - * External dependencies + * WordPress dependencies */ -import { connect } from 'react-redux'; +import { __ } from '@wordpress/i18n'; +import { isUnmodifiedDefaultBlock, withEditorSettings } from '@wordpress/blocks'; +import { Component, compose } from '@wordpress/element'; +import { ifCondition } from '@wordpress/components'; +import { withSelect, withDispatch } from '@wordpress/data'; -/** - * Internal dependencies - */ -import { - getBlockIndex, - getBlockInsertionPoint, - isBlockInsertionPointVisible, - getBlockCount, -} from '../../store/selectors'; +class BlockInsertionPoint extends Component { + constructor() { + super( ...arguments ); + this.onClick = this.onClick.bind( this ); + } -function BlockInsertionPoint( { showInsertionPoint } ) { - if ( ! showInsertionPoint ) { - return null; + onClick() { + const { layout, rootUID, index, ...props } = this.props; + props.insertDefaultBlock( { layout }, rootUID, index ); + props.startTyping(); } - return <div className="editor-block-list__insertion-point" />; -} + render() { + const { showInsertionPoint, showInserter } = this.props; -export default connect( - ( state, { uid, rootUID } ) => { - const blockIndex = uid ? getBlockIndex( state, uid, rootUID ) : -1; - const insertIndex = blockIndex > -1 ? blockIndex + 1 : getBlockCount( state ); - const insertionPoint = getBlockInsertionPoint( state ); + return ( + <div className="editor-block-list__insertion-point"> + { showInsertionPoint && <div className="editor-block-list__insertion-point-indicator" /> } + { showInserter && ( + <button + className="editor-block-list__insertion-point-inserter" + onClick={ this.onClick } + aria-label={ __( 'Insert block' ) } + /> + ) } + </div> + ); + } +} +export default compose( + withEditorSettings( ( { templateLock } ) => ( { templateLock } ) ), + ifCondition( ( { templateLock } ) => ! templateLock ), + withSelect( ( select, { uid, rootUID } ) => { + const { + getBlockIndex, + getBlockInsertionPoint, + getBlock, + isBlockInsertionPointVisible, + isTyping, + } = select( 'core/editor' ); + const blockIndex = uid ? getBlockIndex( uid, rootUID ) : -1; + const insertIndex = blockIndex; + const insertionPoint = getBlockInsertionPoint(); + const block = uid ? getBlock( uid ) : null; + const showInsertionPoint = ( + isBlockInsertionPointVisible() && + insertionPoint.index === insertIndex && + insertionPoint.rootUID === rootUID && + ( ! block || ! isUnmodifiedDefaultBlock( block ) ) + ); return { - showInsertionPoint: ( - isBlockInsertionPointVisible( state ) && - insertionPoint.index === insertIndex && - insertionPoint.rootUID === rootUID - ), + showInserter: ! isTyping(), + index: insertIndex, + showInsertionPoint, + }; + } ), + withDispatch( ( dispatch ) => { + const { insertDefaultBlock, startTyping } = dispatch( 'core/editor' ); + return { + insertDefaultBlock, + startTyping, }; - }, + } ) )( BlockInsertionPoint ); diff --git a/editor/components/block-list/invalid-block-warning.js b/editor/components/block-list/invalid-block-warning.js index c6b60d90ed7422..e3ab117339c98c 100644 --- a/editor/components/block-list/invalid-block-warning.js +++ b/editor/components/block-list/invalid-block-warning.js @@ -1,8 +1,3 @@ -/** - * External dependencies - */ -import { connect } from 'react-redux'; - /** * WordPress dependencies */ @@ -13,46 +8,47 @@ import { createBlock, rawHandler, } from '@wordpress/blocks'; +import { withDispatch } from '@wordpress/data'; /** * Internal dependencies */ -import { replaceBlock } from '../../store/actions'; import Warning from '../warning'; function InvalidBlockWarning( { convertToHTML, convertToBlocks } ) { const hasHTMLBlock = !! getBlockType( 'core/html' ); return ( - <Warning> - <p>{ __( 'This block appears to have been modified externally.' ) }</p> - <p> - <Button onClick={ convertToBlocks } isLarge isPrimary={ ! hasHTMLBlock }> + <Warning + actions={ [ + <Button key="convert" onClick={ convertToBlocks } isLarge isPrimary={ ! hasHTMLBlock }> { __( 'Convert to Blocks' ) } - </Button> - { hasHTMLBlock && ( - <Button onClick={ convertToHTML } isLarge isPrimary> + </Button>, + hasHTMLBlock && ( + <Button key="edit" onClick={ convertToHTML } isLarge isPrimary> { __( 'Edit as HTML' ) } </Button> - ) } - </p> + ), + ] } + > + { __( 'This block appears to have been modified externally.' ) } </Warning> ); } -export default connect( - null, - ( dispatch, { block } ) => ( { +export default withDispatch( ( dispatch, { block } ) => { + const { replaceBlock } = dispatch( 'core/editor' ); + return { convertToHTML() { - dispatch( replaceBlock( block.uid, createBlock( 'core/html', { + replaceBlock( block.uid, createBlock( 'core/html', { content: block.originalContent, - } ) ) ); + } ) ); }, convertToBlocks() { - dispatch( replaceBlock( block.uid, rawHandler( { + replaceBlock( block.uid, rawHandler( { HTML: block.originalContent, mode: 'BLOCKS', - } ) ) ); + } ) ); }, - } ) -)( InvalidBlockWarning ); + }; +} )( InvalidBlockWarning ); diff --git a/editor/components/block-list/layout.js b/editor/components/block-list/layout.js index 7a89328cf0ce5f..16048e04d4b32d 100644 --- a/editor/components/block-list/layout.js +++ b/editor/components/block-list/layout.js @@ -1,7 +1,6 @@ /** * External dependencies */ -import { connect } from 'react-redux'; import { findLast, map, @@ -17,22 +16,16 @@ import 'element-closest'; /** * WordPress dependencies */ -import { Component } from '@wordpress/element'; +import { Component, compose } from '@wordpress/element'; +import { withSelect, withDispatch } from '@wordpress/data'; /** * Internal dependencies */ import './style.scss'; import BlockListBlock from './block'; -import BlockSelectionClearer from '../block-selection-clearer'; +import IgnoreNestedEvents from './ignore-nested-events'; import DefaultBlockAppender from '../default-block-appender'; -import { - isSelectionEnabled, - isMultiSelecting, - getMultiSelectedBlocksStartUid, - getMultiSelectedBlocksEndUid, -} from '../../store/selectors'; -import { startMultiSelect, stopMultiSelect, multiSelect, selectBlock } from '../../store/actions'; class BlockListLayout extends Component { constructor( props ) { @@ -197,7 +190,6 @@ class BlockListLayout extends Component { render() { const { blockUIDs, - showContextualToolbar, layout, isGroupedByLayout, rootUID, @@ -214,7 +206,7 @@ class BlockListLayout extends Component { } ); return ( - <BlockSelectionClearer className={ classes }> + <div className={ classes }> { map( blockUIDs, ( uid, blockIndex ) => ( <BlockListBlock key={ 'block-' + uid } @@ -223,7 +215,6 @@ class BlockListLayout extends Component { blockRef={ this.setBlockRef } onSelectionStart={ this.onSelectionStart } onShiftSelection={ this.onShiftSelection } - showContextualToolbar={ showContextualToolbar } rootUID={ rootUID } layout={ defaultLayout } isFirst={ blockIndex === 0 } @@ -231,39 +222,49 @@ class BlockListLayout extends Component { renderBlockMenu={ renderBlockMenu } /> ) ) } - <DefaultBlockAppender - rootUID={ rootUID } - lastBlockUID={ last( blockUIDs ) } - layout={ defaultLayout } - /> - </BlockSelectionClearer> + <IgnoreNestedEvents childHandledEvents={ [ 'onFocus', 'onClick', 'onKeyDown' ] }> + <DefaultBlockAppender + rootUID={ rootUID } + lastBlockUID={ last( blockUIDs ) } + layout={ defaultLayout } + /> + </IgnoreNestedEvents> + </div> ); } } -export default connect( - ( state ) => ( { - // Reference block selection value directly, since current selectors - // assume either multi-selection (getMultiSelectedBlocksStartUid) or - // singular-selection (getSelectedBlock) exclusively. - selectionStart: getMultiSelectedBlocksStartUid( state ), - selectionEnd: getMultiSelectedBlocksEndUid( state ), - selectionStartUID: state.blockSelection.start, - isSelectionEnabled: isSelectionEnabled( state ), - isMultiSelecting: isMultiSelecting( state ), +export default compose( [ + withSelect( ( select ) => { + const { + isSelectionEnabled, + isMultiSelecting, + getMultiSelectedBlocksStartUid, + getMultiSelectedBlocksEndUid, + getBlockSelectionStart, + } = select( 'core/editor' ); + + return { + selectionStart: getMultiSelectedBlocksStartUid(), + selectionEnd: getMultiSelectedBlocksEndUid(), + selectionStartUID: getBlockSelectionStart(), + isSelectionEnabled: isSelectionEnabled(), + isMultiSelecting: isMultiSelecting(), + }; + } ), + withDispatch( ( dispatch ) => { + const { + startMultiSelect, + stopMultiSelect, + multiSelect, + selectBlock, + } = dispatch( 'core/editor' ); + + return { + onStartMultiSelect: startMultiSelect, + onStopMultiSelect: stopMultiSelect, + onMultiSelect: multiSelect, + onSelect: selectBlock, + }; } ), - ( dispatch ) => ( { - onStartMultiSelect() { - dispatch( startMultiSelect() ); - }, - onStopMultiSelect() { - dispatch( stopMultiSelect() ); - }, - onMultiSelect( start, end ) { - dispatch( multiSelect( start, end ) ); - }, - onSelect( uid ) { - dispatch( selectBlock( uid ) ); - }, - } ) -)( BlockListLayout ); +] )( BlockListLayout ); diff --git a/editor/components/block-list/multi-controls.js b/editor/components/block-list/multi-controls.js index d1dcffa7b38a4b..6c5e1deda30f9c 100644 --- a/editor/components/block-list/multi-controls.js +++ b/editor/components/block-list/multi-controls.js @@ -1,19 +1,20 @@ /** * External dependencies */ -import { connect } from 'react-redux'; +import { first, last } from 'lodash'; + +/** + * WordPress dependencies + */ +import { withSelect } from '@wordpress/data'; /** * Internal dependencies */ import BlockMover from '../block-mover'; import BlockSettingsMenu from '../block-settings-menu'; -import { - getMultiSelectedBlockUids, - isMultiSelecting, -} from '../../store/selectors'; -function BlockListMultiControls( { multiSelectedBlockUids, rootUID, isSelecting } ) { +function BlockListMultiControls( { multiSelectedBlockUids, rootUID, isSelecting, isFirst, isLast } ) { if ( isSelecting ) { return null; } @@ -23,18 +24,33 @@ function BlockListMultiControls( { multiSelectedBlockUids, rootUID, isSelecting key="mover" rootUID={ rootUID } uids={ multiSelectedBlockUids } + isFirst={ isFirst } + isLast={ isLast } />, <BlockSettingsMenu key="menu" + rootUID={ rootUID } uids={ multiSelectedBlockUids } focus />, ]; } -export default connect( ( state ) => { +export default withSelect( ( select, { rootUID } ) => { + const { + getMultiSelectedBlockUids, + isMultiSelecting, + getBlockIndex, + getBlockCount, + } = select( 'core/editor' ); + const uids = getMultiSelectedBlockUids(); + const firstIndex = getBlockIndex( first( uids ), rootUID ); + const lastIndex = getBlockIndex( last( uids ), rootUID ); + return { - multiSelectedBlockUids: getMultiSelectedBlockUids( state ), - isSelecting: isMultiSelecting( state ), + multiSelectedBlockUids: uids, + isSelecting: isMultiSelecting(), + isFirst: firstIndex === 0, + isLast: lastIndex + 1 === getBlockCount(), }; } )( BlockListMultiControls ); diff --git a/editor/components/block-list/style.scss b/editor/components/block-list/style.scss index 05d1db6af689f1..e02edd7b1c3cc2 100644 --- a/editor/components/block-list/style.scss +++ b/editor/components/block-list/style.scss @@ -1,49 +1,142 @@ -.editor-block-list__layout .editor-default-block-appender, +.components-draggable__clone { + & > .editor-block-list__block > .editor-block-list__block-draggable { + background: white; + box-shadow: $shadow-popover; + + @include break-small { + left: 0; + right: 0; + } + } + + // Hide the Block UI when dragging the block + // This ensures the page scroll properly (no sticky elements) + .editor-block-contextual-toolbar, + .editor-block-mover, + .editor-block-settings-menu { + // I think important is fine here to avoid over complexing the selector + display: none !important; + } +} + +.editor-block-list__block-draggable { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: z-index( '.editor-block-list__block-draggable' ); + + > .editor-block-list__block-draggable-inner { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + background-color: $light-gray-200; + visibility: hidden; + + @include break-small { + margin: 0 48px; + } + } + + &.is-visible > .editor-block-list__block-draggable-inner { + visibility: visible; + } + + @include break-small { + left: -$block-side-ui-padding; + right: -$block-side-ui-padding; + + // Full width blocks don't have the place to expand on the side + .editor-block-list__block[data-align="full"] & { + left: 0; + right: 0; + } + } + + + cursor: move;/* Fallback for IE/Edge < 14 */ + cursor: grab; +} + + +// Allow Drag & Drop when clicking on the empty area of the mover and the settings menu +.editor-block-list__block .editor-block-mover, +.editor-block-list__block .editor-block-settings-menu { + pointer-events: none; + + > * { + pointer-events: auto; + } +} + +.editor-block-list__block { + &.is-hidden *, + &.is-hidden > * { + visibility: hidden; + } + + .editor-block-list__block-edit .shared-block-edit-panel * { + z-index: z-index( '.editor-block-list__block-edit .shared-block-edit-panel *' ); + } +} + +.editor-block-list__layout { + // make room in the main content column for the side UI + // the side UI uses negative margins to position itself so as to not affect the block width + @include break-small() { + padding-left: $block-side-ui-padding; + padding-right: $block-side-ui-padding; + } + + // Don't add side padding for nested blocks, @todo see if this can be scoped better + .editor-block-list__block & { + padding-left: 0; + padding-right: 0; + } +} + .editor-block-list__layout .editor-block-list__block { position: relative; - margin-bottom: $block-spacing; padding-left: $block-padding; padding-right: $block-padding; - @include break-small { + @include break-small() { // The block mover needs to stay inside the block to allow clicks when hovering the block - padding-left: $block-padding + $block-mover-padding-visible; - padding-right: $block-padding + $block-mover-padding-visible; + padding-left: $block-padding + $block-side-ui-padding; + padding-right: $block-padding + $block-side-ui-padding; } - // Prevent collapsing margins - // This allows us control over block boundaries and how blocks fit together visually - // It makes things a lot simpler, however it also means block margins and paddings have to be tuned (halved) for the editor. - padding-top: .05px; - padding-bottom: .05px; + // Prevent collapsing margins @todo try and revisit this, it's conducive to theming to allow these to collapse + padding-top: .1px; + padding-bottom: .1px; - // Space every block using margin instead of padding + + // Space every block using margins instead of padding + // This allows margins to collapse, which gives a better representation of how it looks on the frontend .editor-block-list__block-edit { margin-top: $block-padding; margin-bottom: $block-padding; + + // Prevent collapsing margins @todo try and revisit this, it's conducive to theming to allow these to collapse + padding-top: .1px; + padding-bottom: .1px; } - // Block outline container - &:before { - z-index: z-index( '.editor-block-list__block:before' ); - content: ''; - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; - outline: 1px solid transparent; + margin-bottom: $block-spacing; - @include break-small { - left: $block-mover-padding-visible; - right: $block-mover-padding-visible; - } - } - // Block warnings + /** + * Warnings + */ + &.has-warning .editor-block-list__block-edit { position: relative; min-height: 250px; + max-height: 500px; + overflow: hidden; > :not( .editor-warning ) { pointer-events: none; @@ -59,9 +152,13 @@ bottom: 0; left: 0; background-color: rgba( $white, 0.6 ); + background-image: linear-gradient( to bottom, transparent, #fff ); } - // simpler style for a block that has cursor focus (but hasn't been selected) + /** + * Hovered Block style + */ + &.is-selected > .editor-block-mover:before, &.is-hovered > .editor-block-mover:before, &.is-selected > .editor-block-settings-menu:before, @@ -75,69 +172,87 @@ &.is-selected > .editor-block-mover:before, &.is-hovered > .editor-block-mover:before { border-right: 1px solid $light-gray-500; - right: 6px; - } - - &.is-reusable.is-selected > .editor-block-mover:before { - border-right: none; + right: 0; } &.is-selected > .editor-block-settings-menu:before, &.is-hovered > .editor-block-settings-menu:before { border-left: 1px solid $light-gray-500; - left: 6px; + left: 0; } - &.is-reusable.is-selected > .editor-block-settings-menu:before { - border-left: none; + &.is-typing .editor-block-list__empty-block-inserter, + &.is-typing .editor-block-list__side-inserter { + opacity: 0; + } + + .editor-block-list__empty-block-inserter, + .editor-block-list__side-inserter { + opacity: 1; + transition: opacity 0.2s; + } + + /** + * Selected Block style + */ + + .editor-block-list__block-edit { + position: relative; + + &:before { + z-index: z-index( '.editor-block-list__block-edit:before' ); + content: ''; + position: absolute; + top: -$block-padding; + right: -$block-padding; + bottom: -$block-padding; + left: -$block-padding; + outline: 1px solid transparent; + } + } // focused block-style - &.is-selected:before { + &.is-selected > .editor-block-list__block-edit:before { outline: 1px solid $light-gray-500; } - // give reusable blocks a dashed outline - &.is-reusable.is-selected:before { - outline: 1px dashed $light-gray-500; - } + /** + * Selection Style + */ - // selection style for textarea ::-moz-selection { - background: $blue-medium-highlight; + background-color: $blue-medium-highlight; } ::selection { - background: $blue-medium-highlight; + background-color: $blue-medium-highlight; } // selection style for multiple blocks &.is-multi-selected *::selection { - background: transparent; + background-color: transparent; } - &.is-multi-selected:before { + &.is-multi-selected .editor-block-list__block-edit:before { background: $blue-medium-highlight; } - .iframe-overlay { - position: relative; - } + /** + * Shared blocks + */ - .iframe-overlay:before { - content: ''; - display: block; - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; + &.is-shared > .editor-block-mover:before { + border-right: none; } - &.is-selected .iframe-overlay:before { - display: none; + &.is-shared > .editor-block-settings-menu:before { + border-left: none; } + &.is-shared > .editor-block-list__block-edit:before { + outline: 1px dashed $light-gray-500; + } /** * Alignments @@ -158,21 +273,6 @@ } } - // Apply max-width to floated items that have no intrinsic width, like Cover Image or Gallery - &[data-align="left"], - &[data-align="right"] { - > .editor-block-list__block-edit { - max-width: 360px; - width: 100%; - } - - // reset when data-resized - &[data-resized="true"] > .editor-block-list__block-edit { - max-width: none; - width: auto; - } - } - // Left &[data-align="left"] { .editor-block-list__block-edit { // This is in the editor only, on the frontend, the img should be floated @@ -204,44 +304,61 @@ // Full-wide &[data-align="full"] { - padding-left: 0; - padding-right: 0; + + // compensate for main container padding + @include break-small() { + margin-left: -$block-side-ui-padding; + margin-right: -$block-side-ui-padding; + } - &:before { + > .editor-block-list__block-edit { + margin-left: -$block-padding; + margin-right: -$block-padding; + + @include break-small() { + margin-left: -$block-side-ui-padding - $block-padding; + margin-right: -$block-side-ui-padding - $block-padding; + } + + // this explicitly sets the width of the block, to override the fit-content from the image block + figure { + width: 100%; + } + } + + > .editor-block-list__block-edit:before { left: 0; right: 0; border-left-width: 0; border-right-width: 0; } - .editor-block-mover { + // Adjust how movers behave on the full-wide block, and don't affect children + > .editor-block-mover { display: none; } @include break-wide() { - .editor-block-mover { + > .editor-block-mover { display: block; top: -29px; left: 10px; height: auto; + width: auto; + z-index: inherit; &:before { content: none; } } - .editor-block-mover__control { + > .editor-block-mover .editor-block-mover__control { float: left; - margin-right: 8px; } } - .editor-block-settings-menu__control { - float: left; - margin-right: 8px; - } - - .editor-block-settings-menu { + // Also adjust block settings menu + > .editor-block-settings-menu { top: -41px; right: 10px; height: auto; @@ -250,6 +367,10 @@ content: none; } } + + > .editor-block-settings-menu .editor-block-settings-menu__control { + float: left; + } } // Clear floats @@ -258,44 +379,51 @@ } // Dropzones - & > .components-drop-zone { - border: none; + .editor-block-drop-zone { top: -4px; bottom: -3px; - margin: 0 $block-mover-padding-visible; - border-radius: 0; + margin: 0 $block-padding; + } +} - .components-drop-zone__content { - display: none; - } - &.is-close-to-top { - background: none; - border-top: 3px solid $blue-medium-500; - } +/** + * Left and right side UI + */ - &.is-close-to-bottom { - background: none; - border-bottom: 3px solid $blue-medium-500; - } - } +.editor-block-list__block { - // Left and right side UI + // Left and right > .editor-block-settings-menu, > .editor-block-mover { position: absolute; - top: 0; + top: -.9px; // .9px because of the collapsing margins hack, see line 27, @todo revisit when we allow margins to collapse + bottom: -.9px; // utilize full vertical space to increase hoverable area padding: 0; + width: $block-side-ui-width; + + /* Necessary for drag indicator */ + cursor: move;/* Fallback for IE/Edge < 14 */ + cursor: grab; + } + + // Elevate when selected or hovered + &.is-multi-selected, + &.is-selected, + &.is-hovered { + .editor-block-settings-menu, + .editor-block-mover { + z-index: z-index( '.editor-block-list__block.is-{selected,hovered} .editor-block-{settings-menu,mover}' ); + } } // Right side UI > .editor-block-settings-menu { - right: $block-mover-margin; - padding-top: 2px; + right: -$block-side-ui-width; // Mobile display: none; - @include break-small { + @include break-small() { display: flex; flex-direction: column; } @@ -303,18 +431,16 @@ // Left side UI > .editor-block-mover { - left: $block-mover-margin + 2px; - padding-top: 6px; - z-index: z-index( '.editor-block-mover' ); + left: -$block-side-ui-width; // Mobile display: none; - @include break-small { + @include break-small() { display: block; } } - // Mobile tools + // Show side UI inline below the block on mobile .editor-block-list__block-mobile-toolbar { display: flex; flex-direction: row; @@ -350,6 +476,7 @@ // Movers .editor-block-mover { + display: flex; margin-right: auto; .editor-inserter, @@ -360,8 +487,15 @@ } } + +/** + * In-Canvas Inserter + */ + .editor-block-list .editor-inserter { margin: $item-spacing; + cursor: move;/* Fallback for IE/Edge < 14 */ + cursor: grab; .editor-inserter__toggle { color: $dark-gray-300; @@ -371,29 +505,71 @@ .editor-block-list__insertion-point { position: relative; + z-index: z-index( '.editor-block-list__insertion-point' ); +} + +.editor-block-list__insertion-point-indicator { + position: absolute; + top: $block-padding - 1px; // Half the empty space between two blocks, minus the 2px height + height: 2px; + left: 0; + right: 0; + background: $blue-medium-500; +} + +.editor-block-list__insertion-point-inserter { + position: absolute; + background: none; + border: none; + display: block; + top: 0; + height: $block-padding * 2; // Matches the whole empty space between two blocks + width: 100%; + cursor: pointer; + padding: 0; // Unstyle inherited padding from core button &:before { position: absolute; - top: -1px; + top: $block-padding - 1px; // Half the empty space between two blocks, minus the 2px height height: 2px; - left: 0; - right: 0; - background: $blue-medium-500; + left: $block-padding; + right: $block-padding; + background: $dark-gray-100; content: ''; + opacity: 0; + transition: opacity 0.1s linear 0.1s; + } + + &:hover:before { + opacity: 1; + transition: opacity 0.2s linear 0.5s; + } + + &:focus { + outline: none; + } + + // Show focus style when tabbing + &:focus:before { + opacity: 1; + transition: opacity 0.2s linear; + outline: 1px solid $dark-gray-300; + outline-offset: 2px; + } + + // Don't show focus style when clicking + &:focus:active:before { + outline: none; } } .editor-block-list__block > .editor-block-list__insertion-point { position: absolute; - bottom: -10px; - top: auto; + top: -$block-padding; + height: $block-padding * 2; // Matches the whole empty space between two blocks + bottom: auto; left: 0; right: 0; - - @include break-small { - left: $block-mover-padding-visible; - right: $block-mover-padding-visible; - } } .editor-block-list__block .editor-block-list__block-html-textarea { @@ -420,7 +596,8 @@ * Block Toolbar */ -.editor-block-contextual-toolbar { +.editor-block-contextual-toolbar, +.editor-block-list__breadcrumb { position: sticky; z-index: z-index( '.editor-block-contextual-toolbar' ); white-space: nowrap; @@ -433,8 +610,8 @@ margin-bottom: $block-padding + 1px; // Floated items have special needs for the contextual toolbar position - .edit-post-visual-editor .editor-block-list__block[data-align="left"] &, - .edit-post-visual-editor .editor-block-list__block[data-align="right"] & { + .editor-block-list__block[data-align="left"] &, + .editor-block-list__block[data-align="right"] & { margin-bottom: 1px; margin-top: -$block-toolbar-height - 1px; } @@ -442,9 +619,17 @@ // put toolbar snugly to side edges on mobile margin-left: -$block-padding - 1px; // stack borders margin-right: -$block-padding - 1px; + @include break-small() { - margin-left: auto; - margin-right: auto; + // stack borders + margin-left: -$block-padding - $block-side-ui-padding - 1px; + margin-right: -$block-padding - $block-side-ui-padding - 1px; + + // except for wide elements, this causes a horizontal scrollbar + [data-align="full"] & { + margin-left: -$block-padding - $block-side-ui-padding; + margin-right: -$block-padding - $block-side-ui-padding; + } } // on mobile, toolbars fix differently @@ -453,50 +638,62 @@ top: -1px; // stack borders } - .editor-block-toolbar { - border: 1px solid $light-gray-500; - width: 100%; + // Reset pointer-events on children. + & > * { + pointer-events: auto; + } - // this prevents floats from messing up the position - position: absolute; - left: 0; +} - .editor-block-list__block[data-align="right"] & { - left: auto; - right: 0; - } +.editor-block-contextual-toolbar .editor-block-toolbar, +.editor-block-list__breadcrumb .components-toolbar { + border: 1px solid $light-gray-500; + width: 100%; - // remove stacked borders in inline toolbar - > div:first-child { - margin-left: -1px; - } + // this prevents floats from messing up the position + position: absolute; + left: 0; - > .editor-block-switcher:first-child { - margin-left: -2px; - } + .editor-block-list__block[data-align="right"] & { + left: auto; + right: 0; + } - @include break-small() { - width: auto; - } + // remove stacked borders in inline toolbar + > div:first-child { + margin-left: -1px; } - // Reset pointer-events on children. - & > * { - pointer-events: auto; + > .editor-block-switcher:first-child { + margin-left: -2px; } @include break-small() { - margin-left: - $block-padding - 1px; - margin-right: - $block-padding - 1px; + width: auto; } } -.editor-block-list__side-inserter { - position: absolute; - top: 10px; - right: 10px; +.editor-block-list__breadcrumb .components-toolbar { + padding: 0px 12px; + line-height: $block-toolbar-height - 1px; + font-family: $default-font; + font-size: $default-font-size; + color: $dark-gray-500; + cursor: default; + + .components-button { + margin-left: -12px; + margin-right: 12px; + border-right: 1px solid $light-gray-500; + color: $dark-gray-500; + padding-top: 6px; + } +} - @include break-small { - right: $block-mover-padding-visible + 10px; +.editor-block-list__breadcrumb { + opacity: 0; + + &.is-visible { + @include fade_in; } } diff --git a/editor/components/block-list/test/ignore-nested-events.js b/editor/components/block-list/test/ignore-nested-events.js index 73f9c636607062..48559baf29909f 100644 --- a/editor/components/block-list/test/ignore-nested-events.js +++ b/editor/components/block-list/test/ignore-nested-events.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { shallow } from 'enzyme'; +import { mount } from 'enzyme'; /** * Internal dependencies @@ -10,24 +10,24 @@ import IgnoreNestedEvents from '../ignore-nested-events'; describe( 'IgnoreNestedEvents', () => { it( 'passes props to its rendered div', () => { - const wrapper = shallow( + const wrapper = mount( <IgnoreNestedEvents className="foo" /> ); - expect( wrapper.type() ).toBe( 'div' ); + expect( wrapper.find( 'div' ) ).toHaveLength( 1 ); expect( wrapper.prop( 'className' ) ).toBe( 'foo' ); } ); it( 'stops propagation of events to ancestor IgnoreNestedEvents', () => { const spyOuter = jest.fn(); const spyInner = jest.fn(); - const wrapper = shallow( + const wrapper = mount( <IgnoreNestedEvents onClick={ spyOuter }> <IgnoreNestedEvents onClick={ spyInner } /> </IgnoreNestedEvents> ); - wrapper.childAt( 0 ).simulate( 'click' ); + wrapper.find( 'div' ).last().simulate( 'click' ); expect( spyInner ).toHaveBeenCalled(); expect( spyOuter ).not.toHaveBeenCalled(); @@ -36,7 +36,7 @@ describe( 'IgnoreNestedEvents', () => { it( 'stops propagation of child handled events', () => { const spyOuter = jest.fn(); const spyInner = jest.fn(); - const wrapper = shallow( + const wrapper = mount( <IgnoreNestedEvents onClick={ spyOuter }> <IgnoreNestedEvents childHandledEvents={ [ 'onClick' ] }> <div /> @@ -51,4 +51,23 @@ describe( 'IgnoreNestedEvents', () => { expect( spyInner ).not.toHaveBeenCalled(); expect( spyOuter ).not.toHaveBeenCalled(); } ); + + it( 'invokes callback of Handled-suffixed prop if handled', () => { + const spyOuter = jest.fn(); + const spyInner = jest.fn(); + const wrapper = mount( + <IgnoreNestedEvents onClickHandled={ spyOuter }> + <IgnoreNestedEvents childHandledEvents={ [ 'onClick' ] }> + <div /> + <IgnoreNestedEvents onClick={ spyInner } /> + </IgnoreNestedEvents> + </IgnoreNestedEvents> + ); + + const div = wrapper.childAt( 0 ).childAt( 0 ); + div.simulate( 'click' ); + + expect( spyInner ).not.toHaveBeenCalled(); + expect( spyOuter ).toHaveBeenCalled(); + } ); } ); diff --git a/editor/components/block-list/utils.js b/editor/components/block-list/utils.js deleted file mode 100644 index 67a1969753739f..00000000000000 --- a/editor/components/block-list/utils.js +++ /dev/null @@ -1,67 +0,0 @@ -/** - * WordPress dependencies - */ -import { Component } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import BlockList from './'; - -/** - * An object of cached BlockList components - * - * @type {Object} - */ -const INNER_BLOCK_LIST_CACHE = {}; - -/** - * Returns a BlockList component which is already pre-bound to render with a - * given UID as its rootUID prop. It is necessary to cache these components - * because otherwise the rendering of a nested BlockList will cause ancestor - * blocks to re-mount, leading to an endless cycle of remounting inner blocks. - * - * @param {string} uid Block UID to use as root UID of - * BlockList component. - * @param {Function} renderBlockMenu Render function for block menu of - * nested BlockList. - * @param {boolean} showContextualToolbar Whether contextual toolbar is to be - * used. - * - * @return {Component} Pre-bound BlockList component - */ -export function createInnerBlockList( uid, renderBlockMenu, showContextualToolbar ) { - if ( ! INNER_BLOCK_LIST_CACHE[ uid ] ) { - INNER_BLOCK_LIST_CACHE[ uid ] = [ - // The component class: - class extends Component { - componentWillMount() { - INNER_BLOCK_LIST_CACHE[ uid ][ 1 ]++; - } - - componentWillUnmount() { - // If, after decrementing the tracking count, there are no - // remaining instances of the component, remove from cache. - if ( ! INNER_BLOCK_LIST_CACHE[ uid ][ 1 ]-- ) { - delete INNER_BLOCK_LIST_CACHE[ uid ]; - } - } - - render() { - return ( - <BlockList - rootUID={ uid } - renderBlockMenu={ renderBlockMenu } - showContextualToolbar={ showContextualToolbar } - { ...this.props } /> - ); - } - }, - - // A counter tracking active mounted instances: - 0, - ]; - } - - return INNER_BLOCK_LIST_CACHE[ uid ][ 0 ]; -} diff --git a/editor/components/block-mover/index.js b/editor/components/block-mover/index.js index 013414f02bd539..4c77150cc22071 100644 --- a/editor/components/block-mover/index.js +++ b/editor/components/block-mover/index.js @@ -1,113 +1,132 @@ /** * External dependencies */ -import { connect } from 'react-redux'; -import { first } from 'lodash'; +import { first, partial } from 'lodash'; +import classnames from 'classnames'; /** * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { IconButton, withContext } from '@wordpress/components'; -import { getBlockType } from '@wordpress/blocks'; -import { compose } from '@wordpress/element'; +import { IconButton, withInstanceId } from '@wordpress/components'; +import { getBlockType, withEditorSettings } from '@wordpress/blocks'; +import { compose, Component } from '@wordpress/element'; +import { withSelect, withDispatch } from '@wordpress/data'; /** * Internal dependencies */ import './style.scss'; -import { getBlockMoverLabel } from './mover-label'; -import { getBlockIndex, getBlock } from '../../store/selectors'; -import { selectBlock } from '../../store/actions'; +import { getBlockMoverDescription } from './mover-description'; +import { upArrow, downArrow } from './arrows'; -export function BlockMover( { onMoveUp, onMoveDown, isFirst, isLast, uids, blockType, firstIndex, isLocked } ) { - if ( isLocked ) { - return null; +export class BlockMover extends Component { + constructor() { + super( ...arguments ); + this.state = { + isFocused: false, + }; + this.onFocus = this.onFocus.bind( this ); + this.onBlur = this.onBlur.bind( this ); } - // We emulate a disabled state because forcefully applying the `disabled` - // attribute on the button while it has focus causes the screen to change - // to an unfocused state (body as active element) without firing blur on, - // the rendering parent, leaving it unable to react to focus out. - return ( - <div className="editor-block-mover"> - <IconButton - className="editor-block-mover__control" - onClick={ isFirst ? null : onMoveUp } - icon={ <svg tabIndex="-1" width="18" height="18" xmlns="http://www.w3.org/2000/svg"><path d="M12.293 12.207L9 8.914l-3.293 3.293-1.414-1.414L9 6.086l4.707 4.707z" /></svg> } - tooltip={ __( 'Move Up' ) } - label={ getBlockMoverLabel( - uids.length, - blockType && blockType.title, - firstIndex, - isFirst, - isLast, - -1, - ) } - aria-disabled={ isFirst } - /> - <IconButton - className="editor-block-mover__control" - onClick={ isLast ? null : onMoveDown } - icon={ <svg tabIndex="-1" width="18" height="18" xmlns="http://www.w3.org/2000/svg"><path d="M12.293 6.086L9 9.379 5.707 6.086 4.293 7.5 9 12.207 13.707 7.5z" /></svg> } - tooltip={ __( 'Move Down' ) } - label={ getBlockMoverLabel( - uids.length, - blockType && blockType.title, - firstIndex, - isFirst, - isLast, - 1, - ) } - aria-disabled={ isLast } - /> - </div> - ); -} + onFocus() { + this.setState( { + isFocused: true, + } ); + } -/** - * Action creator creator which, given the action type to dispatch and the - * arguments of mapDispatchToProps, creates a prop dispatcher callback for - * managing block movement. - * - * @param {string} type Action type to dispatch. - * @param {Function} dispatch Store dispatch. - * @param {Object} ownProps The wrapped component's own props. - * - * @return {Function} Prop dispatcher callback. - */ -function createOnMove( type, dispatch, ownProps ) { - return () => { - const { uids, rootUID } = ownProps; - if ( uids.length === 1 ) { - dispatch( selectBlock( first( uids ) ) ); + onBlur() { + this.setState( { + isFocused: false, + } ); + } + + render() { + const { onMoveUp, onMoveDown, isFirst, isLast, uids, blockType, firstIndex, isLocked, instanceId, isHidden } = this.props; + const { isFocused } = this.state; + if ( isLocked ) { + return null; } - dispatch( { type, uids, rootUID } ); - }; + // We emulate a disabled state because forcefully applying the `disabled` + // attribute on the button while it has focus causes the screen to change + // to an unfocused state (body as active element) without firing blur on, + // the rendering parent, leaving it unable to react to focus out. + return ( + <div className={ classnames( 'editor-block-mover', { 'is-visible': isFocused || ! isHidden } ) }> + <IconButton + className="editor-block-mover__control" + onClick={ isFirst ? null : onMoveUp } + icon={ upArrow } + label={ __( 'Move up' ) } + aria-describedby={ `editor-block-mover__up-description-${ instanceId }` } + aria-disabled={ isFirst } + onFocus={ this.onFocus } + onBlur={ this.onBlur } + /> + <IconButton + className="editor-block-mover__control" + onClick={ isLast ? null : onMoveDown } + icon={ downArrow } + label={ __( 'Move down' ) } + aria-describedby={ `editor-block-mover__down-description-${ instanceId }` } + aria-disabled={ isLast } + onFocus={ this.onFocus } + onBlur={ this.onBlur } + /> + <span id={ `editor-block-mover__up-description-${ instanceId }` } className="editor-block-mover__description"> + { + getBlockMoverDescription( + uids.length, + blockType && blockType.title, + firstIndex, + isFirst, + isLast, + -1, + ) + } + </span> + <span id={ `editor-block-mover__down-description-${ instanceId }` } className="editor-block-mover__description"> + { + getBlockMoverDescription( + uids.length, + blockType && blockType.title, + firstIndex, + isFirst, + isLast, + 1, + ) + } + </span> + </div> + ); + } } export default compose( - connect( - ( state, ownProps ) => { - const { uids, rootUID } = ownProps; - const block = getBlock( state, first( uids ) ); + withSelect( ( select, { uids, rootUID } ) => { + const { getBlock, getBlockIndex } = select( 'core/editor' ); + const block = getBlock( first( uids ) ); - return ( { - firstIndex: getBlockIndex( state, first( uids ), rootUID ), - blockType: block ? getBlockType( block.name ) : null, - } ); - }, - ( ...args ) => ( { - onMoveDown: createOnMove( 'MOVE_BLOCKS_DOWN', ...args ), - onMoveUp: createOnMove( 'MOVE_BLOCKS_UP', ...args ), - } ) - ), - withContext( 'editor' )( ( settings ) => { + return { + firstIndex: getBlockIndex( first( uids ), rootUID ), + blockType: block ? getBlockType( block.name ) : null, + }; + } ), + withDispatch( ( dispatch, { uids, rootUID } ) => { + const { moveBlocksDown, moveBlocksUp } = dispatch( 'core/editor' ); + return { + onMoveDown: partial( moveBlocksDown, uids, rootUID ), + onMoveUp: partial( moveBlocksUp, uids, rootUID ), + }; + } ), + withEditorSettings( ( settings ) => { const { templateLock } = settings; return { isLocked: templateLock === 'all', }; } ), + withInstanceId, )( BlockMover ); diff --git a/editor/components/block-mover/mover-label.js b/editor/components/block-mover/mover-label.js deleted file mode 100644 index 8ad1a5b7a358dc..00000000000000 --- a/editor/components/block-mover/mover-label.js +++ /dev/null @@ -1,111 +0,0 @@ -/** - * Wordpress dependencies - */ -import { __, sprintf } from '@wordpress/i18n'; - -/** - * Return a label for the block movement controls depending on block position. - * - * @param {number} selectedCount Number of blocks selected. - * @param {string} type Block type - in the case of a single block, should - * define its 'type'. I.e. 'Text', 'Heading', 'Image' etc. - * @param {number} firstIndex The index (position - 1) of the first block selected. - * @param {boolean} isFirst This is the first block. - * @param {boolean} isLast This is the last block. - * @param {number} dir Direction of movement (> 0 is considered to be going - * down, < 0 is up). - * - * @return {string} Label for the block movement controls. - */ -export function getBlockMoverLabel( selectedCount, type, firstIndex, isFirst, isLast, dir ) { - const position = ( firstIndex + 1 ); - - if ( selectedCount > 1 ) { - return getMultiBlockMoverLabel( selectedCount, firstIndex, isFirst, isLast, dir ); - } - - if ( isFirst && isLast ) { - // translators: %s: Type of block (i.e. Text, Image etc) - return sprintf( __( 'Block "%s" is the only block, and cannot be moved' ), type ); - } - - if ( dir > 0 && ! isLast ) { - // moving down - return sprintf( - __( 'Move "%(type)s" block from position %(position)d down to position %(newPosition)d' ), - { - type, - position, - newPosition: ( position + 1 ), - } - ); - } - - if ( dir > 0 && isLast ) { - // moving down, and is the last item - // translators: %s: Type of block (i.e. Text, Image etc) - return sprintf( __( 'Block "%s" is at the end of the content and can’t be moved down' ), type ); - } - - if ( dir < 0 && ! isFirst ) { - // moving up - return sprintf( - __( 'Move "%(type)s" block from position %(position)d up to position %(newPosition)d' ), - { - type, - position, - newPosition: ( position - 1 ), - } - ); - } - - if ( dir < 0 && isFirst ) { - // moving up, and is the first item - // translators: %s: Type of block (i.e. Text, Image etc) - return sprintf( __( 'Block "%s" is at the beginning of the content and can’t be moved up' ), type ); - } -} - -/** - * Return a label for the block movement controls depending on block position. - * - * @param {number} selectedCount Number of blocks selected. - * @param {number} firstIndex The index (position - 1) of the first block selected. - * @param {boolean} isFirst This is the first block. - * @param {boolean} isLast This is the last block. - * @param {number} dir Direction of movement (> 0 is considered to be going - * down, < 0 is up). - * - * @return {string} Label for the block movement controls. - */ -export function getMultiBlockMoverLabel( selectedCount, firstIndex, isFirst, isLast, dir ) { - const position = ( firstIndex + 1 ); - - if ( dir < 0 && isFirst ) { - return __( 'Blocks cannot be moved up as they are already at the top' ); - } - - if ( dir > 0 && isLast ) { - return __( 'Blocks cannot be moved down as they are already at the bottom' ); - } - - if ( dir < 0 && ! isFirst ) { - return sprintf( - __( 'Move %(selectedCount)d blocks from position %(position)d up by one place' ), - { - selectedCount, - position, - } - ); - } - - if ( dir > 0 && ! isLast ) { - return sprintf( - __( 'Move %(selectedCount)d blocks from position %(position)s down by one place' ), - { - selectedCount, - position, - } - ); - } -} diff --git a/editor/components/block-mover/style.scss b/editor/components/block-mover/style.scss index 5e85bcf0ad04c8..3f354c30d8a7b2 100644 --- a/editor/components/block-mover/style.scss +++ b/editor/components/block-mover/style.scss @@ -1,15 +1,22 @@ -// Mover icon buttons$ +.editor-block-mover { + opacity: 0; + + &.is-visible { + @include fade_in; + } +} + +// Mover icon buttons .editor-block-mover__control { display: block; - padding: 2px; - margin: 0 6px 0 4px; border: none; outline: none; background: none; color: $dark-gray-300; cursor: pointer; - border-radius: 50%; - width: $icon-button-size-small; + padding: 0; + width: $block-side-ui-width; + height: $block-side-ui-width; // the side UI can be no taller than 2 * $block-side-ui-width, which matches the height of a line of text &[aria-disabled="true"] { cursor: default; @@ -17,6 +24,23 @@ pointer-events: none; } + // Try a background, only for nested situations @todo + @include break-small() { + .editor-block-list__layout .editor-block-list__layout & { + background: $white; + border-color: $light-gray-500; + border-style: solid; + border-width: 1px; + + &:first-child { + border-width: 1px 1px 0 1px; + } + &:last-child { + border-width: 0 1px 1px 1px; + } + } + } + // apply styles to SVG for movers on the desktop breakpoint @include break-small { // unstyle inherited icon button styles @@ -48,3 +72,7 @@ } } } + +.editor-block-mover__description { + display: none; +} diff --git a/editor/components/block-mover/test/index.js b/editor/components/block-mover/test/index.js index e3aded6ce6e535..0391cc068e7722 100644 --- a/editor/components/block-mover/test/index.js +++ b/editor/components/block-mover/test/index.js @@ -7,6 +7,7 @@ import { shallow } from 'enzyme'; * Internal dependencies */ import { BlockMover } from '../'; +import { upArrow, downArrow } from '../arrows'; describe( 'BlockMover', () => { describe( 'basic rendering', () => { @@ -22,25 +23,33 @@ describe( 'BlockMover', () => { } ); it( 'should render two IconButton components with the following props', () => { - const blockMover = shallow( <BlockMover uids={ selectedUids } blockType={ blockType } firstIndex={ 0 } /> ); + const blockMover = shallow( <BlockMover uids={ selectedUids } blockType={ blockType } firstIndex={ 0 } instanceId={ 1 } /> ); expect( blockMover.hasClass( 'editor-block-mover' ) ).toBe( true ); const moveUp = blockMover.childAt( 0 ); const moveDown = blockMover.childAt( 1 ); + const moveUpDesc = blockMover.childAt( 2 ); + const moveDownDesc = blockMover.childAt( 3 ); expect( moveUp.type().name ).toBe( 'IconButton' ); expect( moveDown.type().name ).toBe( 'IconButton' ); expect( moveUp.props() ).toMatchObject( { className: 'editor-block-mover__control', onClick: undefined, - label: 'Move 2 blocks from position 1 up by one place', + label: 'Move up', + icon: upArrow, 'aria-disabled': undefined, + 'aria-describedby': 'editor-block-mover__up-description-1', } ); expect( moveDown.props() ).toMatchObject( { className: 'editor-block-mover__control', onClick: undefined, - label: 'Move 2 blocks from position 1 down by one place', + label: 'Move down', + icon: downArrow, 'aria-disabled': undefined, + 'aria-describedby': 'editor-block-mover__down-description-1', } ); + expect( moveUpDesc.text() ).toBe( 'Move 2 blocks from position 1 up by one place' ); + expect( moveDownDesc.text() ).toBe( 'Move 2 blocks from position 1 down by one place' ); } ); it( 'should render the up arrow with a onMoveUp callback', () => { @@ -67,7 +76,7 @@ describe( 'BlockMover', () => { expect( moveDown.prop( 'onClick' ) ).toBe( onMoveDown ); } ); - it( 'should render with a disabled up arrown when the block isFirst', () => { + it( 'should render with a disabled up arrow when the block isFirst', () => { const onMoveUp = ( event ) => event; const blockMover = shallow( <BlockMover uids={ selectedUids } diff --git a/editor/components/block-mover/test/mover-label.js b/editor/components/block-mover/test/mover-label.js deleted file mode 100644 index 87c578613536e3..00000000000000 --- a/editor/components/block-mover/test/mover-label.js +++ /dev/null @@ -1,112 +0,0 @@ -/** - * Internal dependencies - */ -import { getBlockMoverLabel, getMultiBlockMoverLabel } from '../mover-label'; - -describe( 'block mover', () => { - const dirUp = -1, - dirDown = 1; - - describe( 'getBlockMoverLabel', () => { - const type = 'TestType'; - - it( 'Should generate a title for the first item moving up', () => { - expect( getBlockMoverLabel( - 1, - type, - 0, - true, - false, - dirUp, - ) ).toBe( - `Block "${ type }" is at the beginning of the content and can’t be moved up` - ); - } ); - - it( 'Should generate a title for the last item moving down', () => { - expect( getBlockMoverLabel( - 1, - type, - 3, - false, - true, - dirDown, - ) ).toBe( `Block "${ type }" is at the end of the content and can’t be moved down` ); - } ); - - it( 'Should generate a title for the second item moving up', () => { - expect( getBlockMoverLabel( - 1, - type, - 1, - false, - false, - dirUp, - ) ).toBe( `Move "${ type }" block from position 2 up to position 1` ); - } ); - - it( 'Should generate a title for the second item moving down', () => { - expect( getBlockMoverLabel( - 1, - type, - 1, - false, - false, - dirDown, - ) ).toBe( `Move "${ type }" block from position 2 down to position 3` ); - } ); - - it( 'Should generate a title for the only item in the list', () => { - expect( getBlockMoverLabel( - 1, - type, - 0, - true, - true, - dirDown, - ) ).toBe( `Block "${ type }" is the only block, and cannot be moved` ); - } ); - } ); - - describe( 'getMultiBlockMoverLabel', () => { - it( 'Should generate a title moving multiple blocks up', () => { - expect( getMultiBlockMoverLabel( - 4, - 1, - false, - true, - dirUp, - ) ).toBe( 'Move 4 blocks from position 2 up by one place' ); - } ); - - it( 'Should generate a title moving multiple blocks down', () => { - expect( getMultiBlockMoverLabel( - 4, - 0, - true, - false, - dirDown, - ) ).toBe( 'Move 4 blocks from position 1 down by one place' ); - } ); - - it( 'Should generate a title for a selection of blocks at the top', () => { - expect( getMultiBlockMoverLabel( - 4, - 1, - true, - true, - dirUp, - ) ).toBe( 'Blocks cannot be moved up as they are already at the top' ); - } ); - - it( 'Should generate a title for a selection of blocks at the bottom', () => { - expect( getMultiBlockMoverLabel( - 4, - 2, - false, - true, - dirDown, - ) ).toBe( 'Blocks cannot be moved down as they are already at the bottom' ); - } ); - } ); -} ); diff --git a/editor/components/block-preview/index.js b/editor/components/block-preview/index.js index 7388183af11220..4b77a80ca48fc6 100644 --- a/editor/components/block-preview/index.js +++ b/editor/components/block-preview/index.js @@ -6,11 +6,14 @@ import { noop } from 'lodash'; /** * WordPress dependencies */ +import { __ } from '@wordpress/i18n'; +import { Component } from '@wordpress/element'; import { createBlock, BlockEdit } from '@wordpress/blocks'; /** * Internal dependencies */ +import { createInnerBlockList } from '../../utils/block-list'; import './style.scss'; /** @@ -19,22 +22,42 @@ import './style.scss'; * @param {Object} props Component props. * @return {WPElement} Rendered element. */ -function BlockPreview( { name, attributes } ) { - const block = createBlock( name, attributes ); - - return ( - <div className="editor-block-preview"> - <div className="editor-block-preview__title">Preview</div> - <div className="editor-block-preview__content"> - <BlockEdit - name={ name } - focus={ false } - attributes={ block.attributes } - setAttributes={ noop } - /> +class BlockPreview extends Component { + getChildContext() { + // Blocks may render their own BlockEdit, in which case we must provide + // a mechanism for them to create their own InnerBlockList. BlockEdit + // is defined in `@wordpress/blocks`, so to avoid a circular dependency + // we inject this function via context. + return { + createInnerBlockList: ( uid ) => { + return createInnerBlockList( uid ); + }, + }; + } + + render() { + const { name, attributes } = this.props; + + const block = createBlock( name, attributes ); + + return ( + <div className="editor-block-preview"> + <div className="editor-block-preview__title">{ __( 'Preview' ) }</div> + <div className="editor-block-preview__content"> + <BlockEdit + name={ name } + focus={ false } + attributes={ block.attributes } + setAttributes={ noop } + /> + </div> </div> - </div> - ); + ); + } } +BlockPreview.childContextTypes = { + createInnerBlockList: noop, +}; + export default BlockPreview; diff --git a/editor/components/block-preview/style.scss b/editor/components/block-preview/style.scss index baa65791c13b0d..15be1a6eda3f83 100644 --- a/editor/components/block-preview/style.scss +++ b/editor/components/block-preview/style.scss @@ -27,4 +27,8 @@ > div section { height: auto; } + + > .shared-block-indicator { + display: none; + } } diff --git a/editor/components/block-selection-clearer/index.js b/editor/components/block-selection-clearer/index.js index df6d8816853729..57b29c6a1bc1ca 100644 --- a/editor/components/block-selection-clearer/index.js +++ b/editor/components/block-selection-clearer/index.js @@ -1,56 +1,73 @@ /** * External dependencies */ -import { connect } from 'react-redux'; import { omit } from 'lodash'; /** * WordPress dependencies */ -import { Component } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import { clearSelectedBlock } from '../../store/actions'; +import { Component, compose } from '@wordpress/element'; +import { withSelect, withDispatch } from '@wordpress/data'; class BlockSelectionClearer extends Component { constructor() { super( ...arguments ); + this.bindContainer = this.bindContainer.bind( this ); - this.onClick = this.onClick.bind( this ); + this.clearSelectionIfFocusTarget = this.clearSelectionIfFocusTarget.bind( this ); } bindContainer( ref ) { this.container = ref; } - onClick( event ) { - if ( event.target === this.container ) { - this.props.clearSelectedBlock(); + /** + * Clears the selected block on focus if the container is the target of the + * focus. This assumes no other descendents have received focus until event + * has bubbled to the container. + * + * @param {FocusEvent} event Focus event. + */ + clearSelectionIfFocusTarget( event ) { + const { + hasSelectedBlock, + hasMultiSelection, + clearSelectedBlock, + } = this.props; + + const hasSelection = ( hasSelectedBlock || hasMultiSelection ); + if ( event.target === this.container && hasSelection ) { + clearSelectedBlock(); } } render() { - const { ...props } = this.props; - - // Disable reason: Clicking the canvas should clear the selection - /* eslint-disable jsx-a11y/no-static-element-interactions */ return ( <div - onMouseDown={ this.onClick } - onTouchStart={ this.onClick } + tabIndex={ -1 } + onFocus={ this.clearSelectionIfFocusTarget } ref={ this.bindContainer } - { ...omit( props, 'clearSelectedBlock' ) } + { ...omit( this.props, [ + 'clearSelectedBlock', + 'hasSelectedBlock', + 'hasMultiSelection', + ] ) } /> ); - /* eslint-enable jsx-a11y/no-static-element-interactions */ } } -export default connect( - undefined, - { - clearSelectedBlock, - }, -)( BlockSelectionClearer ); +export default compose( [ + withSelect( ( select ) => { + const { hasSelectedBlock, hasMultiSelection } = select( 'core/editor' ); + + return { + hasSelectedBlock: hasSelectedBlock(), + hasMultiSelection: hasMultiSelection(), + }; + } ), + withDispatch( ( dispatch ) => { + const { clearSelectedBlock } = dispatch( 'core/editor' ); + return { clearSelectedBlock }; + } ), +] )( BlockSelectionClearer ); diff --git a/editor/components/block-settings-menu/block-mode-toggle.js b/editor/components/block-settings-menu/block-mode-toggle.js index 18ad11ad44375d..2245c9fd702526 100644 --- a/editor/components/block-settings-menu/block-mode-toggle.js +++ b/editor/components/block-settings-menu/block-mode-toggle.js @@ -1,7 +1,6 @@ /** * External dependencies */ -import { connect } from 'react-redux'; import { noop } from 'lodash'; /** @@ -10,14 +9,10 @@ import { noop } from 'lodash'; import { __ } from '@wordpress/i18n'; import { IconButton } from '@wordpress/components'; import { getBlockType, hasBlockSupport } from '@wordpress/blocks'; +import { withSelect, withDispatch } from '@wordpress/data'; +import { compose } from '@wordpress/element'; -/** - * Internal dependencies - */ -import { getBlockMode, getBlock } from '../../store/selectors'; -import { toggleBlockMode } from '../../store/actions'; - -export function BlockModeToggle( { blockType, mode, onToggleMode, small = false } ) { +export function BlockModeToggle( { blockType, mode, onToggleMode, small = false, role } ) { if ( ! hasBlockSupport( blockType, 'html', true ) ) { return null; } @@ -32,25 +27,27 @@ export function BlockModeToggle( { blockType, mode, onToggleMode, small = false onClick={ onToggleMode } icon="html" label={ small ? label : undefined } + role={ role } > { ! small && label } </IconButton> ); } -export default connect( - ( state, { uid } ) => { - const block = getBlock( state, uid ); +export default compose( [ + withSelect( ( select, { uid } ) => { + const { getBlock, getBlockMode } = select( 'core/editor' ); + const block = getBlock( uid ); return { - mode: getBlockMode( state, uid ), + mode: getBlockMode( uid ), blockType: block ? getBlockType( block.name ) : null, }; - }, - ( dispatch, { onToggle = noop, uid } ) => ( { + } ), + withDispatch( ( dispatch, { onToggle = noop, uid } ) => ( { onToggleMode() { - dispatch( toggleBlockMode( uid ) ); + dispatch( 'core/editor' ).toggleBlockMode( uid ); onToggle(); }, - } ) -)( BlockModeToggle ); + } ) ), +] )( BlockModeToggle ); diff --git a/editor/components/block-settings-menu/block-remove-button.js b/editor/components/block-settings-menu/block-remove-button.js index 8756112e61fa25..1790a983834f30 100644 --- a/editor/components/block-settings-menu/block-remove-button.js +++ b/editor/components/block-settings-menu/block-remove-button.js @@ -1,22 +1,18 @@ /** * External dependencies */ -import { connect } from 'react-redux'; import { flow, noop } from 'lodash'; /** * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { IconButton, withContext } from '@wordpress/components'; +import { IconButton } from '@wordpress/components'; import { compose } from '@wordpress/element'; +import { withDispatch } from '@wordpress/data'; +import { withEditorSettings } from '@wordpress/blocks'; -/** - * Internal dependencies - */ -import { removeBlocks } from '../../store/actions'; - -export function BlockRemoveButton( { onRemove, onClick = noop, isLocked, small = false } ) { +export function BlockRemoveButton( { onRemove, onClick = noop, isLocked, small = false, role } ) { if ( isLocked ) { return null; } @@ -29,6 +25,7 @@ export function BlockRemoveButton( { onRemove, onClick = noop, isLocked, small = onClick={ flow( onRemove, onClick ) } icon="trash" label={ small ? label : undefined } + role={ role } > { ! small && label } </IconButton> @@ -36,15 +33,12 @@ export function BlockRemoveButton( { onRemove, onClick = noop, isLocked, small = } export default compose( - connect( - undefined, - ( dispatch, ownProps ) => ( { - onRemove() { - dispatch( removeBlocks( ownProps.uids ) ); - }, - } ) - ), - withContext( 'editor' )( ( settings ) => { + withDispatch( ( dispatch, { uids } ) => ( { + onRemove() { + dispatch( 'core/editor' ).removeBlocks( uids ); + }, + } ) ), + withEditorSettings( ( settings ) => { const { templateLock } = settings; return { diff --git a/editor/components/block-settings-menu/block-transformations.js b/editor/components/block-settings-menu/block-transformations.js index c2cfa7c468a23e..701dea2b33f701 100644 --- a/editor/components/block-settings-menu/block-transformations.js +++ b/editor/components/block-settings-menu/block-transformations.js @@ -1,34 +1,36 @@ /** * External dependencies */ -import { connect } from 'react-redux'; import { noop } from 'lodash'; /** * WordPress dependencies */ -import { __, sprintf } from '@wordpress/i18n'; -import { IconButton, withContext } from '@wordpress/components'; -import { getPossibleBlockTransformations, switchToBlockType } from '@wordpress/blocks'; -import { compose } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { IconButton } from '@wordpress/components'; +import { getPossibleBlockTransformations, switchToBlockType, withEditorSettings } from '@wordpress/blocks'; +import { compose, Fragment } from '@wordpress/element'; +import { withSelect, withDispatch } from '@wordpress/data'; /** * Internal dependencies */ import './style.scss'; -import { getBlock } from '../../store/selectors'; -import { replaceBlocks } from '../../store/actions'; -function BlockTransformations( { blocks, small = false, onTransform, onClick = noop, isLocked } ) { +function BlockTransformations( { blocks, small = false, onTransform, onClick = noop, isLocked, itemsRole } ) { const possibleBlockTransformations = getPossibleBlockTransformations( blocks ); if ( isLocked || ! possibleBlockTransformations.length ) { return null; } return ( - <div className="editor-block-settings-menu__section"> + <Fragment> + <div className="editor-block-settings-menu__separator" /> + <span + className="editor-block-settings-menu__title" + > + { __( 'Transform into:' ) } + </span> { possibleBlockTransformations.map( ( { name, title, icon } ) => { - /* translators: label indicating the transformation of a block into another block */ - const shownText = sprintf( __( 'Turn into %s' ), title ); return ( <IconButton key={ name } @@ -38,36 +40,35 @@ function BlockTransformations( { blocks, small = false, onTransform, onClick = n onClick( event ); } } icon={ icon } - label={ small ? shownText : undefined } + label={ small ? title : undefined } + role={ itemsRole } > - { ! small && shownText } + { ! small && title } </IconButton> ); } ) } - </div> + </Fragment> ); } -export default compose( - connect( - ( state, ownProps ) => { - return { - blocks: ownProps.uids.map( ( uid ) => getBlock( state, uid ) ), - }; +export default compose( [ + withSelect( ( select, ownProps ) => { + return { + blocks: ownProps.uids.map( ( uid ) => select( 'core/editor' ).getBlock( uid ) ), + }; + } ), + withDispatch( ( dispatch, ownProps ) => ( { + onTransform( blocks, name ) { + dispatch( 'core/editor' ).replaceBlocks( + ownProps.uids, + switchToBlockType( blocks, name ) + ); }, - ( dispatch, ownProps ) => ( { - onTransform( blocks, name ) { - dispatch( replaceBlocks( - ownProps.uids, - switchToBlockType( blocks, name ) - ) ); - }, - } ) - ), - withContext( 'editor' )( ( settings ) => { + } ) ), + withEditorSettings( ( settings ) => { const { templateLock } = settings; return { isLocked: !! templateLock, }; } ), -)( BlockTransformations ); +] )( BlockTransformations ); diff --git a/editor/components/block-settings-menu/index.js b/editor/components/block-settings-menu/index.js index 7acdab823fba26..76335db2dfc8da 100644 --- a/editor/components/block-settings-menu/index.js +++ b/editor/components/block-settings-menu/index.js @@ -2,13 +2,14 @@ * External dependencies */ import classnames from 'classnames'; -import { connect } from 'react-redux'; /** * WordPress dependencies */ import { __ } from '@wordpress/i18n'; +import { Component } from '@wordpress/element'; import { IconButton, Dropdown, NavigableMenu } from '@wordpress/components'; +import { withDispatch } from '@wordpress/data'; /** * Internal dependencies @@ -16,66 +17,95 @@ import { IconButton, Dropdown, NavigableMenu } from '@wordpress/components'; import './style.scss'; import BlockModeToggle from './block-mode-toggle'; import BlockRemoveButton from './block-remove-button'; +import BlockDuplicateButton from './block-duplicate-button'; import BlockTransformations from './block-transformations'; -import ReusableBlockSettings from './reusable-block-settings'; +import SharedBlockSettings from './shared-block-settings'; import UnknownConverter from './unknown-converter'; -import { selectBlock } from '../../store/actions'; -function BlockSettingsMenu( { - uids, - onSelect, - focus, - renderBlockMenu = ( { children } ) => children } -) { - const count = uids.length; +export class BlockSettingsMenu extends Component { + constructor() { + super( ...arguments ); + this.state = { + isFocused: false, + }; + this.onFocus = this.onFocus.bind( this ); + this.onBlur = this.onBlur.bind( this ); + } - return ( - <Dropdown - className="editor-block-settings-menu" - contentClassName="editor-block-settings-menu__popover" - position="bottom left" - renderToggle={ ( { onToggle, isOpen } ) => { - const toggleClassname = classnames( 'editor-block-settings-menu__toggle', { - 'is-opened': isOpen, - } ); + onFocus() { + this.setState( { + isFocused: true, + } ); + } - return ( - <IconButton - className={ toggleClassname } - onClick={ () => { - if ( uids.length === 1 ) { - onSelect( uids[ 0 ] ); - } - onToggle(); - } } - icon="ellipsis" - label={ __( 'More Options' ) } - aria-expanded={ isOpen } - focus={ focus } - /> - ); - } } - renderContent={ ( { onClose } ) => ( + onBlur() { + this.setState( { + isFocused: false, + } ); + } + + render() { + const { + uids, + onSelect, + focus, + rootUID, + renderBlockMenu = ( { children } ) => children, + isHidden, + } = this.props; + const { isFocused } = this.state; + const count = uids.length; + + return ( + <Dropdown + className={ classnames( 'editor-block-settings-menu', { + 'is-visible': isFocused || ! isHidden, + } ) } + contentClassName="editor-block-settings-menu__popover" + position="bottom left" + renderToggle={ ( { onToggle, isOpen } ) => { + const toggleClassname = classnames( 'editor-block-settings-menu__toggle', { + 'is-opened': isOpen, + } ); + + return ( + <IconButton + className={ toggleClassname } + onClick={ () => { + if ( uids.length === 1 ) { + onSelect( uids[ 0 ] ); + } + onToggle(); + } } + icon="ellipsis" + label={ __( 'More Options' ) } + aria-expanded={ isOpen } + focus={ focus } + onFocus={ this.onFocus } + onBlur={ this.onBlur } + /> + ); + } } + renderContent={ ( { onClose } ) => ( // Should this just use a DropdownMenu instead of a DropDown ? - <NavigableMenu className="editor-block-settings-menu__content"> - { renderBlockMenu( { onClose, children: [ - count === 1 && <BlockModeToggle key="mode-toggle" uid={ uids[ 0 ] } onToggle={ onClose } />, - count === 1 && <UnknownConverter key="unknown-converter" uid={ uids[ 0 ] } />, - <BlockRemoveButton key="remove" uids={ uids } />, - count === 1 && <ReusableBlockSettings key="reusable-block" uid={ uids[ 0 ] } onToggle={ onClose } />, - <BlockTransformations key="transformations" uids={ uids } onClick={ onClose } />, - ] } ) } - </NavigableMenu> - ) } - /> - ); + <NavigableMenu className="editor-block-settings-menu__content"> + { renderBlockMenu( { onClose, children: [ + count === 1 && <BlockModeToggle key="mode-toggle" uid={ uids[ 0 ] } onToggle={ onClose } role="menuitem" />, + count === 1 && <UnknownConverter key="unknown-converter" uid={ uids[ 0 ] } role="menuitem" />, + <BlockRemoveButton key="remove" uids={ uids } role="menuitem" />, + <BlockDuplicateButton key="duplicate" uids={ uids } rootUID={ rootUID } role="menuitem" />, + count === 1 && <SharedBlockSettings key="shared-block" uid={ uids[ 0 ] } onToggle={ onClose } itemsRole="menuitem" />, + <BlockTransformations key="transformations" uids={ uids } onClick={ onClose } itemsRole="menuitem" />, + ] } ) } + </NavigableMenu> + ) } + /> + ); + } } -export default connect( - undefined, - ( dispatch ) => ( { - onSelect( uid ) { - dispatch( selectBlock( uid ) ); - }, - } ) -)( BlockSettingsMenu ); +export default withDispatch( ( dispatch ) => ( { + onSelect( uid ) { + dispatch( 'core/editor' ).selectBlock( uid ); + }, +} ) )( BlockSettingsMenu ); diff --git a/editor/components/block-settings-menu/reusable-block-settings.js b/editor/components/block-settings-menu/reusable-block-settings.js deleted file mode 100644 index 2a474c34d2ef49..00000000000000 --- a/editor/components/block-settings-menu/reusable-block-settings.js +++ /dev/null @@ -1,99 +0,0 @@ -/** - * External dependencies - */ -import { connect } from 'react-redux'; -import { noop } from 'lodash'; - -/** - * WordPress dependencies - */ -import { Fragment } from '@wordpress/element'; -import { IconButton } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; -import { isReusableBlock } from '@wordpress/blocks'; - -/** - * Internal dependencies - */ -import { - getBlock, - getBlockOrder, - getReusableBlock, -} from '../../store/selectors'; -import { convertBlockToStatic, convertBlockToReusable, deleteReusableBlock } from '../../store/actions'; - -export function ReusableBlockSettings( { - reusableBlock, - isValidForConvert, - onConvertToStatic, - onConvertToReusable, - onDelete, -} ) { - return ( - <Fragment> - { ! reusableBlock && ( - <IconButton - className="editor-block-settings-menu__control" - icon="controls-repeat" - onClick={ onConvertToReusable } - disabled={ ! isValidForConvert } - > - { __( 'Convert to Reusable Block' ) } - </IconButton> - ) } - { reusableBlock && ( - <div className="editor-block-settings-menu__section"> - <IconButton - className="editor-block-settings-menu__control" - icon="controls-repeat" - onClick={ onConvertToStatic } - > - { __( 'Detach from Reusable Block' ) } - </IconButton> - <IconButton - className="editor-block-settings-menu__control" - icon="no" - disabled={ reusableBlock.isTemporary } - onClick={ () => onDelete( reusableBlock.id ) } - > - { __( 'Delete Reusable Block' ) } - </IconButton> - </div> - ) } - </Fragment> - ); -} - -export default connect( - ( state, { uid } ) => { - const block = getBlock( state, uid ); - - return { - isValidForConvert: ! getBlockOrder( state, block.uid ).length, - reusableBlock: isReusableBlock( block ) ? getReusableBlock( state, block.attributes.ref ) : null, - }; - }, - ( dispatch, { uid, onToggle = noop } ) => ( { - onConvertToStatic() { - dispatch( convertBlockToStatic( uid ) ); - onToggle(); - }, - onConvertToReusable() { - dispatch( convertBlockToReusable( uid ) ); - onToggle(); - }, - onDelete( id ) { - // TODO: Make this a <Confirm /> component or similar - // eslint-disable-next-line no-alert - const hasConfirmed = window.confirm( __( - 'Are you sure you want to delete this Reusable Block?\n\n' + - 'It will be permanently removed from all posts and pages that use it.' - ) ); - - if ( hasConfirmed ) { - dispatch( deleteReusableBlock( id ) ); - onToggle(); - } - }, - } ) -)( ReusableBlockSettings ); diff --git a/editor/components/block-settings-menu/style.scss b/editor/components/block-settings-menu/style.scss index 765faac7cf62e4..64af209122f30e 100644 --- a/editor/components/block-settings-menu/style.scss +++ b/editor/components/block-settings-menu/style.scss @@ -1,3 +1,34 @@ +.editor-block-settings-menu { + opacity: 0; + + &.is-visible { + @include fade_in; + } +} + +// The Blocks "More" Menu ellipsis icon button +.editor-block-settings-menu__toggle { + justify-content: center; + padding: 0; + width: $block-side-ui-width; + height: $block-side-ui-width * 2; // same height as a single line of text, our smallest block + + // Try a background, only for nested situations @todo + @include break-small() { + .editor-block-list__layout .editor-block-list__layout & { + background: $white; + border-color: $light-gray-500; + border-style: solid; + border-width: 1px; + } + } + + .dashicon { + transform: rotate( 90deg ); + } +} + +// Popout menu .editor-block-settings-menu__popover { z-index: z-index( '.editor-block-settings-menu__popover' ); @@ -9,57 +40,44 @@ .components-popover__content { width: 182px; } -} -.editor-block-settings-menu__content { - width: 100%; -} - -// The ellipsis icon button -.editor-block-settings-menu__toggle { - border-radius: 50%; - width: auto; - padding: 2px; - margin: 14px 4px 14px 8px; - width: $icon-button-size-small; + .editor-block-settings-menu__content { + width: 100%; + } - .dashicon { - transform: rotate( 90deg ); + .editor-block-settings-menu__separator { + margin-top: $item-spacing; + margin-bottom: $item-spacing; + border-top: 1px solid $light-gray-500; } -} -.editor-block-settings-menu__section { - margin-top: $item-spacing; - margin-bottom: -$item-spacing; - padding: $item-spacing 0; - border-top: 1px solid $light-gray-500; -} + .editor-block-settings-menu__title { + display: block; + padding: 6px; + color: $dark-gray-300; + } -// Popout menu -.editor-block-settings-menu__control { - width: 100%; - justify-content: flex-start; - padding: 8px; - background: none; - outline: none; - border-radius: 0; - color: $dark-gray-500; - cursor: pointer; - @include menu-style__neutral; + // Menu items + .editor-block-settings-menu__control { + width: 100%; + justify-content: flex-start; + padding: 8px; + background: none; + outline: none; + border-radius: 0; + color: $dark-gray-500; + text-align: left; + cursor: pointer; + @include menu-style__neutral; - &:hover, - &:focus, - &:not(:disabled):hover { - @include menu-style__focus; - } + &:hover, + &:focus, + &:not(:disabled):hover { + @include menu-style__focus; + } - .dashicon { - margin-right: 5px; + .dashicon { + margin-right: 5px; + } } } - -.editor-block-settings-menu__section { - margin-top: $item-spacing; - padding-top: $item-spacing; - border-top: 1px solid $light-gray-500; -} diff --git a/editor/components/block-settings-menu/test/reusable-block-settings.js b/editor/components/block-settings-menu/test/reusable-block-settings.js deleted file mode 100644 index d8c81c5138a5fe..00000000000000 --- a/editor/components/block-settings-menu/test/reusable-block-settings.js +++ /dev/null @@ -1,59 +0,0 @@ -/** - * External dependencies - */ -import { shallow } from 'enzyme'; - -/** - * Internal dependencies - */ -import { ReusableBlockSettings } from '../reusable-block-settings'; - -describe( 'ReusableBlockSettings', () => { - it( 'should allow converting a static block to reusable', () => { - const onConvert = jest.fn(); - const wrapper = shallow( - <ReusableBlockSettings - reusableBlock={ null } - onConvertToReusable={ onConvert } - /> - ); - - const text = wrapper.find( 'IconButton' ).children().text(); - expect( text ).toEqual( 'Convert to Reusable Block' ); - - wrapper.find( 'IconButton' ).simulate( 'click' ); - expect( onConvert ).toHaveBeenCalled(); - } ); - - it( 'should allow converting a reusable block to static', () => { - const onConvert = jest.fn(); - const wrapper = shallow( - <ReusableBlockSettings - reusableBlock={ {} } - onConvertToStatic={ onConvert } - /> - ); - - const text = wrapper.find( 'IconButton' ).first().children().text(); - expect( text ).toEqual( 'Detach from Reusable Block' ); - - wrapper.find( 'IconButton' ).first().simulate( 'click' ); - expect( onConvert ).toHaveBeenCalled(); - } ); - - it( 'should allow deleting a reusable block', () => { - const onDelete = jest.fn(); - const wrapper = shallow( - <ReusableBlockSettings - reusableBlock={ { id: 123 } } - onDelete={ onDelete } - /> - ); - - const text = wrapper.find( 'IconButton' ).last().children().text(); - expect( text ).toEqual( 'Delete Reusable Block' ); - - wrapper.find( 'IconButton' ).last().simulate( 'click' ); - expect( onDelete ).toHaveBeenCalledWith( 123 ); - } ); -} ); diff --git a/editor/components/block-settings-menu/unknown-converter.js b/editor/components/block-settings-menu/unknown-converter.js index 1aa1eba63391db..fff64ddb953bf0 100644 --- a/editor/components/block-settings-menu/unknown-converter.js +++ b/editor/components/block-settings-menu/unknown-converter.js @@ -1,7 +1,6 @@ /** * External dependencies */ -import { connect } from 'react-redux'; import { get } from 'lodash'; /** @@ -11,14 +10,9 @@ import { __ } from '@wordpress/i18n'; import { IconButton, withAPIData } from '@wordpress/components'; import { getUnknownTypeHandlerName, rawHandler, serialize } from '@wordpress/blocks'; import { compose } from '@wordpress/element'; +import { withSelect, withDispatch } from '@wordpress/data'; -/** - * Internal dependencies - */ -import { getBlock, getCurrentPostType } from '../../store/selectors'; -import { replaceBlocks } from '../../store/actions'; - -export function UnknownConverter( { block, onReplace, small, user } ) { +export function UnknownConverter( { block, onReplace, small, user, role } ) { if ( ! block || getUnknownTypeHandlerName() !== block.name ) { return null; } @@ -39,6 +33,7 @@ export function UnknownConverter( { block, onReplace, small, user } ) { onClick={ convertToBlocks } icon="screenoptions" label={ small ? label : undefined } + role={ role } > { ! small && label } </IconButton> @@ -46,15 +41,16 @@ export function UnknownConverter( { block, onReplace, small, user } ) { } export default compose( - connect( - ( state, { uid } ) => ( { - block: getBlock( state, uid ), - postType: getCurrentPostType( state ), - } ), - { - onReplace: replaceBlocks, - } - ), + withSelect( ( select, { uid } ) => { + const { getBlock, getCurrentPostType } = select( 'core/editor' ); + return { + block: getBlock( uid ), + postType: getCurrentPostType(), + }; + } ), + withDispatch( ( dispatch ) => ( { + onReplace: dispatch( 'core/editor' ).replaceBlocks, + } ) ), withAPIData( ( { postType } ) => ( { user: `/wp/v2/users/me?post_type=${ postType }&context=edit`, } ) ), diff --git a/editor/components/block-switcher/index.js b/editor/components/block-switcher/index.js index add041a644510c..0cd9392bf05c34 100644 --- a/editor/components/block-switcher/index.js +++ b/editor/components/block-switcher/index.js @@ -1,30 +1,24 @@ -/** - * External dependencies - */ -import { connect } from 'react-redux'; - /** * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { Dropdown, Dashicon, IconButton, Toolbar, NavigableMenu, withContext } from '@wordpress/components'; -import { getBlockType, getPossibleBlockTransformations, switchToBlockType, BlockIcon } from '@wordpress/blocks'; +import { Dropdown, Dashicon, IconButton, Toolbar, NavigableMenu } from '@wordpress/components'; +import { getBlockType, getPossibleBlockTransformations, switchToBlockType, BlockIcon, withEditorSettings } from '@wordpress/blocks'; import { compose } from '@wordpress/element'; import { keycodes } from '@wordpress/utils'; +import { withSelect, withDispatch } from '@wordpress/data'; /** * Internal dependencies */ import './style.scss'; -import { replaceBlocks } from '../../store/actions'; -import { getBlock } from '../../store/selectors'; /** * Module Constants */ const { DOWN } = keycodes; -function BlockSwitcher( { blocks, onTransform, isLocked } ) { +export function BlockSwitcher( { blocks, onTransform, isLocked } ) { const allowedBlocks = getPossibleBlockTransformations( blocks ); if ( isLocked || ! allowedBlocks.length ) { @@ -102,22 +96,20 @@ function BlockSwitcher( { blocks, onTransform, isLocked } ) { } export default compose( - connect( - ( state, ownProps ) => { - return { - blocks: ownProps.uids.map( ( uid ) => getBlock( state, uid ) ), - }; + withSelect( ( select, ownProps ) => { + return { + blocks: ownProps.uids.map( ( uid ) => select( 'core/editor' ).getBlock( uid ) ), + }; + } ), + withDispatch( ( dispatch, ownProps ) => ( { + onTransform( blocks, name ) { + dispatch( 'core/editor' ).replaceBlocks( + ownProps.uids, + switchToBlockType( blocks, name ) + ); }, - ( dispatch, ownProps ) => ( { - onTransform( blocks, name ) { - dispatch( replaceBlocks( - ownProps.uids, - switchToBlockType( blocks, name ) - ) ); - }, - } ) - ), - withContext( 'editor' )( ( settings ) => { + } ) ), + withEditorSettings( ( settings ) => { const { templateLock } = settings; return { diff --git a/editor/components/block-switcher/multi-blocks-switcher.js b/editor/components/block-switcher/multi-blocks-switcher.js index 42a7b14f64882d..1dc4cc21974fe8 100644 --- a/editor/components/block-switcher/multi-blocks-switcher.js +++ b/editor/components/block-switcher/multi-blocks-switcher.js @@ -1,16 +1,15 @@ /** - * External dependencies + * WordPress dependencies */ -import { connect } from 'react-redux'; +import { withSelect } from '@wordpress/data'; /** * Internal dependencies */ import './style.scss'; import BlockSwitcher from './'; -import { getMultiSelectedBlockUids } from '../../store/selectors'; -function MultiBlocksSwitcher( { isMultiBlockSelection, selectedBlockUids } ) { +export function MultiBlocksSwitcher( { isMultiBlockSelection, selectedBlockUids } ) { if ( ! isMultiBlockSelection ) { return null; } @@ -19,9 +18,9 @@ function MultiBlocksSwitcher( { isMultiBlockSelection, selectedBlockUids } ) { ); } -export default connect( - ( state ) => { - const selectedBlockUids = getMultiSelectedBlockUids( state ); +export default withSelect( + ( select ) => { + const selectedBlockUids = select( 'core/editor' ).getMultiSelectedBlockUids(); return { isMultiBlockSelection: selectedBlockUids.length > 1, selectedBlockUids, diff --git a/editor/components/block-switcher/style.scss b/editor/components/block-switcher/style.scss index 0e7d3ade1cb775..dc258edb7b29f6 100644 --- a/editor/components/block-switcher/style.scss +++ b/editor/components/block-switcher/style.scss @@ -24,7 +24,7 @@ box-shadow: $shadow-popover; border: 1px solid $light-gray-500; background: $white; - padding: 3px 3px 0 3px; + padding: 3px 3px 0; } .editor-block-switcher__menu-title { diff --git a/editor/components/block-toolbar/index.js b/editor/components/block-toolbar/index.js index 52a70899f4edd1..76d910551f4f81 100644 --- a/editor/components/block-toolbar/index.js +++ b/editor/components/block-toolbar/index.js @@ -1,19 +1,14 @@ -/** - * External dependencies - */ -import { connect } from 'react-redux'; - /** * WordPress Dependencies */ import { Slot } from '@wordpress/components'; +import { withSelect } from '@wordpress/data'; /** * Internal Dependencies */ import './style.scss'; import BlockSwitcher from '../block-switcher'; -import { getBlockMode, getSelectedBlock } from '../../store/selectors'; function BlockToolbar( { block, mode } ) { if ( ! block || ! block.isValid || mode !== 'visual' ) { @@ -29,11 +24,12 @@ function BlockToolbar( { block, mode } ) { ); } -export default connect( ( state ) => { - const block = getSelectedBlock( state ); +export default withSelect( ( select ) => { + const { getSelectedBlock, getBlockMode } = select( 'core/editor' ); + const block = getSelectedBlock(); - return ( { + return { block, - mode: block ? getBlockMode( state, block.uid ) : null, - } ); + mode: block ? getBlockMode( block.uid ) : null, + }; } )( BlockToolbar ); diff --git a/editor/components/block-toolbar/style.scss b/editor/components/block-toolbar/style.scss index 03d83c75351abd..f015989f05fe04 100644 --- a/editor/components/block-toolbar/style.scss +++ b/editor/components/block-toolbar/style.scss @@ -3,6 +3,7 @@ overflow: auto; // allow horizontal scrolling on mobile flex-grow: 1; width: 100%; + background: $white; .components-toolbar { border: none; diff --git a/editor/components/copy-handler/index.js b/editor/components/copy-handler/index.js index dfc3981d69716c..2349bffffa0f9f 100644 --- a/editor/components/copy-handler/index.js +++ b/editor/components/copy-handler/index.js @@ -1,24 +1,10 @@ -/** - * External dependencies - */ -import { connect } from 'react-redux'; - /** * WordPress dependencies */ -import { Component } from '@wordpress/element'; +import { Component, compose } from '@wordpress/element'; import { serialize } from '@wordpress/blocks'; import { documentHasSelection } from '@wordpress/utils'; - -/** - * Internal dependencies - */ -import { removeBlocks } from '../../store/actions'; -import { - getMultiSelectedBlocks, - getMultiSelectedBlockUids, - getSelectedBlock, -} from '../../store/selectors'; +import { withSelect, withDispatch } from '@wordpress/data'; class CopyHandler extends Component { constructor() { @@ -73,13 +59,20 @@ class CopyHandler extends Component { } } -export default connect( - ( state ) => { +export default compose( [ + withSelect( ( select ) => { + const { + getMultiSelectedBlocks, + getMultiSelectedBlockUids, + getSelectedBlock, + } = select( 'core/editor' ); return { - multiSelectedBlocks: getMultiSelectedBlocks( state ), - multiSelectedBlockUids: getMultiSelectedBlockUids( state ), - selectedBlock: getSelectedBlock( state ), + multiSelectedBlocks: getMultiSelectedBlocks(), + multiSelectedBlockUids: getMultiSelectedBlockUids(), + selectedBlock: getSelectedBlock(), }; - }, - { onRemove: removeBlocks }, -)( CopyHandler ); + } ), + withDispatch( ( dispatch ) => ( { + onRemove: dispatch( 'core/editor' ).removeBlocks, + } ) ), +] )( CopyHandler ); diff --git a/editor/components/default-block-appender/index.js b/editor/components/default-block-appender/index.js index 80e41082ca7a04..39a8a1521fd958 100644 --- a/editor/components/default-block-appender/index.js +++ b/editor/components/default-block-appender/index.js @@ -1,57 +1,82 @@ /** * External dependencies */ -import { connect } from 'react-redux'; +import { get } from 'lodash'; /** * WordPress dependencies */ import { __ } from '@wordpress/i18n'; import { compose } from '@wordpress/element'; -import { isUnmodifiedDefaultBlock } from '@wordpress/blocks'; -import { withContext } from '@wordpress/components'; +import { getDefaultBlockName, withEditorSettings } from '@wordpress/blocks'; +import { decodeEntities } from '@wordpress/utils'; +import { withSelect, withDispatch } from '@wordpress/data'; /** * Internal dependencies */ import './style.scss'; import BlockDropZone from '../block-drop-zone'; -import { appendDefaultBlock, startTyping } from '../../store/actions'; -import { getBlock, getBlockCount } from '../../store/selectors'; +import InserterWithShortcuts from '../inserter-with-shortcuts'; +import Inserter from '../inserter'; -export function DefaultBlockAppender( { isLocked, isVisible, onAppend, showPrompt } ) { +export function DefaultBlockAppender( { + isLocked, + isVisible, + onAppend, + showPrompt, + placeholder, + layout, + rootUID, +} ) { if ( isLocked || ! isVisible ) { return null; } + const value = decodeEntities( placeholder ) || __( 'Write your story' ); + return ( - <div className="editor-default-block-appender"> - <BlockDropZone /> + <div + data-root-uid={ rootUID || '' } + className="editor-default-block-appender"> + <BlockDropZone rootUID={ rootUID } layout={ layout } /> <input + role="button" + aria-label={ __( 'Add block' ) } className="editor-default-block-appender__content" type="text" readOnly onFocus={ onAppend } onClick={ onAppend } onKeyDown={ onAppend } - value={ showPrompt ? __( 'Write your story' ) : '' } + value={ showPrompt ? value : '' } /> + <InserterWithShortcuts rootUID={ rootUID } layout={ layout } /> + <Inserter position="top right" /> </div> ); } export default compose( - connect( - ( state, ownProps ) => { - const isEmpty = ! getBlockCount( state, ownProps.rootUID ); - const lastBlock = getBlock( state, ownProps.lastBlockUID ); - const isLastBlockEmptyDefault = lastBlock && isUnmodifiedDefaultBlock( lastBlock ); + withSelect( ( select, ownProps ) => { + const { + getBlockCount, + getBlock, + } = select( 'core/editor' ); + const isEmpty = ! getBlockCount( ownProps.rootUID ); + const lastBlock = getBlock( ownProps.lastBlockUID ); + const isLastBlockDefault = get( lastBlock, 'name' ) === getDefaultBlockName(); - return { - isVisible: isEmpty || ! isLastBlockEmptyDefault, - showPrompt: isEmpty, - }; - }, - ( dispatch, ownProps ) => ( { + return { + isVisible: isEmpty || ! isLastBlockDefault, + showPrompt: isEmpty, + }; + } ), + withDispatch( ( dispatch, ownProps ) => { + const { + insertDefaultBlock, + startTyping, + } = dispatch( 'core/editor' ); + return { onAppend() { const { layout, rootUID } = ownProps; @@ -60,16 +85,17 @@ export default compose( attributes = { layout }; } - dispatch( appendDefaultBlock( attributes, rootUID ) ); - dispatch( startTyping() ); + insertDefaultBlock( attributes, rootUID ); + startTyping(); }, - } ) - ), - withContext( 'editor' )( ( settings ) => { - const { templateLock } = settings; + }; + } ), + withEditorSettings( ( settings ) => { + const { templateLock, bodyPlaceholder } = settings; return { isLocked: !! templateLock, + placeholder: bodyPlaceholder, }; } ), )( DefaultBlockAppender ); diff --git a/editor/components/default-block-appender/style.scss b/editor/components/default-block-appender/style.scss index 5190f337c44d85..2618a8ae0de40c 100644 --- a/editor/components/default-block-appender/style.scss +++ b/editor/components/default-block-appender/style.scss @@ -2,35 +2,88 @@ $empty-paragraph-height: $text-editor-font-size * 4; .editor-default-block-appender { .editor-default-block-appender__content { + border: none; + background: none; + box-shadow: none; + display: block; + margin: 0; + max-width: none; // fixes a bleed issue from the admin + padding: $block-padding; height: $empty-paragraph-height; + font-size: $editor-font-size; + font-family: $editor-font; + cursor: text; + width: 100%; color: $dark-gray-300; + font-family: $editor-font; outline: 1px solid transparent; transition: 0.2s outline; + } + + // Show quick insertion icons faded until hover + .editor-inserter-with-shortcuts { + .components-icon-button { + color: $light-gray-700; + transition: color 0.2s; + } + } + + // Don't show inserter until mousing + .editor-inserter { + opacity: 0; + } + + &:hover { + .editor-inserter-with-shortcuts { + opacity: 1; + + .components-icon-button { + color: $dark-gray-500; + } + } - &:hover { - outline: 1px solid $light-gray-500; + .editor-inserter { + opacity: 1; } } + + // Dropzone + .components-drop-zone__content-icon { + display: none; + } } -.editor-default-block-appender__content, -input[type=text].editor-default-block-appender__content { - border: none; - background: none; - box-shadow: none; - display: block; - width: 100%; - height: $empty-paragraph-height; - font-size: $editor-font-size; - cursor: text; - margin: 0; - max-width: none; // fixes a bleed issue from the admin - - &:focus { - outline: 1px solid $light-gray-500; +// Left side inserter icon +.editor-block-list__empty-block-inserter, +.editor-default-block-appender .editor-inserter { + position: absolute; + top: $item-spacing; + right: $item-spacing; // show on the right on mobile + transition: opacity 0.2s; + + @include break-small { + left: -$block-side-ui-padding; + right: auto; + } + + .editor-inserter__toggle { + border-radius: 50%; + } + + &:disabled { + display: none; } } -.editor-default-block-appender .components-drop-zone__content-icon { +// Quick block insertion icons on the right +.editor-inserter-with-shortcuts { + position: absolute; + top: $item-spacing; + right: $block-padding; display: none; + + @include break-small { + right: 0; + display: flex; + } } diff --git a/editor/components/default-block-appender/test/__snapshots__/index.js.snap b/editor/components/default-block-appender/test/__snapshots__/index.js.snap index a8cafd88e0773e..0af8402d08e97b 100644 --- a/editor/components/default-block-appender/test/__snapshots__/index.js.snap +++ b/editor/components/default-block-appender/test/__snapshots__/index.js.snap @@ -3,9 +3,11 @@ exports[`DefaultBlockAppender should append a default block when input focused 1`] = ` <div className="editor-default-block-appender" + data-root-uid="" > - <Connect(WrappedComponent) /> + <WithDispatch(WithEditorSettings(BlockDropZone)) /> <input + aria-label="Add block" className="editor-default-block-appender__content" onClick={ [MockFunction] { @@ -29,42 +31,61 @@ exports[`DefaultBlockAppender should append a default block when input focused 1 } } readOnly={true} + role="button" type="text" value="Write your story" /> + <WithEditorSettings(Connect(WithDispatch(InserterWithShortcuts))) /> + <WithSelect(WithDispatch(WithEditorSettings(Inserter))) + position="top right" + /> </div> `; exports[`DefaultBlockAppender should match snapshot 1`] = ` <div className="editor-default-block-appender" + data-root-uid="" > - <Connect(WrappedComponent) /> + <WithDispatch(WithEditorSettings(BlockDropZone)) /> <input + aria-label="Add block" className="editor-default-block-appender__content" onClick={[MockFunction]} onFocus={[MockFunction]} onKeyDown={[MockFunction]} readOnly={true} + role="button" type="text" value="Write your story" /> + <WithEditorSettings(Connect(WithDispatch(InserterWithShortcuts))) /> + <WithSelect(WithDispatch(WithEditorSettings(Inserter))) + position="top right" + /> </div> `; exports[`DefaultBlockAppender should optionally show without prompt 1`] = ` <div className="editor-default-block-appender" + data-root-uid="" > - <Connect(WrappedComponent) /> + <WithDispatch(WithEditorSettings(BlockDropZone)) /> <input + aria-label="Add block" className="editor-default-block-appender__content" onClick={[MockFunction]} onFocus={[MockFunction]} onKeyDown={[MockFunction]} readOnly={true} + role="button" type="text" value="" /> + <WithEditorSettings(Connect(WithDispatch(InserterWithShortcuts))) /> + <WithSelect(WithDispatch(WithEditorSettings(Inserter))) + position="top right" + /> </div> `; diff --git a/editor/components/document-outline/check.js b/editor/components/document-outline/check.js index 9f65e9d09b8f65..80da12a58e8823 100644 --- a/editor/components/document-outline/check.js +++ b/editor/components/document-outline/check.js @@ -1,13 +1,12 @@ /** * External dependencies */ -import { connect } from 'react-redux'; import { filter } from 'lodash'; /** - * Internal dependencies + * WordPress dependencies */ -import { getBlocks } from '../../store/selectors'; +import { withSelect } from '@wordpress/data'; function DocumentOutlineCheck( { blocks, children } ) { const headings = filter( blocks, ( block ) => block.name === 'core/heading' ); @@ -19,8 +18,6 @@ function DocumentOutlineCheck( { blocks, children } ) { return children; } -export default connect( - ( state ) => ( { - blocks: getBlocks( state ), - } ) -)( DocumentOutlineCheck ); +export default withSelect( ( select ) => ( { + blocks: select( 'core/editor' ).getBlocks(), +} ) )( DocumentOutlineCheck ); diff --git a/editor/components/document-outline/index.js b/editor/components/document-outline/index.js index 5f8c4561b646d2..0eaa93a15967be 100644 --- a/editor/components/document-outline/index.js +++ b/editor/components/document-outline/index.js @@ -1,21 +1,20 @@ /** * External dependencies */ -import { connect } from 'react-redux'; -import { filter, countBy } from 'lodash'; +import { countBy, flatMap, get } from 'lodash'; /** * WordPress dependencies */ import { __ } from '@wordpress/i18n'; +import { compose } from '@wordpress/element'; +import { withSelect, withDispatch } from '@wordpress/data'; /** * Internal dependencies */ import './style.scss'; import DocumentOutlineItem from './item'; -import { getBlocks, getEditedPostAttribute } from '../../store/selectors'; -import { selectBlock } from '../../store/actions'; /** * Module constants @@ -56,11 +55,36 @@ const getHeadingLevel = heading => { return 6; } }; +/** + * Returns an array of heading blocks enhanced with the following properties: + * path - An array of blocks that are ancestors of the heading starting from a top-level node. + * Can be an empty array if the heading is a top-level node (is not nested inside another block). + * level - An integer with the heading level. + * isEmpty - Flag indicating if the heading has no content. + * + * @param {?Array} blocks An array of blocks. + * @param {?Array} path An array of blocks that are ancestors of the blocks passed as blocks. + * + * @return {Array} An array of heading blocks enhanced with the properties described above. + */ +const computeOutlineHeadings = ( blocks = [], path = [] ) => { + return flatMap( blocks, ( block = {} ) => { + if ( block.name === 'core/heading' ) { + return { + ...block, + path, + level: getHeadingLevel( block ), + isEmpty: isEmptyHeading( block ), + }; + } + return computeOutlineHeadings( block.innerBlocks, [ ...path, block ] ); + } ); +}; const isEmptyHeading = heading => ! heading.attributes.content || heading.attributes.content.length === 0; -export const DocumentOutline = ( { blocks = [], title, onSelect } ) => { - const headings = filter( blocks, ( block ) => block.name === 'core/heading' ); +export const DocumentOutline = ( { blocks = [], title, onSelect, isTitleSupported } ) => { + const headings = computeOutlineHeadings( blocks ); if ( headings.length < 1 ) { return null; @@ -79,18 +103,14 @@ export const DocumentOutline = ( { blocks = [], title, onSelect } ) => { } }; - const items = headings.map( ( heading ) => ( { - ...heading, - level: getHeadingLevel( heading ), - isEmpty: isEmptyHeading( heading ), - } ) ); - const countByLevel = countBy( items, 'level' ); + const hasTitle = isTitleSupported && title; + const countByLevel = countBy( headings, 'level' ); const hasMultipleH1 = countByLevel[ 1 ] > 1; return ( <div className="document-outline"> <ul> - { title && ( + { hasTitle && ( <DocumentOutlineItem level="Title" isValid @@ -99,7 +119,7 @@ export const DocumentOutline = ( { blocks = [], title, onSelect } ) => { { title } </DocumentOutlineItem> ) } - { items.map( ( item, index ) => { + { headings.map( ( item, index ) => { // Headings remain the same, go up by one, or down by any amount. // Otherwise there are missing levels. const isIncorrectLevel = item.level > prevHeadingLevel + 1; @@ -108,7 +128,7 @@ export const DocumentOutline = ( { blocks = [], title, onSelect } ) => { ! item.isEmpty && ! isIncorrectLevel && !! item.level && - ( item.level !== 1 || ( ! hasMultipleH1 && ! title ) ) + ( item.level !== 1 || ( ! hasMultipleH1 && ! hasTitle ) ) ); prevHeadingLevel = item.level; @@ -118,11 +138,12 @@ export const DocumentOutline = ( { blocks = [], title, onSelect } ) => { level={ `H${ item.level }` } isValid={ isValid } onClick={ () => onSelectHeading( item.uid ) } + path={ item.path } > { item.isEmpty ? emptyHeadingContent : item.attributes.content } { isIncorrectLevel && incorrectLevelContent } { item.level === 1 && hasMultipleH1 && multipleH1Headings } - { title && item.level === 1 && ! hasMultipleH1 && singleH1Headings } + { hasTitle && item.level === 1 && ! hasMultipleH1 && singleH1Headings } </DocumentOutlineItem> ); } ) } @@ -131,16 +152,22 @@ export const DocumentOutline = ( { blocks = [], title, onSelect } ) => { ); }; -export default connect( - ( state ) => { +export default compose( + withSelect( ( select ) => { + const { getEditedPostAttribute, getBlocks } = select( 'core/editor' ); + const { getPostType } = select( 'core' ); + const postType = getPostType( getEditedPostAttribute( 'type' ) ); + return { - title: getEditedPostAttribute( state, 'title' ), - blocks: getBlocks( state ), + title: getEditedPostAttribute( 'title' ), + blocks: getBlocks(), + isTitleSupported: get( postType, [ 'supports', 'title' ], false ), }; - }, - { - onSelect( uid ) { - return selectBlock( uid ); - }, - } + } ), + withDispatch( ( dispatch ) => { + const { selectBlock } = dispatch( 'core/editor' ); + return { + onSelect: selectBlock, + }; + } ) )( DocumentOutline ); diff --git a/editor/components/document-outline/item.js b/editor/components/document-outline/item.js index 5b59614ecca8e0..0b041f1740a7c0 100644 --- a/editor/components/document-outline/item.js +++ b/editor/components/document-outline/item.js @@ -8,11 +8,17 @@ import classnames from 'classnames'; */ import { __ } from '@wordpress/i18n'; +/** + * Internal dependencies + */ +import BlockTitle from '../block-title'; + const TableOfContentsItem = ( { children, isValid, level, onClick, + path = [], } ) => ( <li className={ classnames( @@ -28,6 +34,15 @@ const TableOfContentsItem = ( { onClick={ onClick } > <span className="document-outline__emdash" aria-hidden="true"></span> + { + // path is an array of nodes that are ancestors of the heading starting in the top level node. + // This mapping renders each ancestor to make it easier for the user to know where the headings are nested. + path.map( ( { uid }, index ) => ( + <strong key={ index } className="document-outline__level"> + <BlockTitle uid={ uid } /> + </strong> + ) ) + } <strong className="document-outline__level"> { level } </strong> diff --git a/editor/components/document-outline/style.scss b/editor/components/document-outline/style.scss index f594c98babe4d6..e3ff4ad4ba84de 100644 --- a/editor/components/document-outline/style.scss +++ b/editor/components/document-outline/style.scss @@ -1,33 +1,38 @@ .document-outline { margin: 20px 0; + + ul { + margin: 0; + padding: 0; + } } .document-outline__item { display: flex; margin: 4px 0; - .document-outline__emdash::before { + .document-outline__emdash:before { color: $light-gray-500; margin-right: 4px; } - &.is-h2 .document-outline__emdash::before { + &.is-h2 .document-outline__emdash:before { content: '—'; } - &.is-h3 .document-outline__emdash::before { + &.is-h3 .document-outline__emdash:before { content: '——'; } - &.is-h4 .document-outline__emdash::before { + &.is-h4 .document-outline__emdash:before { content: '———'; } - &.is-h5 .document-outline__emdash::before { + &.is-h5 .document-outline__emdash:before { content: '————'; } - &.is-h6 .document-outline__emdash::before { + &.is-h6 .document-outline__emdash:before { content: '—————'; } } diff --git a/editor/components/document-outline/test/__snapshots__/index.js.snap b/editor/components/document-outline/test/__snapshots__/index.js.snap index 7bf777f6950442..89d0ebb04d6582 100644 --- a/editor/components/document-outline/test/__snapshots__/index.js.snap +++ b/editor/components/document-outline/test/__snapshots__/index.js.snap @@ -10,6 +10,7 @@ exports[`DocumentOutline header blocks present should match snapshot 1`] = ` key="0" level="H2" onClick={[Function]} + path={Array []} > Heading parent </TableOfContentsItem> @@ -18,6 +19,7 @@ exports[`DocumentOutline header blocks present should match snapshot 1`] = ` key="1" level="H3" onClick={[Function]} + path={Array []} > Heading child </TableOfContentsItem> @@ -35,6 +37,7 @@ exports[`DocumentOutline header blocks present should render warnings for multip key="0" level="H1" onClick={[Function]} + path={Array []} > Heading 1 <br @@ -51,6 +54,7 @@ exports[`DocumentOutline header blocks present should render warnings for multip key="1" level="H1" onClick={[Function]} + path={Array []} > Heading 1 <br diff --git a/editor/components/document-outline/test/index.js b/editor/components/document-outline/test/index.js index a38c04c147381d..7151d044a5dbe5 100644 --- a/editor/components/document-outline/test/index.js +++ b/editor/components/document-outline/test/index.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { shallow } from 'enzyme'; +import { mount, shallow } from 'enzyme'; /** * WordPress dependencies @@ -13,6 +13,8 @@ import { createBlock, registerCoreBlocks } from '@wordpress/blocks'; */ import { DocumentOutline } from '../'; +jest.mock( '../../block-title', () => () => 'Block Title' ); + describe( 'DocumentOutline', () => { registerCoreBlocks(); @@ -30,6 +32,8 @@ describe( 'DocumentOutline', () => { nodeName: 'H3', } ); + const nestedHeading = createBlock( 'core/columns', undefined, [ headingChild ] ); + describe( 'no header blocks present', () => { it( 'should not render when no blocks provided', () => { const wrapper = shallow( <DocumentOutline /> ); @@ -74,4 +78,32 @@ describe( 'DocumentOutline', () => { expect( wrapper ).toMatchSnapshot(); } ); } ); + + describe( 'nested headings', () => { + it( 'should render even if the heading is nested', () => { + const tableOfContentItemsSelector = 'TableOfContentsItem'; + const outlineLevelsSelector = '.document-outline__level'; + const outlineItemContentSelector = '.document-outline__item-content'; + + const blocks = [ headingParent, nestedHeading ]; + const wrapper = mount( <DocumentOutline blocks={ blocks } /> ); + + //heading parent and nested heading should appear as items + const tableOfContentItems = wrapper.find( tableOfContentItemsSelector ); + expect( tableOfContentItems ).toHaveLength( 2 ); + + //heading parent test + const firstItemLevels = tableOfContentItems.at( 0 ).find( outlineLevelsSelector ); + expect( firstItemLevels ).toHaveLength( 1 ); + expect( firstItemLevels.at( 0 ).text() ).toEqual( 'H2' ); + expect( tableOfContentItems.at( 0 ).find( outlineItemContentSelector ).text() ).toEqual( 'Heading parent' ); + + //nested heading test + const secondItemLevels = tableOfContentItems.at( 1 ).find( outlineLevelsSelector ); + expect( secondItemLevels ).toHaveLength( 2 ); + expect( secondItemLevels.at( 0 ).text() ).toEqual( 'Block Title' ); + expect( secondItemLevels.at( 1 ).text() ).toEqual( 'H3' ); + expect( tableOfContentItems.at( 1 ).find( outlineItemContentSelector ).text() ).toEqual( 'Heading child' ); + } ); + } ); } ); diff --git a/editor/components/editor-global-keyboard-shortcuts/index.js b/editor/components/editor-global-keyboard-shortcuts/index.js index 1d0fc82d14d86e..74e6df91e970ac 100644 --- a/editor/components/editor-global-keyboard-shortcuts/index.js +++ b/editor/components/editor-global-keyboard-shortcuts/index.js @@ -1,35 +1,25 @@ /** * External dependencies */ -import { connect } from 'react-redux'; import { first, last } from 'lodash'; /** * WordPress dependencies */ import { Component, Fragment, compose } from '@wordpress/element'; -import { KeyboardShortcuts, withContext } from '@wordpress/components'; - -/** - * Internal dependencies - */ -import { getBlockOrder, getMultiSelectedBlockUids } from '../../store/selectors'; -import { - clearSelectedBlock, - multiSelect, - redo, - undo, - autosave, - removeBlocks, -} from '../../store/actions'; +import { KeyboardShortcuts } from '@wordpress/components'; +import { withSelect, withDispatch } from '@wordpress/data'; +import { withEditorSettings } from '@wordpress/blocks'; class EditorGlobalKeyboardShortcuts extends Component { constructor() { super( ...arguments ); + this.selectAll = this.selectAll.bind( this ); this.undoOrRedo = this.undoOrRedo.bind( this ); this.save = this.save.bind( this ); this.deleteSelectedBlocks = this.deleteSelectedBlocks.bind( this ); + this.clearMultiSelection = this.clearMultiSelection.bind( this ); } selectAll( event ) { @@ -64,6 +54,16 @@ class EditorGlobalKeyboardShortcuts extends Component { } } + /** + * Clears current multi-selection, if one exists. + */ + clearMultiSelection() { + const { hasMultiSelection, clearSelectedBlock } = this.props; + if ( hasMultiSelection ) { + clearSelectedBlock(); + } + } + render() { return ( <Fragment> @@ -74,7 +74,7 @@ class EditorGlobalKeyboardShortcuts extends Component { 'mod+shift+z': this.undoOrRedo, backspace: this.deleteSelectedBlocks, del: this.deleteSelectedBlocks, - escape: this.props.clearSelectedBlock, + escape: this.clearMultiSelection, } } /> <KeyboardShortcuts @@ -88,28 +88,44 @@ class EditorGlobalKeyboardShortcuts extends Component { } } -export default compose( - connect( - ( state ) => { - return { - uids: getBlockOrder( state ), - multiSelectedBlockUids: getMultiSelectedBlockUids( state ), - }; - }, - { +export default compose( [ + withSelect( ( select ) => { + const { + getBlockOrder, + getMultiSelectedBlockUids, + hasMultiSelection, + } = select( 'core/editor' ); + + return { + uids: getBlockOrder(), + multiSelectedBlockUids: getMultiSelectedBlockUids(), + hasMultiSelection: hasMultiSelection(), + }; + } ), + withDispatch( ( dispatch ) => { + const { + clearSelectedBlock, + multiSelect, + redo, + undo, + removeBlocks, + autosave, + } = dispatch( 'core/editor' ); + + return { clearSelectedBlock, onMultiSelect: multiSelect, onRedo: redo, onUndo: undo, onRemove: removeBlocks, onSave: autosave, - } - ), - withContext( 'editor' )( ( settings ) => { + }; + } ), + withEditorSettings( ( settings ) => { const { templateLock } = settings; return { isLocked: !! templateLock, }; } ), -)( EditorGlobalKeyboardShortcuts ); +] )( EditorGlobalKeyboardShortcuts ); diff --git a/editor/components/editor-notices/index.js b/editor/components/editor-notices/index.js index c4bfb78652e6ab..50ed1be037eb4f 100644 --- a/editor/components/editor-notices/index.js +++ b/editor/components/editor-notices/index.js @@ -13,10 +13,19 @@ import { NoticeList } from '@wordpress/components'; */ import { removeNotice } from '../../store/actions'; import { getNotices } from '../../store/selectors'; +import TemplateValidationNotice from '../template-validation-notice'; + +function EditorNotices( props ) { + return ( + <NoticeList { ...props }> + <TemplateValidationNotice /> + </NoticeList> + ); +} export default connect( ( state ) => ( { notices: getNotices( state ), } ), { onRemove: removeNotice } -)( NoticeList ); +)( EditorNotices ); diff --git a/editor/components/error-boundary/index.js b/editor/components/error-boundary/index.js index f604612527e72e..f6dfb791792351 100644 --- a/editor/components/error-boundary/index.js +++ b/editor/components/error-boundary/index.js @@ -49,21 +49,20 @@ class ErrorBoundary extends Component { } return ( - <Warning> - <p>{ __( - 'The editor has encountered an unexpected error.' - ) }</p> - <p> - <Button onClick={ this.reboot } isLarge> + <Warning + actions={ [ + <Button key="recovery" onClick={ this.reboot } isLarge> { __( 'Attempt Recovery' ) } - </Button> - <ClipboardButton text={ this.getContent } isLarge> + </Button>, + <ClipboardButton key="copy-post" text={ this.getContent } isLarge> { __( 'Copy Post Text' ) } - </ClipboardButton> - <ClipboardButton text={ error.stack } isLarge> + </ClipboardButton>, + <ClipboardButton key="copy-error" text={ error.stack } isLarge> { __( 'Copy Error' ) } - </ClipboardButton> - </p> + </ClipboardButton>, + ] } + > + { __( 'The editor has encountered an unexpected error.' ) } </Warning> ); } diff --git a/editor/components/index.js b/editor/components/index.js index 46350d874cef02..57bb528463ca87 100644 --- a/editor/components/index.js +++ b/editor/components/index.js @@ -57,6 +57,7 @@ export { default as BlockList } from './block-list'; export { default as BlockMover } from './block-mover'; export { default as BlockSelectionClearer } from './block-selection-clearer'; export { default as BlockSettingsMenu } from './block-settings-menu'; +export { default as BlockTitle } from './block-title'; export { default as BlockToolbar } from './block-toolbar'; export { default as CopyHandler } from './copy-handler'; export { default as DefaultBlockAppender } from './default-block-appender'; @@ -65,6 +66,9 @@ export { default as Inserter } from './inserter'; export { default as MultiBlocksSwitcher } from './block-switcher/multi-blocks-switcher'; export { default as MultiSelectScrollIntoView } from './multi-select-scroll-into-view'; export { default as NavigableToolbar } from './navigable-toolbar'; +export { default as ObserveTyping } from './observe-typing'; +export { default as PreserveScrollInReorder } from './preserve-scroll-in-reorder'; +export { default as SkipToSelectedBlock } from './skip-to-selected-block'; export { default as Warning } from './warning'; export { default as WritingFlow } from './writing-flow'; diff --git a/editor/components/inserter-with-shortcuts/index.js b/editor/components/inserter-with-shortcuts/index.js index 385e91b4190f33..813e36d4c8bb89 100644 --- a/editor/components/inserter-with-shortcuts/index.js +++ b/editor/components/inserter-with-shortcuts/index.js @@ -7,35 +7,29 @@ import { filter, isEmpty } from 'lodash'; /** * WordPress dependencies */ -import { BlockIcon, createBlock, getDefaultBlockName } from '@wordpress/blocks'; +import { BlockIcon, createBlock, getDefaultBlockName, withEditorSettings } from '@wordpress/blocks'; import { compose } from '@wordpress/element'; -import { IconButton, withContext } from '@wordpress/components'; +import { IconButton } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; +import { withDispatch } from '@wordpress/data'; /** * Internal dependencies */ import './style.scss'; -import Inserter from '../inserter'; -import { getFrequentInserterItems } from '../../store/selectors'; -import { replaceBlocks } from '../../store/actions'; +import { getFrecentInserterItems } from '../../store/selectors'; -function InserterWithShortcuts( { items, isLocked, onToggle, onInsert } ) { +function InserterWithShortcuts( { items, isLocked, onInsert } ) { if ( isLocked ) { return null; } const itemsWithoutDefaultBlock = filter( items, ( item ) => item.name !== getDefaultBlockName() || ! isEmpty( item.initialAttributes ) - ).slice( 0, 2 ); + ).slice( 0, 3 ); return ( <div className="editor-inserter-with-shortcuts"> - <Inserter - position="top left" - onToggle={ onToggle } - /> - { itemsWithoutDefaultBlock.map( ( item ) => ( <IconButton key={ item.id } @@ -43,9 +37,7 @@ function InserterWithShortcuts( { items, isLocked, onToggle, onInsert } ) { onClick={ () => onInsert( item ) } label={ sprintf( __( 'Add %s' ), item.title ) } icon={ ( - <span className="editor-inserter-with-shortcuts__block-icon"> - <BlockIcon icon={ item.icon } /> - </span> + <BlockIcon icon={ item.icon } /> ) } /> ) ) } @@ -54,23 +46,31 @@ function InserterWithShortcuts( { items, isLocked, onToggle, onInsert } ) { } export default compose( - withContext( 'editor' )( ( settings ) => { - const { templateLock, blockTypes } = settings; + withEditorSettings( ( settings ) => { + const { templateLock, allowedBlockTypes } = settings; return { isLocked: !! templateLock, - enabledBlockTypes: blockTypes, + allowedBlockTypes, }; } ), connect( - ( state, { enabledBlockTypes } ) => ( { - items: getFrequentInserterItems( state, enabledBlockTypes, 3 ), - } ), - ( dispatch, { uid, layout } ) => ( { + ( state, { allowedBlockTypes } ) => ( { + items: getFrecentInserterItems( state, allowedBlockTypes, 4 ), + } ) + ), + withDispatch( ( dispatch, ownProps ) => { + const { uid, rootUID, layout } = ownProps; + + return { onInsert( { name, initialAttributes } ) { const block = createBlock( name, { ...initialAttributes, layout } ); - return dispatch( replaceBlocks( uid, block ) ); + if ( uid ) { + dispatch( 'core/editor' ).replaceBlocks( uid, block ); + } else { + dispatch( 'core/editor' ).insertBlock( block, undefined, rootUID ); + } }, - } ) - ), + }; + } ), )( InserterWithShortcuts ); diff --git a/editor/components/inserter-with-shortcuts/style.scss b/editor/components/inserter-with-shortcuts/style.scss index ca174cdf311157..11acdf449f4198 100644 --- a/editor/components/inserter-with-shortcuts/style.scss +++ b/editor/components/inserter-with-shortcuts/style.scss @@ -9,8 +9,8 @@ } .editor-inserter-with-shortcuts__block { - margin-right: 5px; - width: 36px; - height: 36px; + margin-right: $block-spacing; + width: $icon-button-size; + height: $icon-button-size; padding-top: 8px; } diff --git a/editor/components/inserter/group.js b/editor/components/inserter/group.js index 4a7be1f9661a96..0cee46106e3fa0 100644 --- a/editor/components/inserter/group.js +++ b/editor/components/inserter/group.js @@ -74,6 +74,7 @@ export default class InserterGroup extends Component { onMouseLeave={ this.createToggleBlockHover( null ) } onFocus={ this.createToggleBlockHover( item ) } onBlur={ this.createToggleBlockHover( null ) } + aria-label={ item.title } // Fix for IE11 and JAWS 2018. > <BlockIcon icon={ item.icon } /> { item.title } diff --git a/editor/components/inserter/index.js b/editor/components/inserter/index.js index 82372dd82fe990..6814c79aa598bd 100644 --- a/editor/components/inserter/index.js +++ b/editor/components/inserter/index.js @@ -7,8 +7,8 @@ import { isEmpty } from 'lodash'; * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { Dropdown, IconButton, withContext } from '@wordpress/components'; -import { createBlock, isUnmodifiedDefaultBlock } from '@wordpress/blocks'; +import { Dropdown, IconButton } from '@wordpress/components'; +import { createBlock, isUnmodifiedDefaultBlock, withEditorSettings } from '@wordpress/blocks'; import { Component, compose } from '@wordpress/element'; import { withSelect, withDispatch } from '@wordpress/data'; @@ -42,6 +42,7 @@ class Inserter extends Component { render() { const { position, + title, children, onInsertBlock, hasSupportedBlocks, @@ -58,6 +59,7 @@ class Inserter extends Component { position={ position } onToggle={ this.onToggle } expandOnMobile + headerTitle={ title } renderToggle={ ( { onToggle, isOpen } ) => ( <IconButton icon="insert" @@ -86,8 +88,9 @@ class Inserter extends Component { export default compose( [ withSelect( ( select ) => ( { - insertionPoint: select( 'core/editor' ).getBlockInsertionPoint, - selectedBlock: select( 'core/editor' ).getSelectedBlock, + title: select( 'core/editor' ).getEditedPostAttribute( 'title' ), + insertionPoint: select( 'core/editor' ).getBlockInsertionPoint(), + selectedBlock: select( 'core/editor' ).getSelectedBlock(), } ) ), withDispatch( ( dispatch, ownProps ) => ( { showInsertionPoint: dispatch( 'core/editor' ).showInsertionPoint, @@ -103,11 +106,11 @@ export default compose( [ return dispatch( 'core/editor' ).insertBlock( insertedBlock, index, rootUID ); }, } ) ), - withContext( 'editor' )( ( settings ) => { - const { blockTypes, templateLock } = settings; + withEditorSettings( ( settings ) => { + const { allowedBlockTypes, templateLock } = settings; return { - hasSupportedBlocks: true === blockTypes || ! isEmpty( blockTypes ), + hasSupportedBlocks: true === allowedBlockTypes || ! isEmpty( allowedBlockTypes ), isLocked: !! templateLock, }; } ), diff --git a/editor/components/inserter/menu.js b/editor/components/inserter/menu.js index 0ff036a9342416..f80016e57768c6 100644 --- a/editor/components/inserter/menu.js +++ b/editor/components/inserter/menu.js @@ -24,9 +24,8 @@ import { TabbableContainer, withInstanceId, withSpokenMessages, - withContext, } from '@wordpress/components'; -import { getCategories, isReusableBlock } from '@wordpress/blocks'; +import { getCategories, isSharedBlock, withEditorSettings } from '@wordpress/blocks'; import { keycodes } from '@wordpress/utils'; /** @@ -35,8 +34,8 @@ import { keycodes } from '@wordpress/utils'; import './style.scss'; import NoBlocks from './no-blocks'; -import { getInserterItems, getRecentInserterItems } from '../../store/selectors'; -import { fetchReusableBlocks } from '../../store/actions'; +import { getInserterItems, getFrecentInserterItems } from '../../store/selectors'; +import { fetchSharedBlocks } from '../../store/actions'; import { default as InserterGroup } from './group'; import BlockPreview from '../block-preview'; @@ -60,7 +59,7 @@ export class InserterMenu extends Component { this.nodes = {}; this.state = { filterValue: '', - tab: 'recent', + tab: 'suggested', selectedItem: null, }; this.filter = this.filter.bind( this ); @@ -69,13 +68,13 @@ export class InserterMenu extends Component { this.sortItems = this.sortItems.bind( this ); this.selectItem = this.selectItem.bind( this ); - this.tabScrollTop = { recent: 0, blocks: 0, embeds: 0 }; + this.tabScrollTop = { suggested: 0, blocks: 0, embeds: 0 }; this.switchTab = this.switchTab.bind( this ); this.previewItem = this.previewItem.bind( this ); } componentDidMount() { - this.props.fetchReusableBlocks(); + this.props.fetchSharedBlocks(); } componentDidUpdate( prevProps, prevState ) { @@ -118,7 +117,7 @@ export class InserterMenu extends Component { } getItemsForTab( tab ) { - const { items, recentItems } = this.props; + const { items, frecentItems } = this.props; // If we're searching, use everything, otherwise just get the items visible in this tab if ( this.state.filterValue ) { @@ -127,19 +126,19 @@ export class InserterMenu extends Component { let predicate; switch ( tab ) { - case 'recent': - return recentItems; + case 'suggested': + return frecentItems; case 'blocks': - predicate = ( item ) => item.category !== 'embed' && item.category !== 'reusable-blocks'; + predicate = ( item ) => item.category !== 'embed' && item.category !== 'shared'; break; case 'embeds': predicate = ( item ) => item.category === 'embed'; break; - case 'saved': - predicate = ( item ) => item.category === 'reusable-blocks'; + case 'shared': + predicate = ( item ) => item.category === 'shared'; break; } @@ -147,7 +146,7 @@ export class InserterMenu extends Component { } sortItems( items ) { - if ( 'recent' === this.state.tab && ! this.state.filterValue ) { + if ( 'suggested' === this.state.tab && ! this.state.filterValue ) { return items; } @@ -220,16 +219,16 @@ export class InserterMenu extends Component { renderTabView( tab ) { const itemsForTab = this.getItemsForTab( tab ); - // If the Recent tab is selected, don't render category headers - if ( 'recent' === tab ) { + // If the Suggested tab is selected, don't render category headers + if ( 'suggested' === tab ) { return this.renderItems( itemsForTab ); } - // If the Saved tab is selected and we have no results, display a friendly message - if ( 'saved' === tab && itemsForTab.length === 0 ) { + // If the Shared tab is selected and we have no results, display a friendly message + if ( 'shared' === tab && itemsForTab.length === 0 ) { return ( <NoBlocks> - { __( 'No saved blocks.' ) } + { __( 'No shared blocks.' ) } </NoBlocks> ); } @@ -248,7 +247,7 @@ export class InserterMenu extends Component { // Passed to TabbableContainer, extending its event-handling logic eventToOffset( event ) { - // If a tab (Recent, Blocks, …) is focused, pressing the down arrow + // If a tab (Suggested, Blocks, …) is focused, pressing the down arrow // moves focus to the selected panel below. if ( event.keyCode === keycodes.DOWN && @@ -270,6 +269,11 @@ export class InserterMenu extends Component { const { selectedItem } = this.state; const isSearching = this.state.filterValue; + // Disable reason: The inserter menu is a modal display, not one which + // is always visible, and one which already incurs this behavior of + // autoFocus via Popover's focusOnMount. + + /* eslint-disable jsx-a11y/no-autofocus */ return ( <TabbableContainer className="editor-inserter__menu" @@ -285,14 +289,15 @@ export class InserterMenu extends Component { placeholder={ __( 'Search for a block' ) } className="editor-inserter__search" onChange={ this.filter } + autoFocus /> { ! isSearching && <TabPanel className="editor-inserter__tabs" activeClass="is-active" onSelect={ this.switchTab } tabs={ [ { - name: 'recent', - title: __( 'Recent' ), + name: 'suggested', + title: __( 'Suggested' ), className: 'editor-inserter__tab', }, { @@ -306,8 +311,8 @@ export class InserterMenu extends Component { className: 'editor-inserter__tab', }, { - name: 'saved', - title: __( 'Saved' ), + name: 'shared', + title: __( 'Shared' ), className: 'editor-inserter__tab', }, ] } @@ -324,30 +329,31 @@ export class InserterMenu extends Component { { this.renderCategories( this.getVisibleItemsByCategory( items ) ) } </div> } - { selectedItem && isReusableBlock( selectedItem ) && + { selectedItem && isSharedBlock( selectedItem ) && <BlockPreview name={ selectedItem.name } attributes={ selectedItem.initialAttributes } /> } </TabbableContainer> ); + /* eslint-enable jsx-a11y/no-autofocus */ } } export default compose( - withContext( 'editor' )( ( settings ) => { - const { blockTypes } = settings; + withEditorSettings( ( settings ) => { + const { allowedBlockTypes } = settings; return { - enabledBlockTypes: blockTypes, + allowedBlockTypes, }; } ), connect( - ( state, ownProps ) => { + ( state, { allowedBlockTypes } ) => { return { - items: getInserterItems( state, ownProps.enabledBlockTypes ), - recentItems: getRecentInserterItems( state, ownProps.enabledBlockTypes ), + items: getInserterItems( state, allowedBlockTypes ), + frecentItems: getFrecentInserterItems( state, allowedBlockTypes ), }; }, - { fetchReusableBlocks } + { fetchSharedBlocks } ), withSpokenMessages, withInstanceId diff --git a/editor/components/inserter/style.scss b/editor/components/inserter/style.scss index f432f8c5ffb42c..5995cf2db70729 100644 --- a/editor/components/inserter/style.scss +++ b/editor/components/inserter/style.scss @@ -30,7 +30,7 @@ input[type="search"].editor-inserter__search { display: block; width: 100%; margin: 0; - padding: 8px 11px; + padding: 11px 16px; position: relative; z-index: 1; border: none; @@ -44,7 +44,7 @@ input[type="search"].editor-inserter__search { } &:focus { - @include square-style__focus-active; + @include square-style__focus; } } @@ -83,16 +83,25 @@ input[type="search"].editor-inserter__search { .editor-inserter__block { display: flex; flex-direction: column; - width: 33%; - border-radius: $button-style__radius-roundrect; + width: calc( 33% - 8px ); + margin: 4px; font-size: $default-font-size; color: $dark-gray-500; - padding: 12px; + padding: 8px 4px; align-items: center; + justify-content: center; cursor: pointer; border: none; - line-height: 20px; + line-height: 1em; background: transparent; + outline: 1px solid $light-gray-200; + min-height: 5em; + overflow: hidden; + word-break: break-word; + + .dashicon { + margin-bottom: 3px; + } &:disabled { @include button-style__disabled; @@ -118,7 +127,7 @@ input[type="search"].editor-inserter__search { text-align: center; display: block; margin: 0; - padding: 12px 14px 12px 14px; + padding: 12px 14px; font-size: $default-font-size; font-weight: 600; margin-top: -1px; // hide the first top border @@ -137,7 +146,7 @@ input[type="search"].editor-inserter__search { display: block; text-align: center; font-style: italic; - padding: 8px; + padding: 24px; } .editor-inserter__tabs { @@ -150,6 +159,15 @@ input[type="search"].editor-inserter__search { .components-tab-panel__tab-content { overflow: auto; + @include square-style__neutral(); + + &:focus { + @include square-style__focus() + } + + &:focus:active { + outline: none; + } @include break-medium { height: $block-inserter-content-height; @@ -161,7 +179,7 @@ input[type="search"].editor-inserter__search { display: flex; justify-content: space-between; position: relative; - background: $light-gray-300; + background: $light-gray-100; border-bottom: 1px solid $light-gray-500; flex-shrink: 0; margin-top: 1px; @@ -172,19 +190,19 @@ input[type="search"].editor-inserter__search { .editor-inserter__tab { border: none; background: none; - border-bottom: 3px solid transparent; - border-top: 3px solid transparent; font-size: $default-font; - padding: 8px 8px; + padding: #{ 8px + 3px } 8px; // Use padding to offset the is-active border, this benefits Windows High Contrast mode width: 100%; border-radius: 0; margin: 0; color: $dark-gray-500; cursor: pointer; + @include square-style__neutral(); &.is-active { + padding-bottom: 8px; font-weight: 600; - border-bottom-color: $blue-medium-500; + border-bottom: 3px solid $blue-medium-500; position: relative; z-index: z-index( '.editor-inserter__tab.is-active' ); } @@ -192,6 +210,6 @@ input[type="search"].editor-inserter__search { &:active, &:focus { z-index: z-index( '.editor-inserter__tab.is-active' ); - @include tab-style__focus-active; + @include square-style__focus(); } } diff --git a/editor/components/inserter/test/menu.js b/editor/components/inserter/test/menu.js index 57a5be1f0b7d31..03dc39bec45025 100644 --- a/editor/components/inserter/test/menu.js +++ b/editor/components/inserter/test/menu.js @@ -64,12 +64,12 @@ const textEmbedItem = { isDisabled: false, }; -const reusableItem = { +const sharedItem = { id: 'core/block/123', name: 'core/block', initialAttributes: { ref: 123 }, - title: 'My reusable block', - category: 'reusable-blocks', + title: 'My shared block', + category: 'shared', isDisabled: false, }; @@ -80,7 +80,7 @@ const items = [ moreItem, youtubeItem, textEmbedItem, - reusableItem, + sharedItem, ]; describe( 'InserterMenu', () => { @@ -88,21 +88,21 @@ describe( 'InserterMenu', () => { // wrapper.find have had to be strengthened (and the filterWhere strengthened also), otherwise two // results would be returned even though only one was in the DOM. - it( 'should show the recent tab by default', () => { + it( 'should show the suggested tab by default', () => { const wrapper = mount( <InserterMenu position={ 'top center' } instanceId={ 1 } items={ [] } - recentItems={ [] } + frecentItems={ [] } debouncedSpeak={ noop } - fetchReusableBlocks={ noop } + fetchSharedBlocks={ noop } blockTypes /> ); const activeCategory = wrapper.find( '.editor-inserter__tab button.is-active' ); - expect( activeCategory.text() ).toBe( 'Recent' ); + expect( activeCategory.text() ).toBe( 'Suggested' ); const visibleBlocks = wrapper.find( '.editor-inserter__block' ); expect( visibleBlocks ).toHaveLength( 0 ); @@ -114,9 +114,9 @@ describe( 'InserterMenu', () => { position={ 'top center' } instanceId={ 1 } items={ [] } - recentItems={ [] } + frecentItems={ [] } debouncedSpeak={ noop } - fetchReusableBlocks={ noop } + fetchSharedBlocks={ noop } /> ); @@ -124,15 +124,15 @@ describe( 'InserterMenu', () => { expect( visibleBlocks ).toHaveLength( 0 ); } ); - it( 'should show the recently used items in the recent tab', () => { + it( 'should show frecently used items in the suggested tab', () => { const wrapper = mount( <InserterMenu position={ 'top center' } instanceId={ 1 } items={ items } - recentItems={ [ advancedTextItem, textItem, someOtherItem ] } + frecentItems={ [ advancedTextItem, textItem, someOtherItem ] } debouncedSpeak={ noop } - fetchReusableBlocks={ noop } + fetchSharedBlocks={ noop } /> ); @@ -149,9 +149,9 @@ describe( 'InserterMenu', () => { position={ 'top center' } instanceId={ 1 } items={ items } - recentItems={ [] } + frecentItems={ [] } debouncedSpeak={ noop } - fetchReusableBlocks={ noop } + fetchSharedBlocks={ noop } /> ); const embedTab = wrapper.find( '.editor-inserter__tab' ) @@ -167,38 +167,38 @@ describe( 'InserterMenu', () => { expect( visibleBlocks.at( 1 ).text() ).toBe( 'A Text Embed' ); } ); - it( 'should show reusable items in the saved tab', () => { + it( 'should show shared items in the shared tab', () => { const wrapper = mount( <InserterMenu position={ 'top center' } instanceId={ 1 } items={ items } - recentItems={ [] } + frecentItems={ [] } debouncedSpeak={ noop } - fetchReusableBlocks={ noop } + fetchSharedBlocks={ noop } /> ); const embedTab = wrapper.find( '.editor-inserter__tab' ) - .filterWhere( ( node ) => node.text() === 'Saved' && node.name() === 'button' ); + .filterWhere( ( node ) => node.text() === 'Shared' && node.name() === 'button' ); embedTab.simulate( 'click' ); const activeCategory = wrapper.find( '.editor-inserter__tab button.is-active' ); - expect( activeCategory.text() ).toBe( 'Saved' ); + expect( activeCategory.text() ).toBe( 'Shared' ); const visibleBlocks = wrapper.find( '.editor-inserter__block' ); expect( visibleBlocks ).toHaveLength( 1 ); - expect( visibleBlocks.at( 0 ).text() ).toBe( 'My reusable block' ); + expect( visibleBlocks.at( 0 ).text() ).toBe( 'My shared block' ); } ); - it( 'should show all items except embeds and reusable blocks in the blocks tab', () => { + it( 'should show all items except embeds and shared blocks in the blocks tab', () => { const wrapper = mount( <InserterMenu position={ 'top center' } instanceId={ 1 } items={ items } - recentItems={ [] } + frecentItems={ [] } debouncedSpeak={ noop } - fetchReusableBlocks={ noop } + fetchSharedBlocks={ noop } /> ); const blocksTab = wrapper.find( '.editor-inserter__tab' ) @@ -222,9 +222,9 @@ describe( 'InserterMenu', () => { position={ 'top center' } instanceId={ 1 } items={ items } - recentItems={ items } + frecentItems={ items } debouncedSpeak={ noop } - fetchReusableBlocks={ noop } + fetchSharedBlocks={ noop } /> ); @@ -239,9 +239,9 @@ describe( 'InserterMenu', () => { position={ 'top center' } instanceId={ 1 } items={ items } - recentItems={ [] } + frecentItems={ [] } debouncedSpeak={ noop } - fetchReusableBlocks={ noop } + fetchSharedBlocks={ noop } /> ); wrapper.setState( { filterValue: 'text' } ); @@ -262,9 +262,9 @@ describe( 'InserterMenu', () => { position={ 'top center' } instanceId={ 1 } items={ items } - recentItems={ [] } + frecentItems={ [] } debouncedSpeak={ noop } - fetchReusableBlocks={ noop } + fetchSharedBlocks={ noop } /> ); wrapper.setState( { filterValue: ' text' } ); diff --git a/editor/components/multi-select-scroll-into-view/index.js b/editor/components/multi-select-scroll-into-view/index.js index 4be0e9579d6fd8..6eafd423ab8e47 100644 --- a/editor/components/multi-select-scroll-into-view/index.js +++ b/editor/components/multi-select-scroll-into-view/index.js @@ -10,6 +10,11 @@ import { Component } from '@wordpress/element'; import { withSelect } from '@wordpress/data'; import { getScrollContainer } from '@wordpress/utils'; +/** + * Internal dependencies + */ +import { getBlockDOMNode } from '../../utils/dom'; + class MultiSelectScrollIntoView extends Component { componentDidUpdate() { // Relies on expectation that `componentDidUpdate` will only be called @@ -29,7 +34,7 @@ class MultiSelectScrollIntoView extends Component { return; } - const extentNode = document.querySelector( '[data-block="' + extentUID + '"]' ); + const extentNode = getBlockDOMNode( extentUID ); if ( ! extentNode ) { return; } diff --git a/editor/components/navigable-toolbar/index.js b/editor/components/navigable-toolbar/index.js index 05ee1e41483cbb..0df6a33a29bace 100644 --- a/editor/components/navigable-toolbar/index.js +++ b/editor/components/navigable-toolbar/index.js @@ -1,3 +1,8 @@ +/** + * External dependencies + */ +import { cond, matchesProperty } from 'lodash'; + /** * WordPress dependencies */ @@ -5,6 +10,12 @@ import { NavigableMenu, KeyboardShortcuts } from '@wordpress/components'; import { Component, findDOMNode } from '@wordpress/element'; import { focus, keycodes } from '@wordpress/utils'; +/** + * Browser dependencies + */ + +const { Node, getSelection } = window; + /** * Module Constants */ @@ -13,9 +24,14 @@ const { ESCAPE } = keycodes; class NavigableToolbar extends Component { constructor() { super( ...arguments ); + this.bindNode = this.bindNode.bind( this ); this.focusToolbar = this.focusToolbar.bind( this ); - this.onToolbarKeyDown = this.onToolbarKeyDown.bind( this ); + this.focusSelection = this.focusSelection.bind( this ); + + this.switchOnKeyDown = cond( [ + [ matchesProperty( [ 'keyCode' ], ESCAPE ), this.focusSelection ], + ] ); } bindNode( ref ) { @@ -32,17 +48,27 @@ class NavigableToolbar extends Component { } } - onToolbarKeyDown( event ) { - if ( event.keyCode !== ESCAPE ) { + /** + * Programmatically shifts focus to the element where the current selection + * exists, if there is a selection. + */ + focusSelection() { + // Ensure that a selection exists. + const selection = getSelection(); + if ( ! selection ) { return; } - // Is there a better way to focus the selected block - // TODO: separate focused/selected block state and use Redux actions instead - const selectedBlock = document.querySelector( '.editor-block-list__block.is-selected .editor-block-list__block-edit' ); - if ( !! selectedBlock ) { - event.stopPropagation(); - selectedBlock.focus(); + // Focus node may be a text node, which cannot be focused directly. + // Find its parent element instead. + const { focusNode } = selection; + let focusElement = focusNode; + if ( focusElement.nodeType !== Node.ELEMENT_NODE ) { + focusElement = focusElement.parentElement; + } + + if ( focusElement ) { + focusElement.focus(); } } @@ -54,7 +80,7 @@ class NavigableToolbar extends Component { role="toolbar" deep ref={ this.bindNode } - onKeyDown={ this.onToolbarKeyDown } + onKeyDown={ this.switchOnKeyDown } { ...props } > <KeyboardShortcuts diff --git a/editor/components/page-attributes/check.js b/editor/components/page-attributes/check.js index 240c2c632f6b92..6ea5bafbec0898 100644 --- a/editor/components/page-attributes/check.js +++ b/editor/components/page-attributes/check.js @@ -1,22 +1,17 @@ /** * External dependencies */ -import { connect } from 'react-redux'; import { get, isEmpty } from 'lodash'; /** * WordPress dependencies */ -import { withAPIData, withContext } from '@wordpress/components'; +import { withEditorSettings } from '@wordpress/blocks'; import { compose } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import { getCurrentPostType } from '../../store/selectors'; +import { withSelect } from '@wordpress/data'; export function PageAttributesCheck( { availableTemplates, postType, children } ) { - const supportsPageAttributes = get( postType, 'data.supports.page-attributes', false ); + const supportsPageAttributes = get( postType, 'supports.page-attributes', false ); // Only render fields if post type supports page attributes or available templates exist. if ( ! supportsPageAttributes && isEmpty( availableTemplates ) ) { @@ -26,30 +21,21 @@ export function PageAttributesCheck( { availableTemplates, postType, children } return children; } -const applyConnect = connect( - ( state ) => { - return { - postTypeSlug: getCurrentPostType( state ), - }; - } -); +const applyWithSelect = withSelect( ( select ) => { + const { getEditedPostAttribute } = select( 'core/editor' ); + const { getPostType } = select( 'core' ); + return { + postType: getPostType( getEditedPostAttribute( 'type' ) ), + }; +} ); -const applyWithContext = withContext( 'editor' )( +const applyWithEditorSettings = withEditorSettings( ( settings ) => ( { availableTemplates: settings.availableTemplates, } ) ); -const applyWithAPIData = withAPIData( ( props ) => { - const { postTypeSlug } = props; - - return { - postType: `/wp/v2/types/${ postTypeSlug }?context=edit`, - }; -} ); - export default compose( [ - applyConnect, - applyWithAPIData, - applyWithContext, + applyWithSelect, + applyWithEditorSettings, ] )( PageAttributesCheck ); diff --git a/editor/components/page-attributes/parent.js b/editor/components/page-attributes/parent.js index 576f9fb882f801..e03de14e647d6d 100644 --- a/editor/components/page-attributes/parent.js +++ b/editor/components/page-attributes/parent.js @@ -1,7 +1,6 @@ /** * External dependencies */ -import { connect } from 'react-redux'; import { get } from 'lodash'; import { stringify } from 'querystringify'; @@ -9,19 +8,14 @@ import { stringify } from 'querystringify'; * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { TreeSelect, withInstanceId, withAPIData } from '@wordpress/components'; +import { TreeSelect, withAPIData } from '@wordpress/components'; import { compose } from '@wordpress/element'; import { buildTermsTree } from '@wordpress/utils'; - -/** - * Internal dependencies - */ -import { getCurrentPostId, getEditedPostAttribute, getCurrentPostType } from '../../store/selectors'; -import { editPost } from '../../store/actions'; +import { withSelect, withDispatch } from '@wordpress/data'; export function PageAttributesParent( { parent, postType, items, onUpdateParent } ) { - const isHierarchical = get( postType, 'data.hierarchical', false ); - const parentPageLabel = get( postType, 'data.labels.parent_item_colon' ); + const isHierarchical = get( postType, 'hierarchical', false ); + const parentPageLabel = get( postType, 'labels.parent_item_colon' ); const pageItems = get( items, 'data', [] ); if ( ! isHierarchical || ! parentPageLabel || ! pageItems.length ) { return null; @@ -43,31 +37,30 @@ export function PageAttributesParent( { parent, postType, items, onUpdateParent ); } -const applyConnect = connect( - ( state ) => { - return { - postId: getCurrentPostId( state ), - parent: getEditedPostAttribute( state, 'parent' ), - postTypeSlug: getCurrentPostType( state ), - }; - }, - { - onUpdateParent( parent ) { - return editPost( { parent: parent || 0 } ); - }, - } -); +const applyWithSelect = withSelect( ( select ) => { + const { getPostType } = select( 'core' ); + const { getCurrentPostId, getEditedPostAttribute } = select( 'core/editor' ); + const postTypeSlug = getEditedPostAttribute( 'type' ); + return { + postId: getCurrentPostId(), + parent: getEditedPostAttribute( 'parent' ), + postType: getPostType( postTypeSlug ), + postTypeSlug, + }; +} ); -const applyWithAPIDataPostType = withAPIData( ( props ) => { - const { postTypeSlug } = props; +const applyWithDispatch = withDispatch( ( dispatch ) => { + const { editPost } = dispatch( 'core/editor' ); return { - postType: `/wp/v2/types/${ postTypeSlug }?context=edit`, + onUpdateParent( parent ) { + editPost( { parent: parent || 0 } ); + }, }; } ); const applyWithAPIDataItems = withAPIData( ( props, { type } ) => { const { postTypeSlug, postId } = props; - const isHierarchical = get( props, 'postType.data.hierarchical', false ); + const isHierarchical = get( props, 'postType.hierarchical', false ); const queryString = stringify( { context: 'edit', per_page: 100, @@ -81,8 +74,7 @@ const applyWithAPIDataItems = withAPIData( ( props, { type } ) => { } ); export default compose( [ - applyConnect, - applyWithAPIDataPostType, + applyWithSelect, + applyWithDispatch, applyWithAPIDataItems, - withInstanceId, ] )( PageAttributesParent ); diff --git a/editor/components/page-attributes/template.js b/editor/components/page-attributes/template.js index 211433d1e2d587..dca952edc2dd89 100644 --- a/editor/components/page-attributes/template.js +++ b/editor/components/page-attributes/template.js @@ -8,8 +8,9 @@ import { isEmpty, map } from 'lodash'; * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { withContext, withInstanceId } from '@wordpress/components'; +import { withInstanceId } from '@wordpress/components'; import { compose } from '@wordpress/element'; +import { withEditorSettings } from '@wordpress/blocks'; /** * Internal dependencies @@ -54,7 +55,7 @@ const applyConnect = connect( } ); -const applyWithContext = withContext( 'editor' )( +const applyWithEditorSettings = withEditorSettings( ( settings ) => ( { availableTemplates: settings.availableTemplates, } ) @@ -62,6 +63,6 @@ const applyWithContext = withContext( 'editor' )( export default compose( applyConnect, - applyWithContext, + applyWithEditorSettings, withInstanceId, )( PageTemplate ); diff --git a/editor/components/page-attributes/test/check.js b/editor/components/page-attributes/test/check.js index 515bab96684947..3faa6405ca9d90 100644 --- a/editor/components/page-attributes/test/check.js +++ b/editor/components/page-attributes/test/check.js @@ -10,10 +10,8 @@ import { PageAttributesCheck } from '../check'; describe( 'PageAttributesCheck', () => { const postType = { - data: { - supports: { - 'page-attributes': true, - }, + supports: { + 'page-attributes': true, }, }; diff --git a/editor/components/post-author/check.js b/editor/components/post-author/check.js index 7fe10b6e7f886a..19378514933294 100644 --- a/editor/components/post-author/check.js +++ b/editor/components/post-author/check.js @@ -17,7 +17,7 @@ import PostTypeSupportCheck from '../post-type-support-check'; import { getCurrentPostType } from '../../store/selectors'; export function PostAuthorCheck( { user, users, children } ) { - const authors = filter( users.data, ( { capabilities } ) => capabilities.level_1 ); + const authors = filter( users.data, ( { capabilities } ) => get( capabilities, [ 'level_1' ], false ) ); const userCanPublishPosts = get( user.data, [ 'post_type_capabilities', 'publish_posts' ], false ); if ( ! userCanPublishPosts || authors.length < 2 ) { diff --git a/editor/components/post-author/index.js b/editor/components/post-author/index.js index 13bb9894217ba4..a4e79a36bd9c3c 100644 --- a/editor/components/post-author/index.js +++ b/editor/components/post-author/index.js @@ -2,7 +2,7 @@ * External dependencies */ import { connect } from 'react-redux'; -import { filter } from 'lodash'; +import { get, filter } from 'lodash'; /** * WordPress dependencies @@ -39,7 +39,7 @@ export class PostAuthor extends Component { // See: https://codex.wordpress.org/Roles_and_Capabilities#User_Levels const { users } = this.props; return filter( users.data, ( user ) => { - return user.capabilities.level_1; + return get( user, [ 'capabilities', 'level_1' ], false ); } ); } diff --git a/editor/components/post-comments/index.js b/editor/components/post-comments/index.js index d981298ac8971e..7730bb7807ab36 100644 --- a/editor/components/post-comments/index.js +++ b/editor/components/post-comments/index.js @@ -26,7 +26,6 @@ function PostComments( { commentStatus = 'open', instanceId, ...props } ) { key="toggle" checked={ commentStatus === 'open' } onChange={ onToggleComments } - showHint={ false } id={ commentsToggleId } />, ]; @@ -42,4 +41,3 @@ export default connect( editPost, } )( withInstanceId( PostComments ) ); - diff --git a/editor/components/post-featured-image/index.js b/editor/components/post-featured-image/index.js index 60324a284815ef..4577937f010fec 100644 --- a/editor/components/post-featured-image/index.js +++ b/editor/components/post-featured-image/index.js @@ -1,31 +1,29 @@ /** * External dependencies */ -import { connect } from 'react-redux'; import { get } from 'lodash'; /** * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { Button, Spinner, ResponsiveWrapper, withAPIData } from '@wordpress/components'; +import { Button, Spinner, ResponsiveWrapper } from '@wordpress/components'; import { MediaUpload } from '@wordpress/blocks'; import { compose } from '@wordpress/element'; +import { withSelect, withDispatch } from '@wordpress/data'; /** * Internal dependencies */ import './style.scss'; import PostFeaturedImageCheck from './check'; -import { getCurrentPostType, getEditedPostAttribute } from '../../store/selectors'; -import { editPost } from '../../store/actions'; //used when labels from post tyoe were not yet loaded or when they are not present. const DEFAULT_SET_FEATURE_IMAGE_LABEL = __( 'Set featured image' ); const DEFAULT_REMOVE_FEATURE_IMAGE_LABEL = __( 'Remove featured image' ); function PostFeaturedImage( { featuredImageId, onUpdateImage, onRemoveImage, media, postType } ) { - const postLabel = get( postType, 'data.labels', {} ); + const postLabel = get( postType, 'labels', {} ); return ( <PostFeaturedImageCheck> @@ -38,15 +36,15 @@ function PostFeaturedImage( { featuredImageId, onUpdateImage, onRemoveImage, med modalClass="editor-post-featured-image__media-modal" render={ ( { open } ) => ( <Button className="button-link editor-post-featured-image__preview" onClick={ open } > - { media && !! media.data && + { media && <ResponsiveWrapper - naturalWidth={ media.data.media_details.width } - naturalHeight={ media.data.media_details.height } + naturalWidth={ media.media_details.width } + naturalHeight={ media.media_details.height } > - <img src={ media.data.source_url } alt={ __( 'Featured image' ) } /> + <img src={ media.source_url } alt={ __( 'Featured image' ) } /> </ResponsiveWrapper> } - { media && media.isLoading && <Spinner /> } + { ! media && <Spinner /> } </Button> ) } /> @@ -79,31 +77,31 @@ function PostFeaturedImage( { featuredImageId, onUpdateImage, onRemoveImage, med ); } -const applyConnect = connect( - ( state ) => { - return { - featuredImageId: getEditedPostAttribute( state, 'featured_media' ), - postTypeName: getCurrentPostType( state ), - }; - }, - { +const applyWithSelect = withSelect( ( select ) => { + const { getMedia, getPostType } = select( 'core' ); + const { getEditedPostAttribute } = select( 'core/editor' ); + const featuredImageId = getEditedPostAttribute( 'featured_media' ); + + return { + media: featuredImageId ? getMedia( featuredImageId ) : null, + postType: getPostType( getEditedPostAttribute( 'type' ) ), + featuredImageId, + }; +} ); + +const applyWithDispatch = withDispatch( ( dispatch ) => { + const { editPost } = dispatch( 'core/editor' ); + return { onUpdateImage( image ) { - return editPost( { featured_media: image.id } ); + editPost( { featured_media: image.id } ); }, onRemoveImage() { - return editPost( { featured_media: 0 } ); + editPost( { featured_media: 0 } ); }, - } -); - -const applyWithAPIData = withAPIData( ( { featuredImageId, postTypeName } ) => { - return { - media: featuredImageId ? `/wp/v2/media/${ featuredImageId }` : undefined, - postType: postTypeName ? `/wp/v2/types/${ postTypeName }?context=edit` : undefined, }; } ); export default compose( - applyConnect, - applyWithAPIData, + applyWithSelect, + applyWithDispatch, )( PostFeaturedImage ); diff --git a/editor/components/post-format/check.js b/editor/components/post-format/check.js index 945779b99da48e..c76eeee37719b2 100644 --- a/editor/components/post-format/check.js +++ b/editor/components/post-format/check.js @@ -1,10 +1,19 @@ +/** + * WordPress dependencies + */ +import { withEditorSettings } from '@wordpress/blocks'; + /** * Internal dependencies */ import PostTypeSupportCheck from '../post-type-support-check'; -function PostFormatCheck( props ) { - return <PostTypeSupportCheck { ...props } supportKeys="post-formats" />; +function PostFormatCheck( { disablePostFormats, ...props } ) { + return ! disablePostFormats && + <PostTypeSupportCheck { ...props } supportKeys="post-formats" />; } -export default PostFormatCheck; +export default withEditorSettings( + ( { disablePostFormats } ) => ( { disablePostFormats } ) +)( PostFormatCheck ); + diff --git a/editor/components/post-pending-status/index.js b/editor/components/post-pending-status/index.js index 88e0f52cde5a78..72ce837f6ffe36 100644 --- a/editor/components/post-pending-status/index.js +++ b/editor/components/post-pending-status/index.js @@ -31,7 +31,6 @@ export function PostPendingStatus( { instanceId, status, onUpdateStatus } ) { id={ pendingId } checked={ status === 'pending' } onChange={ togglePendingStatus } - showHint={ false } /> </PostPendingStatusCheck> ); diff --git a/editor/components/post-permalink/index.js b/editor/components/post-permalink/index.js index 830de1932ac3f8..c7c19f8b5e5a4c 100644 --- a/editor/components/post-permalink/index.js +++ b/editor/components/post-permalink/index.js @@ -53,7 +53,7 @@ class PostPermalink extends Component { <Dashicon icon="admin-links" /> <span className="editor-post-permalink__label">{ __( 'Permalink:' ) }</span> <Button className="editor-post-permalink__link" href={ link } target="_blank"> - { link } + { decodeURI( link ) } </Button> <ClipboardButton className="button" diff --git a/editor/components/post-pingbacks/index.js b/editor/components/post-pingbacks/index.js index e598f27a6fc468..d211e2e499233e 100644 --- a/editor/components/post-pingbacks/index.js +++ b/editor/components/post-pingbacks/index.js @@ -26,7 +26,6 @@ function PostPingbacks( { pingStatus = 'open', instanceId, ...props } ) { key="toggle" checked={ pingStatus === 'open' } onChange={ onTogglePingback } - showHint={ false } id={ pingbacksToggleId } />, ]; @@ -42,4 +41,3 @@ export default connect( editPost, } )( withInstanceId( PostPingbacks ) ); - diff --git a/editor/components/post-preview-button/index.js b/editor/components/post-preview-button/index.js index 04a717e9fb8925..d59bfea42324fd 100644 --- a/editor/components/post-preview-button/index.js +++ b/editor/components/post-preview-button/index.js @@ -7,7 +7,7 @@ import { connect } from 'react-redux'; * WordPress dependencies */ import { Component } from '@wordpress/element'; -import { IconButton } from '@wordpress/components'; +import { Button } from '@wordpress/components'; import { _x } from '@wordpress/i18n'; /** @@ -74,6 +74,10 @@ export class PostPreviewButton extends Component { this.getWindowTarget() ); + // When popup is closed, delete reference to avoid later assignment of + // location in a post update. + this.previewWindow.onbeforeunload = () => delete this.previewWindow; + const markup = ` <div> <p>Please wait&hellip;</p> @@ -105,14 +109,16 @@ export class PostPreviewButton extends Component { const { link, isSaveable } = this.props; return ( - <IconButton + <Button + className="editor-post-preview" + isLarge href={ link } onClick={ this.saveForPreview } target={ this.getWindowTarget() } - icon="visibility" disabled={ ! isSaveable } - label={ _x( 'Preview', 'imperative verb' ) } - /> + > + { _x( 'Preview', 'imperative verb' ) } + </Button> ); } } diff --git a/editor/components/post-publish-button/index.js b/editor/components/post-publish-button/index.js index 070b55b8af1d14..88520d4eef8c93 100644 --- a/editor/components/post-publish-button/index.js +++ b/editor/components/post-publish-button/index.js @@ -36,6 +36,7 @@ export function PostPublishButton( { isSaveable, user, onSubmit = noop, + forceIsSaving, } ) { const isButtonEnabled = user.data && ! isSaving && isPublishable && isSaveable; const isContributor = ! get( user.data, [ 'post_type_capabilities', 'publish_posts' ], false ); @@ -69,18 +70,18 @@ export function PostPublishButton( { disabled={ ! isButtonEnabled } className={ className } > - <PublishButtonLabel /> + <PublishButtonLabel forceIsSaving={ forceIsSaving } /> </Button> ); } const applyConnect = connect( - ( state ) => ( { - isSaving: isSavingPost( state ), + ( state, { forceIsSaving, forceIsDirty } ) => ( { + isSaving: forceIsSaving || isSavingPost( state ), isBeingScheduled: isEditedPostBeingScheduled( state ), visibility: getEditedPostVisibility( state ), isSaveable: isEditedPostSaveable( state ), - isPublishable: isEditedPostPublishable( state ), + isPublishable: forceIsDirty || isEditedPostPublishable( state ), postType: getCurrentPostType( state ), } ), { diff --git a/editor/components/post-publish-button/label.js b/editor/components/post-publish-button/label.js index 9b2d3f5e92c058..41932ac16aa368 100644 --- a/editor/components/post-publish-button/label.js +++ b/editor/components/post-publish-button/label.js @@ -37,6 +37,8 @@ export function PublishButtonLabel( { return __( 'Publishing…' ); } else if ( isPublished && isSaving ) { return __( 'Updating…' ); + } else if ( isBeingScheduled && isSaving ) { + return __( 'Scheduling…' ); } if ( isContributor ) { @@ -51,10 +53,10 @@ export function PublishButtonLabel( { } const applyConnect = connect( - ( state ) => ( { + ( state, { forceIsSaving } ) => ( { isPublished: isCurrentPostPublished( state ), isBeingScheduled: isEditedPostBeingScheduled( state ), - isSaving: isSavingPost( state ), + isSaving: forceIsSaving || isSavingPost( state ), isPublishing: isPublishingPost( state ), postType: getCurrentPostType( state ), } ) diff --git a/editor/components/post-publish-button/test/label.js b/editor/components/post-publish-button/test/label.js index 0c6ef68e8a0320..ec9f9107b06dff 100644 --- a/editor/components/post-publish-button/test/label.js +++ b/editor/components/post-publish-button/test/label.js @@ -36,6 +36,11 @@ describe( 'PublishButtonLabel', () => { expect( label ).toBe( 'Updating…' ); } ); + it( 'should show scheduling if scheduled and saving in progress', () => { + const label = PublishButtonLabel( { user, isBeingScheduled: true, isSaving: true } ); + expect( label ).toBe( 'Scheduling…' ); + } ); + it( 'should show publish if not published and saving in progress', () => { const label = PublishButtonLabel( { user, isPublished: false, isSaving: true } ); expect( label ).toBe( 'Publish' ); diff --git a/editor/components/post-publish-panel/index.js b/editor/components/post-publish-panel/index.js index 73d44549cf25d8..a4b37bcef3baae 100644 --- a/editor/components/post-publish-panel/index.js +++ b/editor/components/post-publish-panel/index.js @@ -21,6 +21,7 @@ import PostPublishPanelPostpublish from './postpublish'; import { getCurrentPostType, isCurrentPostPublished, + isCurrentPostScheduled, isSavingPost, isEditedPostDirty, } from '../../store/selectors'; @@ -28,21 +29,21 @@ import { class PostPublishPanel extends Component { constructor() { super( ...arguments ); - this.onPublish = this.onPublish.bind( this ); + this.onSubmit = this.onSubmit.bind( this ); this.state = { loading: false, - published: false, + submitted: false, }; } componentWillReceiveProps( newProps ) { if ( - newProps.isPublished && - ! this.state.published && - ! newProps.isSaving + ! this.state.submitted && + ! newProps.isSaving && + ( newProps.isPublished || newProps.isScheduled ) ) { this.setState( { - published: true, + submitted: true, loading: false, } ); } @@ -56,25 +57,32 @@ class PostPublishPanel extends Component { } } - onPublish() { + onSubmit() { + const { user, onClose } = this.props; + const userCanPublishPosts = get( user.data, [ 'post_type_capabilities', 'publish_posts' ], false ); + const isContributor = user.data && ! userCanPublishPosts; + if ( isContributor ) { + onClose(); + return; + } this.setState( { loading: true } ); } render() { - const { onClose, user } = this.props; - const { loading, published } = this.state; - const canPublish = get( user.data, [ 'post_type_capabilities', 'publish_posts' ], false ); - + const { isScheduled, onClose, forceIsDirty, forceIsSaving } = this.props; + const { loading, submitted } = this.state; return ( <div className="editor-post-publish-panel"> <div className="editor-post-publish-panel__header"> - { ! published && ( + { ! submitted && ( <div className="editor-post-publish-panel__header-publish-button"> - <PostPublishButton onSubmit={ this.onPublish } /> + <PostPublishButton onSubmit={ this.onSubmit } forceIsDirty={ forceIsDirty } forceIsSaving={ forceIsSaving } /> </div> ) } - { published && ( - <div className="editor-post-publish-panel__header-published">{ __( 'Published' ) }</div> + { submitted && ( + <div className="editor-post-publish-panel__header-published"> + { isScheduled ? __( 'Scheduled' ) : __( 'Published' ) } + </div> ) } <IconButton onClick={ onClose } @@ -83,9 +91,9 @@ class PostPublishPanel extends Component { /> </div> <div className="editor-post-publish-panel__content"> - { canPublish && ! loading && ! published && <PostPublishPanelPrepublish /> } - { loading && ! published && <Spinner /> } - { published && <PostPublishPanelPostpublish /> } + { ! loading && ! submitted && <PostPublishPanelPrepublish /> } + { loading && ! submitted && <Spinner /> } + { submitted && <PostPublishPanelPostpublish /> } </div> </div> ); @@ -97,6 +105,7 @@ const applyConnect = connect( return { postType: getCurrentPostType( state ), isPublished: isCurrentPostPublished( state ), + isScheduled: isCurrentPostScheduled( state ), isSaving: isSavingPost( state ), isDirty: isEditedPostDirty( state ), }; diff --git a/editor/components/post-publish-panel/postpublish.js b/editor/components/post-publish-panel/postpublish.js index eead47a067ecee..5d8bbbe178cbcb 100644 --- a/editor/components/post-publish-panel/postpublish.js +++ b/editor/components/post-publish-panel/postpublish.js @@ -2,19 +2,19 @@ * External Dependencies */ import { get } from 'lodash'; -import { connect } from 'react-redux'; /** * WordPress Dependencies */ -import { PanelBody, Button, ClipboardButton, withAPIData } from '@wordpress/components'; +import { PanelBody, Button, ClipboardButton } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; -import { Component, compose } from '@wordpress/element'; +import { Component, Fragment } from '@wordpress/element'; +import { withSelect } from '@wordpress/data'; /** - * Internal Dependencies + * Internal dependencies */ -import { getCurrentPost, getCurrentPostType } from '../../store/selectors'; +import PostScheduleLabel from '../post-schedule/label'; class PostPublishPanelPostpublish extends Component { constructor() { @@ -48,16 +48,23 @@ class PostPublishPanelPostpublish extends Component { } render() { - const { post, postType } = this.props; - const viewPostLabel = get( postType, [ 'data', 'labels', 'view_item' ] ); + const { isScheduled, post, postType } = this.props; + const viewPostLabel = get( postType, [ 'labels', 'view_item' ] ); + + const postPublishNonLinkHeader = isScheduled ? + <Fragment>{ __( 'is now scheduled. It will go live on' ) } <PostScheduleLabel />.</Fragment> : + __( 'is now live.' ); + const postPublishBodyText = isScheduled ? + __( 'The post address will be:' ) : + __( 'What\'s next?' ); return ( <div className="post-publish-panel__postpublish"> <PanelBody className="post-publish-panel__postpublish-header"> - <a href={ post.link }>{ post.title || __( '(no title)' ) }</a>{ __( ' is now live.' ) } + <a href={ post.link }>{ post.title || __( '(no title)' ) }</a> { postPublishNonLinkHeader } </PanelBody> <PanelBody> - <div><strong>{ __( 'What\'s next?' ) }</strong></div> + <div><strong>{ postPublishBodyText }</strong></div> <input className="post-publish-panel__postpublish-link-input" readOnly @@ -65,9 +72,11 @@ class PostPublishPanelPostpublish extends Component { onFocus={ this.onSelectInput } /> <div className="post-publish-panel__postpublish-buttons"> - <Button className="button" href={ post.link }> - { viewPostLabel } - </Button> + { ! isScheduled && ( + <Button className="button" href={ post.link }> + { viewPostLabel } + </Button> + ) } <ClipboardButton className="button" text={ post.link } onCopy={ this.onCopy }> { this.state.showCopyConfirmation ? __( 'Copied!' ) : __( 'Copy Link' ) } @@ -79,24 +88,13 @@ class PostPublishPanelPostpublish extends Component { } } -const applyConnect = connect( - ( state ) => { - return { - post: getCurrentPost( state ), - postTypeSlug: getCurrentPostType( state ), - }; - } -); - -const applyWithAPIData = withAPIData( ( props ) => { - const { postTypeSlug } = props; +export default withSelect( ( select ) => { + const { getEditedPostAttribute, getCurrentPost, isCurrentPostScheduled } = select( 'core/editor' ); + const { getPostType } = select( 'core' ); return { - postType: `/wp/v2/types/${ postTypeSlug }?context=edit`, + post: getCurrentPost(), + postType: getPostType( getEditedPostAttribute( 'type' ) ), + isScheduled: isCurrentPostScheduled(), }; -} ); - -export default compose( [ - applyConnect, - applyWithAPIData, -] )( PostPublishPanelPostpublish ); +} )( PostPublishPanelPostpublish ); diff --git a/editor/components/post-publish-panel/style.scss b/editor/components/post-publish-panel/style.scss index 87ffb0af337a5b..7b4d139ecaf8df 100644 --- a/editor/components/post-publish-panel/style.scss +++ b/editor/components/post-publish-panel/style.scss @@ -58,7 +58,7 @@ border: none; .components-panel__body-toggle { - font-weight: normal; + font-weight: 400; } } @@ -96,9 +96,7 @@ .post-publish-panel__postpublish-link-input[readonly] { width: 100%; - border: 1px solid $light-gray-500; padding: 10px; - border-radius: $button-style__radius-roundrect; margin: 15px 0; background: $white; overflow: hidden; diff --git a/editor/components/post-publish-panel/toggle.js b/editor/components/post-publish-panel/toggle.js index 8601c34d5bc4f4..e7bc94c531a54e 100644 --- a/editor/components/post-publish-panel/toggle.js +++ b/editor/components/post-publish-panel/toggle.js @@ -19,22 +19,37 @@ import { isSavingPost, isEditedPostSaveable, isEditedPostPublishable, + isCurrentPostPending, isCurrentPostPublished, isEditedPostBeingScheduled, + isCurrentPostScheduled, getCurrentPostType, } from '../../store/selectors'; -function PostPublishPanelToggle( { user, isSaving, isPublishable, isSaveable, isPublished, isBeingScheduled, onToggle, isOpen } ) { +function PostPublishPanelToggle( { + user, + isSaving, + isPublishable, + isSaveable, + isPublished, + isBeingScheduled, + isPending, + isScheduled, + onToggle, + isOpen, + forceIsDirty, + forceIsSaving, +} ) { const isButtonEnabled = ( - ! isSaving && isPublishable && isSaveable + ! isSaving && ! forceIsSaving && isPublishable && isSaveable ) || isPublished; const userCanPublishPosts = get( user.data, [ 'post_type_capabilities', 'publish_posts' ], false ); const isContributor = user.data && ! userCanPublishPosts; - const showToggle = ! isContributor && ! isPublished && ! isBeingScheduled; + const showToggle = ! isPublished && ! ( isScheduled && isBeingScheduled ) && ! ( isPending && isContributor ); if ( ! showToggle ) { - return <PostPublishButton />; + return <PostPublishButton forceIsDirty={ forceIsDirty } forceIsSaving={ forceIsSaving } />; } return ( @@ -46,7 +61,7 @@ function PostPublishPanelToggle( { user, isSaving, isPublishable, isSaveable, is disabled={ ! isButtonEnabled } isBusy={ isSaving && isPublished } > - { __( 'Publish...' ) } + { isBeingScheduled ? __( 'Schedule…' ) : __( 'Publish…' ) } </Button> ); } @@ -56,7 +71,9 @@ const applyConnect = connect( isSaving: isSavingPost( state ), isSaveable: isEditedPostSaveable( state ), isPublishable: isEditedPostPublishable( state ), + isPending: isCurrentPostPending( state ), isPublished: isCurrentPostPublished( state ), + isScheduled: isCurrentPostScheduled( state ), isBeingScheduled: isEditedPostBeingScheduled( state ), postType: getCurrentPostType( state ), } ), diff --git a/editor/components/post-saved-state/index.js b/editor/components/post-saved-state/index.js index 841a3bddf0a6f7..2a49c7146b433d 100644 --- a/editor/components/post-saved-state/index.js +++ b/editor/components/post-saved-state/index.js @@ -1,82 +1,101 @@ -/** - * External dependencies - */ -import { connect } from 'react-redux'; -import classnames from 'classnames'; - /** * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { Dashicon, Button } from '@wordpress/components'; +import { Dashicon, IconButton, withSafeTimeout } from '@wordpress/components'; +import { Component, compose } from '@wordpress/element'; +import { withSelect, withDispatch } from '@wordpress/data'; /** * Internal dependencies */ import './style.scss'; import PostSwitchToDraftButton from '../post-switch-to-draft-button'; -import { savePost } from '../../store/actions'; -import { - isEditedPostNew, - isCurrentPostPublished, - isEditedPostDirty, - isSavingPost, - isEditedPostSaveable, - getCurrentPost, -} from '../../store/selectors'; /** * Component showing whether the post is saved or not and displaying save links. * * @param {Object} Props Component Props. - * @return {WPElement} WordPress Element. */ -export function PostSavedState( { isNew, isPublished, isDirty, isSaving, isSaveable, onSave } ) { - const className = 'editor-post-saved-state'; - - if ( isSaving ) { - return ( - <span className={ className }> - { __( 'Saving' ) } - </span> - ); +export class PostSavedState extends Component { + constructor() { + super( ...arguments ); + this.state = { + forceSavedMessage: false, + }; } - if ( isPublished ) { - return <PostSwitchToDraftButton className={ classnames( className, 'button-link' ) } />; + componentDidUpdate( prevProps ) { + if ( prevProps.isSaving && ! this.props.isSaving ) { + this.setState( { forceSavedMessage: true } ); + this.props.setTimeout( () => { + this.setState( { forceSavedMessage: false } ); + }, 1000 ); + } } - if ( ! isSaveable ) { - return null; - } + render() { + const { isNew, isPublished, isDirty, isSaving, isSaveable, onSave } = this.props; + const { forceSavedMessage } = this.state; + if ( isSaving ) { + return ( + <span className="editor-post-saved-state is-saving"> + <Dashicon icon="cloud" /> + { __( 'Saving' ) } + </span> + ); + } + + if ( isPublished ) { + return <PostSwitchToDraftButton />; + } + + if ( ! isSaveable ) { + return null; + } + + if ( forceSavedMessage || ( ! isNew && ! isDirty ) ) { + return ( + <span className="editor-post-saved-state is-saved"> + <Dashicon icon="saved" /> + { __( 'Saved' ) } + </span> + ); + } - if ( ! isNew && ! isDirty ) { return ( - <span className={ className }> - <Dashicon icon="saved" /> - { __( 'Saved' ) } - </span> + <IconButton + className="editor-post-save-draft" + onClick={ onSave } + icon="cloud-upload" + > + { __( 'Save Draft' ) } + </IconButton> ); } - - return ( - <Button className={ classnames( className, 'button-link' ) } onClick={ onSave }> - <span className="editor-post-saved-state__mobile">{ __( 'Save' ) }</span> - <span className="editor-post-saved-state__desktop">{ __( 'Save Draft' ) }</span> - </Button> - ); } -export default connect( - ( state, { forceIsDirty, forceIsSaving } ) => ( { - post: getCurrentPost( state ), - isNew: isEditedPostNew( state ), - isPublished: isCurrentPostPublished( state ), - isDirty: forceIsDirty || isEditedPostDirty( state ), - isSaving: forceIsSaving || isSavingPost( state ), - isSaveable: isEditedPostSaveable( state ), +export default compose( [ + withSelect( ( select, { forceIsDirty, forceIsSaving } ) => { + const { + isEditedPostNew, + isCurrentPostPublished, + isEditedPostDirty, + isSavingPost, + isEditedPostSaveable, + getCurrentPost, + } = select( 'core/editor' ); + return { + post: getCurrentPost(), + isNew: isEditedPostNew(), + isPublished: isCurrentPostPublished(), + isDirty: forceIsDirty || isEditedPostDirty(), + isSaving: forceIsSaving || isSavingPost(), + isSaveable: isEditedPostSaveable(), + }; } ), - { - onSave: savePost, - } -)( PostSavedState ); + withDispatch( ( dispatch ) => ( { + onSave: dispatch( 'core/editor' ).savePost, + } ) ), + withSafeTimeout, +] )( PostSavedState ); diff --git a/editor/components/post-saved-state/style.scss b/editor/components/post-saved-state/style.scss index abbc14ec234f67..01d87bc2cde650 100644 --- a/editor/components/post-saved-state/style.scss +++ b/editor/components/post-saved-state/style.scss @@ -1,33 +1,36 @@ .editor-post-saved-state { display: flex; align-items: center; - margin-right: $item-spacing; color: $dark-gray-500; + overflow: hidden; - .dashicon { - margin-right: 4px; - margin-left: -4px; + &.is-saving { + animation: loading_fade .5s infinite; } - .wp-core-ui &.button-link { - margin-right: $item-spacing; - padding: 0; - - &:hover { - background: none; - } + .dashicon { + display: inline-block; + flex: 0 0 auto; } } -.editor-post-saved-state__mobile { - @include break-small { - display: none; +.editor-post-saved-state, +.editor-post-save-draft { + white-space: nowrap; + padding: 8px 4px; + width: $icon-button-size - 8px; + + .dashicon { + margin-right: 8px; } -} -.editor-post-saved-state__desktop { - display: none; - @include break-small { - display: inline; + @include break-small() { + padding: 8px; + width: auto; + text-indent: inherit; + + .dashicon { + margin-right: 4px; + } } } diff --git a/editor/components/post-saved-state/test/__snapshots__/index.js.snap b/editor/components/post-saved-state/test/__snapshots__/index.js.snap index 709e770b9e43b9..74ce5219263fa9 100644 --- a/editor/components/post-saved-state/test/__snapshots__/index.js.snap +++ b/editor/components/post-saved-state/test/__snapshots__/index.js.snap @@ -1,7 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`PostSavedState returns a switch to draft link if the post is published 1`] = ` -<Connect(PostSwitchToDraftButton) - className="editor-post-saved-state button-link" -/> -`; +exports[`PostSavedState returns a switch to draft link if the post is published 1`] = `<Connect(PostSwitchToDraftButton) />`; diff --git a/editor/components/post-saved-state/test/index.js b/editor/components/post-saved-state/test/index.js index bd45a24b8aa74b..2e49f2db8c0616 100644 --- a/editor/components/post-saved-state/test/index.js +++ b/editor/components/post-saved-state/test/index.js @@ -18,7 +18,7 @@ describe( 'PostSavedState', () => { isSaveable={ false } /> ); - expect( wrapper.text() ).toBe( 'Saving' ); + expect( wrapper.text() ).toContain( 'Saving' ); } ); it( 'returns null if the post is not saveable', () => { @@ -63,8 +63,7 @@ describe( 'PostSavedState', () => { onSave={ saveSpy } /> ); - expect( wrapper.name() ).toBe( 'Button' ); - expect( wrapper.childAt( 0 ).text() ).toBe( 'Save' ); + expect( wrapper.name() ).toBe( 'IconButton' ); wrapper.simulate( 'click' ); expect( saveSpy ).toHaveBeenCalled(); } ); diff --git a/editor/components/post-sticky/index.js b/editor/components/post-sticky/index.js index 8ad519b7065d2e..890305f56a589e 100644 --- a/editor/components/post-sticky/index.js +++ b/editor/components/post-sticky/index.js @@ -27,7 +27,6 @@ export function PostSticky( { onUpdateSticky, postSticky = false, instanceId } ) key="toggle" checked={ postSticky } onChange={ () => onUpdateSticky( ! postSticky ) } - showHint={ false } id={ stickyToggleId } /> </PostStickyCheck> diff --git a/editor/components/post-switch-to-draft-button/index.js b/editor/components/post-switch-to-draft-button/index.js index 275d0543706462..6570c8cd3dd039 100644 --- a/editor/components/post-switch-to-draft-button/index.js +++ b/editor/components/post-switch-to-draft-button/index.js @@ -18,7 +18,7 @@ import { isCurrentPostPublished, } from '../../store/selectors'; -function PostSwitchToDraftButton( { className, isSaving, isPublished, onClick } ) { +function PostSwitchToDraftButton( { isSaving, isPublished, onClick } ) { if ( ! isPublished ) { return null; } @@ -32,7 +32,7 @@ function PostSwitchToDraftButton( { className, isSaving, isPublished, onClick } return ( <Button - className={ className } + className="editor-post-switch-to-draft" isLarge onClick={ onSwitch } disabled={ isSaving } diff --git a/editor/components/post-taxonomies/flat-term-selector.js b/editor/components/post-taxonomies/flat-term-selector.js index 7b0e0d168211c4..3573925c7b5c7e 100644 --- a/editor/components/post-taxonomies/flat-term-selector.js +++ b/editor/components/post-taxonomies/flat-term-selector.js @@ -2,7 +2,8 @@ * External dependencies */ import { connect } from 'react-redux'; -import { get, unescape as unescapeString, find, throttle } from 'lodash'; +import { isEmpty, get, unescape as unescapeString, find, throttle, uniqBy, invoke } from 'lodash'; +import { stringify } from 'querystring'; /** * WordPress dependencies @@ -17,13 +18,17 @@ import { FormTokenField, withAPIData } from '@wordpress/components'; import { getEditedPostAttribute } from '../../store/selectors'; import { editPost } from '../../store/actions'; +/** + * Module constants + */ const DEFAULT_QUERY = { per_page: 100, orderby: 'count', order: 'desc', - _fields: [ 'id', 'name' ], + _fields: 'id,name', }; const MAX_TERMS_SUGGESTIONS = 20; +const isSameTermName = ( termA, termB ) => termA.toLowerCase() === termB.toLowerCase(); class FlatTermSelector extends Component { constructor() { @@ -39,9 +44,12 @@ class FlatTermSelector extends Component { } componentDidMount() { - if ( this.props.terms ) { + if ( ! isEmpty( this.props.terms ) ) { this.setState( { loading: false } ); - this.initRequest = this.fetchTerms( { include: this.props.terms } ); + this.initRequest = this.fetchTerms( { + include: this.props.terms.join( ',' ), + per_page: 100, + } ); this.initRequest.then( () => { this.setState( { loading: false } ); @@ -60,10 +68,8 @@ class FlatTermSelector extends Component { } componentWillUnmount() { - this.initRequest.abort(); - if ( this.searchRequest ) { - this.searchRequest.abort(); - } + invoke( this.initRequest, [ 'abort' ] ); + invoke( this.searchRequest, [ 'abort' ] ); } componentWillReceiveProps( newProps ) { @@ -74,8 +80,8 @@ class FlatTermSelector extends Component { fetchTerms( params = {} ) { const query = { ...DEFAULT_QUERY, ...params }; - const Collection = wp.api.getTaxonomyCollection( this.props.slug ); - const request = new Collection().fetch( { data: query } ); + const basePath = wp.api.getTaxonomyRoute( this.props.slug ); + const request = wp.apiRequest( { path: `/wp/v2/${ basePath }?${ stringify( query ) }` } ); request.then( ( terms ) => { this.setState( ( state ) => ( { availableTerms: state.availableTerms.concat( @@ -89,10 +95,14 @@ class FlatTermSelector extends Component { } updateSelectedTerms( terms = [] ) { - const selectedTerms = terms.map( ( termId ) => { + const selectedTerms = terms.reduce( ( result, termId ) => { const termObject = find( this.state.availableTerms, ( term ) => term.id === termId ); - return termObject ? termObject.name : ''; - } ); + if ( termObject ) { + result.push( termObject.name ); + } + + return result; + }, [] ); this.setState( { selectedTerms, } ); @@ -101,60 +111,61 @@ class FlatTermSelector extends Component { findOrCreateTerm( termName ) { return new Promise( ( resolve, reject ) => { // Tries to create a term or fetch it if it already exists - const Model = wp.api.getTaxonomyModel( this.props.slug ); - new Model( { name: termName } ).save() - .then( resolve, ( xhr ) => { - const errorCode = xhr.responseJSON && xhr.responseJSON.code; - if ( errorCode === 'term_exists' ) { - // search the new category created since last fetch - this.addRequest = new Model().fetch( - { data: { ...DEFAULT_QUERY, search: termName } } - ); - return this.addRequest.then( searchResult => { - resolve( find( searchResult, result => result.name === termName ) ); - }, reject ); - } - reject( xhr ); - } ); + const basePath = wp.api.getTaxonomyRoute( this.props.slug ); + wp.apiRequest( { + path: `/wp/v2/${ basePath }`, + method: 'POST', + data: { name: termName }, + } ).then( resolve, ( xhr ) => { + const errorCode = xhr.responseJSON && xhr.responseJSON.code; + if ( errorCode === 'term_exists' ) { + // search the new category created since last fetch + this.addRequest = wp.apiRequest( { + path: `/wp/v2/${ basePath }?${ stringify( { ...DEFAULT_QUERY, search: termName } ) }`, + } ); + return this.addRequest.then( searchResult => { + resolve( find( searchResult, result => isSameTermName( result.name, termName ) ) ); + }, reject ); + } + reject( xhr ); + } ); } ); } onChange( termNames ) { - this.setState( { selectedTerms: termNames } ); - const newTermNames = termNames.filter( ( termName ) => - ! find( this.state.availableTerms, ( term ) => term.name === termName ) + const uniqueTerms = uniqBy( termNames, ( term ) => term.toLowerCase() ); + this.setState( { selectedTerms: uniqueTerms } ); + const newTermNames = uniqueTerms.filter( ( termName ) => + ! find( this.state.availableTerms, ( term ) => isSameTermName( term.name, termName ) ) ); const termNamesToIds = ( names, availableTerms ) => { return names .map( ( termName ) => - find( availableTerms, ( term ) => term.name === termName ).id + find( availableTerms, ( term ) => isSameTermName( term.name, termName ) ).id ); }; if ( newTermNames.length === 0 ) { - return this.props.onUpdateTerms( termNamesToIds( termNames, this.state.availableTerms ), this.props.restBase ); + return this.props.onUpdateTerms( termNamesToIds( uniqueTerms, this.state.availableTerms ), this.props.restBase ); } Promise .all( newTermNames.map( this.findOrCreateTerm ) ) .then( ( newTerms ) => { const newAvailableTerms = this.state.availableTerms.concat( newTerms ); this.setState( { availableTerms: newAvailableTerms } ); - return this.props.onUpdateTerms( termNamesToIds( termNames, newAvailableTerms ), this.props.restBase ); + return this.props.onUpdateTerms( termNamesToIds( uniqueTerms, newAvailableTerms ), this.props.restBase ); } ); } searchTerms( search = '' ) { - if ( this.searchRequest ) { - this.searchRequest.abort(); - } + invoke( this.searchRequest, [ 'abort' ] ); this.searchRequest = this.fetchTerms( { search } ); } render() { - const { label, slug, taxonomy } = this.props; + const { slug, taxonomy } = this.props; const { loading, availableTerms, selectedTerms } = this.state; const termNames = availableTerms.map( ( term ) => term.name ); - const newTermPlaceholderLabel = get( taxonomy, [ 'data', 'labels', 'add_new_item' ], @@ -170,24 +181,21 @@ class FlatTermSelector extends Component { const removeTermLabel = sprintf( _x( 'Remove %s: %%s', 'term' ), singularName ); return ( - <div className="editor-post-taxonomies__flat-terms-selector"> - <h3 className="editor-post-taxonomies__flat-terms-selector-title">{ label }</h3> - <FormTokenField - value={ selectedTerms } - displayTransform={ unescapeString } - suggestions={ termNames } - onChange={ this.onChange } - onInputChange={ this.searchTerms } - maxSuggestions={ MAX_TERMS_SUGGESTIONS } - disabled={ loading } - placeholder={ newTermPlaceholderLabel } - messages={ { - added: termAddedLabel, - removed: termRemovedLabel, - remove: removeTermLabel, - } } - /> - </div> + <FormTokenField + value={ selectedTerms } + displayTransform={ unescapeString } + suggestions={ termNames } + onChange={ this.onChange } + onInputChange={ this.searchTerms } + maxSuggestions={ MAX_TERMS_SUGGESTIONS } + disabled={ loading } + placeholder={ newTermPlaceholderLabel } + messages={ { + added: termAddedLabel, + removed: termRemovedLabel, + remove: removeTermLabel, + } } + /> ); } } diff --git a/editor/components/post-taxonomies/hierarchical-term-selector.js b/editor/components/post-taxonomies/hierarchical-term-selector.js index 1bb9c0bc45890f..e8ab4d4144de47 100644 --- a/editor/components/post-taxonomies/hierarchical-term-selector.js +++ b/editor/components/post-taxonomies/hierarchical-term-selector.js @@ -2,7 +2,8 @@ * External dependencies */ import { connect } from 'react-redux'; -import { get, unescape as unescapeString, without, find, some } from 'lodash'; +import { get, unescape as unescapeString, without, find, some, invoke } from 'lodash'; +import { stringify } from 'querystring'; /** * WordPress dependencies @@ -18,11 +19,14 @@ import { buildTermsTree } from '@wordpress/utils'; import { getEditedPostAttribute } from '../../store/selectors'; import { editPost } from '../../store/actions'; +/** + * Module Constants + */ const DEFAULT_QUERY = { per_page: 100, orderby: 'count', order: 'desc', - _fields: [ 'id', 'name', 'parent' ], + _fields: 'id,name,parent', }; class HierarchicalTermSelector extends Component { @@ -73,7 +77,7 @@ class HierarchicalTermSelector extends Component { findTerm( terms, parent, name ) { return find( terms, term => { return ( ( ! term.parent && ! parent ) || parseInt( term.parent ) === parseInt( parent ) ) && - term.name === name; + term.name.toLowerCase() === name.toLowerCase(); } ); } @@ -104,19 +108,23 @@ class HierarchicalTermSelector extends Component { adding: true, } ); // Tries to create a term or fetch it if it already exists - const Model = wp.api.getTaxonomyModel( this.props.slug ); - this.addRequest = new Model( { - name: formName, - parent: formParent ? formParent : undefined, - } ).save(); + const basePath = wp.api.getTaxonomyRoute( this.props.slug ); + this.addRequest = wp.apiRequest( { + path: `/wp/v2/${ basePath }`, + method: 'POST', + data: { + name: formName, + parent: formParent ? formParent : undefined, + }, + } ); this.addRequest .then( resolve, ( xhr ) => { const errorCode = xhr.responseJSON && xhr.responseJSON.code; if ( errorCode === 'term_exists' ) { // search the new category created since last fetch - this.addRequest = new Model().fetch( - { data: { ...DEFAULT_QUERY, parent: formParent || 0, search: formName } } - ); + this.addRequest = wp.apiRequest( { + path: `/wp/v2/${ basePath }?${ stringify( { ...DEFAULT_QUERY, parent: formParent || 0, search: formName } ) }`, + } ); return this.addRequest.then( searchResult => { resolve( this.findTerm( searchResult, formParent, formName ) ); }, reject ); @@ -137,6 +145,7 @@ class HierarchicalTermSelector extends Component { ) ); this.props.speak( termAddedMessage, 'assertive' ); + this.addRequest = null; this.setState( { adding: false, formName: '', @@ -149,6 +158,7 @@ class HierarchicalTermSelector extends Component { if ( xhr.statusText === 'abort' ) { return; } + this.addRequest = null; this.setState( { adding: false, } ); @@ -156,36 +166,34 @@ class HierarchicalTermSelector extends Component { } componentDidMount() { - const Collection = wp.api.getTaxonomyCollection( this.props.slug ); - this.fetchRequest = new Collection() - .fetch( { data: DEFAULT_QUERY } ) - .done( ( terms ) => { + const basePath = wp.api.getTaxonomyRoute( this.props.slug ); + this.fetchRequest = wp.apiRequest( { path: `/wp/v2/${ basePath }?${ stringify( DEFAULT_QUERY ) }` } ); + this.fetchRequest.then( + ( terms ) => { // resolve const availableTermsTree = buildTermsTree( terms ); + this.fetchRequest = null; this.setState( { loading: false, availableTermsTree, availableTerms: terms, } ); - } ) - .fail( ( xhr ) => { + }, + ( xhr ) => { // reject if ( xhr.statusText === 'abort' ) { return; } + this.fetchRequest = null; this.setState( { loading: false, } ); - } ); + } + ); } componentWillUnmount() { - if ( this.fetchRequest ) { - this.fetchRequest.abort(); - } - - if ( this.addRequest ) { - this.addRequest.abort(); - } + invoke( this.fetchRequest, [ 'abort' ] ); + invoke( this.addRequest, [ 'abort' ] ); } renderTerms( renderedTerms ) { @@ -214,7 +222,7 @@ class HierarchicalTermSelector extends Component { } render() { - const { label, slug, taxonomy, instanceId } = this.props; + const { slug, taxonomy, instanceId } = this.props; const { availableTermsTree, availableTerms, formName, formParent, loading, showForm } = this.state; const labelWithFallback = ( labelProperty, fallbackIsCategory, fallbackIsNotCategory ) => get( taxonomy, @@ -241,54 +249,52 @@ class HierarchicalTermSelector extends Component { const inputId = `editor-post-taxonomies__hierarchical-terms-input-${ instanceId }`; /* eslint-disable jsx-a11y/no-onchange */ - return ( - <div className="editor-post-taxonomies__hierarchical-terms-selector"> - <h3 className="editor-post-taxonomies__hierarchical-terms-selector-title">{ label }</h3> - { this.renderTerms( availableTermsTree ) } - { ! loading && + return [ + ...this.renderTerms( availableTermsTree ), + ! loading && ( + <button + key="term-add-button" + onClick={ this.onToggleForm } + className="button-link editor-post-taxonomies__hierarchical-terms-add" + aria-expanded={ showForm } + > + { newTermButtonLabel } + </button> + ), + showForm && ( + <form onSubmit={ this.onAddTerm } key="hierarchical-terms-form"> + <label + htmlFor={ inputId } + className="editor-post-taxonomies__hierarchical-terms-label" + > + { newTermLabel } + </label> + <input + type="text" + id={ inputId } + className="editor-post-taxonomies__hierarchical-terms-input" + value={ formName } + onChange={ this.onChangeFormName } + required + /> + { !! availableTerms.length && + <TreeSelect + label={ parentSelectLabel } + noOptionLabel={ noParentOption } + onChange={ this.onChangeFormParent } + selectedId={ formParent } + tree={ availableTermsTree } + /> + } <button - onClick={ this.onToggleForm } - className="button-link editor-post-taxonomies__hierarchical-terms-add" - aria-expanded={ showForm } + type="submit" + className="button editor-post-taxonomies__hierarchical-terms-submit" > - { newTermButtonLabel } + { newTermSubmitLabel } </button> - } - { showForm && - <form onSubmit={ this.onAddTerm }> - <label - htmlFor={ inputId } - className="editor-post-taxonomies__hierarchical-terms-label" - > - { newTermLabel } - </label> - <input - type="text" - id={ inputId } - className="editor-post-taxonomies__hierarchical-terms-input" - value={ formName } - onChange={ this.onChangeFormName } - required - /> - { !! availableTerms.length && - <TreeSelect - label={ parentSelectLabel } - noOptionLabel={ noParentOption } - onChange={ this.onChangeFormParent } - selectedId={ formParent } - tree={ availableTermsTree } - /> - } - <button - type="submit" - className="button editor-post-taxonomies__hierarchical-terms-submit" - > - { newTermSubmitLabel } - </button> - </form> - } - </div> - ); + </form> + ), + ]; /* eslint-enable jsx-a11y/no-onchange */ } } diff --git a/editor/components/post-taxonomies/index.js b/editor/components/post-taxonomies/index.js index efe35d8963e79c..b21b17566c56f0 100644 --- a/editor/components/post-taxonomies/index.js +++ b/editor/components/post-taxonomies/index.js @@ -2,13 +2,13 @@ * External Dependencies */ import { connect } from 'react-redux'; -import { filter, includes } from 'lodash'; +import { filter, identity, includes } from 'lodash'; /** * WordPress dependencies */ import { withAPIData } from '@wordpress/components'; -import { compose } from '@wordpress/element'; +import { compose, Fragment } from '@wordpress/element'; /** * Internal dependencies @@ -18,24 +18,25 @@ import HierarchicalTermSelector from './hierarchical-term-selector'; import FlatTermSelector from './flat-term-selector'; import { getCurrentPostType } from '../../store/selectors'; -export function PostTaxonomies( { postType, taxonomies } ) { +export function PostTaxonomies( { postType, taxonomies, taxonomyWrapper = identity } ) { const availableTaxonomies = filter( taxonomies.data, ( taxonomy ) => includes( taxonomy.types, postType ) ); - - return ( - <div> - { availableTaxonomies.map( ( taxonomy ) => { - const TaxonomyComponent = taxonomy.hierarchical ? HierarchicalTermSelector : FlatTermSelector; - return ( - <TaxonomyComponent - key={ taxonomy.slug } - label={ taxonomy.name } - restBase={ taxonomy.rest_base } - slug={ taxonomy.slug } - /> - ); - } ) } - </div> - ); + const visibleTaxonomies = filter( availableTaxonomies, ( taxonomy ) => taxonomy.visibility.show_ui ); + return visibleTaxonomies.map( ( taxonomy ) => { + const TaxonomyComponent = taxonomy.hierarchical ? HierarchicalTermSelector : FlatTermSelector; + return ( + <Fragment key={ `taxonomy-${ taxonomy.slug }` }> + { + taxonomyWrapper( + <TaxonomyComponent + restBase={ taxonomy.rest_base } + slug={ taxonomy.slug } + />, + taxonomy + ) + } + </Fragment> + ); + } ); } const applyConnect = connect( diff --git a/editor/components/post-taxonomies/style.scss b/editor/components/post-taxonomies/style.scss index e56d8078db1462..bec8bfcd7fab0d 100644 --- a/editor/components/post-taxonomies/style.scss +++ b/editor/components/post-taxonomies/style.scss @@ -1,14 +1,3 @@ -.editor-post-taxonomies__flat-terms-selector, -.editor-post-taxonomies__hierarchical-terms-selector { - margin-top: $panel-padding; -} - -.editor-sidebar .editor-post-taxonomies__flat-terms-selector-title, -.editor-sidebar .editor-post-taxonomies__hierarchical-terms-selector-title { - display: block; - margin-bottom: 10px; -} - .editor-post-taxonomies__hierarchical-terms-choice { margin-bottom: 5px; } diff --git a/editor/components/post-taxonomies/test/index.js b/editor/components/post-taxonomies/test/index.js index cd0dd2ac12df6f..392058aec2bbef 100644 --- a/editor/components/post-taxonomies/test/index.js +++ b/editor/components/post-taxonomies/test/index.js @@ -16,38 +16,94 @@ describe( 'PostTaxonomies', () => { <PostTaxonomies postType="page" taxonomies={ taxonomies } /> ); - expect( wrapper.children() ).toHaveLength( 0 ); + expect( wrapper.at( 0 ) ).toHaveLength( 0 ); } ); it( 'should render taxonomy components for taxonomies assigned to post type', () => { - const taxonomies = { - data: [ - { - name: 'Categories', - slug: 'category', - types: [ 'post', 'page' ], - hierarchical: true, - rest_base: 'categories', - }, - { - name: 'Genres', - slug: 'genre', - types: [ 'book' ], - hierarchical: true, - rest_base: 'genres', - }, - ], + const genresTaxonomy = { + name: 'Genres', + slug: 'genre', + types: [ 'book' ], + hierarchical: true, + rest_base: 'genres', + visibility: { + show_ui: true, + }, }; - const wrapper = shallow( - <PostTaxonomies postType="page" taxonomies={ taxonomies } /> + const categoriesTaxonomy = { + name: 'Categories', + slug: 'category', + types: [ 'post', 'page' ], + hierarchical: true, + rest_base: 'categories', + visibility: { + show_ui: true, + }, + }; + + const wrapperOne = shallow( + <PostTaxonomies postType="book" + taxonomies={ { + data: [ genresTaxonomy, categoriesTaxonomy ], + } } + /> ); - expect( wrapper.children() ).toHaveLength( 1 ); - expect( wrapper.childAt( 0 ).props() ).toEqual( { - label: 'Categories', - slug: 'category', - restBase: 'categories', - } ); + expect( wrapperOne.at( 0 ) ).toHaveLength( 1 ); + + const wrapperTwo = shallow( + <PostTaxonomies postType="book" + taxonomies={ { + data: [ + genresTaxonomy, + { + ...categoriesTaxonomy, + types: [ 'post', 'page', 'book' ], + }, + ], + } } + /> + ); + + expect( wrapperTwo.at( 0 ) ).toHaveLength( 2 ); + } ); + + it( 'should not render taxonomy components that hide their ui', () => { + const genresTaxonomy = { + name: 'Genres', + slug: 'genre', + types: [ 'book' ], + hierarchical: true, + rest_base: 'genres', + visibility: { + show_ui: true, + }, + }; + + const wrapperOne = shallow( + <PostTaxonomies postType="book" + taxonomies={ { + data: [ genresTaxonomy ], + } } + /> + ); + + expect( wrapperOne.at( 0 ) ).toHaveLength( 1 ); + + const wrapperTwo = shallow( + <PostTaxonomies postType="book" + taxonomies={ { + data: [ + { + ...genresTaxonomy, + visibility: { show_ui: false }, + }, + ], + } } + /> + ); + + expect( wrapperTwo.at( 0 ) ).toHaveLength( 0 ); } ); } ); diff --git a/editor/components/post-text-editor/index.js b/editor/components/post-text-editor/index.js index 6f7062a62579c2..76b191dbaac13e 100644 --- a/editor/components/post-text-editor/index.js +++ b/editor/components/post-text-editor/index.js @@ -1,8 +1,8 @@ /** * External dependencies */ -import { connect } from 'react-redux'; import Textarea from 'react-autosize-textarea'; +import { connect } from 'react-redux'; /** * WordPress dependencies @@ -15,44 +15,58 @@ import { parse } from '@wordpress/blocks'; */ import './style.scss'; import { getEditedPostContent } from '../../store/selectors'; -import { editPost, resetBlocks } from '../../store/actions'; +import { editPost, resetBlocks, checkTemplateValidity } from '../../store/actions'; class PostTextEditor extends Component { - constructor( props ) { + constructor() { super( ...arguments ); - this.onChange = this.onChange.bind( this ); - this.onPersist = this.onPersist.bind( this ); + this.startEditing = this.startEditing.bind( this ); + this.edit = this.edit.bind( this ); + this.stopEditing = this.stopEditing.bind( this ); this.state = { - initialValue: props.value, + value: null, + isDirty: false, }; } - onChange( event ) { - this.props.onChange( event.target.value ); + componentWillReceiveProps( nextProps ) { + // If we receive a new value while we're editing (but before we've made + // changes), go ahead and clobber the local state + if ( this.props.value !== nextProps.value && this.state.value && ! this.state.isDirty ) { + this.setState( { value: nextProps.value } ); + } + } + + startEditing() { + // Copying the post content into local state ensures that edits won't be + // clobbered by changes to global editor state + this.setState( { value: this.props.value } ); } - onPersist( event ) { - const { value } = event.target; - if ( value !== this.state.initialValue ) { - this.props.onPersist( value ); + edit( event ) { + const value = event.target.value; + this.props.onChange( value ); + this.setState( { value, isDirty: true } ); + } - this.setState( { - initialValue: value, - } ); + stopEditing() { + if ( this.state.isDirty ) { + this.props.onPersist( this.state.value ); } + + this.setState( { value: null, isDirty: false } ); } render() { - const { value } = this.props; - return ( <Textarea autoComplete="off" - value={ value } - onChange={ this.onChange } - onBlur={ this.onPersist } + value={ this.state.value || this.props.value } + onFocus={ this.startEditing } + onChange={ this.edit } + onBlur={ this.stopEditing } className="editor-post-text-editor" /> ); @@ -68,7 +82,10 @@ export default connect( return editPost( { content } ); }, onPersist( content ) { - return resetBlocks( parse( content ) ); + return [ + resetBlocks( parse( content ) ), + checkTemplateValidity(), + ]; }, } )( PostTextEditor ); diff --git a/editor/components/post-text-editor/style.scss b/editor/components/post-text-editor/style.scss index bf71e2e6b8fd4d..cf5c73585fdd2e 100644 --- a/editor/components/post-text-editor/style.scss +++ b/editor/components/post-text-editor/style.scss @@ -45,7 +45,7 @@ } .editor-post-text-editor__bold { - font-weight: bold; + font-weight: 600; } .editor-post-text-editor__italic { diff --git a/editor/components/post-title/index.js b/editor/components/post-title/index.js index aad6417c3f2158..060320dc293a9e 100644 --- a/editor/components/post-title/index.js +++ b/editor/components/post-title/index.js @@ -1,7 +1,6 @@ /** * External dependencies */ -import { connect } from 'react-redux'; import Textarea from 'react-autosize-textarea'; import classnames from 'classnames'; @@ -10,17 +9,17 @@ import classnames from 'classnames'; */ import { __ } from '@wordpress/i18n'; import { Component, compose } from '@wordpress/element'; -import { keycodes } from '@wordpress/utils'; -import { createBlock, getDefaultBlockName } from '@wordpress/blocks'; -import { withContext } from '@wordpress/components'; +import { keycodes, decodeEntities } from '@wordpress/utils'; +import { withSelect, withDispatch } from '@wordpress/data'; +import { KeyboardShortcuts, withInstanceId, withFocusOutside } from '@wordpress/components'; +import { withEditorSettings } from '@wordpress/blocks'; /** * Internal dependencies */ import './style.scss'; import PostPermalink from '../post-permalink'; -import { getEditedPostAttribute } from '../../store/selectors'; -import { insertBlock, editPost, clearSelectedBlock } from '../../store/actions'; +import PostTypeSupportCheck from '../post-type-support-check'; /** * Constants @@ -32,47 +31,19 @@ class PostTitle extends Component { constructor() { super( ...arguments ); - this.bindContainer = this.bindNode.bind( this, 'container' ); - this.bindTextarea = this.bindNode.bind( this, 'textarea' ); this.onChange = this.onChange.bind( this ); this.onSelect = this.onSelect.bind( this ); this.onUnselect = this.onUnselect.bind( this ); - this.onSelectionChange = this.onSelectionChange.bind( this ); this.onKeyDown = this.onKeyDown.bind( this ); - this.blurIfOutside = this.blurIfOutside.bind( this ); - - this.nodes = {}; + this.redirectHistory = this.redirectHistory.bind( this ); this.state = { isSelected: false, }; } - componentDidMount() { - document.addEventListener( 'selectionchange', this.onSelectionChange ); - } - - componentWillUnmount() { - document.removeEventListener( 'selectionchange', this.onSelectionChange ); - } - - bindNode( name, node ) { - this.nodes[ name ] = node; - } - - onSelectionChange() { - const textarea = this.nodes.textarea.textarea; - if ( - document.activeElement === textarea && - textarea.selectionStart !== textarea.selectionEnd - ) { - this.onSelect(); - } - } - - onChange( event ) { - const newTitle = event.target.value.replace( REGEXP_NEWLINES, ' ' ); - this.props.onUpdate( newTitle ); + handleFocusOutside() { + this.onUnselect(); } onSelect() { @@ -84,10 +55,9 @@ class PostTitle extends Component { this.setState( { isSelected: false } ); } - blurIfOutside( event ) { - if ( ! this.nodes.container.contains( event.relatedTarget ) ) { - this.onUnselect(); - } + onChange( event ) { + const newTitle = event.target.value.replace( REGEXP_NEWLINES, ' ' ); + this.props.onUpdate( newTitle ); } onKeyDown( event ) { @@ -97,59 +67,102 @@ class PostTitle extends Component { } } + /** + * Emulates behavior of an undo or redo on its corresponding key press + * combination. This is a workaround to React's treatment of undo in a + * controlled textarea where characters are updated one at a time. + * Instead, leverage the store's undo handling of title changes. + * + * @see https://github.com/facebook/react/issues/8514 + * + * @param {KeyboardEvent} event Key event. + */ + redirectHistory( event ) { + if ( event.shiftKey ) { + this.props.onRedo(); + } else { + this.props.onUndo(); + } + + event.preventDefault(); + } + render() { - const { title, placeholder } = this.props; + const { title, placeholder, instanceId } = this.props; const { isSelected } = this.state; const className = classnames( 'editor-post-title', { 'is-selected': isSelected } ); + const decodedPlaceholder = decodeEntities( placeholder ); return ( - <div - ref={ this.bindContainer } - onFocus={ this.onSelect } - onBlur={ this.blurIfOutside } - className={ className } - tabIndex={ -1 /* Necessary for Firefox to include relatedTarget in blur event */ } - > - { isSelected && <PostPermalink /> } - <div> - <Textarea - ref={ this.bindTextarea } - className="editor-post-title__input" - value={ title } - onChange={ this.onChange } - placeholder={ placeholder || __( 'Add title' ) } - onClick={ this.onSelect } - onKeyDown={ this.onKeyDown } - onKeyPress={ this.onUnselect } - /> + <PostTypeSupportCheck supportKeys="title"> + <div className={ className }> + { isSelected && <PostPermalink /> } + <KeyboardShortcuts + shortcuts={ { + 'mod+z': this.redirectHistory, + 'mod+shift+z': this.redirectHistory, + } } + > + <label htmlFor={ `post-title-${ instanceId }` } className="screen-reader-text"> + { decodedPlaceholder || __( 'Add title' ) } + </label> + <Textarea + id={ `post-title-${ instanceId }` } + className="editor-post-title__input" + value={ title } + onChange={ this.onChange } + placeholder={ decodedPlaceholder || __( 'Add title' ) } + onFocus={ this.onSelect } + onKeyDown={ this.onKeyDown } + onKeyPress={ this.onUnselect } + /> + </KeyboardShortcuts> </div> - </div> + </PostTypeSupportCheck> ); } } -const applyConnect = connect( - ( state ) => ( { - title: getEditedPostAttribute( state, 'title' ), - } ), - { +const applyWithSelect = withSelect( ( select ) => { + const { getEditedPostAttribute } = select( 'core/editor' ); + + return { + title: getEditedPostAttribute( 'title' ), + }; +} ); + +const applyWithDispatch = withDispatch( ( dispatch ) => { + const { + insertDefaultBlock, + editPost, + clearSelectedBlock, + undo, + redo, + } = dispatch( 'core/editor' ); + + return { onEnterPress() { - return insertBlock( createBlock( getDefaultBlockName() ), 0 ); + insertDefaultBlock( undefined, undefined, 0 ); }, onUpdate( title ) { - return editPost( { title } ); + editPost( { title } ); }, + onUndo: undo, + onRedo: redo, clearSelectedBlock, - } -); + }; +} ); -const applyEditorSettings = withContext( 'editor' )( +const applyEditorSettings = withEditorSettings( ( settings ) => ( { placeholder: settings.titlePlaceholder, } ) ); export default compose( - applyConnect, - applyEditorSettings + applyWithSelect, + applyWithDispatch, + applyEditorSettings, + withInstanceId, + withFocusOutside )( PostTitle ); diff --git a/editor/components/post-title/style.scss b/editor/components/post-title/style.scss index edbfa15ff20b0c..8b589eb4d84764 100644 --- a/editor/components/post-title/style.scss +++ b/editor/components/post-title/style.scss @@ -2,44 +2,24 @@ position: relative; padding: 5px 0; - div { + .editor-post-title__input { + display: block; + width: 100%; + padding: #{ $block-padding + 5px } $block-padding; + margin: 0; + box-shadow: none; border: 1px solid transparent; - font-size: $editor-font-size; - transition: 0.2s outline; - margin-top: 0; - margin-bottom: 0; - padding: $block-padding; - } + font-family: $editor-font; + line-height: $default-line-height; - &:hover div { - border: 1px solid $light-gray-500; - transition: 0.2s outline; + // Match h1 heading + font-size: 2.441em; + font-weight: 600; } - &.is-selected div { + &.is-selected .editor-post-title__input, + .editor-post-title__input:hover { border: 1px solid $light-gray-500; - transition: 0.2s outline; - } -} - -.editor-post-title textarea.editor-post-title__input { - display: block; - font-size: 2em; - font-family: $editor-font; - line-height: $default-line-height; - outline: none; - border: none; - box-shadow: none; - width: 100%; - padding: 5px 0; - margin: 0; - - // inherited from h1 - font-weight: 600; - - &:focus { - outline: none; - box-shadow: none; } } diff --git a/editor/components/post-type-support-check/index.js b/editor/components/post-type-support-check/index.js index c2be5f2b04b57f..17ac99e0ed9117 100644 --- a/editor/components/post-type-support-check/index.js +++ b/editor/components/post-type-support-check/index.js @@ -1,23 +1,16 @@ /** * External dependencies */ -import { connect } from 'react-redux'; import { get, some, castArray } from 'lodash'; /** * WordPress dependencies */ -import { withAPIData } from '@wordpress/components'; -import { compose } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import { getCurrentPostType } from '../../store/selectors'; +import { withSelect } from '@wordpress/data'; function PostTypeSupportCheck( { postType, children, supportKeys } ) { const isSupported = some( - castArray( supportKeys ), key => get( postType, [ 'data', 'supports', key ], false ) + castArray( supportKeys ), key => get( postType, [ 'supports', key ], false ) ); if ( ! isSupported ) { @@ -27,21 +20,10 @@ function PostTypeSupportCheck( { postType, children, supportKeys } ) { return children; } -const applyConnect = connect( - ( state ) => { - return { - postTypeName: getCurrentPostType( state ), - }; - } -); - -const applyWithAPIData = withAPIData( ( { postTypeName } ) => { +export default withSelect( ( select ) => { + const { getEditedPostAttribute } = select( 'core/editor' ); + const { getPostType } = select( 'core' ); return { - postType: postTypeName ? `/wp/v2/types/${ postTypeName }?context=edit` : undefined, + postType: getPostType( getEditedPostAttribute( 'type' ) ), }; -} ); - -export default compose( - applyConnect, - applyWithAPIData, -)( PostTypeSupportCheck ); +} )( PostTypeSupportCheck ); diff --git a/editor/components/provider/index.js b/editor/components/provider/index.js index 1258e274e21d1d..1f4fca57cccf6d 100644 --- a/editor/components/provider/index.js +++ b/editor/components/provider/index.js @@ -3,13 +3,13 @@ */ import { bindActionCreators } from 'redux'; import { Provider as ReduxProvider } from 'react-redux'; -import { flow, pick, noop } from 'lodash'; +import { flow, pick } from 'lodash'; /** * WordPress Dependencies */ import { createElement, Component } from '@wordpress/element'; -import { RichTextProvider } from '@wordpress/blocks'; +import { RichTextProvider, EditorSettings } from '@wordpress/blocks'; import { APIProvider, DropZoneProvider, @@ -22,73 +22,37 @@ import { import { setupEditor, undo, redo, createUndoLevel } from '../../store/actions'; import store from '../../store'; -/** - * The default editor settings - * You can override any default settings when calling initializeEditor - * - * alignWide boolean Enable/Disable Wide/Full Alignments - * - * @var {Object} DEFAULT_SETTINGS - */ -const DEFAULT_SETTINGS = { - alignWide: false, - colors: [ - '#f78da7', - '#cf2e2e', - '#ff6900', - '#fcb900', - '#7bdcb5', - '#00d084', - '#8ed1fc', - '#0693e3', - '#eee', - '#abb8c3', - '#313131', - ], - - // This is current max width of the block inner area - // It's used to constraint image resizing and this value could be overriden later by themes - maxWidth: 608, - - // Allowed block types for the editor, defaulting to true (all supported). - blockTypes: true, -}; - class EditorProvider extends Component { constructor( props ) { super( ...arguments ); this.store = store; - this.settings = { - ...DEFAULT_SETTINGS, - ...props.settings, - }; - // Assume that we don't need to initialize in the case of an error recovery. if ( ! props.recovery ) { - this.store.dispatch( setupEditor( props.post, this.settings ) ); - } - } - - getChildContext() { - return { - editor: this.settings, - }; - } - - componentWillReceiveProps( nextProps ) { - if ( - nextProps.settings !== this.props.settings - ) { - // eslint-disable-next-line no-console - console.error( 'The Editor Provider Props are immutable.' ); + this.store.dispatch( + setupEditor( props.post, { + ...EditorSettings.defaultSettings, + ...this.props.settings, + } ) + ); } } render() { - const { children } = this.props; + const { children, settings } = this.props; const providers = [ + // Editor settings provider + [ + EditorSettings.Provider, + { + value: { + ...EditorSettings.defaultSettings, + ...settings, + }, + }, + ], + // Redux provider: // // - context.store @@ -152,8 +116,4 @@ class EditorProvider extends Component { } } -EditorProvider.childContextTypes = { - editor: noop, -}; - export default EditorProvider; diff --git a/editor/components/table-of-contents/panel.js b/editor/components/table-of-contents/panel.js index 387622e712963b..e9f2cc188fd4e7 100644 --- a/editor/components/table-of-contents/panel.js +++ b/editor/components/table-of-contents/panel.js @@ -1,8 +1,3 @@ -/** - * External dependencies - */ -import { countBy } from 'lodash'; - /** * WordPress dependencies */ @@ -16,12 +11,15 @@ import { withSelect } from '@wordpress/data'; import WordCount from '../word-count'; import DocumentOutline from '../document-outline'; -function TableOfContentsPanel( { blocks } ) { - const blockCount = countBy( blocks, 'name' ); - +function TableOfContentsPanel( { headingCount, paragraphCount, numberOfBlocks } ) { return ( <Fragment> - <div className="table-of-contents__counts"> + <div + className="table-of-contents__counts" + role="note" + aria-label={ __( 'Document Statistics' ) } + tabIndex="0" + > <div className="table-of-contents__count"> { __( 'Words' ) } <WordCount /> @@ -29,23 +27,23 @@ function TableOfContentsPanel( { blocks } ) { <div className="table-of-contents__count"> { __( 'Headings' ) } <span className="table-of-contents__number"> - { blockCount[ 'core/heading' ] || 0 } + { headingCount } </span> </div> <div className="table-of-contents__count"> { __( 'Paragraphs' ) } <span className="table-of-contents__number"> - { blockCount[ 'core/paragraph' ] || 0 } + { paragraphCount } </span> </div> <div className="table-of-contents__count"> { __( 'Blocks' ) } <span className="table-of-contents__number"> - { blocks.length } + { numberOfBlocks } </span> </div> </div> - { blockCount[ 'core/heading' ] > 0 && ( + { headingCount > 0 && ( <Fragment> <hr /> <span className="table-of-contents__title"> @@ -59,7 +57,10 @@ function TableOfContentsPanel( { blocks } ) { } export default withSelect( ( select ) => { + const { getGlobalBlockCount } = select( 'core/editor' ); return { - blocks: select( 'core/editor' ).getBlocks(), + headingCount: getGlobalBlockCount( 'core/heading' ), + paragraphCount: getGlobalBlockCount( 'core/paragraph' ), + numberOfBlocks: getGlobalBlockCount(), }; } )( TableOfContentsPanel ); diff --git a/editor/components/warning/index.js b/editor/components/warning/index.js index 2b1c94c5ce8b35..8fd4cc397e9159 100644 --- a/editor/components/warning/index.js +++ b/editor/components/warning/index.js @@ -1,12 +1,26 @@ +/** + * WordPress dependencies + */ +import { Children } from '@wordpress/element'; + /** * Internal dependencies */ import './style.scss'; -function Warning( { children } ) { +function Warning( { actions, children } ) { return ( <div className="editor-warning"> { children } + { Children.count( actions ) > 0 && ( + <div className="editor-warning__actions"> + { Children.map( actions, ( action, i ) => ( + <span key={ i } className="editor-warning__action"> + { action } + </span> + ) ) } + </div> + ) } </div> ); } diff --git a/editor/components/warning/style.scss b/editor/components/warning/style.scss index 909c855f6aef1e..4adcbd1bd2b315 100644 --- a/editor/components/warning/style.scss +++ b/editor/components/warning/style.scss @@ -1,33 +1,30 @@ .editor-warning { z-index: z-index( '.editor-warning' ); position: absolute; - top: 50%; + top: 15px; left: 50%; - transform: translate( -50%, -50% ); + transform: translateX( -50% ); display: flex; flex-direction: column; justify-content: space-around; align-items: center; width: 96%; max-width: 780px; - padding: 20px 20px 0px 20px; + padding: 20px; background-color: $white; border: 1px solid $light-gray-500; text-align: center; line-height: $default-line-height; box-shadow: $shadow-popover; + font-family: $default-font; + font-size: $default-font-size; +} - p:first-child { - margin-top: 0; - } - - .edit-post-visual-editor & p { - width: 100%; - font-family: $default-font; - font-size: $default-font-size; - } +.editor-warning__actions { + display: flex; + margin-top: 20px; +} - .components-button { - margin: 0 #{ $item-spacing / 2 } 5px; - } +.editor-warning__action { + margin: 0 #{ $item-spacing / 2 } 5px; } diff --git a/editor/components/warning/test/index.js b/editor/components/warning/test/index.js index b0e76e426fcd6b..805b649a018b45 100644 --- a/editor/components/warning/test/index.js +++ b/editor/components/warning/test/index.js @@ -11,14 +11,25 @@ import Warning from '../index'; describe( 'Warning', () => { it( 'should match snapshot', () => { const wrapper = shallow( <Warning>error</Warning> ); + expect( wrapper ).toMatchSnapshot(); } ); + it( 'should has valid class', () => { const wrapper = shallow( <Warning /> ); + expect( wrapper.hasClass( 'editor-warning' ) ).toBe( true ); + expect( wrapper.find( '.editor-warning__actions' ) ).toHaveLength( 0 ); } ); + it( 'should show child error message element', () => { - const wrapper = shallow( <Warning><p>message</p></Warning> ); - expect( wrapper.find( 'p' ).text() ).toBe( 'message' ); + const wrapper = shallow( <Warning actions={ <button /> }>Message</Warning> ); + + const actions = wrapper.find( '.editor-warning__actions' ); + const action = actions.childAt( 0 ); + + expect( actions ).toHaveLength( 1 ); + expect( action.hasClass( 'editor-warning__action' ) ).toBe( true ); + expect( action.childAt( 0 ).type() ).toBe( 'button' ); } ); } ); diff --git a/editor/components/writing-flow/index.js b/editor/components/writing-flow/index.js index 99dc1600c31bd7..2a3950daea3cb4 100644 --- a/editor/components/writing-flow/index.js +++ b/editor/components/writing-flow/index.js @@ -2,8 +2,7 @@ * External dependencies */ import { connect } from 'react-redux'; -import 'element-closest'; -import { find, last, reverse, get } from 'lodash'; +import { overEvery, find, findLast, reverse, get } from 'lodash'; /** * WordPress dependencies @@ -12,6 +11,7 @@ import { Component } from '@wordpress/element'; import { keycodes, focus, + isTextField, computeCaretRect, isHorizontalEdge, isVerticalEdge, @@ -22,7 +22,7 @@ import { /** * Internal dependencies */ -import { BlockListBlock } from '../block-list/block'; +import './style.scss'; import { getPreviousBlockUid, getNextBlockUid, @@ -32,18 +32,31 @@ import { } from '../../store/selectors'; import { multiSelect, - appendDefaultBlock, selectBlock, } from '../../store/actions'; +import { + isBlockFocusStop, + isInSameBlock, +} from '../../utils/dom'; /** * Module Constants */ + const { UP, DOWN, LEFT, RIGHT } = keycodes; -function isElementNonEmpty( el ) { - return !! el.innerText.trim(); -} +/** + * Given an element, returns true if the element is a tabbable text field, or + * false otherwise. + * + * @param {Element} element Element to test. + * + * @return {boolean} Whether element is a tabbable text field. + */ +const isTabbableTextField = overEvery( [ + isTextField, + focus.tabbable.isTabbableIndex, +] ); class WritingFlow extends Component { constructor() { @@ -52,6 +65,15 @@ class WritingFlow extends Component { this.onKeyDown = this.onKeyDown.bind( this ); this.bindContainer = this.bindContainer.bind( this ); this.clearVerticalRect = this.clearVerticalRect.bind( this ); + this.focusLastTextField = this.focusLastTextField.bind( this ); + + /** + * Here a rectangle is stored while moving the caret vertically so + * vertical position of the start position can be restored. + * This is to recreate browser behaviour across blocks. + * + * @type {?DOMRect} + */ this.verticalRect = null; } @@ -63,81 +85,67 @@ class WritingFlow extends Component { this.verticalRect = null; } - getEditables( target ) { - const outer = target.closest( '.editor-block-list__block-edit' ); - if ( ! outer || target === outer ) { - return [ target ]; - } - - const elements = outer.querySelectorAll( '[contenteditable="true"]' ); - return [ ...elements ]; - } - - getVisibleTabbables() { - return focus.tabbable - .find( this.container ) - .filter( ( node ) => ( - node.nodeName === 'INPUT' || - node.nodeName === 'TEXTAREA' || - node.contentEditable === 'true' || - node.classList.contains( 'editor-block-list__block-edit' ) - ) ); - } - + /** + * Returns the optimal tab target from the given focused element in the + * desired direction. A preference is made toward text fields, falling back + * to the block focus stop if no other candidates exist for the block. + * + * @param {Element} target Currently focused text field. + * @param {boolean} isReverse True if considering as the first field. + * + * @return {?Element} Optimal tab target, if one exists. + */ getClosestTabbable( target, isReverse ) { - let focusableNodes = this.getVisibleTabbables(); + // Since the current focus target is not guaranteed to be a text field, + // find all focusables. Tabbability is considered later. + let focusableNodes = focus.focusable.find( this.container ); if ( isReverse ) { focusableNodes = reverse( focusableNodes ); } - focusableNodes = focusableNodes.slice( focusableNodes.indexOf( target ) ); + // Consider as candidates those focusables after the current target. + // It's assumed this can only be reached if the target is focusable + // (on its keydown event), so no need to verify it exists in the set. + focusableNodes = focusableNodes.slice( focusableNodes.indexOf( target ) + 1 ); - return find( focusableNodes, ( node, i, array ) => { - if ( node.contains( target ) ) { + function isTabCandidate( node, i, array ) { + // Not a candidate if the node is not tabbable. + if ( ! focus.tabbable.isTabbableIndex( node ) ) { return false; } - const nextNode = array[ i + 1 ]; - - // Skip node if it contains a focusable node. - if ( nextNode && node.contains( nextNode ) ) { + // Prefer text fields, but settle for block focus stop. + if ( ! isTextField( node ) && ! isBlockFocusStop( node ) ) { return false; } - return true; - } ); - } - - isInLastNonEmptyBlock( target ) { - const tabbables = this.getVisibleTabbables(); - - // Find last tabbable, compare with target - const lastTabbable = last( tabbables ); - if ( ! lastTabbable || ! lastTabbable.contains( target ) ) { - return false; - } - - // Find block-level ancestor of said last tabbable - const blockEl = lastTabbable.closest( '.' + BlockListBlock.className ); - const blockIndex = tabbables.indexOf( blockEl ); + // If navigating out of a block (in reverse), don't consider its + // block focus stop. + if ( node.contains( target ) ) { + return false; + } - // Unexpected, so we'll leave quietly. - if ( blockIndex === -1 ) { - return false; - } + // In case of block focus stop, check to see if there's a better + // text field candidate within. + for ( let offset = 1, nextNode; ( nextNode = array[ i + offset ] ); offset++ ) { + // Abort if no longer testing descendents of focus stop. + if ( ! node.contains( nextNode ) ) { + break; + } + + // Apply same tests by recursion. This is important to consider + // nestable blocks where we don't want to settle for the inner + // block focus stop. + if ( isTabCandidate( nextNode, i + offset, array ) ) { + return false; + } + } - // Maybe there are no descendants, and the target is the block itself? - if ( lastTabbable === blockEl ) { - return isElementNonEmpty( blockEl ); + return true; } - // Otherwise, find the descendants of the ancestor, i.e. the target and - // its siblings, and check them instead. - return tabbables - .slice( blockIndex + 1 ) - .some( ( el ) => - blockEl.contains( el ) && isElementNonEmpty( el ) ); + return find( focusableNodes, isTabCandidate ); } expandSelection( currentStartUid, isReverse ) { @@ -158,30 +166,20 @@ class WritingFlow extends Component { } } - isEditableEdge( moveUp, target ) { - const editables = this.getEditables( target ); - const index = editables.indexOf( target ); - const edgeIndex = moveUp ? 0 : editables.length - 1; - return editables.length > 0 && index === edgeIndex; - } - /** - * Function called to ensure the block parent of the target node is selected. + * Returns true if the given target field is the last in its block which + * can be considered for tab transition. For example, in a block with two + * text fields, this would return true when reversing from the first of the + * two fields, but false when reversing from the second. + * + * @param {Element} target Currently focused text field. + * @param {boolean} isReverse True if considering as the first field. * - * @param {DOMElement} target + * @return {boolean} Whether field is at edge for tab transition. */ - selectParentBlock( target ) { - if ( ! target ) { - return; - } - - const parentBlock = target.hasAttribute( 'data-block' ) ? target : target.closest( '[data-block]' ); - if ( - parentBlock && - ( ! this.props.selectedBlockUID || parentBlock.getAttribute( 'data-block' ) !== this.props.selectedBlockUID ) - ) { - this.props.onSelectBlock( parentBlock.getAttribute( 'data-block' ) ); - } + isTabbableEdge( target, isReverse ) { + const closestTabbable = this.getClosestTabbable( target, isReverse ); + return ! isInSameBlock( target, closestTabbable ); } onKeyDown( event ) { @@ -210,7 +208,7 @@ class WritingFlow extends Component { // Shift key is down and existing block multi-selection event.preventDefault(); this.expandSelection( selectionStart, isReverse ); - } else if ( isNav && isShift && this.isEditableEdge( isReverse, target ) && isNavEdge( target, isReverse, true ) ) { + } else if ( isNav && isShift && this.isTabbableEdge( target, isReverse ) && isNavEdge( target, isReverse, true ) ) { // Shift key is down, but no existing block multi-selection event.preventDefault(); this.expandSelection( selectedBlockUID, isReverse ); @@ -220,21 +218,25 @@ class WritingFlow extends Component { this.moveSelection( isReverse ); } else if ( isVertical && isVerticalEdge( target, isReverse, isShift ) ) { const closestTabbable = this.getClosestTabbable( target, isReverse ); - placeCaretAtVerticalEdge( closestTabbable, isReverse, this.verticalRect ); - this.selectParentBlock( closestTabbable ); - event.preventDefault(); + if ( closestTabbable ) { + placeCaretAtVerticalEdge( closestTabbable, isReverse, this.verticalRect ); + event.preventDefault(); + } } else if ( isHorizontal && isHorizontalEdge( target, isReverse, isShift ) ) { const closestTabbable = this.getClosestTabbable( target, isReverse ); placeCaretAtHorizontalEdge( closestTabbable, isReverse ); - this.selectParentBlock( closestTabbable ); event.preventDefault(); } + } - if ( isDown && ! isShift && ! hasMultiSelection && - this.isInLastNonEmptyBlock( target ) && - isVerticalEdge( target, false, false ) - ) { - this.props.onBottomReached(); + /** + * Sets focus to the end of the last tabbable text field, if one exists. + */ + focusLastTextField() { + const focusableNodes = focus.focusable.find( this.container ); + const target = findLast( focusableNodes, isTabbableTextField ); + if ( target ) { + placeCaretAtHorizontalEdge( target, true ); } } @@ -245,12 +247,20 @@ class WritingFlow extends Component { // bubbling events from children to determine focus transition intents. /* eslint-disable jsx-a11y/no-static-element-interactions */ return ( - <div - ref={ this.bindContainer } - onKeyDown={ this.onKeyDown } - onMouseDown={ this.clearVerticalRect } - > - { children } + <div className="editor-writing-flow"> + <div + ref={ this.bindContainer } + onKeyDown={ this.onKeyDown } + onMouseDown={ this.clearVerticalRect } + > + { children } + </div> + <div + aria-hidden + tabIndex={ -1 } + onClick={ this.focusLastTextField } + className="editor-writing-flow__click-redirect" + /> </div> ); /* eslint-disable jsx-a11y/no-static-element-interactions */ @@ -267,7 +277,6 @@ export default connect( } ), { onMultiSelect: multiSelect, - onBottomReached: appendDefaultBlock, onSelectBlock: selectBlock, } )( WritingFlow ); diff --git a/editor/store/actions.js b/editor/store/actions.js index dc6561e872cb9c..47005fd67a4ad3 100644 --- a/editor/store/actions.js +++ b/editor/store/actions.js @@ -4,6 +4,14 @@ import uuid from 'uuid/v4'; import { partial, castArray } from 'lodash'; +/** + * WordPress dependencies + */ +import { + getDefaultBlockName, + createBlock, +} from '@wordpress/blocks'; + /** * Returns an action object used in signalling that editor has initialized with * the specified post object and editor settings. @@ -39,9 +47,9 @@ export function resetPost( post ) { /** * Returns an action object used to setup the editor state when first opening an editor. * - * @param {Object} post Post object. - * @param {Array} blocks Array of blocks. - * @param {Object} edits Initial edited attributes object. + * @param {Object} post Post object. + * @param {Array} blocks Array of blocks. + * @param {Object} edits Initial edited attributes object. * * @return {Object} Action object. */ @@ -70,6 +78,22 @@ export function resetBlocks( blocks ) { }; } +/** + * Returns an action object used in signalling that blocks have been received. + * Unlike resetBlocks, these should be appended to the existing known set, not + * replacing. + * + * @param {Object[]} blocks Array of block objects. + * + * @return {Object} Action object. + */ +export function receiveBlocks( blocks ) { + return { + type: 'RECEIVE_BLOCKS', + blocks, + }; +} + /** * Returns an action object used in signalling that the block attributes with * the specified UID has been updated. @@ -167,6 +191,7 @@ export function replaceBlocks( uids, blocks ) { type: 'REPLACE_BLOCKS', uids: castArray( uids ), blocks: castArray( blocks ), + time: Date.now(), }; } @@ -183,6 +208,51 @@ export function replaceBlock( uid, block ) { return replaceBlocks( uid, block ); } +/** + * Action creator creator which, given the action type to dispatch + * creates a prop dispatcher callback for + * managing block movement. + * + * @param {string} type Action type to dispatch. + * + * @return {Function} Prop dispatcher callback. + */ +function createOnMove( type ) { + return ( uids, rootUID ) => { + return { + type, + uids, + rootUID, + }; + }; +} + +export const moveBlocksDown = createOnMove( 'MOVE_BLOCKS_DOWN' ); +export const moveBlocksUp = createOnMove( 'MOVE_BLOCKS_UP' ); + +/** + * Returns an action object signalling that an indexed block should be moved + * to a new index. + * + * @param {?string} uid The UID of the block. + * @param {?string} fromRootUID root UID source. + * @param {?string} toRootUID root UID destination. + * @param {?string} layout layout to move the block into. + * @param {number} index The index to move the block into. + * + * @return {Object} Action object. + */ +export function moveBlockToPosition( uid, fromRootUID, toRootUID, layout, index ) { + return { + type: 'MOVE_BLOCK_TO_POSITION', + fromRootUID, + toRootUID, + uid, + index, + layout, + }; +} + /** * Returns an action object used in signalling that a single block should be * inserted, optionally at a specific index respective a root block list. @@ -213,6 +283,7 @@ export function insertBlocks( blocks, index, rootUID ) { blocks: castArray( blocks ), index, rootUID, + time: Date.now(), }; } @@ -239,6 +310,42 @@ export function hideInsertionPoint() { }; } +/** + * Returns an action object resetting the template validity. + * + * @param {boolean} isValid template validity flag. + * + * @return {Object} Action object. + */ +export function setTemplateValidity( isValid ) { + return { + type: 'SET_TEMPLATE_VALIDITY', + isValid, + }; +} + +/** + * Returns an action object tocheck the template validity. + * + * @return {Object} Action object. + */ +export function checkTemplateValidity() { + return { + type: 'CHECK_TEMPLATE_VALIDITY', + }; +} + +/** + * Returns an action object synchronize the template with the list of blocks + * + * @return {Object} Action object. + */ +export function synchronizeTemplate() { + return { + type: 'SYNCHRONIZE_TEMPLATE', + }; +} + export function editPost( edits ) { return { type: 'EDIT_POST', @@ -319,14 +426,16 @@ export function createUndoLevel() { * Returns an action object used in signalling that the blocks * corresponding to the specified UID set are to be removed. * - * @param {string[]} uids Block UIDs. + * @param {string[]} uids Block UIDs. + * @param {boolean} selectPrevious True if the previous block should be selected when a block is removed. * * @return {Object} Action object. */ -export function removeBlocks( uids ) { +export function removeBlocks( uids, selectPrevious = true ) { return { type: 'REMOVE_BLOCKS', uids, + selectPrevious, }; } @@ -334,12 +443,13 @@ export function removeBlocks( uids ) { * Returns an action object used in signalling that the block with the * specified UID is to be removed. * - * @param {string} uid Block UID. + * @param {string} uid Block UID. + * @param {boolean} selectPrevious True if the previous block should be selected when a block is removed. * * @return {Object} Action object. */ -export function removeBlock( uid ) { - return removeBlocks( [ uid ] ); +export function removeBlock( uid, selectPrevious = true ) { + return removeBlocks( [ uid ], selectPrevious ); } /** @@ -427,71 +537,86 @@ export const createErrorNotice = partial( createNotice, 'error' ); export const createWarningNotice = partial( createNotice, 'warning' ); /** - * Returns an action object used to fetch a single reusable block or all - * reusable blocks from the REST API into the store. + * Returns an action object used to fetch a single shared block or all shared + * blocks from the REST API into the store. * - * @param {?string} id If given, only a single reusable block with this ID will + * @param {?string} id If given, only a single shared block with this ID will * be fetched. * * @return {Object} Action object. */ -export function fetchReusableBlocks( id ) { +export function fetchSharedBlocks( id ) { return { - type: 'FETCH_REUSABLE_BLOCKS', + type: 'FETCH_SHARED_BLOCKS', id, }; } /** - * Returns an action object used to insert or update a reusable block into - * the store. + * Returns an action object used in signalling that shared blocks have been + * received. `results` is an array of objects containing: + * - `sharedBlock` - Details about how the shared block is persisted. + * - `parsedBlock` - The original block. * - * @param {Object} id The ID of the reusable block to update. - * @param {Object} reusableBlock The new reusable block object. Any omitted keys - * are not changed. + * @param {Object[]} results Shared blocks received. * * @return {Object} Action object. */ -export function updateReusableBlock( id, reusableBlock ) { +export function receiveSharedBlocks( results ) { return { - type: 'UPDATE_REUSABLE_BLOCK', + type: 'RECEIVE_SHARED_BLOCKS', + results, + }; +} + +/** + * Returns an action object used to save a shared block that's in the store to + * the REST API. + * + * @param {Object} id The ID of the shared block to save. + * + * @return {Object} Action object. + */ +export function saveSharedBlock( id ) { + return { + type: 'SAVE_SHARED_BLOCK', id, - reusableBlock, }; } /** - * Returns an action object used to save a reusable block that's in the store - * to the REST API. + * Returns an action object used to delete a shared block via the REST API. * - * @param {Object} id The ID of the reusable block to save. + * @param {number} id The ID of the shared block to delete. * * @return {Object} Action object. */ -export function saveReusableBlock( id ) { +export function deleteSharedBlock( id ) { return { - type: 'SAVE_REUSABLE_BLOCK', + type: 'DELETE_SHARED_BLOCK', id, }; } /** - * Returns an action object used to delete a reusable block via the REST API. + * Returns an action object used in signalling that a shared block's title is + * to be updated. * - * @param {number} id The ID of the reusable block to delete. + * @param {number} id The ID of the shared block to update. + * @param {string} title The new title. * * @return {Object} Action object. */ -export function deleteReusableBlock( id ) { +export function updateSharedBlockTitle( id, title ) { return { - type: 'DELETE_REUSABLE_BLOCK', + type: 'UPDATE_SHARED_BLOCK_TITLE', id, + title, }; } /** - * Returns an action object used to convert a reusable block into a static - * block. + * Returns an action object used to convert a shared block into a static block. * * @param {Object} uid The ID of the block to attach. * @@ -505,32 +630,33 @@ export function convertBlockToStatic( uid ) { } /** - * Returns an action object used to convert a static block into a reusable - * block. + * Returns an action object used to convert a static block into a shared block. * * @param {Object} uid The ID of the block to detach. * * @return {Object} Action object. */ -export function convertBlockToReusable( uid ) { +export function convertBlockToShared( uid ) { return { - type: 'CONVERT_BLOCK_TO_REUSABLE', + type: 'CONVERT_BLOCK_TO_SHARED', uid, }; } /** * Returns an action object used in signalling that a new block of the default - * type should be appended to the block list. + * type should be added to the block list. * * @param {?Object} attributes Optional attributes of the block to assign. * @param {?string} rootUID Optional root UID of block list to append. + * @param {?number} index Optional index where to insert the default block * * @return {Object} Action object */ -export function appendDefaultBlock( attributes, rootUID ) { +export function insertDefaultBlock( attributes, rootUID, index ) { + const block = createBlock( getDefaultBlockName(), attributes ); + return { - type: 'APPEND_DEFAULT_BLOCK', - attributes, - rootUID, + ...insertBlock( block, index, rootUID ), + isProvisional: true, }; } diff --git a/editor/store/defaults.js b/editor/store/defaults.js index 27a94b95814677..eeea12bb320a0b 100644 --- a/editor/store/defaults.js +++ b/editor/store/defaults.js @@ -1,4 +1,3 @@ export const PREFERENCES_DEFAULTS = { - recentInserts: [], insertUsage: {}, }; diff --git a/editor/store/effects.js b/editor/store/effects.js index f403134a948b11..bf40697c05d5d7 100644 --- a/editor/store/effects.js +++ b/editor/store/effects.js @@ -2,7 +2,7 @@ * External dependencies */ import { BEGIN, COMMIT, REVERT } from 'redux-optimist'; -import { get, has, includes, map, castArray, uniqueId } from 'lodash'; +import { get, includes, last, map, castArray, uniqueId } from 'lodash'; /** * WordPress dependencies @@ -13,10 +13,10 @@ import { switchToBlockType, createBlock, serialize, - createReusableBlock, - isReusableBlock, - getDefaultBlockName, + isSharedBlock, getDefaultBlockForPostFormat, + doBlocksMatchTemplate, + synchronizeBlocksWithTemplate, } from '@wordpress/blocks'; import { __ } from '@wordpress/i18n'; import { speak } from '@wordpress/a11y'; @@ -28,15 +28,21 @@ import { getPostEditUrl, getWPAdminURL } from '../utils/url'; import { setupEditorState, resetPost, + receiveBlocks, + receiveSharedBlocks, + replaceBlock, replaceBlocks, createSuccessNotice, createErrorNotice, removeNotice, savePost, - updateReusableBlock, - saveReusableBlock, + saveSharedBlock, insertBlock, + removeBlocks, selectBlock, + removeBlock, + resetBlocks, + setTemplateValidity, } from './actions'; import { getCurrentPost, @@ -49,9 +55,16 @@ import { isEditedPostSaveable, getBlock, getBlockCount, + getBlockRootUID, getBlocks, - getReusableBlock, + getSharedBlock, + getPreviousBlockUid, + getProvisionalBlockUID, + getSelectedBlock, + isBlockSelected, + getTemplate, POST_UPDATE_TRANSACTION_ID, + getTemplateLock, } from './selectors'; /** @@ -59,7 +72,24 @@ import { */ const SAVE_POST_NOTICE_ID = 'SAVE_POST_NOTICE_ID'; const TRASH_POST_NOTICE_ID = 'TRASH_POST_NOTICE_ID'; -const REUSABLE_BLOCK_NOTICE_ID = 'REUSABLE_BLOCK_NOTICE_ID'; +const SHARED_BLOCK_NOTICE_ID = 'SHARED_BLOCK_NOTICE_ID'; + +/** + * Effect handler returning an action to remove the provisional block, if one + * is set. + * + * @param {Object} action Action object. + * @param {Object} store Data store instance. + * + * @return {?Object} Remove action, if provisional block is set. + */ +export function removeProvisionalBlock( action, store ) { + const state = store.getState(); + const provisionalBlockUID = getProvisionalBlockUID( state ); + if ( provisionalBlockUID && ! isBlockSelected( state, provisionalBlockUID ) ) { + return removeBlock( provisionalBlockUID, false ); + } +} export default { REQUEST_POST_UPDATE( action, store ) { @@ -79,27 +109,30 @@ export default { optimist: { type: BEGIN, id: POST_UPDATE_TRANSACTION_ID }, } ); dispatch( removeNotice( SAVE_POST_NOTICE_ID ) ); - const Model = wp.api.getPostTypeModel( getCurrentPostType( state ) ); - new Model( toSend ).save().done( ( newPost ) => { - dispatch( resetPost( newPost ) ); - dispatch( { - type: 'REQUEST_POST_UPDATE_SUCCESS', - previousPost: post, - post: newPost, - optimist: { type: COMMIT, id: POST_UPDATE_TRANSACTION_ID }, - } ); - } ).fail( ( err ) => { - dispatch( { - type: 'REQUEST_POST_UPDATE_FAILURE', - error: get( err, 'responseJSON', { - code: 'unknown_error', - message: __( 'An unknown error occurred.' ), - } ), - post, - edits, - optimist: { type: REVERT, id: POST_UPDATE_TRANSACTION_ID }, - } ); - } ); + const basePath = wp.api.getPostTypeRoute( getCurrentPostType( state ) ); + wp.apiRequest( { path: `/wp/v2/${ basePath }/${ post.id }`, method: 'PUT', data: toSend } ).then( + ( newPost ) => { + dispatch( resetPost( newPost ) ); + dispatch( { + type: 'REQUEST_POST_UPDATE_SUCCESS', + previousPost: post, + post: newPost, + optimist: { type: COMMIT, id: POST_UPDATE_TRANSACTION_ID }, + } ); + }, + ( err ) => { + dispatch( { + type: 'REQUEST_POST_UPDATE_FAILURE', + error: get( err, 'responseJSON', { + code: 'unknown_error', + message: __( 'An unknown error occurred.' ), + } ), + post, + edits, + optimist: { type: REVERT, id: POST_UPDATE_TRANSACTION_ID }, + } ); + } + ); }, REQUEST_POST_UPDATE_SUCCESS( action, store ) { const { previousPost, post } = action; @@ -171,9 +204,9 @@ export default { TRASH_POST( action, store ) { const { dispatch, getState } = store; const { postId } = action; - const Model = wp.api.getPostTypeModel( getCurrentPostType( getState() ) ); + const basePath = wp.api.getPostTypeRoute( getCurrentPostType( getState() ) ); dispatch( removeNotice( TRASH_POST_NOTICE_ID ) ); - new Model( { id: postId } ).destroy().then( + wp.apiRequest( { path: `/wp/v2/${ basePath }/${ postId }`, method: 'DELETE' } ).then( () => { dispatch( { ...action, @@ -278,19 +311,18 @@ export default { // Parse content as blocks let blocks; + let isValidTemplate = true; if ( post.content.raw ) { blocks = parse( post.content.raw ); + + // Unlocked templates are considered always valid because they act as default values only. + isValidTemplate = ( + ! settings.template || + settings.templateLock !== 'all' || + doBlocksMatchTemplate( blocks, settings.template ) + ); } else if ( settings.template ) { - const createBlocksFromTemplate = ( template ) => { - return map( template, ( [ name, attributes, innerBlocksTemplate ] ) => { - return createBlock( - name, - attributes, - createBlocksFromTemplate( innerBlocksTemplate ) - ); - } ); - }; - blocks = createBlocksFromTemplate( settings.template ); + blocks = synchronizeBlocksWithTemplate( [], settings.template ); } else if ( getDefaultBlockForPostFormat( post.format ) ) { blocks = [ createBlock( getDefaultBlockForPostFormat( post.format ) ) ]; } else { @@ -304,38 +336,71 @@ export default { edits.status = 'draft'; } - return setupEditorState( post, blocks, edits ); + return [ + setTemplateValidity( isValidTemplate ), + setupEditorState( post, blocks, edits ), + ]; + }, + SYNCHRONIZE_TEMPLATE( action, { getState } ) { + const state = getState(); + const blocks = getBlocks( state ); + const template = getTemplate( state ); + const updatedBlockList = synchronizeBlocksWithTemplate( blocks, template ); + + return [ + resetBlocks( updatedBlockList ), + setTemplateValidity( true ), + ]; + }, + CHECK_TEMPLATE_VALIDITY( action, { getState } ) { + const state = getState(); + const blocks = getBlocks( state ); + const template = getTemplate( state ); + const templateLock = getTemplateLock( state ); + const isValid = ( + ! template || + templateLock !== 'all' || + doBlocksMatchTemplate( blocks, template ) + ); + + return setTemplateValidity( isValid ); }, - FETCH_REUSABLE_BLOCKS( action, store ) { + FETCH_SHARED_BLOCKS( action, store ) { // TODO: these are potentially undefined, this fix is in place - // until there is a filter to not use reusable blocks if undefined - if ( ! has( wp, 'api.models.Blocks' ) && ! has( wp, 'api.collections.Blocks' ) ) { + // until there is a filter to not use shared blocks if undefined + const basePath = wp.api.getPostTypeRoute( 'wp_block' ); + if ( ! basePath ) { return; } + const { id } = action; const { dispatch } = store; let result; if ( id ) { - result = new wp.api.models.Blocks( { id } ).fetch(); + result = wp.apiRequest( { path: `/wp/v2/${ basePath }/${ id }` } ); } else { - result = new wp.api.collections.Blocks().fetch(); + result = wp.apiRequest( { path: `/wp/v2/${ basePath }` } ); } result.then( - ( reusableBlockOrBlocks ) => { + ( sharedBlockOrBlocks ) => { + dispatch( receiveSharedBlocks( map( + castArray( sharedBlockOrBlocks ), + ( sharedBlock ) => ( { + sharedBlock, + parsedBlock: parse( sharedBlock.content )[ 0 ], + } ) + ) ) ); + dispatch( { - type: 'FETCH_REUSABLE_BLOCKS_SUCCESS', + type: 'FETCH_SHARED_BLOCKS_SUCCESS', id, - reusableBlocks: castArray( reusableBlockOrBlocks ).map( ( { id: itemId, title, content } ) => { - const [ { name: type, attributes } ] = parse( content ); - return { id: itemId, title, type, attributes }; - } ), } ); }, ( error ) => { dispatch( { - type: 'FETCH_REUSABLE_BLOCKS_FAILURE', + type: 'FETCH_SHARED_BLOCKS_FAILURE', id, error: error.responseJSON || { code: 'unknown_error', @@ -345,120 +410,144 @@ export default { } ); }, - SAVE_REUSABLE_BLOCK( action, store ) { + RECEIVE_SHARED_BLOCKS( action ) { + return receiveBlocks( map( action.results, 'parsedBlock' ) ); + }, + SAVE_SHARED_BLOCK( action, store ) { // TODO: these are potentially undefined, this fix is in place - // until there is a filter to not use reusable blocks if undefined - if ( ! has( wp, 'api.models.Blocks' ) ) { + // until there is a filter to not use shared blocks if undefined + const basePath = wp.api.getPostTypeRoute( 'wp_block' ); + if ( ! basePath ) { return; } const { id } = action; - const { getState, dispatch } = store; + const { dispatch } = store; + const state = store.getState(); + + const { uid, title, isTemporary } = getSharedBlock( state, id ); + const { name, attributes, innerBlocks } = getBlock( state, uid ); + const content = serialize( createBlock( name, attributes, innerBlocks ) ); - const { title, type, attributes, isTemporary } = getReusableBlock( getState(), id ); - const content = serialize( createBlock( type, attributes ) ); - const requestData = isTemporary ? { title, content } : { id, title, content }; + const data = isTemporary ? { title, content } : { id, title, content }; + const path = isTemporary ? `/wp/v2/${ basePath }` : `/wp/v2/${ basePath }/${ id }`; + const method = isTemporary ? 'POST' : 'PUT'; - new wp.api.models.Blocks( requestData ).save().then( - ( updatedReusableBlock ) => { + wp.apiRequest( { path, data, method } ).then( + ( updatedSharedBlock ) => { dispatch( { - type: 'SAVE_REUSABLE_BLOCK_SUCCESS', - updatedId: updatedReusableBlock.id, + type: 'SAVE_SHARED_BLOCK_SUCCESS', + updatedId: updatedSharedBlock.id, id, } ); const message = isTemporary ? __( 'Block created.' ) : __( 'Block updated.' ); - dispatch( createSuccessNotice( message, { id: REUSABLE_BLOCK_NOTICE_ID } ) ); + dispatch( createSuccessNotice( message, { id: SHARED_BLOCK_NOTICE_ID } ) ); }, ( error ) => { - dispatch( { type: 'SAVE_REUSABLE_BLOCK_FAILURE', id } ); - const message = __( 'An unknown error occured.' ); + dispatch( { type: 'SAVE_SHARED_BLOCK_FAILURE', id } ); + const message = __( 'An unknown error occurred.' ); dispatch( createErrorNotice( get( error.responseJSON, 'message', message ), { - id: REUSABLE_BLOCK_NOTICE_ID, + id: SHARED_BLOCK_NOTICE_ID, spokenMessage: message, } ) ); } ); }, - DELETE_REUSABLE_BLOCK( action, store ) { + DELETE_SHARED_BLOCK( action, store ) { // TODO: these are potentially undefined, this fix is in place - // until there is a filter to not use reusable blocks if undefined - if ( ! has( wp, 'api.models.Blocks' ) ) { + // until there is a filter to not use shared blocks if undefined + const basePath = wp.api.getPostTypeRoute( 'wp_block' ); + if ( ! basePath ) { return; } const { id } = action; const { getState, dispatch } = store; - // Don't allow a reusable block with a temporary ID to be deleted - const reusableBlock = getReusableBlock( getState(), id ); - if ( ! reusableBlock || reusableBlock.isTemporary ) { + // Don't allow a shared block with a temporary ID to be deleted + const sharedBlock = getSharedBlock( getState(), id ); + if ( ! sharedBlock || sharedBlock.isTemporary ) { return; } - // Remove any other blocks that reference this reusable block + // Remove any other blocks that reference this shared block const allBlocks = getBlocks( getState() ); - const associatedBlocks = allBlocks.filter( block => isReusableBlock( block ) && block.attributes.ref === id ); + const associatedBlocks = allBlocks.filter( block => isSharedBlock( block ) && block.attributes.ref === id ); const associatedBlockUids = associatedBlocks.map( block => block.uid ); const transactionId = uniqueId(); dispatch( { - type: 'REMOVE_REUSABLE_BLOCK', + type: 'REMOVE_SHARED_BLOCK', id, - associatedBlockUids, optimist: { type: BEGIN, id: transactionId }, } ); - new wp.api.models.Blocks( { id } ).destroy().then( + // Remove the parsed block. + dispatch( removeBlocks( [ + ...associatedBlockUids, + sharedBlock.uid, + ] ) ); + + wp.apiRequest( { path: `/wp/v2/${ basePath }/${ id }`, method: 'DELETE' } ).then( () => { dispatch( { - type: 'DELETE_REUSABLE_BLOCK_SUCCESS', + type: 'DELETE_SHARED_BLOCK_SUCCESS', id, optimist: { type: COMMIT, id: transactionId }, } ); const message = __( 'Block deleted.' ); - dispatch( createSuccessNotice( message, { id: REUSABLE_BLOCK_NOTICE_ID } ) ); + dispatch( createSuccessNotice( message, { id: SHARED_BLOCK_NOTICE_ID } ) ); }, ( error ) => { dispatch( { - type: 'DELETE_REUSABLE_BLOCK_FAILURE', + type: 'DELETE_SHARED_BLOCK_FAILURE', id, optimist: { type: REVERT, id: transactionId }, } ); - const message = __( 'An unknown error occured.' ); + const message = __( 'An unknown error occurred.' ); dispatch( createErrorNotice( get( error.responseJSON, 'message', message ), { - id: REUSABLE_BLOCK_NOTICE_ID, + id: SHARED_BLOCK_NOTICE_ID, spokenMessage: message, } ) ); } ); }, CONVERT_BLOCK_TO_STATIC( action, store ) { - const { getState, dispatch } = store; - - const oldBlock = getBlock( getState(), action.uid ); - const reusableBlock = getReusableBlock( getState(), oldBlock.attributes.ref ); - const newBlock = createBlock( reusableBlock.type, reusableBlock.attributes ); - dispatch( replaceBlocks( [ oldBlock.uid ], [ newBlock ] ) ); + const state = store.getState(); + const oldBlock = getBlock( state, action.uid ); + const sharedBlock = getSharedBlock( state, oldBlock.attributes.ref ); + const referencedBlock = getBlock( state, sharedBlock.uid ); + const newBlock = createBlock( referencedBlock.name, referencedBlock.attributes ); + store.dispatch( replaceBlock( oldBlock.uid, newBlock ) ); }, - CONVERT_BLOCK_TO_REUSABLE( action, store ) { + CONVERT_BLOCK_TO_SHARED( action, store ) { const { getState, dispatch } = store; - const oldBlock = getBlock( getState(), action.uid ); - const reusableBlock = createReusableBlock( oldBlock.name, oldBlock.attributes ); - const newBlock = createBlock( 'core/block', { - ref: reusableBlock.id, - layout: oldBlock.attributes.layout, - } ); - dispatch( updateReusableBlock( reusableBlock.id, reusableBlock ) ); - dispatch( saveReusableBlock( reusableBlock.id ) ); - dispatch( replaceBlocks( [ oldBlock.uid ], [ newBlock ] ) ); - }, - APPEND_DEFAULT_BLOCK( action ) { - const { attributes, rootUID } = action; - const block = createBlock( getDefaultBlockName(), attributes ); + const parsedBlock = getBlock( getState(), action.uid ); + const sharedBlock = { + id: uniqueId( 'shared' ), + uid: parsedBlock.uid, + title: __( 'Untitled block' ), + }; + + dispatch( receiveSharedBlocks( [ { + sharedBlock, + parsedBlock, + } ] ) ); + + dispatch( saveSharedBlock( sharedBlock.id ) ); + + dispatch( replaceBlock( + parsedBlock.uid, + createBlock( 'core/block', { + ref: sharedBlock.id, + layout: parsedBlock.attributes.layout, + } ) + ) ); - return insertBlock( block, undefined, rootUID ); + // Re-add the original block to the store, since replaceBlock() will have removed it + dispatch( receiveBlocks( [ parsedBlock ] ) ); }, CREATE_NOTICE( { notice: { content, spokenMessage } } ) { const message = spokenMessage || content; @@ -475,4 +564,37 @@ export default { return insertBlock( createBlock( blockName ) ); } }, + + CLEAR_SELECTED_BLOCK: removeProvisionalBlock, + + SELECT_BLOCK: removeProvisionalBlock, + + MULTI_SELECT: removeProvisionalBlock, + + REMOVE_BLOCKS( action, { getState, dispatch } ) { + // if the action says previous block should not be selected don't do anything. + if ( ! action.selectPrevious ) { + return; + } + + const firstRemovedBlockUID = action.uids[ 0 ]; + const state = getState(); + const currentSelectedBlock = getSelectedBlock( state ); + + // recreate the state before the block was removed. + const previousState = { ...state, editor: { present: last( state.editor.past ) } }; + + // rootUID of the removed block. + const rootUID = getBlockRootUID( previousState, firstRemovedBlockUID ); + + // UID of the block that was before the removed block + // or the rootUID if the removed block was the first amongst his siblings. + const blockUIDToSelect = getPreviousBlockUid( previousState, firstRemovedBlockUID ) || rootUID; + + // Dispatch select block action if the currently selected block + // is not already the block we want to be selected. + if ( blockUIDToSelect !== currentSelectedBlock ) { + dispatch( selectBlock( blockUIDToSelect ) ); + } + }, }; diff --git a/editor/store/middlewares.js b/editor/store/middlewares.js index 4846b7fb8984ae..fd082a4dec3360 100644 --- a/editor/store/middlewares.js +++ b/editor/store/middlewares.js @@ -38,10 +38,8 @@ function applyMiddlewares( store ) { chain = middlewares.map( middleware => middleware( middlewareAPI ) ); enhancedDispatch = flowRight( ...chain )( store.dispatch ); - return { - ...store, - dispatch: enhancedDispatch, - }; + store.dispatch = enhancedDispatch; + return store; } export default applyMiddlewares; diff --git a/editor/store/reducer.js b/editor/store/reducer.js index 0c35349803ed35..d36fbc32fe7494 100644 --- a/editor/store/reducer.js +++ b/editor/store/reducer.js @@ -2,7 +2,6 @@ * External dependencies */ import optimist from 'redux-optimist'; -import { combineReducers } from 'redux'; import { flow, reduce, @@ -16,12 +15,16 @@ import { omitBy, keys, isEqual, + includes, + overSome, + get, } from 'lodash'; /** * WordPress dependencies */ -import { isReusableBlock } from '@wordpress/blocks'; +import { isSharedBlock } from '@wordpress/blocks'; +import { combineReducers } from '@wordpress/data'; /** * Internal dependencies @@ -29,6 +32,7 @@ import { isReusableBlock } from '@wordpress/blocks'; import withHistory from '../utils/with-history'; import withChangeDetection from '../utils/with-change-detection'; import { PREFERENCES_DEFAULTS } from './defaults'; +import { insertAt, moveTo } from './array'; /** * Returns a post attribute value, flattening nested rendered content using its @@ -97,28 +101,72 @@ function getFlattenedBlocks( blocks ) { } /** - * Option for the history reducer. When the block ID and updated attirbute keys - * are the same as previously, the history reducer should overwrite its present - * state. + * Returns true if the two object arguments have the same keys, or false + * otherwise. * - * @param {Object} action The currently dispatched action. - * @param {Object} previousAction The previously dispatched action. + * @param {Object} a First object. + * @param {Object} b Second object. * - * @return {boolean} Whether or not to overwrite present state. + * @return {boolean} Whether the two objects have the same keys. */ -function shouldOverwriteState( action, previousAction ) { - if ( - previousAction && +export function hasSameKeys( a, b ) { + return isEqual( keys( a ), keys( b ) ); +} + +/** + * Returns true if, given the currently dispatching action and the previously + * dispatched action, the two actions are updating the same block attribute, or + * false otherwise. + * + * @param {Object} action Currently dispatching action. + * @param {Object} previousAction Previously dispatched action. + * + * @return {boolean} Whether actions are updating the same block attribute. + */ +export function isUpdatingSameBlockAttribute( action, previousAction ) { + return ( action.type === 'UPDATE_BLOCK_ATTRIBUTES' && - action.type === previousAction.type - ) { - const attributes = keys( action.attributes ); - const previousAttributes = keys( previousAction.attributes ); + action.uid === previousAction.uid && + hasSameKeys( action.attributes, previousAction.attributes ) + ); +} + +/** + * Returns true if, given the currently dispatching action and the previously + * dispatched action, the two actions are editing the same post property, or + * false otherwise. + * + * @param {Object} action Currently dispatching action. + * @param {Object} previousAction Previously dispatched action. + * + * @return {boolean} Whether actions are updating the same post property. + */ +export function isUpdatingSamePostProperty( action, previousAction ) { + return ( + action.type === 'EDIT_POST' && + hasSameKeys( action.edits, previousAction.edits ) + ); +} - return action.uid === previousAction.uid && isEqual( attributes, previousAttributes ); +/** + * Returns true if, given the currently dispatching action and the previously + * dispatched action, the two actions are modifying the same property such that + * undo history should be batched. + * + * @param {Object} action Currently dispatching action. + * @param {Object} previousAction Previously dispatched action. + * + * @return {boolean} Whether to overwrite present state. + */ +export function shouldOverwriteState( action, previousAction ) { + if ( ! previousAction || action.type !== previousAction.type ) { + return false; } - return false; + return overSome( [ + isUpdatingSameBlockAttribute, + isUpdatingSamePostProperty, + ] )( action, previousAction ); } /** @@ -169,6 +217,7 @@ export const editor = flow( [ // Track undo history, starting at editor initialization. withHistory( { resetTypes: [ 'SETUP_EDITOR_STATE' ], + ignoreTypes: [ 'RECEIVE_BLOCKS' ], shouldOverwriteState, } ), @@ -176,6 +225,7 @@ export const editor = flow( [ // editor initialization firing post reset as an effect. withChangeDetection( { resetTypes: [ 'SETUP_EDITOR_STATE', 'RESET_POST' ], + ignoreTypes: [ 'RECEIVE_BLOCKS' ], } ), ] )( { edits( state = {}, action ) { @@ -228,6 +278,12 @@ export const editor = flow( [ case 'SETUP_EDITOR_STATE': return getFlattenedBlocks( action.blocks ); + case 'RECEIVE_BLOCKS': + return { + ...state, + ...getFlattenedBlocks( action.blocks ), + }; + case 'UPDATE_BLOCK_ATTRIBUTES': // Ignore updates if block isn't known if ( ! state[ action.uid ] ) { @@ -263,6 +319,23 @@ export const editor = flow( [ }, }; + case 'MOVE_BLOCK_TO_POSITION': + // Avoid creating a new instance if the layout didn't change. + if ( state[ action.uid ].attributes.layout === action.layout ) { + return state; + } + + return { + ...state, + [ action.uid ]: { + ...state[ action.uid ], + attributes: { + ...state[ action.uid ].attributes, + layout: action.layout, + }, + }, + }; + case 'UPDATE_BLOCK': // Ignore updates if block isn't known if ( ! state[ action.uid ] ) { @@ -296,10 +369,10 @@ export const editor = flow( [ case 'REMOVE_BLOCKS': return omit( state, action.uids ); - case 'SAVE_REUSABLE_BLOCK_SUCCESS': { + case 'SAVE_SHARED_BLOCK_SUCCESS': { const { id, updatedId } = action; - // If a temporary reusable block is saved, we swap the temporary id with the final one + // If a temporary shared block is saved, we swap the temporary id with the final one if ( id === updatedId ) { return state; } @@ -318,9 +391,6 @@ export const editor = flow( [ return block; } ); } - - case 'REMOVE_REUSABLE_BLOCK': - return omit( state, action.associatedBlockUids ); } return state; @@ -332,29 +402,50 @@ export const editor = flow( [ case 'SETUP_EDITOR_STATE': return mapBlockOrder( action.blocks ); + case 'RECEIVE_BLOCKS': + return { + ...state, + ...omit( mapBlockOrder( action.blocks ), '' ), + }; + case 'INSERT_BLOCKS': { const { rootUID = '', blocks } = action; - const subState = state[ rootUID ] || []; const mappedBlocks = mapBlockOrder( blocks, rootUID ); - const { index = subState.length } = action; return { ...state, ...mappedBlocks, - [ rootUID ]: [ - ...subState.slice( 0, index ), - ...mappedBlocks[ rootUID ], - ...subState.slice( index ), - ], + [ rootUID ]: insertAt( subState, mappedBlocks[ rootUID ], index ), + }; + } + + case 'MOVE_BLOCK_TO_POSITION': { + const { fromRootUID = '', toRootUID = '', uid } = action; + const { index = state[ toRootUID ].length } = action; + + // Moving inside the same parent block + if ( fromRootUID === toRootUID ) { + const subState = state[ toRootUID ]; + const fromIndex = subState.indexOf( uid ); + return { + ...state, + [ toRootUID ]: moveTo( state[ toRootUID ], fromIndex, index ), + }; + } + + // Moving from a parent block to another + return { + ...state, + [ fromRootUID ]: without( state[ fromRootUID ], uid ), + [ toRootUID ]: insertAt( state[ toRootUID ], uid, index ), }; } case 'MOVE_BLOCKS_UP': { const { uids, rootUID = '' } = action; const firstUid = first( uids ); - const lastUid = last( uids ); const subState = state[ rootUID ]; if ( ! subState.length || firstUid === first( subState ) ) { @@ -362,17 +453,10 @@ export const editor = flow( [ } const firstIndex = subState.indexOf( firstUid ); - const lastIndex = subState.indexOf( lastUid ); - const swappedUid = subState[ firstIndex - 1 ]; return { ...state, - [ rootUID ]: [ - ...subState.slice( 0, firstIndex - 1 ), - ...uids, - swappedUid, - ...subState.slice( lastIndex + 1 ), - ], + [ rootUID ]: moveTo( subState, firstIndex, firstIndex - 1, uids.length ), }; } @@ -387,17 +471,10 @@ export const editor = flow( [ } const firstIndex = subState.indexOf( firstUid ); - const lastIndex = subState.indexOf( lastUid ); - const swappedUid = subState[ lastIndex + 1 ]; return { ...state, - [ rootUID ]: [ - ...subState.slice( 0, firstIndex ), - swappedUid, - ...uids, - ...subState.slice( lastIndex + 2 ), - ], + [ rootUID ]: moveTo( subState, firstIndex, firstIndex + 1, uids.length ), }; } @@ -411,6 +488,10 @@ export const editor = flow( [ return flow( [ ( nextState ) => omit( nextState, uids ), + ( nextState ) => ( { + ...nextState, + ...omit( mappedBlocks, '' ), + } ), ( nextState ) => mapValues( nextState, ( subState ) => ( reduce( subState, ( result, uid ) => { if ( uid === uids[ 0 ] ) { @@ -427,27 +508,19 @@ export const editor = flow( [ return result; }, [] ) ) ), - ] )( { - ...state, - ...omit( mappedBlocks, '' ), - } ); + ] )( state ); } case 'REMOVE_BLOCKS': - case 'REMOVE_REUSABLE_BLOCK': { - const { type, uids, associatedBlockUids } = action; - const uidsToRemove = type === 'REMOVE_BLOCKS' ? uids : associatedBlockUids; - return flow( [ // Remove inner block ordering for removed blocks - ( nextState ) => omit( nextState, uidsToRemove ), + ( nextState ) => omit( nextState, action.uids ), // Remove deleted blocks from other blocks' orderings ( nextState ) => mapValues( nextState, ( subState ) => ( - without( subState, ...uidsToRemove ) + without( subState, ...action.uids ) ) ), ] )( state ); - } } return state; @@ -579,14 +652,30 @@ export function blockSelection( state = { initialPosition: null, isMultiSelecting: false, }; + case 'REMOVE_BLOCKS': + if ( ! action.uids || ! action.uids.length || action.uids.indexOf( state.start ) === -1 ) { + return state; + } + return { + ...state, + start: null, + end: null, + initialPosition: null, + isMultiSelecting: false, + }; case 'REPLACE_BLOCKS': - if ( ! action.blocks || ! action.blocks.length || action.uids.indexOf( state.start ) === -1 ) { + if ( action.uids.indexOf( state.start ) === -1 ) { return state; } + + // If there is replacement block(s), assign first's UID as the next + // selected block. If empty replacement, reset to null. + const nextSelectedBlockUID = get( action.blocks, [ 0, 'uid' ], null ); + return { ...state, - start: action.blocks[ 0 ].uid, - end: action.blocks[ 0 ].uid, + start: nextSelectedBlockUID, + end: nextSelectedBlockUID, initialPosition: null, isMultiSelecting: false, }; @@ -600,6 +689,48 @@ export function blockSelection( state = { return state; } +/** + * Reducer returning the UID of the provisional block. A provisional block is + * one which is to be removed if it does not receive updates in the time until + * the next selection or block reset. + * + * @param {string} state Current state. + * @param {Object} action Dispatched action. + * + * @return {string} Updated state. + */ +export function provisionalBlockUID( state = null, action ) { + switch ( action.type ) { + case 'INSERT_BLOCKS': + if ( action.isProvisional ) { + return first( action.blocks ).uid; + } + break; + + case 'RESET_BLOCKS': + return null; + + case 'UPDATE_BLOCK_ATTRIBUTES': + case 'UPDATE_BLOCK': + case 'CONVERT_BLOCK_TO_SHARED': + const { uid } = action; + if ( uid === state ) { + return null; + } + break; + + case 'REPLACE_BLOCKS': + case 'REMOVE_BLOCKS': + const { uids } = action; + if ( includes( uids, state ) ) { + return null; + } + break; + } + + return state; +} + export function blocksMode( state = {}, action ) { if ( action.type === 'TOGGLE_BLOCK_MODE' ) { const { uid } = action; @@ -633,6 +764,32 @@ export function isInsertionPointVisible( state = false, action ) { return state; } +/** + * Reducer returning whether the post blocks match the defined template or not. + * + * @param {Object} state Current state. + * @param {Object} action Dispatched action. + * + * @return {boolean} Updated state. + */ +export function template( state = { isValid: true }, action ) { + switch ( action.type ) { + case 'SETUP_EDITOR': + return { + ...state, + template: action.settings.template, + lock: action.settings.templateLock, + }; + case 'SET_TEMPLATE_VALIDITY': + return { + ...state, + isValid: action.isValid, + }; + } + + return state; +} + /** * Reducer returning the user preferences. * @@ -647,25 +804,21 @@ export function isInsertionPointVisible( state = false, action ) { export function preferences( state = PREFERENCES_DEFAULTS, action ) { switch ( action.type ) { case 'INSERT_BLOCKS': + case 'REPLACE_BLOCKS': return action.blocks.reduce( ( prevState, block ) => { let id = block.name; const insert = { name: block.name }; - if ( isReusableBlock( block ) ) { + if ( isSharedBlock( block ) ) { insert.ref = block.attributes.ref; id += '/' + block.attributes.ref; } - const isSameAsInsert = ( { name, ref } ) => name === insert.name && ref === insert.ref; - return { ...prevState, - recentInserts: [ - insert, - ...reject( prevState.recentInserts, isSameAsInsert ), - ], insertUsage: { ...prevState.insertUsage, [ id ]: { + time: action.time, count: prevState.insertUsage[ id ] ? prevState.insertUsage[ id ].count + 1 : 1, insert, }, @@ -673,11 +826,10 @@ export function preferences( state = PREFERENCES_DEFAULTS, action ) { }; }, state ); - case 'REMOVE_REUSABLE_BLOCK': + case 'REMOVE_SHARED_BLOCK': return { ...state, insertUsage: omitBy( state.insertUsage, ( { insert } ) => insert.ref === action.id ), - recentInserts: reject( state.recentInserts, insert => insert.ref === action.id ), }; } @@ -744,50 +896,60 @@ export function notices( state = [], action ) { return state; } -export const reusableBlocks = combineReducers( { +export const sharedBlocks = combineReducers( { data( state = {}, action ) { switch ( action.type ) { - case 'FETCH_REUSABLE_BLOCKS_SUCCESS': { - return reduce( action.reusableBlocks, ( newState, reusableBlock ) => ( { - ...newState, - [ reusableBlock.id ]: reusableBlock, - } ), state ); + case 'RECEIVE_SHARED_BLOCKS': { + return reduce( action.results, ( nextState, result ) => { + const { id, title } = result.sharedBlock; + const { uid } = result.parsedBlock; + + const value = { uid, title }; + + if ( ! isEqual( nextState[ id ], value ) ) { + if ( nextState === state ) { + nextState = { ...nextState }; + } + + nextState[ id ] = value; + } + + return nextState; + }, state ); } - case 'UPDATE_REUSABLE_BLOCK': { - const { id, reusableBlock } = action; - const existingReusableBlock = state[ id ]; + case 'UPDATE_SHARED_BLOCK_TITLE': { + const { id, title } = action; + + if ( ! state[ id ] || state[ id ].title === title ) { + return state; + } return { ...state, [ id ]: { - ...existingReusableBlock, - ...reusableBlock, - attributes: { - ...( existingReusableBlock && existingReusableBlock.attributes ), - ...reusableBlock.attributes, - }, + ...state[ id ], + title, }, }; } - case 'SAVE_REUSABLE_BLOCK_SUCCESS': { + case 'SAVE_SHARED_BLOCK_SUCCESS': { const { id, updatedId } = action; - // If a temporary reusable block is saved, we swap the temporary id with the final one + // If a temporary shared block is saved, we swap the temporary id with the final one if ( id === updatedId ) { return state; } + + const value = state[ id ]; return { ...omit( state, id ), - [ updatedId ]: { - ...omit( state[ id ], [ 'id', 'isTemporary' ] ), - id: updatedId, - }, + [ updatedId ]: value, }; } - case 'REMOVE_REUSABLE_BLOCK': { + case 'REMOVE_SHARED_BLOCK': { const { id } = action; return omit( state, id ); } @@ -798,7 +960,7 @@ export const reusableBlocks = combineReducers( { isFetching( state = {}, action ) { switch ( action.type ) { - case 'FETCH_REUSABLE_BLOCKS': { + case 'FETCH_SHARED_BLOCKS': { const { id } = action; if ( ! id ) { return state; @@ -810,8 +972,8 @@ export const reusableBlocks = combineReducers( { }; } - case 'FETCH_REUSABLE_BLOCKS_SUCCESS': - case 'FETCH_REUSABLE_BLOCKS_FAILURE': { + case 'FETCH_SHARED_BLOCKS_SUCCESS': + case 'FETCH_SHARED_BLOCKS_FAILURE': { const { id } = action; return omit( state, id ); } @@ -822,14 +984,14 @@ export const reusableBlocks = combineReducers( { isSaving( state = {}, action ) { switch ( action.type ) { - case 'SAVE_REUSABLE_BLOCK': + case 'SAVE_SHARED_BLOCK': return { ...state, [ action.id ]: true, }; - case 'SAVE_REUSABLE_BLOCK_SUCCESS': - case 'SAVE_REUSABLE_BLOCK_FAILURE': { + case 'SAVE_SHARED_BLOCK_SUCCESS': + case 'SAVE_SHARED_BLOCK_FAILURE': { const { id } = action; return omit( state, id ); } @@ -844,10 +1006,12 @@ export default optimist( combineReducers( { currentPost, isTyping, blockSelection, + provisionalBlockUID, blocksMode, isInsertionPointVisible, preferences, saving, notices, - reusableBlocks, + sharedBlocks, + template, } ) ); diff --git a/editor/store/selectors.js b/editor/store/selectors.js index e4f63a1e0f3fe0..7285e02d7e9511 100644 --- a/editor/store/selectors.js +++ b/editor/store/selectors.js @@ -1,7 +1,6 @@ /** * External dependencies */ -import moment from 'moment'; import { map, first, @@ -9,6 +8,7 @@ import { has, last, reduce, + size, compact, find, unionWith, @@ -23,6 +23,8 @@ import createSelector from 'rememo'; import { serialize, getBlockType, getBlockTypes } from '@wordpress/blocks'; import { __ } from '@wordpress/i18n'; import { addQueryArgs } from '@wordpress/url'; +import { moment } from '@wordpress/date'; +import { deprecated } from '@wordpress/utils'; /*** * Module constants @@ -216,6 +218,17 @@ export function getEditedPostVisibility( state ) { return 'public'; } +/** + * Returns true if post is pending review. + * + * @param {Object} state Global application state. + * + * @return {boolean} Whether current post is pending review. + */ +export function isCurrentPostPending( state ) { + return getCurrentPost( state ).status === 'pending'; +} + /** * Return true if the current post has already been published. * @@ -230,6 +243,17 @@ export function isCurrentPostPublished( state ) { ( post.status === 'future' && moment( post.date ).isBefore( moment() ) ); } +/** + * Returns true if post is already scheduled. + * + * @param {Object} state Global application state. + * + * @return {boolean} Whether current post is scheduled to be posted. + */ +export function isCurrentPostScheduled( state ) { + return getCurrentPost( state ).status === 'future' && ! isCurrentPostPublished( state ); +} + /** * Return true if the post being edited can be published. * @@ -283,11 +307,11 @@ export function isEditedPostEmpty( state ) { * @return {boolean} Whether the post has been published. */ export function isEditedPostBeingScheduled( state ) { - const date = getEditedPostAttribute( state, 'date' ); + const date = moment( getEditedPostAttribute( state, 'date' ) ); // Adding 1 minute as an error threshold between the server and the client dates. const now = moment().add( 1, 'minute' ); - return moment( date ).isAfter( now ); + return date.isAfter( now ); } /** @@ -300,7 +324,7 @@ export function isEditedPostBeingScheduled( state ) { export function getDocumentTitle( state ) { let title = getEditedPostAttribute( state, 'title' ); - if ( ! title.trim() ) { + if ( ! title || ! title.trim() ) { title = isCleanNewPost( state ) ? __( 'New post' ) : __( '(Untitled)' ); } return title; @@ -357,6 +381,20 @@ export const getBlockDependantsCacheBust = createSelector( ), ); +/** + * Returns a block's name given its UID, or null if no block exists with the + * UID. + * + * @param {Object} state Editor state. + * @param {string} uid Block unique ID. + * + * @return {string} Block name. + */ +export function getBlockName( state, uid ) { + const block = state.editor.present.blocksByUid[ uid ]; + return block ? block.name : null; +} + /** * Returns a block given its unique ID. This is a parsed copy of the block, * containing its `blockName`, identifier (`uid`), and current `attributes` @@ -439,6 +477,44 @@ export const getBlocks = createSelector( ] ); +/** + * Returns the total number of blocks, or the total number of blocks with a specific name in a post. + * The number returned includes nested blocks. + * + * @param {Object} state Global application state. + * @param {?String} blockName Optional block name, if specified only blocks of that type will be counted. + * + * @return {number} Number of blocks in the post, or number of blocks with name equal to blockName. + */ +export const getGlobalBlockCount = createSelector( + ( state, blockName ) => { + if ( ! blockName ) { + return size( state.editor.present.blocksByUid ); + } + return reduce( + state.editor.present.blocksByUid, + ( count, block ) => block.name === blockName ? count + 1 : count, + 0 + ); + }, + ( state ) => [ + state.editor.present.blocksByUid, + ] +); + +export const getBlocksByUID = createSelector( + ( state, uids ) => { + return map( uids, ( uid ) => getBlock( state, uid ) ); + }, + ( state ) => [ + state.editor.present.blocksByUid, + state.editor.present.blockOrder, + state.editor.present.edits.meta, + state.currentPost.meta, + state.editor.present.blocksByUid, + ] +); + /** * Returns the number of blocks currently present in the post. * @@ -451,6 +527,32 @@ export function getBlockCount( state, rootUID ) { return getBlockOrder( state, rootUID ).length; } +/** + * Returns the current block selection start. This value may be null, and it + * may represent either a singular block selection or multi-selection start. + * A selection is singular if its start and end match. + * + * @param {Object} state Global application state. + * + * @return {?string} UID of block selection start. + */ +export function getBlockSelectionStart( state ) { + return state.blockSelection.start; +} + +/** + * Returns the current block selection end. This value may be null, and it + * may represent either a singular block selection or multi-selection end. + * A selection is singular if its start and end match. + * + * @param {Object} state Global application state. + * + * @return {?string} UID of block selection end. + */ +export function getBlockSelectionEnd( state ) { + return state.blockSelection.end; +} + /** * Returns the number of blocks currently selected in the post. * @@ -468,6 +570,18 @@ export function getSelectedBlockCount( state ) { return state.blockSelection.start ? 1 : 0; } +/** + * Returns true if there is a single selected block, or false otherwise. + * + * @param {Object} state Editor state. + * + * @return {boolean} Whether a single block is selected. + */ +export function hasSelectedBlock( state ) { + const { start, end } = state.blockSelection; + return !! start && start === end; +} + /** * Returns the currently selected block, or null if there is no selected block. * @@ -832,7 +946,23 @@ export function isBlockWithinSelection( state, uid ) { } /** - * Whether in the process of multi-selecting or not. + * Returns true if a multi-selection has been made, or false otherwise. + * + * @param {Object} state Editor state. + * + * @return {boolean} Whether multi-selection has been made. + */ +export function hasMultiSelection( state ) { + const { start, end } = state.blockSelection; + return start !== end; +} + +/** + * Whether in the process of multi-selecting or not. This flag is only true + * while the multi-selection is being selected (by mouse move), and is false + * once the multi-selection has been settled. + * + * @see hasMultiSelection * * @param {Object} state Global application state. * @@ -911,6 +1041,36 @@ export function isBlockInsertionPointVisible( state ) { return state.isInsertionPointVisible; } +/** + * Returns whether the blocks matches the template or not. + * + * @param {boolean} state + * @return {?boolean} Whether the template is valid or not. + */ +export function isValidTemplate( state ) { + return state.template.isValid; +} + +/** + * Returns the defined block template + * + * @param {boolean} state + * @return {?Arary} Block Template + */ +export function getTemplate( state ) { + return state.template.template; +} + +/** + * Returns the defined block template lock + * + * @param {boolean} state + * @return {?string} Block Template Lock + */ +export function getTemplateLock( state ) { + return state.template.lock; +} + /** * Returns true if the post is currently being saved, or false otherwise. * @@ -1032,7 +1192,7 @@ export function getNotices( state ) { /** * An item that appears in the inserter. Inserting this item will create a new - * block. Inserter items encapsulate both regular blocks and reusable blocks. + * block. Inserter items encapsulate both regular blocks and shared blocks. * * @typedef {Object} Editor.InserterItem * @property {string} id Unique identifier for the item. @@ -1049,17 +1209,17 @@ export function getNotices( state ) { * Given a regular block type, constructs an item that appears in the inserter. * * @param {Object} state Global application state. - * @param {string[]|boolean} enabledBlockTypes Enabled block types, or true/false to enable/disable all types. + * @param {string[]|boolean} allowedBlockTypes Allowed block types, or true/false to enable/disable all types. * @param {Object} blockType Block type, likely from getBlockType(). * * @return {Editor.InserterItem} Item that appears in inserter. */ -function buildInserterItemFromBlockType( state, enabledBlockTypes, blockType ) { - if ( ! enabledBlockTypes || ! blockType ) { +function buildInserterItemFromBlockType( state, allowedBlockTypes, blockType ) { + if ( ! allowedBlockTypes || ! blockType ) { return null; } - const blockTypeIsDisabled = Array.isArray( enabledBlockTypes ) && ! enabledBlockTypes.includes( blockType.name ); + const blockTypeIsDisabled = Array.isArray( allowedBlockTypes ) && ! includes( allowedBlockTypes, blockType.name ); if ( blockTypeIsDisabled ) { return null; } @@ -1081,35 +1241,41 @@ function buildInserterItemFromBlockType( state, enabledBlockTypes, blockType ) { } /** - * Given a reusable block, constructs an item that appears in the inserter. + * Given a shared block, constructs an item that appears in the inserter. * - * @param {string[]|boolean} enabledBlockTypes Enabled block types, or true/false to enable/disable all types. - * @param {Object} reusableBlock Reusable block, likely from getReusableBlock(). + * @param {Object} state Global application state. + * @param {string[]|boolean} allowedBlockTypes Allowed block types, or true/false to enable/disable all types. + * @param {Object} sharedBlock Shared block, likely from getSharedBlock(). * * @return {Editor.InserterItem} Item that appears in inserter. */ -function buildInserterItemFromReusableBlock( enabledBlockTypes, reusableBlock ) { - if ( ! enabledBlockTypes || ! reusableBlock ) { +function buildInserterItemFromSharedBlock( state, allowedBlockTypes, sharedBlock ) { + if ( ! allowedBlockTypes || ! sharedBlock ) { return null; } - const blockTypeIsDisabled = Array.isArray( enabledBlockTypes ) && ! enabledBlockTypes.includes( 'core/block' ); + const blockTypeIsDisabled = Array.isArray( allowedBlockTypes ) && ! includes( allowedBlockTypes, 'core/block' ); if ( blockTypeIsDisabled ) { return null; } - const referencedBlockType = getBlockType( reusableBlock.type ); + const referencedBlock = getBlock( state, sharedBlock.uid ); + if ( ! referencedBlock ) { + return null; + } + + const referencedBlockType = getBlockType( referencedBlock.name ); if ( ! referencedBlockType ) { return null; } return { - id: `core/block/${ reusableBlock.id }`, + id: `core/block/${ sharedBlock.id }`, name: 'core/block', - initialAttributes: { ref: reusableBlock.id }, - title: reusableBlock.title, + initialAttributes: { ref: sharedBlock.id }, + title: sharedBlock.title, icon: referencedBlockType.icon, - category: 'reusable-blocks', + category: 'shared', keywords: [], isDisabled: false, }; @@ -1117,24 +1283,33 @@ function buildInserterItemFromReusableBlock( enabledBlockTypes, reusableBlock ) /** * Determines the items that appear in the the inserter. Includes both static - * items (e.g. a regular block type) and dynamic items (e.g. a reusable block). + * items (e.g. a regular block type) and dynamic items (e.g. a shared block). * * @param {Object} state Global application state. - * @param {string[]|boolean} enabledBlockTypes Enabled block types, or true/false to enable/disable all types. + * @param {string[]|boolean} allowedBlockTypes Allowed block types, or true/false to enable/disable all types. * * @return {Editor.InserterItem[]} Items that appear in inserter. */ -export function getInserterItems( state, enabledBlockTypes = true ) { - if ( ! enabledBlockTypes ) { +export function getInserterItems( state, allowedBlockTypes ) { + if ( allowedBlockTypes === undefined ) { + allowedBlockTypes = true; + deprecated( 'getInserterItems with no allowedBlockTypes argument', { + version: '2.8', + alternative: 'getInserterItems with an explcit allowedBlockTypes argument', + plugin: 'Gutenberg', + } ); + } + + if ( ! allowedBlockTypes ) { return []; } const staticItems = getBlockTypes().map( blockType => - buildInserterItemFromBlockType( state, enabledBlockTypes, blockType ) + buildInserterItemFromBlockType( state, allowedBlockTypes, blockType ) ); - const dynamicItems = getReusableBlocks( state ).map( reusableBlock => - buildInserterItemFromReusableBlock( enabledBlockTypes, reusableBlock ) + const dynamicItems = getSharedBlocks( state ).map( sharedBlock => + buildInserterItemFromSharedBlock( state, allowedBlockTypes, sharedBlock ) ); const items = [ ...staticItems, ...dynamicItems ]; @@ -1154,86 +1329,133 @@ function fillWithCommonBlocks( inserts ) { return unionWith( items, commonInserts, areInsertsEqual ); } -function getItemsFromInserts( state, inserts, enabledBlockTypes = true, maximum = MAX_RECENT_BLOCKS ) { - if ( ! enabledBlockTypes ) { +function getItemsFromInserts( state, inserts, allowedBlockTypes, maximum = MAX_RECENT_BLOCKS ) { + if ( ! allowedBlockTypes ) { return []; } const items = fillWithCommonBlocks( inserts ).map( insert => { if ( insert.ref ) { - const reusableBlock = getReusableBlock( state, insert.ref ); - return buildInserterItemFromReusableBlock( enabledBlockTypes, reusableBlock ); + const sharedBlock = getSharedBlock( state, insert.ref ); + return buildInserterItemFromSharedBlock( state, allowedBlockTypes, sharedBlock ); } const blockType = getBlockType( insert.name ); - return buildInserterItemFromBlockType( state, enabledBlockTypes, blockType ); + return buildInserterItemFromBlockType( state, allowedBlockTypes, blockType ); } ); return compact( items ).slice( 0, maximum ); } /** - * Determines the items that appear in the 'Recent' tab of the inserter. + * Returns a list of items which the user is likely to want to insert. These + * are ordered by 'frecency', which is a heuristic that combines block usage + * frequency and recency. + * + * https://en.wikipedia.org/wiki/Frecency * * @param {Object} state Global application state. - * @param {string[]|boolean} enabledBlockTypes Enabled block types, or true/false to enable/disable all types. + * @param {string[]|boolean} allowedBlockTypes Allowed block types, or true/false to enable/disable all types. * @param {number} maximum Number of items to return. * * @return {Editor.InserterItem[]} Items that appear in the 'Recent' tab. */ -export function getRecentInserterItems( state, enabledBlockTypes = true, maximum = MAX_RECENT_BLOCKS ) { - return getItemsFromInserts( state, state.preferences.recentInserts, enabledBlockTypes, maximum ); +export function getFrecentInserterItems( state, allowedBlockTypes, maximum = MAX_RECENT_BLOCKS ) { + if ( allowedBlockTypes === undefined ) { + allowedBlockTypes = true; + deprecated( 'getFrecentInserterItems with no allowedBlockTypes argument', { + version: '2.8', + alternative: 'getFrecentInserterItems with an explcit allowedBlockTypes argument', + plugin: 'Gutenberg', + } ); + } + + const calculateFrecency = ( time, count ) => { + if ( ! time ) { + return count; + } + + const duration = Date.now() - time; + switch ( true ) { + case duration < 3600: + return count * 4; + case duration < ( 24 * 3600 ): + return count * 2; + case duration < ( 7 * 24 * 3600 ): + return count / 2; + default: + return count / 4; + } + }; + + const sortedInserts = values( state.preferences.insertUsage ) + .sort( ( a, b ) => calculateFrecency( b.time, b.count ) - calculateFrecency( a.time, a.count ) ) + .map( ( { insert } ) => insert ); + return getItemsFromInserts( state, sortedInserts, allowedBlockTypes, maximum ); } /** - * Determines the items that appear in the inserter with shortcuts based on the block usage + * Returns the shared block with the given ID. * - * @param {Object} state Global application state. - * @param {string[]|boolean} enabledBlockTypes Enabled block types, or true/false to enable/disable all types. - * @param {number} maximum Number of items to return. + * @param {Object} state Global application state. + * @param {number|string} ref The shared block's ID. * - * @return {Editor.InserterItem[]} Items that appear in the 'Recent' tab. + * @return {Object} The shared block, or null if none exists. */ -export function getFrequentInserterItems( state, enabledBlockTypes = true, maximum = MAX_RECENT_BLOCKS ) { - const sortedInserts = values( state.preferences.insertUsage ) - .sort( ( a, b ) => b.count - a.count ) - .map( ( { insert } ) => insert ); - return getItemsFromInserts( state, sortedInserts, enabledBlockTypes, maximum ); -} +export const getSharedBlock = createSelector( + ( state, ref ) => { + const block = state.sharedBlocks.data[ ref ]; + if ( ! block ) { + return null; + } + + const isTemporary = isNaN( parseInt( ref ) ); + + return { + ...block, + id: isTemporary ? ref : +ref, + isTemporary, + }; + }, + ( state, ref ) => [ + state.sharedBlocks.data[ ref ], + ], +); /** - * Returns the reusable block with the given ID. + * Returns whether or not the shared block with the given ID is being saved. * * @param {Object} state Global application state. - * @param {string} ref The reusable block's ID. + * @param {string} ref The shared block's ID. * - * @return {Object} The reusable block, or null if none exists. + * @return {boolean} Whether or not the shared block is being saved. */ -export function getReusableBlock( state, ref ) { - return state.reusableBlocks.data[ ref ] || null; +export function isSavingSharedBlock( state, ref ) { + return state.sharedBlocks.isSaving[ ref ] || false; } /** - * Returns whether or not the reusable block with the given ID is being saved. + * Returns true if the shared block with the given ID is being fetched, or + * false otherwise. * - * @param {*} state Global application state. - * @param {*} ref The reusable block's ID. + * @param {Object} state Global application state. + * @param {string} ref The shared block's ID. * - * @return {boolean} Whether or not the reusable block is being saved. + * @return {boolean} Whether the shared block is being fetched. */ -export function isSavingReusableBlock( state, ref ) { - return state.reusableBlocks.isSaving[ ref ] || false; +export function isFetchingSharedBlock( state, ref ) { + return !! state.sharedBlocks.isFetching[ ref ]; } /** - * Returns an array of all reusable blocks. + * Returns an array of all shared blocks. * * @param {Object} state Global application state. * - * @return {Array} An array of all reusable blocks. + * @return {Array} An array of all shared blocks. */ -export function getReusableBlocks( state ) { - return Object.values( state.reusableBlocks.data ); +export function getSharedBlocks( state ) { + return map( state.sharedBlocks.data, ( value, ref ) => getSharedBlock( state, ref ) ); } /** @@ -1283,3 +1505,14 @@ export function isPublishingPost( state ) { // considered published return !! stateBeforeRequest && ! isCurrentPostPublished( stateBeforeRequest ); } + +/** + * Returns the provisional block UID, or null if there is no provisional block. + * + * @param {Object} state Editor state. + * + * @return {?string} Provisional block UID, if set. + */ +export function getProvisionalBlockUID( state ) { + return state.provisionalBlockUID; +} diff --git a/editor/store/test/actions.js b/editor/store/test/actions.js index f73555a4866bbf..97a0291b9f23db 100644 --- a/editor/store/test/actions.js +++ b/editor/store/test/actions.js @@ -5,12 +5,11 @@ import { replaceBlocks, startTyping, stopTyping, - fetchReusableBlocks, - updateReusableBlock, - saveReusableBlock, - deleteReusableBlock, + fetchSharedBlocks, + saveSharedBlock, + deleteSharedBlock, convertBlockToStatic, - convertBlockToReusable, + convertBlockToShared, toggleSelection, setupEditor, resetPost, @@ -163,6 +162,7 @@ describe( 'actions', () => { type: 'REPLACE_BLOCKS', uids: [ 'chicken' ], blocks: [ block ], + time: expect.any( Number ), } ); } ); } ); @@ -177,6 +177,7 @@ describe( 'actions', () => { type: 'REPLACE_BLOCKS', uids: [ 'chicken' ], blocks, + time: expect.any( Number ), } ); } ); } ); @@ -187,10 +188,12 @@ describe( 'actions', () => { uid: 'ribs', }; const index = 5; - expect( insertBlock( block, index ) ).toEqual( { + expect( insertBlock( block, index, 'test_uid' ) ).toEqual( { type: 'INSERT_BLOCKS', blocks: [ block ], index, + rootUID: 'test_uid', + time: expect.any( Number ), } ); } ); } ); @@ -201,10 +204,12 @@ describe( 'actions', () => { uid: 'ribs', } ]; const index = 3; - expect( insertBlocks( blocks, index ) ).toEqual( { + expect( insertBlocks( blocks, index, 'test_uid' ) ).toEqual( { type: 'INSERT_BLOCKS', blocks, index, + rootUID: 'test_uid', + time: expect.any( Number ), } ); } ); } ); @@ -296,6 +301,7 @@ describe( 'actions', () => { expect( removeBlocks( uids ) ).toEqual( { type: 'REMOVE_BLOCKS', uids, + selectPrevious: true, } ); } ); } ); @@ -308,6 +314,14 @@ describe( 'actions', () => { uids: [ uid, ], + selectPrevious: true, + } ); + expect( removeBlock( uid, false ) ).toEqual( { + type: 'REMOVE_BLOCKS', + uids: [ + uid, + ], + selectPrevious: false, } ); } ); } ); @@ -446,53 +460,34 @@ describe( 'actions', () => { } ); } ); - describe( 'fetchReusableBlocks', () => { - it( 'should return the FETCH_REUSABLE_BLOCKS action', () => { - expect( fetchReusableBlocks() ).toEqual( { - type: 'FETCH_REUSABLE_BLOCKS', + describe( 'fetchSharedBlocks', () => { + it( 'should return the FETCH_SHARED_BLOCKS action', () => { + expect( fetchSharedBlocks() ).toEqual( { + type: 'FETCH_SHARED_BLOCKS', } ); } ); it( 'should take an optional id argument', () => { const id = '358b59ee-bab3-4d6f-8445-e8c6971a5605'; - expect( fetchReusableBlocks( id ) ).toEqual( { - type: 'FETCH_REUSABLE_BLOCKS', - id, - } ); - } ); - } ); - - describe( 'updateReusableBlock', () => { - it( 'should return the UPDATE_REUSABLE_BLOCK action', () => { - const id = '358b59ee-bab3-4d6f-8445-e8c6971a5605'; - const reusableBlock = { - id, - name: 'My cool block', - type: 'core/paragraph', - attributes: { - content: 'Hello!', - }, - }; - expect( updateReusableBlock( id, reusableBlock ) ).toEqual( { - type: 'UPDATE_REUSABLE_BLOCK', + expect( fetchSharedBlocks( id ) ).toEqual( { + type: 'FETCH_SHARED_BLOCKS', id, - reusableBlock, } ); } ); } ); - describe( 'saveReusableBlock', () => { + describe( 'saveSharedBlock', () => { const id = '358b59ee-bab3-4d6f-8445-e8c6971a5605'; - expect( saveReusableBlock( id ) ).toEqual( { - type: 'SAVE_REUSABLE_BLOCK', + expect( saveSharedBlock( id ) ).toEqual( { + type: 'SAVE_SHARED_BLOCK', id, } ); } ); - describe( 'deleteReusableBlock', () => { + describe( 'deleteSharedBlock', () => { const id = 123; - expect( deleteReusableBlock( id ) ).toEqual( { - type: 'DELETE_REUSABLE_BLOCK', + expect( deleteSharedBlock( id ) ).toEqual( { + type: 'DELETE_SHARED_BLOCK', id, } ); } ); @@ -505,10 +500,10 @@ describe( 'actions', () => { } ); } ); - describe( 'convertBlockToReusable', () => { + describe( 'convertBlockToShared', () => { const uid = '358b59ee-bab3-4d6f-8445-e8c6971a5605'; - expect( convertBlockToReusable( uid ) ).toEqual( { - type: 'CONVERT_BLOCK_TO_REUSABLE', + expect( convertBlockToShared( uid ) ).toEqual( { + type: 'CONVERT_BLOCK_TO_SHARED', uid, } ); } ); diff --git a/editor/store/test/effects.js b/editor/store/test/effects.js index b429783483913f..7bddd4de66d8a5 100644 --- a/editor/store/test/effects.js +++ b/editor/store/test/effects.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { noop, reduce, set } from 'lodash'; +import { noop, set, reduce } from 'lodash'; /** * WordPress dependencies @@ -11,7 +11,6 @@ import { unregisterBlockType, registerBlockType, createBlock, - createReusableBlock, } from '@wordpress/blocks'; /** @@ -19,30 +18,73 @@ import { */ import { setupEditorState, - resetBlocks, mergeBlocks, replaceBlocks, savePost, - updateReusableBlock, - saveReusableBlock, - deleteReusableBlock, - fetchReusableBlocks, - convertBlockToStatic, - convertBlockToReusable, selectBlock, -} from '../../store/actions'; + removeBlock, + createErrorNotice, + fetchSharedBlocks, + receiveSharedBlocks, + receiveBlocks, + saveSharedBlock, + deleteSharedBlock, + removeBlocks, + resetBlocks, + convertBlockToStatic, + convertBlockToShared, + setTemplateValidity, +} from '../actions'; +import effects, { + removeProvisionalBlock, +} from '../effects'; +import * as selectors from '../selectors'; import reducer from '../reducer'; -import effects from '../effects'; -import * as selectors from '../../store/selectors'; - -// Make all generated UUIDs the same for testing -jest.mock( 'uuid/v4', () => { - return jest.fn( () => 'this-is-a-mock-uuid' ); -} ); describe( 'effects', () => { const defaultBlockSettings = { save: () => 'Saved', category: 'common', title: 'block title' }; + describe( 'removeProvisionalBlock()', () => { + const store = { getState: () => {} }; + + beforeAll( () => { + selectors.getProvisionalBlockUID = jest.spyOn( selectors, 'getProvisionalBlockUID' ); + selectors.isBlockSelected = jest.spyOn( selectors, 'isBlockSelected' ); + } ); + + beforeEach( () => { + selectors.getProvisionalBlockUID.mockReset(); + selectors.isBlockSelected.mockReset(); + } ); + + afterAll( () => { + selectors.getProvisionalBlockUID.mockRestore(); + selectors.isBlockSelected.mockRestore(); + } ); + + it( 'should return nothing if there is no provisional block', () => { + const action = removeProvisionalBlock( {}, store ); + + expect( action ).toBeUndefined(); + } ); + + it( 'should return nothing if there is a provisional block and it is selected', () => { + selectors.getProvisionalBlockUID.mockReturnValue( 'chicken' ); + selectors.isBlockSelected.mockImplementation( ( state, uid ) => uid === 'chicken' ); + const action = removeProvisionalBlock( {}, store ); + + expect( action ).toBeUndefined(); + } ); + + it( 'should return remove action for provisional block', () => { + selectors.getProvisionalBlockUID.mockReturnValue( 'chicken' ); + selectors.isBlockSelected.mockImplementation( ( state, uid ) => uid === 'ribs' ); + const action = removeProvisionalBlock( {}, store ); + + expect( action ).toEqual( removeBlock( 'chicken', false ) ); + } ); + } ); + describe( '.MERGE_BLOCKS', () => { const handler = effects.MERGE_BLOCKS; const defaultGetBlock = selectors.getBlock; @@ -106,11 +148,14 @@ describe( 'effects', () => { expect( dispatch ).toHaveBeenCalledTimes( 2 ); expect( dispatch ).toHaveBeenCalledWith( selectBlock( 'chicken', -1 ) ); - expect( dispatch ).toHaveBeenCalledWith( replaceBlocks( [ 'chicken', 'ribs' ], [ { - uid: 'chicken', - name: 'core/test-block', - attributes: { content: 'chicken ribs' }, - } ] ) ); + expect( dispatch ).toHaveBeenCalledWith( { + ...replaceBlocks( [ 'chicken', 'ribs' ], [ { + uid: 'chicken', + name: 'core/test-block', + attributes: { content: 'chicken ribs' }, + } ] ), + time: expect.any( Number ), + } ); } ); it( 'should not merge the blocks have different types without transformation', () => { @@ -201,11 +246,14 @@ describe( 'effects', () => { expect( dispatch ).toHaveBeenCalledTimes( 2 ); // expect( dispatch ).toHaveBeenCalledWith( focusBlock( 'chicken', { offset: -1 } ) ); - expect( dispatch ).toHaveBeenCalledWith( replaceBlocks( [ 'chicken', 'ribs' ], [ { - uid: 'chicken', - name: 'core/test-block', - attributes: { content: 'chicken ribs' }, - } ] ) ); + expect( dispatch ).toHaveBeenCalledWith( { + ...replaceBlocks( [ 'chicken', 'ribs' ], [ { + uid: 'chicken', + name: 'core/test-block', + attributes: { content: 'chicken ribs' }, + } ] ), + time: expect.any( Number ), + } ); } ); } ); @@ -394,6 +442,62 @@ describe( 'effects', () => { } ); } ); + describe( '.REQUEST_POST_UPDATE_FAILURE', () => { + it( 'should dispatch a notice on failure when publishing a draft fails.', () => { + const handler = effects.REQUEST_POST_UPDATE_FAILURE; + const dispatch = jest.fn(); + const store = { getState: () => {}, dispatch }; + + const action = { + post: { + id: 1, + title: { + raw: 'A History of Pork', + }, + content: { + raw: '', + }, + status: 'draft', + }, + edits: { + status: 'publish', + }, + }; + + handler( action, store ); + + expect( dispatch ).toHaveBeenCalledTimes( 1 ); + expect( dispatch ).toHaveBeenCalledWith( createErrorNotice( 'Publishing failed', { id: 'SAVE_POST_NOTICE_ID' } ) ); + } ); + + it( 'should dispatch a notice on failure when trying to update a draft.', () => { + const handler = effects.REQUEST_POST_UPDATE_FAILURE; + const dispatch = jest.fn(); + const store = { getState: () => {}, dispatch }; + + const action = { + post: { + id: 1, + title: { + raw: 'A History of Pork', + }, + content: { + raw: '', + }, + status: 'draft', + }, + edits: { + status: 'draft', + }, + }; + + handler( action, store ); + + expect( dispatch ).toHaveBeenCalledTimes( 1 ); + expect( dispatch ).toHaveBeenCalledWith( createErrorNotice( 'Updating failed', { id: 'SAVE_POST_NOTICE_ID' } ) ); + } ); + } ); + describe( '.SETUP_EDITOR', () => { const handler = effects.SETUP_EDITOR; @@ -417,7 +521,10 @@ describe( 'effects', () => { const result = handler( { post, settings: {} } ); - expect( result ).toEqual( setupEditorState( post, [], {} ) ); + expect( result ).toEqual( [ + setTemplateValidity( true ), + setupEditorState( post, [], {} ), + ] ); } ); it( 'should return block reset with non-empty content', () => { @@ -435,8 +542,11 @@ describe( 'effects', () => { const result = handler( { post, settings: {} } ); - expect( result.blocks ).toHaveLength( 1 ); - expect( result ).toEqual( setupEditorState( post, result.blocks, {} ) ); + expect( result[ 1 ].blocks ).toHaveLength( 1 ); + expect( result ).toEqual( [ + setTemplateValidity( true ), + setupEditorState( post, result[ 1 ].blocks, {} ), + ] ); } ); it( 'should return post setup action only if auto-draft', () => { @@ -453,11 +563,14 @@ describe( 'effects', () => { const result = handler( { post, settings: {} } ); - expect( result ).toEqual( setupEditorState( post, [], { title: 'A History of Pork', status: 'draft' } ) ); + expect( result ).toEqual( [ + setTemplateValidity( true ), + setupEditorState( post, [], { title: 'A History of Pork', status: 'draft' } ), + ] ); } ); } ); - describe( 'reusable block effects', () => { + describe( 'shared block effects', () => { beforeAll( () => { registerBlockType( 'core/test-block', { title: 'Test block', @@ -468,7 +581,7 @@ describe( 'effects', () => { }, } ); registerBlockType( 'core/block', { - title: 'Reusable Block', + title: 'Shared Block', category: 'common', save: () => null, attributes: { @@ -482,86 +595,83 @@ describe( 'effects', () => { unregisterBlockType( 'core/block' ); } ); - describe( '.FETCH_REUSABLE_BLOCKS', () => { - const handler = effects.FETCH_REUSABLE_BLOCKS; + describe( '.FETCH_SHARED_BLOCKS', () => { + const handler = effects.FETCH_SHARED_BLOCKS; - it( 'should fetch multiple reusable blocks', () => { + it( 'should fetch multiple shared blocks', () => { const promise = Promise.resolve( [ { - id: 'a9691cf9-ecaa-42bd-a9ca-49587e817647', + id: 123, title: 'My cool block', - content: '<!-- wp:core/test-block {"name":"Big Bird"} /-->', + content: '<!-- wp:test-block {"name":"Big Bird"} /-->', }, ] ); - set( global, 'wp.api.collections.Blocks', class { - fetch() { - return promise; - } - } ); + set( global, 'wp.api.getPostTypeRoute', () => 'blocks' ); + set( global, 'wp.apiRequest', () => promise ); const dispatch = jest.fn(); - const store = { getState: () => {}, dispatch }; + const store = { getState: noop, dispatch }; - handler( fetchReusableBlocks(), store ); + handler( fetchSharedBlocks(), store ); return promise.then( () => { - expect( dispatch ).toHaveBeenCalledWith( { - type: 'FETCH_REUSABLE_BLOCKS_SUCCESS', - reusableBlocks: [ + expect( dispatch ).toHaveBeenCalledWith( + receiveSharedBlocks( [ { - id: 'a9691cf9-ecaa-42bd-a9ca-49587e817647', - title: 'My cool block', - type: 'core/test-block', - attributes: { - name: 'Big Bird', + sharedBlock: { + id: 123, + title: 'My cool block', + content: '<!-- wp:test-block {"name":"Big Bird"} /-->', }, + parsedBlock: expect.objectContaining( { + name: 'core/test-block', + attributes: { name: 'Big Bird' }, + } ), }, - ], + ] ) + ); + expect( dispatch ).toHaveBeenCalledWith( { + type: 'FETCH_SHARED_BLOCKS_SUCCESS', + id: undefined, } ); } ); } ); - it( 'should fetch a single reusable block', () => { - const id = 123; - - let modelAttributes; + it( 'should fetch a single shared block', () => { const promise = Promise.resolve( { - id, + id: 123, title: 'My cool block', - content: '<!-- wp:core/test-block {"name":"Big Bird"} /-->', + content: '<!-- wp:test-block {"name":"Big Bird"} /-->', } ); - set( global, 'wp.api.models.Blocks', class { - constructor( attributes ) { - modelAttributes = attributes; - } - - fetch() { - return promise; - } - } ); + set( global, 'wp.api.getPostTypeRoute', () => 'blocks' ); + set( global, 'wp.apiRequest', () => promise ); const dispatch = jest.fn(); - const store = { getState: () => {}, dispatch }; + const store = { getState: noop, dispatch }; - handler( fetchReusableBlocks( id ), store ); + handler( fetchSharedBlocks( 123 ), store ); - expect( modelAttributes ).toEqual( { id } ); return promise.then( () => { - expect( dispatch ).toHaveBeenCalledWith( { - type: 'FETCH_REUSABLE_BLOCKS_SUCCESS', - id, - reusableBlocks: [ + expect( dispatch ).toHaveBeenCalledWith( + receiveSharedBlocks( [ { - id, - title: 'My cool block', - type: 'core/test-block', - attributes: { - name: 'Big Bird', + sharedBlock: { + id: 123, + title: 'My cool block', + content: '<!-- wp:test-block {"name":"Big Bird"} /-->', }, + parsedBlock: expect.objectContaining( { + name: 'core/test-block', + attributes: { name: 'Big Bird' }, + } ), }, - ], + ] ) + ); + expect( dispatch ).toHaveBeenCalledWith( { + type: 'FETCH_SHARED_BLOCKS_SUCCESS', + id: 123, } ); } ); } ); @@ -569,20 +679,17 @@ describe( 'effects', () => { it( 'should handle an API error', () => { const promise = Promise.reject( {} ); - set( global, 'wp.api.collections.Blocks', class { - fetch() { - return promise; - } - } ); + set( global, 'wp.api.getPostTypeRoute', () => 'blocks' ); + set( global, 'wp.apiRequest', () => promise ); const dispatch = jest.fn(); - const store = { getState: () => {}, dispatch }; + const store = { getState: noop, dispatch }; - handler( fetchReusableBlocks(), store ); + handler( fetchSharedBlocks(), store ); return promise.catch( () => { expect( dispatch ).toHaveBeenCalledWith( { - type: 'FETCH_REUSABLE_BLOCKS_FAILURE', + type: 'FETCH_SHARED_BLOCKS_FAILURE', error: { code: 'unknown_error', message: 'An unknown error occurred.', @@ -592,45 +699,59 @@ describe( 'effects', () => { } ); } ); - describe( '.SAVE_REUSABLE_BLOCK', () => { - const handler = effects.SAVE_REUSABLE_BLOCK; + describe( '.RECEIVE_SHARED_BLOCKS', () => { + const handler = effects.RECEIVE_SHARED_BLOCKS; - it( 'should save a reusable block and swaps its id', () => { - let modelAttributes; - const promise = Promise.resolve( { id: 3 } ); + it( 'should receive parsed blocks', () => { + const action = receiveSharedBlocks( [ + { + parsedBlock: { uid: 'broccoli' }, + }, + ] ); - set( global, 'wp.api.models.Blocks', class { - constructor( attributes ) { - modelAttributes = attributes; - } + expect( handler( action ) ).toEqual( receiveBlocks( [ + { uid: 'broccoli' }, + ] ) ); + } ); + } ); - save() { - return promise; - } - } ); + describe( '.SAVE_SHARED_BLOCK', () => { + const handler = effects.SAVE_SHARED_BLOCK; + + it( 'should save a shared block and swap its id', () => { + let modelAttributes; + const promise = Promise.resolve( { id: 456 } ); - const reusableBlock = createReusableBlock( 'core/test-block', { - name: 'Big Bird', + set( global, 'wp.api.getPostTypeRoute', () => 'blocks' ); + set( global, 'wp.apiRequest', ( request ) => { + modelAttributes = request.data; + return promise; } ); - const initialState = reducer( undefined, {} ); - const action = updateReusableBlock( reusableBlock.id, reusableBlock ); - const state = reducer( initialState, action ); + const sharedBlock = { id: 123, title: 'My cool block' }; + const parsedBlock = createBlock( 'core/test-block', { name: 'Big Bird' } ); + + const state = reduce( [ + receiveSharedBlocks( [ { sharedBlock, parsedBlock } ] ), + receiveBlocks( [ parsedBlock ] ), + ], reducer, undefined ); const dispatch = jest.fn(); const store = { getState: () => state, dispatch }; - handler( saveReusableBlock( reusableBlock.id ), store ); + handler( saveSharedBlock( 123 ), store ); expect( modelAttributes ).toEqual( { - title: 'Untitled block', + id: 123, + title: 'My cool block', content: '<!-- wp:test-block {\"name\":\"Big Bird\"} /-->', } ); + return promise.then( () => { expect( dispatch ).toHaveBeenCalledWith( { - type: 'SAVE_REUSABLE_BLOCK_SUCCESS', - id: reusableBlock.id, - updatedId: 3, + type: 'SAVE_SHARED_BLOCK_SUCCESS', + id: 123, + updatedId: 456, } ); } ); } ); @@ -638,79 +759,69 @@ describe( 'effects', () => { it( 'should handle an API error', () => { const promise = Promise.reject( {} ); - set( global, 'wp.api.models.Blocks', class { - save() { - return promise; - } - } ); + set( global, 'wp.api.getPostTypeRoute', () => 'blocks' ); + set( global, 'wp.apiRequest', () => promise ); - const reusableBlock = createReusableBlock( 'core/test-block', { - name: 'Big Bird', - } ); + const sharedBlock = { id: 123, title: 'My cool block' }; + const parsedBlock = createBlock( 'core/test-block', { name: 'Big Bird' } ); - const initialState = reducer( undefined, {} ); - const action = updateReusableBlock( reusableBlock.id, reusableBlock ); - const state = reducer( initialState, action ); + const state = reduce( [ + receiveSharedBlocks( [ { sharedBlock, parsedBlock } ] ), + receiveBlocks( [ parsedBlock ] ), + ], reducer, undefined ); const dispatch = jest.fn(); const store = { getState: () => state, dispatch }; - handler( saveReusableBlock( reusableBlock.id ), store ); + handler( saveSharedBlock( 123 ), store ); return promise.catch( () => { expect( dispatch ).toHaveBeenCalledWith( { - type: 'SAVE_REUSABLE_BLOCK_FAILURE', - id: reusableBlock.id, + type: 'SAVE_SHARED_BLOCK_FAILURE', + id: 123, } ); } ); } ); } ); - describe( '.DELETE_REUSABLE_BLOCK', () => { - const handler = effects.DELETE_REUSABLE_BLOCK; + describe( '.DELETE_SHARED_BLOCK', () => { + const handler = effects.DELETE_SHARED_BLOCK; - it( 'should delete a reusable block', () => { - let modelAttributes; + it( 'should delete a shared block', () => { const promise = Promise.resolve( {} ); - set( global, 'wp.api.models.Blocks', class { - constructor( attributes ) { - modelAttributes = attributes; - } - - destroy() { - return promise; - } - } ); + set( global, 'wp.api.getPostTypeRoute', () => 'blocks' ); + set( global, 'wp.apiRequest', () => promise ); - const id = 123; + const associatedBlock = createBlock( 'core/block', { ref: 123 } ); + const sharedBlock = { id: 123, title: 'My cool block' }; + const parsedBlock = createBlock( 'core/test-block', { name: 'Big Bird' } ); - const associatedBlock = createBlock( 'core/block', { - ref: id, - } ); - - const actions = [ + const state = reduce( [ resetBlocks( [ associatedBlock ] ), - updateReusableBlock( id, {} ), - ]; - const state = actions.reduce( reducer, undefined ); + receiveSharedBlocks( [ { sharedBlock, parsedBlock } ] ), + receiveBlocks( [ parsedBlock ] ), + ], reducer, undefined ); const dispatch = jest.fn(); const store = { getState: () => state, dispatch }; - handler( deleteReusableBlock( id ), store ); + handler( deleteSharedBlock( 123 ), store ); expect( dispatch ).toHaveBeenCalledWith( { - type: 'REMOVE_REUSABLE_BLOCK', - id, - associatedBlockUids: [ associatedBlock.uid ], + type: 'REMOVE_SHARED_BLOCK', + id: 123, optimist: expect.any( Object ), } ); - expect( modelAttributes ).toEqual( { id } ); + + expect( dispatch ).toHaveBeenCalledWith( + removeBlocks( [ associatedBlock.uid, parsedBlock.uid ] ) + ); + return promise.then( () => { expect( dispatch ).toHaveBeenCalledWith( { - type: 'DELETE_REUSABLE_BLOCK_SUCCESS', - id, + type: 'DELETE_SHARED_BLOCK_SUCCESS', + id: 123, optimist: expect.any( Object ), } ); } ); @@ -719,40 +830,44 @@ describe( 'effects', () => { it( 'should handle an API error', () => { const promise = Promise.reject( {} ); - set( global, 'wp.api.models.Blocks', class { - destroy() { - return promise; - } - } ); + set( global, 'wp.api.getPostTypeRoute', () => 'blocks' ); + set( global, 'wp.apiRequest', () => promise ); + + const sharedBlock = { id: 123, title: 'My cool block' }; + const parsedBlock = createBlock( 'core/test-block', { name: 'Big Bird' } ); - const state = reducer( undefined, updateReusableBlock( 123, {} ) ); + const state = reduce( [ + receiveSharedBlocks( [ { sharedBlock, parsedBlock } ] ), + receiveBlocks( [ parsedBlock ] ), + ], reducer, undefined ); const dispatch = jest.fn(); const store = { getState: () => state, dispatch }; - handler( deleteReusableBlock( 123 ), store ); + handler( deleteSharedBlock( 123 ), store ); return promise.catch( () => { expect( dispatch ).toHaveBeenCalledWith( { - type: 'DELETE_REUSABLE_BLOCK_FAILURE', + type: 'DELETE_SHARED_BLOCK_FAILURE', id: 123, optimist: expect.any( Object ), } ); } ); } ); - it( 'should not save reusable blocks with temporary IDs', () => { - const reusableBlock = { - id: -123, - isTemporary: true, - }; + it( 'should not save shared blocks with temporary IDs', () => { + const sharedBlock = { id: 'shared1', title: 'My cool block' }; + const parsedBlock = createBlock( 'core/test-block', { name: 'Big Bird' } ); - const state = reducer( undefined, updateReusableBlock( -123, reusableBlock ) ); + const state = reduce( [ + receiveSharedBlocks( [ { sharedBlock, parsedBlock } ] ), + receiveBlocks( [ parsedBlock ] ), + ], reducer, undefined ); const dispatch = jest.fn(); const store = { getState: () => state, dispatch }; - handler( deleteReusableBlock( -123 ), store ); + handler( deleteSharedBlock( 'shared1' ), store ); expect( dispatch ).not.toHaveBeenCalled(); } ); @@ -761,68 +876,77 @@ describe( 'effects', () => { describe( '.CONVERT_BLOCK_TO_STATIC', () => { const handler = effects.CONVERT_BLOCK_TO_STATIC; - it( 'should convert a reusable block into a static block', () => { - const reusableBlock = createReusableBlock( 'core/test-block', { - name: 'Big Bird', - } ); - const staticBlock = createBlock( 'core/block', { - ref: reusableBlock.id, - } ); + it( 'should convert a shared block into a static block', () => { + const associatedBlock = createBlock( 'core/block', { ref: 123 } ); + const sharedBlock = { id: 123, title: 'My cool block' }; + const parsedBlock = createBlock( 'core/test-block', { name: 'Big Bird' } ); - const actions = [ - resetBlocks( [ staticBlock ] ), - updateReusableBlock( reusableBlock.id, reusableBlock ), - ]; - const initialState = reducer( undefined, {} ); - const state = reduce( actions, reducer, initialState ); + const state = reduce( [ + resetBlocks( [ associatedBlock ] ), + receiveSharedBlocks( [ { sharedBlock, parsedBlock } ] ), + receiveBlocks( [ parsedBlock ] ), + ], reducer, undefined ); const dispatch = jest.fn(); const store = { getState: () => state, dispatch }; - handler( convertBlockToStatic( staticBlock.uid ), store ); + handler( convertBlockToStatic( associatedBlock.uid ), store ); - expect( dispatch ).toHaveBeenCalledWith( - replaceBlocks( - [ staticBlock.uid ], - createBlock( reusableBlock.type, reusableBlock.attributes ) - ) - ); + expect( dispatch ).toHaveBeenCalledWith( { + type: 'REPLACE_BLOCKS', + uids: [ associatedBlock.uid ], + blocks: [ + expect.objectContaining( { + name: 'core/test-block', + attributes: { name: 'Big Bird' }, + } ), + ], + time: expect.any( Number ), + } ); } ); } ); - describe( '.CONVERT_BLOCK_TO_REUSABLE', () => { - const handler = effects.CONVERT_BLOCK_TO_REUSABLE; - - it( 'should convert a static block into a reusable block', () => { - const staticBlock = createBlock( 'core/test-block', { - name: 'Big Bird', - } ); + describe( '.CONVERT_BLOCK_TO_SHARED', () => { + const handler = effects.CONVERT_BLOCK_TO_SHARED; - const initialState = reducer( undefined, {} ); - const state = reducer( initialState, resetBlocks( [ staticBlock ] ) ); + it( 'should convert a static block into a shared block', () => { + const staticBlock = createBlock( 'core/block', { ref: 123 } ); + const state = reducer( undefined, resetBlocks( [ staticBlock ] ) ); const dispatch = jest.fn(); const store = { getState: () => state, dispatch }; - handler( convertBlockToReusable( staticBlock.uid ), store ); + handler( convertBlockToShared( staticBlock.uid ), store ); expect( dispatch ).toHaveBeenCalledWith( - updateReusableBlock( expect.any( Number ), { - id: expect.any( Number ), - isTemporary: true, - title: 'Untitled block', - type: staticBlock.name, - attributes: staticBlock.attributes, - } ) + receiveSharedBlocks( [ { + sharedBlock: { + id: expect.stringMatching( /^shared/ ), + uid: staticBlock.uid, + title: 'Untitled block', + }, + parsedBlock: staticBlock, + } ] ) ); + expect( dispatch ).toHaveBeenCalledWith( - saveReusableBlock( expect.any( Number ) ) + saveSharedBlock( expect.stringMatching( /^shared/ ) ), ); + + expect( dispatch ).toHaveBeenCalledWith( { + type: 'REPLACE_BLOCKS', + uids: [ staticBlock.uid ], + blocks: [ + expect.objectContaining( { + name: 'core/block', + attributes: { ref: expect.stringMatching( /^shared/ ) }, + } ), + ], + time: expect.any( Number ), + } ); + expect( dispatch ).toHaveBeenCalledWith( - replaceBlocks( - [ staticBlock.uid ], - [ createBlock( 'core/block', { ref: expect.any( Number ) } ) ] - ) + receiveBlocks( [ staticBlock ] ), ); } ); } ); diff --git a/editor/store/test/reducer.js b/editor/store/test/reducer.js index 81df4129511e29..c326a2ce433295 100644 --- a/editor/store/test/reducer.js +++ b/editor/store/test/reducer.js @@ -18,6 +18,10 @@ import { * Internal dependencies */ import { + hasSameKeys, + isUpdatingSameBlockAttribute, + isUpdatingSamePostProperty, + shouldOverwriteState, getPostRawValue, editor, currentPost, @@ -26,12 +30,224 @@ import { preferences, saving, notices, + provisionalBlockUID, blocksMode, isInsertionPointVisible, - reusableBlocks, + sharedBlocks, + template, } from '../reducer'; describe( 'state', () => { + describe( 'hasSameKeys()', () => { + it( 'returns false if two objects do not have the same keys', () => { + const a = { foo: 10 }; + const b = { bar: 10 }; + + expect( hasSameKeys( a, b ) ).toBe( false ); + } ); + + it( 'returns false if two objects have the same keys', () => { + const a = { foo: 10 }; + const b = { foo: 20 }; + + expect( hasSameKeys( a, b ) ).toBe( true ); + } ); + } ); + + describe( 'isUpdatingSameBlockAttribute()', () => { + it( 'should return false if not updating block attributes', () => { + const action = { + type: 'EDIT_POST', + edits: {}, + }; + const previousAction = { + type: 'EDIT_POST', + edits: {}, + }; + + expect( isUpdatingSameBlockAttribute( action, previousAction ) ).toBe( false ); + } ); + + it( 'should return false if not updating the same block', () => { + const action = { + type: 'UPDATE_BLOCK_ATTRIBUTES', + uid: '9db792c6-a25a-495d-adbd-97d56a4c4189', + attributes: { + foo: 10, + }, + }; + const previousAction = { + type: 'UPDATE_BLOCK_ATTRIBUTES', + uid: 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1', + attributes: { + foo: 20, + }, + }; + + expect( isUpdatingSameBlockAttribute( action, previousAction ) ).toBe( false ); + } ); + + it( 'should return false if not updating the same block attributes', () => { + const action = { + type: 'UPDATE_BLOCK_ATTRIBUTES', + uid: '9db792c6-a25a-495d-adbd-97d56a4c4189', + attributes: { + foo: 10, + }, + }; + const previousAction = { + type: 'UPDATE_BLOCK_ATTRIBUTES', + uid: '9db792c6-a25a-495d-adbd-97d56a4c4189', + attributes: { + bar: 20, + }, + }; + + expect( isUpdatingSameBlockAttribute( action, previousAction ) ).toBe( false ); + } ); + + it( 'should return true if updating the same block attributes', () => { + const action = { + type: 'UPDATE_BLOCK_ATTRIBUTES', + uid: '9db792c6-a25a-495d-adbd-97d56a4c4189', + attributes: { + foo: 10, + }, + }; + const previousAction = { + type: 'UPDATE_BLOCK_ATTRIBUTES', + uid: '9db792c6-a25a-495d-adbd-97d56a4c4189', + attributes: { + foo: 20, + }, + }; + + expect( isUpdatingSameBlockAttribute( action, previousAction ) ).toBe( true ); + } ); + } ); + + describe( 'isUpdatingSamePostProperty()', () => { + it( 'should return false if not editing post', () => { + const action = { + type: 'UPDATE_BLOCK_ATTRIBUTES', + uid: 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1', + attributes: { + foo: 10, + }, + }; + const previousAction = { + type: 'UPDATE_BLOCK_ATTRIBUTES', + uid: 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1', + attributes: { + foo: 10, + }, + }; + + expect( isUpdatingSamePostProperty( action, previousAction ) ).toBe( false ); + } ); + + it( 'should return false if not editing the same post properties', () => { + const action = { + type: 'EDIT_POST', + edits: { + foo: 10, + }, + }; + const previousAction = { + type: 'EDIT_POST', + edits: { + bar: 20, + }, + }; + + expect( isUpdatingSamePostProperty( action, previousAction ) ).toBe( false ); + } ); + + it( 'should return true if updating the same post properties', () => { + const action = { + type: 'EDIT_POST', + edits: { + foo: 10, + }, + }; + const previousAction = { + type: 'EDIT_POST', + edits: { + foo: 20, + }, + }; + + expect( isUpdatingSamePostProperty( action, previousAction ) ).toBe( true ); + } ); + } ); + + describe( 'shouldOverwriteState()', () => { + it( 'should return false if no previous action', () => { + const action = { + type: 'EDIT_POST', + edits: { + foo: 10, + }, + }; + const previousAction = undefined; + + expect( shouldOverwriteState( action, previousAction ) ).toBe( false ); + } ); + + it( 'should return false if the action types are different', () => { + const action = { + type: 'EDIT_POST', + edits: { + foo: 10, + }, + }; + const previousAction = { + type: 'EDIT_DIFFERENT_POST', + edits: { + foo: 20, + }, + }; + + expect( shouldOverwriteState( action, previousAction ) ).toBe( false ); + } ); + + it( 'should return true if updating same block attribute', () => { + const action = { + type: 'UPDATE_BLOCK_ATTRIBUTES', + uid: '9db792c6-a25a-495d-adbd-97d56a4c4189', + attributes: { + foo: 10, + }, + }; + const previousAction = { + type: 'UPDATE_BLOCK_ATTRIBUTES', + uid: '9db792c6-a25a-495d-adbd-97d56a4c4189', + attributes: { + foo: 20, + }, + }; + + expect( shouldOverwriteState( action, previousAction ) ).toBe( true ); + } ); + + it( 'should return true if updating same post property', () => { + const action = { + type: 'EDIT_POST', + edits: { + foo: 10, + }, + }; + const previousAction = { + type: 'EDIT_POST', + edits: { + foo: 20, + }, + }; + + expect( shouldOverwriteState( action, previousAction ) ).toBe( true ); + } ); + } ); + describe( 'getPostRawValue', () => { it( 'returns original value for non-rendered content', () => { const value = getPostRawValue( '' ); @@ -183,6 +399,70 @@ describe( 'state', () => { } ); } ); + it( 'should replace the block even if the new block uid is the same', () => { + const originalState = editor( undefined, { + type: 'RESET_BLOCKS', + blocks: [ { + uid: 'chicken', + name: 'core/test-block', + attributes: {}, + innerBlocks: [], + } ], + } ); + const replacedState = editor( originalState, { + type: 'REPLACE_BLOCKS', + uids: [ 'chicken' ], + blocks: [ { + uid: 'chicken', + name: 'core/freeform', + innerBlocks: [], + } ], + } ); + + expect( Object.keys( replacedState.present.blocksByUid ) ).toHaveLength( 1 ); + expect( values( originalState.present.blocksByUid )[ 0 ].name ).toBe( 'core/test-block' ); + expect( values( replacedState.present.blocksByUid )[ 0 ].name ).toBe( 'core/freeform' ); + expect( values( replacedState.present.blocksByUid )[ 0 ].uid ).toBe( 'chicken' ); + expect( replacedState.present.blockOrder ).toEqual( { + '': [ 'chicken' ], + chicken: [], + } ); + + const nestedBlock = { + uid: 'chicken', + name: 'core/test-block', + attributes: {}, + innerBlocks: [], + }; + const wrapperBlock = createBlock( 'core/test-block', {}, [ nestedBlock ] ); + const replacementNestedBlock = { + uid: 'chicken', + name: 'core/freeform', + attributes: {}, + innerBlocks: [], + }; + + const originalNestedState = editor( undefined, { + type: 'RESET_BLOCKS', + blocks: [ wrapperBlock ], + } ); + + const replacedNestedState = editor( originalNestedState, { + type: 'REPLACE_BLOCKS', + uids: [ nestedBlock.uid ], + blocks: [ replacementNestedBlock ], + } ); + + expect( replacedNestedState.present.blockOrder ).toEqual( { + '': [ wrapperBlock.uid ], + [ wrapperBlock.uid ]: [ replacementNestedBlock.uid ], + [ replacementNestedBlock.uid ]: [], + } ); + + expect( originalNestedState.present.blocksByUid.chicken.name ).toBe( 'core/test-block' ); + expect( replacedNestedState.present.blocksByUid.chicken.name ).toBe( 'core/freeform' ); + } ); + it( 'should update the block', () => { const original = editor( undefined, { type: 'RESET_BLOCKS', @@ -211,7 +491,7 @@ describe( 'state', () => { } ); } ); - it( 'should update the reusable block reference if the temporary id is swapped', () => { + it( 'should update the shared block reference if the temporary id is swapped', () => { const original = editor( undefined, { type: 'RESET_BLOCKS', blocks: [ { @@ -226,7 +506,7 @@ describe( 'state', () => { } ); const state = editor( deepFreeze( original ), { - type: 'SAVE_REUSABLE_BLOCK_SUCCESS', + type: 'SAVE_SHARED_BLOCK_SUCCESS', id: 'random-uid', updatedId: 3, } ); @@ -602,7 +882,7 @@ describe( 'state', () => { expect( state.present.blockOrder[ '' ] ).toEqual( [ 'kumquat', 'persimmon', 'loquat' ] ); } ); - it( 'should remove associated blocks when deleting a reusable block', () => { + it( 'should move block to lower index', () => { const original = editor( undefined, { type: 'RESET_BLOCKS', blocks: [ { @@ -615,22 +895,78 @@ describe( 'state', () => { name: 'core/test-block', attributes: {}, innerBlocks: [], + }, { + uid: 'veggies', + name: 'core/test-block', + attributes: {}, + innerBlocks: [], } ], } ); const state = editor( original, { - type: 'REMOVE_REUSABLE_BLOCK', - id: 123, - associatedBlockUids: [ 'chicken', 'veggies' ], + type: 'MOVE_BLOCK_TO_POSITION', + uid: 'ribs', + index: 0, } ); - expect( state.present.blockOrder[ '' ] ).toEqual( [ 'ribs' ] ); - expect( state.present.blocksByUid ).toEqual( { - ribs: { + expect( state.present.blockOrder[ '' ] ).toEqual( [ 'ribs', 'chicken', 'veggies' ] ); + } ); + + it( 'should move block to higher index', () => { + const original = editor( undefined, { + type: 'RESET_BLOCKS', + blocks: [ { + uid: 'chicken', + name: 'core/test-block', + attributes: {}, + innerBlocks: [], + }, { uid: 'ribs', name: 'core/test-block', attributes: {}, - }, + innerBlocks: [], + }, { + uid: 'veggies', + name: 'core/test-block', + attributes: {}, + innerBlocks: [], + } ], + } ); + const state = editor( original, { + type: 'MOVE_BLOCK_TO_POSITION', + uid: 'ribs', + index: 2, + } ); + + expect( state.present.blockOrder[ '' ] ).toEqual( [ 'chicken', 'veggies', 'ribs' ] ); + } ); + + it( 'should not move block if passed same index', () => { + const original = editor( undefined, { + type: 'RESET_BLOCKS', + blocks: [ { + uid: 'chicken', + name: 'core/test-block', + attributes: {}, + innerBlocks: [], + }, { + uid: 'ribs', + name: 'core/test-block', + attributes: {}, + innerBlocks: [], + }, { + uid: 'veggies', + name: 'core/test-block', + attributes: {}, + innerBlocks: [], + } ], + } ); + const state = editor( original, { + type: 'MOVE_BLOCK_TO_POSITION', + uid: 'ribs', + index: 1, } ); + + expect( state.present.blockOrder[ '' ] ).toEqual( [ 'chicken', 'ribs', 'veggies' ] ); } ); describe( 'edits()', () => { @@ -869,7 +1205,7 @@ describe( 'state', () => { expect( state.past ).toHaveLength( 2 ); } ); - it( 'should not overwrite present history if updating same attributes', () => { + it( 'should not overwrite present history if updating different attributes', () => { let state; state = editor( state, { @@ -1172,6 +1508,22 @@ describe( 'state', () => { } ); } ); + it( 'should reset if replacing with empty set', () => { + const original = deepFreeze( { start: 'chicken', end: 'chicken' } ); + const state = blockSelection( original, { + type: 'REPLACE_BLOCKS', + uids: [ 'chicken' ], + blocks: [], + } ); + + expect( state ).toEqual( { + start: null, + end: null, + initialPosition: null, + isMultiSelecting: false, + } ); + } ); + it( 'should keep the selected block', () => { const original = deepFreeze( { start: 'chicken', end: 'chicken' } ); const state = blockSelection( original, { @@ -1185,6 +1537,41 @@ describe( 'state', () => { expect( state ).toBe( original ); } ); + + it( 'should remove the selection if we are removing the selected block', () => { + const original = deepFreeze( { + start: 'chicken', + end: 'chicken', + initialPosition: null, + isMultiSelecting: false, + } ); + const state = blockSelection( original, { + type: 'REMOVE_BLOCKS', + uids: [ 'chicken' ], + } ); + + expect( state ).toEqual( { + start: null, + end: null, + initialPosition: null, + isMultiSelecting: false, + } ); + } ); + + it( 'should keep the selection if we are not removing the selected block', () => { + const original = deepFreeze( { + start: 'chicken', + end: 'chicken', + initialPosition: null, + isMultiSelecting: false, + } ); + const state = blockSelection( original, { + type: 'REMOVE_BLOCKS', + uids: [ 'ribs' ], + } ); + + expect( state ).toBe( original ); + } ); } ); describe( 'preferences()', () => { @@ -1196,26 +1583,24 @@ describe( 'state', () => { const state = preferences( undefined, {} ); expect( state ).toEqual( { - recentInserts: [], insertUsage: {}, } ); } ); it( 'should record recently used blocks', () => { - const state = preferences( deepFreeze( { recentInserts: [], insertUsage: {} } ), { + const state = preferences( deepFreeze( { insertUsage: {} } ), { type: 'INSERT_BLOCKS', blocks: [ { uid: 'bacon', name: 'core-embed/twitter', } ], + time: 123456, } ); expect( state ).toEqual( { - recentInserts: [ - { name: 'core-embed/twitter' }, - ], insertUsage: { 'core-embed/twitter': { + time: 123456, count: 1, insert: { name: 'core-embed/twitter' }, }, @@ -1223,9 +1608,9 @@ describe( 'state', () => { } ); const twoRecentBlocks = preferences( deepFreeze( { - recentInserts: [], insertUsage: { 'core-embed/twitter': { + time: 123456, count: 1, insert: { name: 'core-embed/twitter' }, }, @@ -1240,19 +1625,18 @@ describe( 'state', () => { name: 'core/block', attributes: { ref: 123 }, } ], + time: 123457, } ); expect( twoRecentBlocks ).toEqual( { - recentInserts: [ - { name: 'core/block', ref: 123 }, - { name: 'core-embed/twitter' }, - ], insertUsage: { 'core-embed/twitter': { + time: 123457, count: 2, insert: { name: 'core-embed/twitter' }, }, 'core/block/123': { + time: 123457, count: 1, insert: { name: 'core/block', ref: 123 }, }, @@ -1260,15 +1644,11 @@ describe( 'state', () => { } ); } ); - it( 'should remove recorded reusable blocks that are deleted', () => { + it( 'should remove recorded shared blocks that are deleted', () => { const initialState = { - recentInserts: [ - { name: 'core-embed/twitter' }, - { name: 'core/block', ref: 123 }, - { name: 'core/block', ref: 456 }, - ], insertUsage: { 'core/block/123': { + time: 1000, count: 1, insert: { name: 'core/block', ref: 123 }, }, @@ -1276,15 +1656,11 @@ describe( 'state', () => { }; const state = preferences( deepFreeze( initialState ), { - type: 'REMOVE_REUSABLE_BLOCK', + type: 'REMOVE_SHARED_BLOCK', id: 123, } ); expect( state ).toEqual( { - recentInserts: [ - { name: 'core-embed/twitter' }, - { name: 'core/block', ref: 456 }, - ], insertUsage: {}, } ); } ); @@ -1417,6 +1793,100 @@ describe( 'state', () => { } ); } ); + describe( 'provisionalBlockUID()', () => { + const PROVISIONAL_UPDATE_ACTION_TYPES = [ + 'UPDATE_BLOCK_ATTRIBUTES', + 'UPDATE_BLOCK', + 'CONVERT_BLOCK_TO_SHARED', + ]; + + const PROVISIONAL_REPLACE_ACTION_TYPES = [ + 'REPLACE_BLOCKS', + 'REMOVE_BLOCKS', + ]; + + it( 'returns null by default', () => { + const state = provisionalBlockUID( undefined, {} ); + + expect( state ).toBe( null ); + } ); + + it( 'returns the uid of the first inserted provisional block', () => { + const state = provisionalBlockUID( null, { + type: 'INSERT_BLOCKS', + isProvisional: true, + blocks: [ + { uid: 'chicken' }, + ], + } ); + + expect( state ).toBe( 'chicken' ); + } ); + + it( 'does not return uid of block if not provisional', () => { + const state = provisionalBlockUID( null, { + type: 'INSERT_BLOCKS', + blocks: [ + { uid: 'chicken' }, + ], + } ); + + expect( state ).toBe( null ); + } ); + + it( 'returns null on block reset', () => { + const state = provisionalBlockUID( 'chicken', { + type: 'RESET_BLOCKS', + } ); + + expect( state ).toBe( null ); + } ); + + it( 'returns null on block update', () => { + PROVISIONAL_UPDATE_ACTION_TYPES.forEach( ( type ) => { + const state = provisionalBlockUID( 'chicken', { + type, + uid: 'chicken', + } ); + + expect( state ).toBe( null ); + } ); + } ); + + it( 'does not return null if update occurs to non-provisional block', () => { + PROVISIONAL_UPDATE_ACTION_TYPES.forEach( ( type ) => { + const state = provisionalBlockUID( 'chicken', { + type, + uid: 'ribs', + } ); + + expect( state ).toBe( 'chicken' ); + } ); + } ); + + it( 'returns null if replacement of provisional block', () => { + PROVISIONAL_REPLACE_ACTION_TYPES.forEach( ( type ) => { + const state = provisionalBlockUID( 'chicken', { + type, + uids: [ 'chicken' ], + } ); + + expect( state ).toBe( null ); + } ); + } ); + + it( 'does not return null if replacement of non-provisional block', () => { + PROVISIONAL_REPLACE_ACTION_TYPES.forEach( ( type ) => { + const state = provisionalBlockUID( 'chicken', { + type, + uids: [ 'ribs' ], + } ); + + expect( state ).toBe( 'chicken' ); + } ); + } ); + } ); + describe( 'blocksMode', () => { it( 'should set mode to html if not set', () => { const action = { @@ -1439,9 +1909,9 @@ describe( 'state', () => { } ); } ); - describe( 'reusableBlocks()', () => { + describe( 'sharedBlocks()', () => { it( 'should start out empty', () => { - const state = reusableBlocks( undefined, {} ); + const state = sharedBlocks( undefined, {} ); expect( state ).toEqual( { data: {}, isFetching: {}, @@ -1449,143 +1919,77 @@ describe( 'state', () => { } ); } ); - it( 'should add fetched reusable blocks', () => { - const reusableBlock = { - id: 123, - name: 'My cool block', - type: 'core/paragraph', - attributes: { - content: 'Hello!', - }, - }; - - const state = reusableBlocks( {}, { - type: 'FETCH_REUSABLE_BLOCKS_SUCCESS', - reusableBlocks: [ reusableBlock ], - } ); - - expect( state ).toEqual( { - data: { - [ reusableBlock.id ]: reusableBlock, - }, - isFetching: {}, - isSaving: {}, - } ); - } ); - - it( 'should add a reusable block', () => { - const reusableBlock = { - id: 123, - name: 'My cool block', - type: 'core/paragraph', - attributes: { - content: 'Hello!', - }, - }; - - const state = reusableBlocks( {}, { - type: 'UPDATE_REUSABLE_BLOCK', - id: reusableBlock.id, - reusableBlock, + it( 'should add received shared blocks', () => { + const state = sharedBlocks( {}, { + type: 'RECEIVE_SHARED_BLOCKS', + results: [ { + sharedBlock: { + id: 123, + title: 'My cool block', + }, + parsedBlock: { + uid: 'foo', + }, + } ], } ); expect( state ).toEqual( { data: { - [ reusableBlock.id ]: reusableBlock, + 123: { uid: 'foo', title: 'My cool block' }, }, isFetching: {}, isSaving: {}, } ); } ); - it( 'should update a reusable block', () => { - const id = 123; + it( 'should update a shared block', () => { const initialState = { data: { - [ id ]: { - id, - name: 'My cool block', - type: 'core/paragraph', - attributes: { - content: 'Hello!', - dropCap: true, - }, - }, + 123: { uid: '', title: '' }, }, isFetching: {}, isSaving: {}, }; - const state = reusableBlocks( initialState, { - type: 'UPDATE_REUSABLE_BLOCK', - id, - reusableBlock: { - name: 'My better block', - attributes: { - content: 'Yo!', - }, - }, + const state = sharedBlocks( initialState, { + type: 'UPDATE_SHARED_BLOCK_TITLE', + id: 123, + title: 'My block', } ); expect( state ).toEqual( { data: { - [ id ]: { - id, - name: 'My better block', - type: 'core/paragraph', - attributes: { - content: 'Yo!', - dropCap: true, - }, - }, + 123: { uid: '', title: 'My block' }, }, isFetching: {}, isSaving: {}, } ); } ); - it( 'should update the reusable block\'s id if it was temporary', () => { - const id = 123; + it( 'should update the shared block\'s id if it was temporary', () => { const initialState = { data: { - [ id ]: { - id, - isTemporary: true, - name: 'My cool block', - type: 'core/paragraph', - attributes: { - content: 'Hello!', - dropCap: true, - }, - }, + shared1: { uid: '', title: '' }, }, isSaving: {}, }; - const state = reusableBlocks( initialState, { - type: 'SAVE_REUSABLE_BLOCK_SUCCESS', - id, - updatedId: 3, + const state = sharedBlocks( initialState, { + type: 'SAVE_SHARED_BLOCK_SUCCESS', + id: 'shared1', + updatedId: 123, } ); expect( state ).toEqual( { data: { - 3: { - id: 3, - name: 'My cool block', - type: 'core/paragraph', - attributes: { - content: 'Hello!', - dropCap: true, - }, - }, + 123: { uid: '', title: '' }, }, isFetching: {}, isSaving: {}, } ); } ); - it( 'should remove a reusable block', () => { + it( 'should remove a shared block', () => { const id = 123; const initialState = { data: { @@ -1603,8 +2007,8 @@ describe( 'state', () => { isSaving: {}, }; - const state = reusableBlocks( deepFreeze( initialState ), { - type: 'REMOVE_REUSABLE_BLOCK', + const state = sharedBlocks( deepFreeze( initialState ), { + type: 'REMOVE_SHARED_BLOCK', id, } ); @@ -1615,7 +2019,7 @@ describe( 'state', () => { } ); } ); - it( 'should indicate that a reusable block is fetching', () => { + it( 'should indicate that a shared block is fetching', () => { const id = 123; const initialState = { data: {}, @@ -1623,8 +2027,8 @@ describe( 'state', () => { isSaving: {}, }; - const state = reusableBlocks( initialState, { - type: 'FETCH_REUSABLE_BLOCKS', + const state = sharedBlocks( initialState, { + type: 'FETCH_SHARED_BLOCKS', id, } ); @@ -1637,7 +2041,7 @@ describe( 'state', () => { } ); } ); - it( 'should stop indicating that a reusable block is saving when the fetch succeeded', () => { + it( 'should stop indicating that a shared block is saving when the fetch succeeded', () => { const id = 123; const initialState = { data: { @@ -1649,8 +2053,8 @@ describe( 'state', () => { isSaving: {}, }; - const state = reusableBlocks( initialState, { - type: 'FETCH_REUSABLE_BLOCKS_SUCCESS', + const state = sharedBlocks( initialState, { + type: 'FETCH_SHARED_BLOCKS_SUCCESS', id, updatedId: id, } ); @@ -1664,7 +2068,7 @@ describe( 'state', () => { } ); } ); - it( 'should stop indicating that a reusable block is fetching when there is an error', () => { + it( 'should stop indicating that a shared block is fetching when there is an error', () => { const id = 123; const initialState = { data: {}, @@ -1674,8 +2078,8 @@ describe( 'state', () => { isSaving: {}, }; - const state = reusableBlocks( initialState, { - type: 'FETCH_REUSABLE_BLOCKS_FAILURE', + const state = sharedBlocks( initialState, { + type: 'FETCH_SHARED_BLOCKS_FAILURE', id, } ); @@ -1686,7 +2090,7 @@ describe( 'state', () => { } ); } ); - it( 'should indicate that a reusable block is saving', () => { + it( 'should indicate that a shared block is saving', () => { const id = 123; const initialState = { data: {}, @@ -1694,8 +2098,8 @@ describe( 'state', () => { isSaving: {}, }; - const state = reusableBlocks( initialState, { - type: 'SAVE_REUSABLE_BLOCK', + const state = sharedBlocks( initialState, { + type: 'SAVE_SHARED_BLOCK', id, } ); @@ -1708,7 +2112,7 @@ describe( 'state', () => { } ); } ); - it( 'should stop indicating that a reusable block is saving when the save succeeded', () => { + it( 'should stop indicating that a shared block is saving when the save succeeded', () => { const id = 123; const initialState = { data: { @@ -1720,8 +2124,8 @@ describe( 'state', () => { }, }; - const state = reusableBlocks( initialState, { - type: 'SAVE_REUSABLE_BLOCK_SUCCESS', + const state = sharedBlocks( initialState, { + type: 'SAVE_SHARED_BLOCK_SUCCESS', id, updatedId: id, } ); @@ -1735,7 +2139,7 @@ describe( 'state', () => { } ); } ); - it( 'should stop indicating that a reusable block is saving when there is an error', () => { + it( 'should stop indicating that a shared block is saving when there is an error', () => { const id = 123; const initialState = { data: {}, @@ -1745,8 +2149,8 @@ describe( 'state', () => { }, }; - const state = reusableBlocks( initialState, { - type: 'SAVE_REUSABLE_BLOCK_FAILURE', + const state = sharedBlocks( initialState, { + type: 'SAVE_SHARED_BLOCK_FAILURE', id, } ); @@ -1757,4 +2161,32 @@ describe( 'state', () => { } ); } ); } ); + + describe( 'template', () => { + it( 'should default to visible', () => { + const state = template( undefined, {} ); + + expect( state ).toEqual( { isValid: true } ); + } ); + + it( 'should set the template', () => { + const blockTemplate = [ [ 'core/paragraph' ] ]; + const state = template( undefined, { + type: 'SETUP_EDITOR', + settings: { template: blockTemplate, templateLock: 'all' }, + } ); + + expect( state ).toEqual( { isValid: true, template: blockTemplate, lock: 'all' } ); + } ); + + it( 'should reset the validity flag', () => { + const original = deepFreeze( { isValid: false, template: [] } ); + const state = template( original, { + type: 'SET_TEMPLATE_VALIDITY', + isValid: true, + } ); + + expect( state ).toEqual( { isValid: true, template: [] } ); + } ); + } ); } ); diff --git a/editor/store/test/selectors.js b/editor/store/test/selectors.js index 9cb79e54dc3349..e89c0f445f0ab2 100644 --- a/editor/store/test/selectors.js +++ b/editor/store/test/selectors.js @@ -1,7 +1,6 @@ /** * External dependencies */ -import moment from 'moment'; import { filter, property, union } from 'lodash'; /** @@ -9,6 +8,7 @@ import { filter, property, union } from 'lodash'; */ import { __ } from '@wordpress/i18n'; import { registerBlockType, unregisterBlockType, registerCoreBlocks, getBlockTypes } from '@wordpress/blocks'; +import { moment } from '@wordpress/date'; /** * Internal dependencies @@ -30,19 +30,24 @@ const { getDocumentTitle, getEditedPostExcerpt, getEditedPostVisibility, + isCurrentPostPending, isCurrentPostPublished, + isCurrentPostScheduled, isEditedPostPublishable, isEditedPostSaveable, isEditedPostEmpty, isEditedPostBeingScheduled, getEditedPostPreviewLink, getBlockDependantsCacheBust, + getBlockName, getBlock, getBlocks, getBlockCount, + hasSelectedBlock, getSelectedBlock, getBlockRootUID, getEditedPostAttribute, + getGlobalBlockCount, getMultiSelectedBlockUids, getMultiSelectedBlocks, getMultiSelectedBlocksStartUid, @@ -53,6 +58,7 @@ const { getNextBlockUid, isBlockSelected, isBlockWithinSelection, + hasMultiSelection, isBlockMultiSelected, isFirstMultiSelectedBlock, getBlockMode, @@ -64,15 +70,19 @@ const { didPostSaveRequestFail, getSuggestedPostFormat, getNotices, - getReusableBlock, - isSavingReusableBlock, + getSharedBlock, + isSavingSharedBlock, + isFetchingSharedBlock, isSelectionEnabled, - getReusableBlocks, + getSharedBlocks, getStateBeforeOptimisticTransaction, isPublishingPost, getInserterItems, - getRecentInserterItems, - getFrequentInserterItems, + getFrecentInserterItems, + getProvisionalBlockUID, + isValidTemplate, + getTemplate, + getTemplateLock, POST_UPDATE_TRANSACTION_ID, } = selectors; @@ -581,6 +591,36 @@ describe( 'selectors', () => { } ); } ); + describe( 'isCurrentPostPending', () => { + it( 'should return true for posts in pending state', () => { + const state = { + currentPost: { + status: 'pending', + }, + }; + + expect( isCurrentPostPending( state ) ).toBe( true ); + } ); + + it( 'should return false for draft posts', () => { + const state = { + currentPost: { + status: 'draft', + }, + }; + + expect( isCurrentPostPending( state ) ).toBe( false ); + } ); + + it( 'should return false if status is unknown', () => { + const state = { + currentPost: {}, + }; + + expect( isCurrentPostPending( state ) ).toBe( false ); + } ); + } ); + describe( 'isCurrentPostPublished', () => { it( 'should return true for public posts', () => { const state = { @@ -624,6 +664,48 @@ describe( 'selectors', () => { } ); } ); + describe( 'isCurrentPostScheduled', () => { + it( 'should return true for future scheduled posts', () => { + const state = { + currentPost: { + status: 'future', + date: '2100-05-30T17:21:39', + }, + }; + + expect( isCurrentPostScheduled( state ) ).toBe( true ); + } ); + + it( 'should return false for old scheduled posts that were already published', () => { + const state = { + currentPost: { + status: 'future', + date: '2016-05-30T17:21:39', + }, + }; + + expect( isCurrentPostScheduled( state ) ).toBe( false ); + } ); + + it( 'should return false for auto draft posts', () => { + const state = { + currentPost: { + status: 'auto-draft', + }, + }; + + expect( isCurrentPostScheduled( state ) ).toBe( false ); + } ); + + it( 'should return false if status is unknown', () => { + const state = { + currentPost: {}, + }; + + expect( isCurrentPostScheduled( state ) ).toBe( false ); + } ); + } ); + describe( 'isEditedPostPublishable', () => { it( 'should return true for pending posts', () => { const state = { @@ -1142,6 +1224,51 @@ describe( 'selectors', () => { } ); } ); + describe( 'getBlockName', () => { + it( 'returns null if no block by uid', () => { + const state = { + currentPost: {}, + editor: { + present: { + blocksByUid: {}, + blockOrder: {}, + edits: {}, + }, + }, + }; + + const name = getBlockName( state, 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1' ); + + expect( name ).toBe( null ); + } ); + + it( 'returns block name', () => { + const state = { + currentPost: {}, + editor: { + present: { + blocksByUid: { + 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1': { + uid: 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1', + name: 'core/paragraph', + attributes: {}, + }, + }, + blockOrder: { + '': [ 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1' ], + 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1': [], + }, + edits: {}, + }, + }, + }; + + const name = getBlockName( state, 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1' ); + + expect( name ).toBe( 'core/paragraph' ); + } ); + } ); + describe( 'getBlock', () => { it( 'should return the block', () => { const state = { @@ -1325,6 +1452,87 @@ describe( 'selectors', () => { } ); } ); + describe( 'hasSelectedBlock', () => { + it( 'should return false if no selection', () => { + const state = { + blockSelection: { + start: null, + end: null, + }, + }; + + expect( hasSelectedBlock( state ) ).toBe( false ); + } ); + + it( 'should return false if multi-selection', () => { + const state = { + blockSelection: { + start: 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1', + end: '9db792c6-a25a-495d-adbd-97d56a4c4189', + }, + }; + + expect( hasSelectedBlock( state ) ).toBe( false ); + } ); + + it( 'should return true if singular selection', () => { + const state = { + blockSelection: { + start: 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1', + end: 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1', + }, + }; + + expect( hasSelectedBlock( state ) ).toBe( true ); + } ); + } ); + + describe( 'getGlobalBlockCount', () => { + it( 'should return the global number of top-level blocks in the post', () => { + const state = { + editor: { + present: { + blocksByUid: { + 23: { uid: 23, name: 'core/heading', attributes: {} }, + 123: { uid: 123, name: 'core/paragraph', attributes: {} }, + }, + }, + }, + }; + + expect( getGlobalBlockCount( state ) ).toBe( 2 ); + } ); + + it( 'should return the global umber of blocks of a given type', () => { + const state = { + editor: { + present: { + blocksByUid: { + 123: { uid: 123, name: 'core/columns', attributes: {} }, + 456: { uid: 456, name: 'core/paragraph', attributes: {} }, + 789: { uid: 789, name: 'core/paragraph', attributes: {} }, + 124: { uid: 123, name: 'core/heading', attributes: {} }, + }, + }, + }, + }; + + expect( getGlobalBlockCount( state, 'core/heading' ) ).toBe( 1 ); + } ); + + it( 'should return 0 if no blocks exist', () => { + const state = { + editor: { + present: { + blocksByUid: { + }, + }, + }, + }; + expect( getGlobalBlockCount( state ) ).toBe( 0 ); + expect( getGlobalBlockCount( state, 'core/heading' ) ).toBe( 0 ); + } ); + } ); describe( 'getSelectedBlock', () => { it( 'should return null if no block is selected', () => { const state = { @@ -1804,6 +2012,41 @@ describe( 'selectors', () => { } ); } ); + describe( 'hasMultiSelection', () => { + it( 'should return false if no selection', () => { + const state = { + blockSelection: { + start: null, + end: null, + }, + }; + + expect( hasMultiSelection( state ) ).toBe( false ); + } ); + + it( 'should return false if singular selection', () => { + const state = { + blockSelection: { + start: 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1', + end: 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1', + }, + }; + + expect( hasMultiSelection( state ) ).toBe( false ); + } ); + + it( 'should return true if multi-selection', () => { + const state = { + blockSelection: { + start: 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1', + end: '9db792c6-a25a-495d-adbd-97d56a4c4189', + }, + }; + + expect( hasMultiSelection( state ) ).toBe( true ); + } ); + } ); + describe( 'isBlockMultiSelected', () => { const state = { editor: { @@ -2278,13 +2521,13 @@ describe( 'selectors', () => { }, }, currentPost: {}, - reusableBlocks: { + sharedBlocks: { data: {}, }, }; const blockTypes = getBlockTypes().filter( blockType => ! blockType.isPrivate ); - expect( getInserterItems( state ) ).toHaveLength( blockTypes.length ); + expect( getInserterItems( state, true ) ).toHaveLength( blockTypes.length ); } ); it( 'should properly list a regular block type', () => { @@ -2297,7 +2540,7 @@ describe( 'selectors', () => { }, }, currentPost: {}, - reusableBlocks: { + sharedBlocks: { data: {}, }, }; @@ -2330,7 +2573,7 @@ describe( 'selectors', () => { }, }, currentPost: {}, - reusableBlocks: { + sharedBlocks: { data: {}, }, }; @@ -2339,23 +2582,21 @@ describe( 'selectors', () => { expect( items[ 0 ].isDisabled ).toBe( true ); } ); - it( 'should properly list reusable blocks', () => { + it( 'should properly list shared blocks', () => { const state = { editor: { present: { - blocksByUid: {}, + blocksByUid: { + carrot: { name: 'core/test-block' }, + }, blockOrder: {}, edits: {}, }, }, currentPost: {}, - reusableBlocks: { + sharedBlocks: { data: { - 123: { - id: 123, - title: 'My reusable block', - type: 'core/test-block', - }, + 123: { uid: 'carrot', title: 'My shared block' }, }, }, }; @@ -2365,9 +2606,9 @@ describe( 'selectors', () => { id: 'core/block/123', name: 'core/block', initialAttributes: { ref: 123 }, - title: 'My reusable block', + title: 'My shared block', icon: 'test', - category: 'reusable-blocks', + category: 'shared', keywords: [], isDisabled: false, }, @@ -2379,89 +2620,76 @@ describe( 'selectors', () => { } ); } ); - describe( 'getRecentInserterItems', () => { + describe( 'getFrecentInserterItems', () => { beforeAll( () => { registerCoreBlocks(); } ); - it( 'should return the 9 most recently used blocks', () => { + it( 'should return the most frecently used blocks', () => { const state = { preferences: { - recentInserts: [ - { name: 'core/deleted-block' }, // Deleted blocks should be filtered out - { name: 'core/block', ref: 456 }, // Deleted reusable blocks should be filtered out - { name: 'core/paragraph' }, - { name: 'core/block', ref: 123 }, - { name: 'core/image' }, - { name: 'core/quote' }, - { name: 'core/gallery' }, - { name: 'core/heading' }, - { name: 'core/list' }, - { name: 'core/video' }, - { name: 'core/audio' }, - { name: 'core/code' }, - ], + insertUsage: { + 'core/deleted-block': { time: 1000, count: 10, insert: { name: 'core/deleted-block' } }, // Deleted blocks should be filtered out + 'core/block/456': { time: 1000, count: 4, insert: { name: 'core/block', ref: 456 } }, // Deleted shared blocks should be filtered out + 'core/image': { time: 1000, count: 3, insert: { name: 'core/image' } }, + 'core/block/123': { time: 1000, count: 5, insert: { name: 'core/block', ref: 123 } }, + 'core/paragraph': { time: 1000, count: 2, insert: { name: 'core/paragraph' } }, + }, }, editor: { present: { + blocksByUid: { + carrot: { name: 'core/test-block' }, + }, blockOrder: [], + edits: {}, }, }, - reusableBlocks: { + sharedBlocks: { data: { - 123: { id: 123, type: 'core/test-block' }, + 123: { uid: 'carrot' }, }, }, + currentPost: {}, }; - expect( getRecentInserterItems( state ) ).toMatchObject( [ - { name: 'core/paragraph', initialAttributes: {} }, + expect( getFrecentInserterItems( state, true, 3 ) ).toMatchObject( [ { name: 'core/block', initialAttributes: { ref: 123 } }, { name: 'core/image', initialAttributes: {} }, - { name: 'core/quote', initialAttributes: {} }, - { name: 'core/gallery', initialAttributes: {} }, - { name: 'core/heading', initialAttributes: {} }, - { name: 'core/list', initialAttributes: {} }, - { name: 'core/video', initialAttributes: {} }, - { name: 'core/audio', initialAttributes: {} }, + { name: 'core/paragraph', initialAttributes: {} }, ] ); } ); - it( 'should pad list out with blocks from the common category', () => { + it( 'should weight by time', () => { const state = { preferences: { - recentInserts: [ - { name: 'core/paragraph' }, - ], + insertUsage: { + 'core/image': { time: Date.now() - 1000, count: 2, insert: { name: 'core/image' } }, + 'core/paragraph': { time: Date.now() - 4000, count: 3, insert: { name: 'core/paragraph' } }, + }, }, editor: { present: { blockOrder: [], }, }, + sharedBlocks: { + data: {}, + }, }; - // We should get back 8 items with no duplicates - const items = getRecentInserterItems( state ); - const blockNames = items.map( item => item.name ); - expect( union( blockNames ) ).toHaveLength( 9 ); - } ); - } ); - - describe( 'getFrequentInserterItems', () => { - beforeAll( () => { - registerCoreBlocks(); + expect( getFrecentInserterItems( state, true, 2 ) ).toMatchObject( [ + { name: 'core/image', initialAttributes: {} }, + { name: 'core/paragraph', initialAttributes: {} }, + ] ); } ); - it( 'should return the 8 most recently used blocks', () => { + it( 'should be backwards-compatible with old preferences values', () => { const state = { preferences: { insertUsage: { - 'core/deleted-block': { count: 10, insert: { name: 'core/deleted-block' } }, // Deleted blocks should be filtered out - 'core/block/456': { count: 4, insert: { name: 'core/block', ref: 456 } }, // Deleted reusable blocks should be filtered out - 'core/image': { count: 3, insert: { name: 'core/image' } }, - 'core/block/123': { count: 5, insert: { name: 'core/block', ref: 123 } }, - 'core/paragraph': { count: 2, insert: { name: 'core/paragraph' } }, + 'core/image': { time: Date.now(), count: 1, insert: { name: 'core/image' } }, + 'core/paragraph': { time: undefined, count: 5, insert: { name: 'core/paragraph' } }, }, }, editor: { @@ -2469,17 +2697,14 @@ describe( 'selectors', () => { blockOrder: [], }, }, - reusableBlocks: { - data: { - 123: { id: 123, type: 'core/test-block' }, - }, + sharedBlocks: { + data: {}, }, }; - expect( getFrequentInserterItems( state, true, 3 ) ).toMatchObject( [ - { name: 'core/block', initialAttributes: { ref: 123 } }, - { name: 'core/image', initialAttributes: {} }, + expect( getFrecentInserterItems( state, true, 2 ) ).toMatchObject( [ { name: 'core/paragraph', initialAttributes: {} }, + { name: 'core/image', initialAttributes: {} }, ] ); } ); @@ -2487,7 +2712,7 @@ describe( 'selectors', () => { const state = { preferences: { insertUsage: { - 'core/image': { count: 2, insert: { name: 'core/paragraph' } }, + 'core/image': { time: 1000, count: 2, insert: { name: 'core/paragraph' } }, }, }, editor: { @@ -2498,114 +2723,146 @@ describe( 'selectors', () => { }; // We should get back 4 items with no duplicates - const items = getFrequentInserterItems( state, true, 4 ); + const items = getFrecentInserterItems( state, true, 4 ); const blockNames = items.map( item => item.name ); expect( union( blockNames ) ).toHaveLength( 4 ); } ); } ); - describe( 'getReusableBlock', () => { - it( 'should return a reusable block', () => { - const id = '358b59ee-bab3-4d6f-8445-e8c6971a5605'; - const expectedReusableBlock = { - id, - name: 'My cool block', - type: 'core/paragraph', - attributes: { - content: 'Hello!', + describe( 'getSharedBlock', () => { + it( 'should return a shared block', () => { + const state = { + sharedBlocks: { + data: { + 8109: { + uid: 'foo', + title: 'My cool block', + }, + }, }, }; + + const actualSharedBlock = getSharedBlock( state, 8109 ); + expect( actualSharedBlock ).toEqual( { + id: 8109, + isTemporary: false, + uid: 'foo', + title: 'My cool block', + } ); + } ); + + it( 'should return a temporary shared block', () => { const state = { - reusableBlocks: { + sharedBlocks: { data: { - [ id ]: expectedReusableBlock, + shared1: { + uid: 'foo', + title: 'My cool block', + }, }, }, }; - const actualReusableBlock = getReusableBlock( state, id ); - expect( actualReusableBlock ).toEqual( expectedReusableBlock ); + const actualSharedBlock = getSharedBlock( state, 'shared1' ); + expect( actualSharedBlock ).toEqual( { + id: 'shared1', + isTemporary: true, + uid: 'foo', + title: 'My cool block', + } ); } ); - it( 'should return null when no reusable block exists', () => { + it( 'should return null when no shared block exists', () => { const state = { - reusableBlocks: { + sharedBlocks: { data: {}, }, }; - const reusableBlock = getReusableBlock( state, '358b59ee-bab3-4d6f-8445-e8c6971a5605' ); - expect( reusableBlock ).toBeNull(); + const sharedBlock = getSharedBlock( state, '358b59ee-bab3-4d6f-8445-e8c6971a5605' ); + expect( sharedBlock ).toBeNull(); } ); } ); - describe( 'isSavingReusableBlock', () => { + describe( 'isSavingSharedBlock', () => { it( 'should return false when the block is not being saved', () => { const state = { - reusableBlocks: { + sharedBlocks: { isSaving: {}, }, }; - const isSaving = isSavingReusableBlock( state, '358b59ee-bab3-4d6f-8445-e8c6971a5605' ); + const isSaving = isSavingSharedBlock( state, 5187 ); expect( isSaving ).toBe( false ); } ); it( 'should return true when the block is being saved', () => { - const id = '358b59ee-bab3-4d6f-8445-e8c6971a5605'; const state = { - reusableBlocks: { + sharedBlocks: { isSaving: { - [ id ]: true, + 5187: true, }, }, }; - const isSaving = isSavingReusableBlock( state, id ); + const isSaving = isSavingSharedBlock( state, 5187 ); expect( isSaving ).toBe( true ); } ); } ); - describe( 'getReusableBlocks', () => { - it( 'should return an array of reusable blocks', () => { - const reusableBlock1 = { - id: '358b59ee-bab3-4d6f-8445-e8c6971a5605', - name: 'My cool block', - type: 'core/paragraph', - attributes: { - content: 'Hello!', + describe( 'isFetchingSharedBlock', () => { + it( 'should return false when the block is not being fetched', () => { + const state = { + sharedBlocks: { + isFetching: {}, }, }; - const reusableBlock2 = { - id: '687e1a87-cca1-41f2-a782-197ddaea9abf', - name: 'My neat block', - type: 'core/paragraph', - attributes: { - content: 'Goodbye!', + + const isFetching = isFetchingSharedBlock( state, 5187 ); + expect( isFetching ).toBe( false ); + } ); + + it( 'should return true when the block is being fetched', () => { + const state = { + sharedBlocks: { + isFetching: { + 5187: true, + }, }, }; + + const isFetching = isFetchingSharedBlock( state, 5187 ); + expect( isFetching ).toBe( true ); + } ); + } ); + + describe( 'getSharedBlocks', () => { + it( 'should return an array of shared blocks', () => { const state = { - reusableBlocks: { + sharedBlocks: { data: { - [ reusableBlock1.id ]: reusableBlock1, - [ reusableBlock2.id ]: reusableBlock2, + 123: { uid: 'carrot' }, + shared1: { uid: 'broccoli' }, }, }, }; - const reusableBlocks = getReusableBlocks( state ); - expect( reusableBlocks ).toEqual( [ reusableBlock1, reusableBlock2 ] ); + const sharedBlocks = getSharedBlocks( state ); + expect( sharedBlocks ).toEqual( [ + { id: 123, isTemporary: false, uid: 'carrot' }, + { id: 'shared1', isTemporary: true, uid: 'broccoli' }, + ] ); } ); - it( 'should return an empty array when no reusable blocks exist', () => { + it( 'should return an empty array when no shared blocks exist', () => { const state = { - reusableBlocks: { + sharedBlocks: { data: {}, }, }; - const reusableBlocks = getReusableBlocks( state ); - expect( reusableBlocks ).toEqual( [] ); + const sharedBlocks = getSharedBlocks( state ); + expect( sharedBlocks ).toEqual( [] ); } ); } ); @@ -2779,4 +3036,61 @@ describe( 'selectors', () => { expect( isPublishing ).toBe( true ); } ); } ); + + describe( 'getProvisionalBlockUID()', () => { + it( 'should return null if not set', () => { + const provisionalBlockUID = getProvisionalBlockUID( { + provisionalBlockUID: null, + } ); + + expect( provisionalBlockUID ).toBe( null ); + } ); + + it( 'should return UID of provisional block', () => { + const provisionalBlockUID = getProvisionalBlockUID( { + provisionalBlockUID: 'chicken', + } ); + + expect( provisionalBlockUID ).toBe( 'chicken' ); + } ); + } ); + + describe( 'isValidTemplate', () => { + it( 'should return true if template is valid', () => { + const state = { + template: { isValid: true }, + }; + + expect( isValidTemplate( state ) ).toBe( true ); + } ); + + it( 'should return false if template is not valid', () => { + const state = { + template: { isValid: false }, + }; + + expect( isValidTemplate( state ) ).toBe( false ); + } ); + } ); + + describe( 'getTemplate', () => { + it( 'should return the template object', () => { + const template = []; + const state = { + template: { isValid: true, template }, + }; + + expect( getTemplate( state ) ).toBe( template ); + } ); + } ); + + describe( 'getTemplateLock', () => { + it( 'should return the template object', () => { + const state = { + template: { isValid: true, lock: 'all' }, + }; + + expect( getTemplateLock( state ) ).toBe( 'all' ); + } ); + } ); } ); diff --git a/editor/utils/with-change-detection/index.js b/editor/utils/with-change-detection/index.js index f5213642096e97..8b738e50015d66 100644 --- a/editor/utils/with-change-detection/index.js +++ b/editor/utils/with-change-detection/index.js @@ -8,8 +8,9 @@ import { includes } from 'lodash'; * returned reducer will include a `isDirty` property on the object reflecting * whether the original reference of the reducer has changed. * - * @param {?Object} options Optional options. - * @param {?Array} options.resetTypes Action types upon which to reset dirty. + * @param {?Object} options Optional options. + * @param {?Array} options.ignoreTypes Action types upon which to skip check. + * @param {?Array} options.resetTypes Action types upon which to reset dirty. * * @return {Function} Higher-order reducer. */ @@ -17,6 +18,10 @@ const withChangeDetection = ( options = {} ) => ( reducer ) => { return ( state, action ) => { const nextState = reducer( state, action ); + if ( includes( options.ignoreTypes, action.type ) ) { + return nextState; + } + // Reset at: // - Initial state // - Reset types diff --git a/editor/utils/with-change-detection/test/index.js b/editor/utils/with-change-detection/test/index.js index fb00d3b66ead81..728ef8b22a5500 100644 --- a/editor/utils/with-change-detection/test/index.js +++ b/editor/utils/with-change-detection/test/index.js @@ -49,6 +49,18 @@ describe( 'withChangeDetection()', () => { expect( state ).toEqual( { count: 1, isDirty: false } ); } ); + it( 'should allow ignore types as option', () => { + const reducer = withChangeDetection( { ignoreTypes: [ 'INCREMENT' ] } )( originalReducer ); + + let state; + + state = reducer( undefined, {} ); + expect( state ).toEqual( { count: 0, isDirty: false } ); + + state = reducer( deepFreeze( state ), { type: 'INCREMENT' } ); + expect( state ).toEqual( { count: 1, isDirty: false } ); + } ); + it( 'should preserve isDirty into non-resetting non-reference-changing types', () => { const reducer = withChangeDetection( { resetTypes: [ 'RESET' ] } )( originalReducer ); diff --git a/editor/utils/with-history/index.js b/editor/utils/with-history/index.js index 364a0bc29d6fdc..cc90f0d4b66478 100644 --- a/editor/utils/with-history/index.js +++ b/editor/utils/with-history/index.js @@ -1,18 +1,48 @@ /** * External dependencies */ -import { includes, first, last, drop, dropRight } from 'lodash'; +import { overSome, includes, first, last, drop, dropRight } from 'lodash'; + +/** + * Default options for withHistory reducer enhancer. Refer to withHistory + * documentation for options explanation. + * + * @see withHistory + * + * @type {Object} + */ +const DEFAULT_OPTIONS = { + resetTypes: [], + ignoreTypes: [], + shouldOverwriteState: () => false, +}; /** * Higher-order reducer creator which transforms the result of the original * reducer into an object tracking its own history (past, present, future). * - * @param {?Object} options Optional options. - * @param {?Array} options.resetTypes Action types upon which to clear past. + * @param {?Object} options Optional options. + * @param {?Array} options.resetTypes Action types upon which to + * clear past. + * @param {?Array} options.ignoreTypes Action types upon which to + * avoid history tracking. + * @param {?Function} options.shouldOverwriteState Function receiving last and + * current actions, returning + * boolean indicating whether + * present should be merged, + * rather than add undo level. * * @return {Function} Higher-order reducer. */ const withHistory = ( options = {} ) => ( reducer ) => { + options = { ...DEFAULT_OPTIONS, ...options }; + + // `ignoreTypes` is simply a convenience for `shouldOverwriteState` + options.shouldOverwriteState = overSome( [ + options.shouldOverwriteState, + ( action ) => includes( options.ignoreTypes, action.type ), + ] ); + const initialState = { past: [], present: reducer( undefined, {} ), diff --git a/editor/utils/with-history/test/index.js b/editor/utils/with-history/test/index.js index c7730786688ac9..10757a09471468 100644 --- a/editor/utils/with-history/test/index.js +++ b/editor/utils/with-history/test/index.js @@ -101,6 +101,21 @@ describe( 'withHistory', () => { } ); } ); + it( 'should ignore history by options.ignoreTypes', () => { + const reducer = withHistory( { ignoreTypes: [ 'INCREMENT' ] } )( counter ); + + let state; + state = reducer( undefined, {} ); + state = reducer( state, { type: 'INCREMENT' } ); + state = reducer( state, { type: 'INCREMENT' } ); + + expect( state ).toEqual( { + past: [ 0 ], // Needs at least one history + present: 2, + future: [], + } ); + } ); + it( 'should return same reference if state has not changed', () => { const reducer = withHistory()( counter ); const original = reducer( undefined, {} ); diff --git a/element/index.js b/element/index.js index e35c715b077386..56ad7dc410ac67 100644 --- a/element/index.js +++ b/element/index.js @@ -1,17 +1,33 @@ /** * External dependencies */ -import { createElement, Component, cloneElement, Children, Fragment } from 'react'; +import { + createElement, + createContext, + createRef, + Component, + cloneElement, + Children, + Fragment, +} from 'react'; import { render, findDOMNode, createPortal, unmountComponentAtNode } from 'react-dom'; -import { renderToStaticMarkup } from 'react-dom/server'; import { camelCase, flowRight, isString, upperFirst, - isEmpty, } from 'lodash'; +/** + * WordPress dependencies + */ +import { deprecated } from '@wordpress/utils'; + +/** + * Internal dependencies + */ +import serialize from './serialize'; + /** * Returns a new element of given type. Type can be either a string tag name or * another function which itself returns an element. @@ -26,6 +42,15 @@ import { */ export { createElement }; +/** + * Returns an object tracking a reference to a rendered element via its + * `current` property as either a DOMElement or Element, dependent upon the + * type of element rendered with the ref attribute. + * + * @return {Object} Ref object. + */ +export { createRef }; + /** * Renders a given element into the target DOM node. * @@ -71,6 +96,15 @@ export { Children }; */ export { Fragment }; +/** + * Creates a context object containing two components: a provider and consumer. + * + * @param {Object} defaultValue Data stored in the context. + * + * @return {Object} Context object. + */ +export { createContext }; + /** * Creates a portal into which a component can be rendered. * @@ -88,14 +122,7 @@ export { createPortal }; * * @return {string} HTML. */ -export function renderToString( element ) { - let rendered = renderToStaticMarkup( element ); - - // Drop raw HTML wrappers (support dangerous inner HTML without wrapper) - rendered = rendered.replace( /<\/?wp-raw-html>/g, '' ); - - return rendered; -} +export { serialize as renderToString }; /** * Concatenate two or more React children objects. @@ -158,11 +185,38 @@ export { flowRight as compose }; * @return {string} Wrapped display name. */ export function getWrapperDisplayName( BaseComponent, wrapperName ) { + deprecated( 'getWrapperDisplayName', { + version: '2.7', + alternative: 'wp.element.createHigherOrderComponent', + plugin: 'Gutenberg', + } ); + const { displayName = BaseComponent.name || 'Component' } = BaseComponent; return `${ upperFirst( camelCase( wrapperName ) ) }(${ displayName })`; } +/** + * Given a function mapping a component to an enhanced component and modifier + * name, returns the enhanced component augmented with a generated displayName. + * + * @param {Function} mapComponentToEnhancedComponent Function mapping component + * to enhanced component. + * @param {string} modifierName Seed name from which to + * generated display name. + * + * @return {WPComponent} Component class with generated display name assigned. + */ +export function createHigherOrderComponent( mapComponentToEnhancedComponent, modifierName ) { + return ( OriginalComponent ) => { + const EnhancedComponent = mapComponentToEnhancedComponent( OriginalComponent ); + const { displayName = OriginalComponent.name || 'Component' } = OriginalComponent; + EnhancedComponent.displayName = `${ upperFirst( camelCase( modifierName ) ) }(${ displayName })`; + + return EnhancedComponent; + }; +} + /** * Component used as equivalent of Fragment with unescaped HTML, in cases where * it is desirable to render dangerous HTML without needing a wrapper element. @@ -174,14 +228,10 @@ export function getWrapperDisplayName( BaseComponent, wrapperName ) { * @return {WPElement} Dangerously-rendering element. */ export function RawHTML( { children, ...props } ) { - // Render wrapper only if props are non-empty. - const tagName = isEmpty( props ) ? 'wp-raw-html' : 'div'; - - // Merge HTML into assigned props. - props = { + // The DIV wrapper will be stripped by serializer, unless there are + // non-children props present. + return createElement( 'div', { dangerouslySetInnerHTML: { __html: children }, ...props, - }; - - return createElement( tagName, props ); + } ); } diff --git a/element/test/index.js b/element/test/index.js index af182d573c7655..8bfa39e905be69 100644 --- a/element/test/index.js +++ b/element/test/index.js @@ -9,10 +9,10 @@ import { shallow } from 'enzyme'; import { Component, createElement, + createHigherOrderComponent, concatChildren, renderToString, switchChildrenNodeName, - getWrapperDisplayName, RawHTML, } from '../'; @@ -51,6 +51,21 @@ describe( 'element', () => { ) ).toBe( '<strong>Courgette</strong>' ); } ); + it( 'should escape attributes and html', () => { + const result = renderToString( createElement( 'a', { + href: '/index.php?foo=bar&qux=<"scary">', + style: { + backgroundColor: 'red', + }, + }, '<"WordPress" & Friends>' ) ); + + expect( result ).toBe( + '<a href="/index.php?foo=bar&amp;qux=<&quot;scary&quot;>" style="background-color:red">' + + '&lt;"WordPress" &amp; Friends>' + + '</a>' + ); + } ); + it( 'strips raw html wrapper', () => { const html = '<p>So scary!</p>'; @@ -108,42 +123,64 @@ describe( 'element', () => { } ); } ); - describe( 'getWrapperDisplayName()', () => { + describe( 'createHigherOrderComponent', () => { it( 'should use default name for anonymous function', () => { - expect( getWrapperDisplayName( () => <div />, 'test' ) ).toBe( 'Test(Component)' ); + const TestComponent = createHigherOrderComponent( + OriginalComponent => OriginalComponent, + 'withTest' + )( () => <div /> ); + + expect( TestComponent.displayName ).toBe( 'WithTest(Component)' ); } ); it( 'should use camel case starting with upper for wrapper prefix ', () => { - expect( getWrapperDisplayName( () => <div />, 'one-two_threeFOUR' ) ).toBe( 'OneTwoThreeFour(Component)' ); + const TestComponent = createHigherOrderComponent( + OriginalComponent => OriginalComponent, + 'with-one-two_threeFOUR' + )( () => <div /> ); + + expect( TestComponent.displayName ).toBe( 'WithOneTwoThreeFour(Component)' ); } ); it( 'should use function name', () => { function SomeComponent() { return <div />; } + const TestComponent = createHigherOrderComponent( + OriginalComponent => OriginalComponent, + 'withTest' + )( SomeComponent ); - expect( getWrapperDisplayName( SomeComponent, 'test' ) ).toBe( 'Test(SomeComponent)' ); + expect( TestComponent.displayName ).toBe( 'WithTest(SomeComponent)' ); } ); it( 'should use component class name', () => { - class SomeComponent extends Component { + class SomeAnotherComponent extends Component { render() { return <div />; } } + const TestComponent = createHigherOrderComponent( + OriginalComponent => OriginalComponent, + 'withTest' + )( SomeAnotherComponent ); - expect( getWrapperDisplayName( SomeComponent, 'test' ) ).toBe( 'Test(SomeComponent)' ); + expect( TestComponent.displayName ).toBe( 'WithTest(SomeAnotherComponent)' ); } ); it( 'should use displayName property', () => { - class SomeComponent extends Component { + class SomeYetAnotherComponent extends Component { render() { return <div />; } } - SomeComponent.displayName = 'CustomDisplayName'; + SomeYetAnotherComponent.displayName = 'CustomDisplayName'; + const TestComponent = createHigherOrderComponent( + OriginalComponent => OriginalComponent, + 'withTest' + )( SomeYetAnotherComponent ); - expect( getWrapperDisplayName( SomeComponent, 'test' ) ).toBe( 'Test(CustomDisplayName)' ); + expect( TestComponent.displayName ).toBe( 'WithTest(CustomDisplayName)' ); } ); } ); @@ -156,7 +193,7 @@ describe( 'element', () => { </RawHTML> ); - expect( element.type() ).toBe( 'wp-raw-html' ); + expect( element.type() ).toBe( 'div' ); expect( element.prop( 'dangerouslySetInnerHTML' ).__html ).toBe( html ); expect( element.prop( 'children' ) ).toBe( undefined ); } ); diff --git a/gutenberg.php b/gutenberg.php index f90dd634ff8a4f..31172632cabc37 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: 2.2.0 + * Version: 2.6.0 * Author: Gutenberg Team * * @package gutenberg @@ -162,6 +162,12 @@ function gutenberg_init( $return, $post ) { add_filter( 'screen_options_show_screen', '__return_false' ); add_filter( 'admin_body_class', 'gutenberg_add_admin_body_class' ); + /** + * Remove the emoji script as it is incompatible with both React and any + * contenteditable fields. + */ + remove_action( 'admin_print_scripts', 'print_emoji_detection_script' ); + require_once ABSPATH . 'wp-admin/admin-header.php'; the_gutenberg_project(); @@ -521,7 +527,7 @@ function toggleDropdown() { * * @since 1.5.0 * - * @param string $classes Space seperated string of classes being added to the body tag. + * @param string $classes Space separated string of classes being added to the body tag. * @return string The $classes string, with gutenberg-editor-page appended. */ function gutenberg_add_admin_body_class( $classes ) { diff --git a/i18n/README.md b/i18n/README.md deleted file mode 100644 index a9a63d62a14858..00000000000000 --- a/i18n/README.md +++ /dev/null @@ -1,80 +0,0 @@ -i18n -====== - -Internationalization utilities for client-side localization. - -https://codex.wordpress.org/I18n_for_WordPress_Developers - -## Usage - -Include `wp-i18n` as a script dependency when [enqueueing](https://developer.wordpress.org/reference/functions/wp_enqueue_script/) or [registering](https://developer.wordpress.org/reference/functions/wp_register_script/) a script for your plugin. - -```php -function myplugin_enqueue_scripts() { - wp_enqueue_script( 'myplugin', plugins_url( 'script.js', __FILE__ ), array( 'wp-i18n' ) ); -} -add_action( 'admin_enqueue_scripts', 'myplugin_enqueue_scripts' ); -``` - -The script dependency will add a new `wp.i18n` object to your browser's global scope when loaded. In most cases you'll find parallels between [WordPress PHP localization functions](https://codex.wordpress.org/I18n_for_WordPress_Developers#Strings_for_Translation) and those on the `wp.i18n` object: - -```js -wp.i18n.sprintf( wp.i18n._n( '%d hat', '%d hats', 4 ), 4 ) -// 4 hats -``` - -Note that you will not need to specify [domain](https://codex.wordpress.org/I18n_for_WordPress_Developers#Text_Domains) for the strings. - -## Build - -Included is a [custom Babel plugin](./babel-plugin.js) which, when integrated into a Babel configuration, will scan all processed JavaScript files for use of localization functions. It then compiles these into a [gettext POT formatted](https://en.wikipedia.org/wiki/Gettext) file as a template for translation. By default the output file will be written to `gettext.pot` of the root project directory. This can be overridden using the `"output"` option of the plugin: - -```json -[ "babel-plugin-wp-i18n", { - "output": "languages/myplugin.pot" -} ] -``` - -If you include the `.pot` file in your project's repository, you should be sure to rebuild it with every commit that introduces or modifies localized strings. When handling merge conflicts on the `.pot` file, you can assume that simply rebuilding will generate a file that is up to date with the current files of the project. - -## API - -`wp.i18n.__( text: string ): string` - -Retrieve the translation of text. - -See: https://developer.wordpress.org/reference/functions/__/ - -`wp.i18n._x( text: string, context: string ): string` - -Retrieve translated string with gettext context. - -See: https://developer.wordpress.org/reference/functions/_x/ - -`wp.i18n._n( single: string, plural: string, number: Number ): string` - -Translates and retrieves the singular or plural form based on the supplied number. - -See: https://developer.wordpress.org/reference/functions/_n/ - -`wp.i18n._nx( single: string, plural: string, number: Number, context: string ): string` - -Translates and retrieves the singular or plural form based on the supplied number, with gettext context. - -See: https://developer.wordpress.org/reference/functions/_nx/ - -`wp.i18n.sprintf( format: string, ...args: mixed[] ): string` - -Returns a formatted string. - -See: http://www.diveintojavascript.com/projects/javascript-sprintf - -`wp.i18n.setLocaleData( data: Object )` - -Creates a new Jed instance with specified locale data configuration. - -`wp.i18n.getI18n(): Jed` - -Returns the current Jed instance, initializing with a default configuration if not already assigned. - -See: http://messageformat.github.io/Jed/ diff --git a/i18n/babel-plugin.js b/i18n/babel-plugin.js deleted file mode 100644 index a06a10e6ad8aef..00000000000000 --- a/i18n/babel-plugin.js +++ /dev/null @@ -1,345 +0,0 @@ -/** - * Credits: - * - * babel-gettext-extractor - * https://github.com/getsentry/babel-gettext-extractor - * - * The MIT License (MIT) - * - * Copyright (c) 2015 jruchaud - * Copyright (c) 2015 Sentry - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -/** - * External dependencies - */ - -const { po } = require( 'gettext-parser' ); -const { pick, reduce, uniq, forEach, sortBy, isEqual, merge, isEmpty } = require( 'lodash' ); -const { relative, sep } = require( 'path' ); -const { writeFileSync } = require( 'fs' ); - -/** - * Default output headers if none specified in plugin options. - * - * @type {Object} - */ -const DEFAULT_HEADERS = { - 'content-type': 'text/plain; charset=UTF-8', - 'x-generator': 'babel-plugin-wp-i18n', -}; - -/** - * Default functions to parse if none specified in plugin options. Each key is - * a CallExpression name (or member name) and the value an array corresponding - * to translation key argument position. - * - * @type {Object} - */ -const DEFAULT_FUNCTIONS = { - __: [ 'msgid' ], - _n: [ 'msgid', 'msgid_plural' ], - _x: [ 'msgid', 'msgctxt' ], - _nx: [ 'msgid', 'msgctxt', 'msgid_plural' ], -}; - -/** - * Default file output if none specified. - * - * @type {string} - */ -const DEFAULT_OUTPUT = 'gettext.pot'; - -/** - * Set of keys which are valid to be assigned into a translation object. - * - * @type {string[]} - */ -const VALID_TRANSLATION_KEYS = [ 'msgid', 'msgid_plural', 'msgctxt' ]; - -/** - * Regular expression matching translator comment value. - * - * @type {RegExp} - */ -const REGEXP_TRANSLATOR_COMMENT = /^\s*translators:\s*([\s\S]+)/im; - -/** - * Given an argument node (or recursed node), attempts to return a string - * represenation of that node's value. - * - * @param {Object} node AST node. - * - * @return {string} String value. - */ -function getNodeAsString( node ) { - switch ( node.type ) { - case 'BinaryExpression': - return ( - getNodeAsString( node.left ) + - getNodeAsString( node.right ) - ); - - case 'StringLiteral': - return node.value; - - default: - return ''; - } -} - -/** - * Returns translator comment for a given AST traversal path if one exists. - * - * @param {Object} path Traversal path. - * @param {number} _originalNodeLine Private: In recursion, line number of - * the original node passed. - * - * @return {?string} Translator comment. - */ -function getTranslatorComment( path, _originalNodeLine ) { - const { node, parent, parentPath } = path; - - // Assign original node line so we can keep track in recursion whether a - // matched comment or parent occurs on the same or previous line - if ( ! _originalNodeLine ) { - _originalNodeLine = node.loc.start.line; - } - - let comment; - forEach( node.leadingComments, ( commentNode ) => { - const { line } = commentNode.loc.end; - if ( line < _originalNodeLine - 1 || line > _originalNodeLine ) { - return; - } - - const match = commentNode.value.match( REGEXP_TRANSLATOR_COMMENT ); - if ( match ) { - // Extract text from matched translator prefix - comment = match[ 1 ].split( '\n' ).map( ( text ) => text.trim() ).join( ' ' ); - - // False return indicates to Lodash to break iteration - return false; - } - } ); - - if ( comment ) { - return comment; - } - - if ( ! parent || ! parent.loc || ! parentPath ) { - return; - } - - // Only recurse as long as parent node is on the same or previous line - const { line } = parent.loc.start; - if ( line >= _originalNodeLine - 1 && line <= _originalNodeLine ) { - return getTranslatorComment( parentPath, _originalNodeLine ); - } -} - -/** - * Returns true if the specified key of a function is valid for assignment in - * the translation object. - * - * @param {string} key Key to test. - * - * @return {boolean} Whether key is valid for assignment. - */ -function isValidTranslationKey( key ) { - return -1 !== VALID_TRANSLATION_KEYS.indexOf( key ); -} - -/** - * Given two translation objects, returns true if valid translation keys match, - * or false otherwise. - * - * @param {Object} a First translation object. - * @param {Object} b Second translation object. - * - * @return {boolean} Whether valid translation keys match. - */ -function isSameTranslation( a, b ) { - return isEqual( - pick( a, VALID_TRANSLATION_KEYS ), - pick( b, VALID_TRANSLATION_KEYS ) - ); -} - -module.exports = function() { - const strings = {}; - let nplurals = 2, - baseData; - - return { - visitor: { - CallExpression( path, state ) { - const { callee } = path.node; - - // Determine function name by direct invocation or property name - let name; - if ( 'MemberExpression' === callee.type ) { - name = callee.property.name; - } else { - name = callee.name; - } - - // Skip unhandled functions - const functionKeys = ( state.opts.functions || DEFAULT_FUNCTIONS )[ name ]; - if ( ! functionKeys ) { - return; - } - - // Assign translation keys by argument position - const translation = path.node.arguments.reduce( ( memo, arg, i ) => { - const key = functionKeys[ i ]; - if ( isValidTranslationKey( key ) ) { - memo[ key ] = getNodeAsString( arg ); - } - - return memo; - }, {} ); - - // Can only assign translation with usable msgid - if ( ! translation.msgid ) { - return; - } - - // At this point we assume we'll save data, so initialize if - // we haven't already - if ( ! baseData ) { - baseData = { - charset: 'utf-8', - headers: state.opts.headers || DEFAULT_HEADERS, - translations: { - '': { - '': { - msgid: '', - msgstr: [], - }, - }, - }, - }; - - for ( const key in baseData.headers ) { - baseData.translations[ '' ][ '' ].msgstr.push( `${ key }: ${ baseData.headers[ key ] };\n` ); - } - - // Attempt to exract nplurals from header - const pluralsMatch = ( baseData.headers[ 'plural-forms' ] || '' ).match( /nplurals\s*=\s*(\d+);/ ); - if ( pluralsMatch ) { - nplurals = pluralsMatch[ 1 ]; - } - } - - // Create empty msgstr or array of empty msgstr by nplurals - if ( translation.msgid_plural ) { - translation.msgstr = Array.from( Array( nplurals ) ).map( () => '' ); - } else { - translation.msgstr = ''; - } - - // Assign file reference comment, ensuring consistent pathname - // reference between Win32 and POSIX - const { filename } = this.file.opts; - const pathname = relative( '.', filename ).split( sep ).join( '/' ); - translation.comments = { - reference: pathname + ':' + path.node.loc.start.line, - }; - - // If exists, also assign translator comment - const translator = getTranslatorComment( path ); - if ( translator ) { - translation.comments.translator = translator; - } - - // Create context grouping for translation if not yet exists - const { msgctxt = '', msgid } = translation; - if ( ! strings[ filename ].hasOwnProperty( msgctxt ) ) { - strings[ filename ][ msgctxt ] = {}; - } - - strings[ filename ][ msgctxt ][ msgid ] = translation; - }, - Program: { - enter() { - strings[ this.file.opts.filename ] = {}; - }, - exit( path, state ) { - const { filename } = this.file.opts; - if ( isEmpty( strings[ filename ] ) ) { - delete strings[ filename ]; - return; - } - - // Sort translations by filename for deterministic output - const files = Object.keys( strings ).sort(); - - // Combine translations from each file grouped by context - const translations = reduce( files, ( memo, file ) => { - for ( const context in strings[ file ] ) { - // Within the same file, sort translations by line - const sortedTranslations = sortBy( - strings[ file ][ context ], - 'comments.reference' - ); - - forEach( sortedTranslations, ( translation ) => { - const { msgctxt = '', msgid } = translation; - if ( ! memo.hasOwnProperty( msgctxt ) ) { - memo[ msgctxt ] = {}; - } - - // Merge references if translation already exists - if ( isSameTranslation( translation, memo[ msgctxt ][ msgid ] ) ) { - translation.comments.reference = uniq( [ - memo[ msgctxt ][ msgid ].comments.reference, - translation.comments.reference, - ].join( '\n' ).split( '\n' ) ).join( '\n' ); - } - - memo[ msgctxt ][ msgid ] = translation; - } ); - } - - return memo; - }, {} ); - - // Merge translations from individual files into headers - const data = merge( {}, baseData, { translations } ); - - // Ideally we could wait until Babel has finished parsing - // all files or at least asynchronously write, but the - // Babel loader doesn't expose these entry points and async - // write may hit file lock (need queue). - const compiled = po.compile( data ); - writeFileSync( state.opts.output || DEFAULT_OUTPUT, compiled ); - this.hasPendingWrite = false; - }, - }, - }, - }; -}; - -module.exports.getNodeAsString = getNodeAsString; -module.exports.getTranslatorComment = getTranslatorComment; -module.exports.isValidTranslationKey = isValidTranslationKey; -module.exports.isSameTranslation = isSameTranslation; diff --git a/i18n/index.js b/i18n/index.js deleted file mode 100644 index 463987af76437a..00000000000000 --- a/i18n/index.js +++ /dev/null @@ -1,102 +0,0 @@ -/** - * External dependencies - */ -import Jed from 'jed'; - -let i18n; - -/** - * Creates a new Jed instance with specified locale data configuration. - * - * @see http://messageformat.github.io/Jed/ - * - * @param {Object} data Locale data configuration. - */ -export function setLocaleData( data ) { - i18n = new Jed( data ); -} - -/** - * Returns the current Jed instance, initializing with a default configuration - * if not already assigned. - * - * @return {Jed} Jed instance. - */ -export function getI18n() { - if ( ! i18n ) { - setLocaleData( { '': {} } ); - } - - return i18n; -} - -/** - * Retrieve the translation of text. - * - * @see https://developer.wordpress.org/reference/functions/__/ - * - * @param {string} text Text to translate. - * - * @return {string} Translated text. - */ -export function __( text ) { - return getI18n().gettext( text ); -} - -/** - * Retrieve translated string with gettext context. - * - * @see https://developer.wordpress.org/reference/functions/_x/ - * - * @param {string} text Text to translate. - * @param {string} context Context information for the translators. - * - * @return {string} Translated context string without pipe. - */ -export function _x( text, context ) { - return getI18n().pgettext( context, text ); -} - -/** - * Translates and retrieves the singular or plural form based on the supplied - * number. - * - * @see https://developer.wordpress.org/reference/functions/_n/ - * - * @param {string} single The text to be used if the number is singular. - * @param {string} plural The text to be used if the number is plural. - * @param {number} number The number to compare against to use either the - * singular or plural form. - * - * @return {string} The translated singular or plural form. - */ -export function _n( single, plural, number ) { - return getI18n().ngettext( single, plural, number ); -} - -/** - * Translates and retrieves the singular or plural form based on the supplied - * number, with gettext context. - * - * @see https://developer.wordpress.org/reference/functions/_nx/ - * - * @param {string} single The text to be used if the number is singular. - * @param {string} plural The text to be used if the number is plural. - * @param {number} number The number to compare against to use either the - * singular or plural form. - * @param {string} context Context information for the translators. - * - * @return {string} The translated singular or plural form. - */ -export function _nx( single, plural, number, context ) { - return getI18n().npgettext( context, single, plural, number ); -} - -/** - * Returns a formatted string. - * - * @see http://www.diveintojavascript.com/projects/javascript-sprintf - * - * @type {string} - */ -export const sprintf = Jed.sprintf; diff --git a/i18n/test/babel-plugin.js b/i18n/test/babel-plugin.js deleted file mode 100644 index ca5c1ea898a38e..00000000000000 --- a/i18n/test/babel-plugin.js +++ /dev/null @@ -1,125 +0,0 @@ -/** - * External dependencies - */ -import { transform } from 'babel-core'; -import traverse from 'babel-traverse'; - -/** - * Internal dependencies - */ -import babelPlugin from '../babel-plugin'; - -describe( 'babel-plugin', () => { - const { - getNodeAsString, - getTranslatorComment, - isValidTranslationKey, - isSameTranslation, - } = babelPlugin; - - describe( '.isValidTranslationKey()', () => { - it( 'should return false if not one of valid keys', () => { - expect( isValidTranslationKey( 'foo' ) ).toBe( false ); - } ); - - it( 'should return true if one of valid keys', () => { - expect( isValidTranslationKey( 'msgid' ) ).toBe( true ); - } ); - } ); - - describe( '.isSameTranslation()', () => { - it( 'should return false if any translation keys differ', () => { - const a = { msgid: 'foo' }; - const b = { msgid: 'bar' }; - - expect( isSameTranslation( a, b ) ).toBe( false ); - } ); - - it( 'should return true if all translation keys the same', () => { - const a = { msgid: 'foo', comments: { reference: 'a' } }; - const b = { msgid: 'foo', comments: { reference: 'b' } }; - - expect( isSameTranslation( a, b ) ).toBe( true ); - } ); - } ); - - describe( '.getTranslatorComment()', () => { - function getCommentFromString( string ) { - let comment; - traverse( transform( string ).ast, { - CallExpression( path ) { - comment = getTranslatorComment( path ); - }, - } ); - - return comment; - } - - it( 'should not return translator comment on same line but after call expression', () => { - const comment = getCommentFromString( '__( \'Hello world\' ); // translators: Greeting' ); - - expect( comment ).toBeUndefined(); - } ); - - it( 'should return translator comment on leading comments', () => { - const comment = getCommentFromString( '// translators: Greeting\n__( \'Hello world\' );' ); - - expect( comment ).toBe( 'Greeting' ); - } ); - - it( 'should be case insensitive to translator prefix', () => { - const comment = getCommentFromString( '// TrANslAtORs: Greeting\n__( \'Hello world\' );' ); - - expect( comment ).toBe( 'Greeting' ); - } ); - - it( 'should traverse up parents until it encounters comment', () => { - const comment = getCommentFromString( '// translators: Greeting\nconst string = __( \'Hello world\' );' ); - - expect( comment ).toBe( 'Greeting' ); - } ); - - it( 'should not consider comment if it does not end on same or previous line', () => { - const comment = getCommentFromString( '// translators: Greeting\n\n__( \'Hello world\' );' ); - - expect( comment ).toBeUndefined(); - } ); - - it( 'should use multi-line comment starting many lines previous', () => { - const comment = getCommentFromString( '/* translators: Long comment\nspanning multiple \nlines */\nconst string = __( \'Hello world\' );' ); - - expect( comment ).toBe( 'Long comment spanning multiple lines' ); - } ); - } ); - - describe( '.getNodeAsString()', () => { - function getNodeAsStringFromArgument( source ) { - let string; - traverse( transform( source ).ast, { - CallExpression( path ) { - string = getNodeAsString( path.node.arguments[ 0 ] ); - }, - } ); - - return string; - } - - it( 'should returns an empty string by default', () => { - const string = getNodeAsStringFromArgument( '__( {} );' ); - - expect( string ).toBe( '' ); - } ); - - it( 'should return a string value', () => { - const string = getNodeAsStringFromArgument( '__( "hello" );' ); - - expect( string ).toBe( 'hello' ); - } ); - - it( 'should be a concatenated binary expression string value', () => { - const string = getNodeAsStringFromArgument( '__( "hello" + " world" );' ); - - expect( string ).toBe( 'hello world' ); - } ); - } ); -} ); diff --git a/languages/README.md b/languages/README.md index 97a4dfbee34ca3..3f3ba1d4478c68 100644 --- a/languages/README.md +++ b/languages/README.md @@ -4,7 +4,7 @@ Languages The generated POT template file is not included in this repository. To create this file locally, follow instructions from [CONTRIBUTING.md](https://github.com/WordPress/gutenberg/blob/master/CONTRIBUTING.md) to install the project, then run the following command: ``` -npm run gettext-strings +npm run build ``` After the build completes, you'll find a `gutenberg.pot` strings file in this directory. diff --git a/lib/class-wp-block-type.php b/lib/class-wp-block-type.php index 38c58155381496..39f08e54fddc28 100644 --- a/lib/class-wp-block-type.php +++ b/lib/class-wp-block-type.php @@ -18,7 +18,6 @@ class WP_Block_Type { * Block type key. * * @since 0.6.0 - * @access public * @var string */ public $name; @@ -27,7 +26,6 @@ class WP_Block_Type { * Block type render callback. * * @since 0.6.0 - * @access public * @var callable */ public $render_callback; @@ -36,7 +34,6 @@ class WP_Block_Type { * Block type attributes property schemas. * * @since 0.10.0 - * @access public * @var array */ public $attributes; @@ -45,7 +42,6 @@ class WP_Block_Type { * Block type editor script handle. * * @since 2.0.0 - * @access public * @var string */ public $editor_script; @@ -54,7 +50,6 @@ class WP_Block_Type { * Block type front end script handle. * * @since 2.0.0 - * @access public * @var string */ public $script; @@ -63,7 +58,6 @@ class WP_Block_Type { * Block type editor style handle. * * @since 2.0.0 - * @access public * @var string */ public $editor_style; @@ -72,7 +66,6 @@ class WP_Block_Type { * Block type front end style handle. * * @since 2.0.0 - * @access public * @var string */ public $style; @@ -83,7 +76,6 @@ class WP_Block_Type { * Will populate object properties from the provided arguments. * * @since 0.6.0 - * @access public * * @see register_block_type() * @@ -101,7 +93,6 @@ public function __construct( $block_type, $args = array() ) { * Renders the block type output for given attributes. * * @since 0.6.0 - * @access public * * @param array $attributes Optional. Block attributes. Default empty array. * @return string Rendered block type output. @@ -120,7 +111,7 @@ public function render( $attributes = array() ) { * Returns true if the block type is dynamic, or false otherwise. A dynamic * block is one which defers its rendering to occur on-demand at runtime. * - * @returns boolean Whether block type is dynamic. + * @return boolean Whether block type is dynamic. */ public function is_dynamic() { return is_callable( $this->render_callback ); @@ -164,7 +155,6 @@ public function prepare_attributes_for_render( $attributes ) { * Sets block type properties. * * @since 0.6.0 - * @access public * * @param array|string $args Array or string of arguments for registering a block type. */ diff --git a/lib/class-wp-rest-blocks-controller.php b/lib/class-wp-rest-blocks-controller.php index 2b19e2a5f0b0e7..7435c63e56b9e8 100644 --- a/lib/class-wp-rest-blocks-controller.php +++ b/lib/class-wp-rest-blocks-controller.php @@ -1,6 +1,6 @@ <?php /** - * Reusable Blocks REST API: WP_REST_Blocks_Controller class + * Shared blocks REST API: WP_REST_Blocks_Controller class * * @package gutenberg * @since 0.10.0 @@ -8,14 +8,47 @@ /** * Controller which provides a REST endpoint for Gutenberg to read, create, - * edit and delete reusable blocks. Blocks are stored as posts with the - * wp_block post type. + * edit and delete shared blocks. Blocks are stored as posts with the wp_block + * post type. * * @since 0.10.0 * * @see WP_REST_Controller */ class WP_REST_Blocks_Controller extends WP_REST_Posts_Controller { + /** + * Checks if a block can be read. + * + * @since 2.1.0 + * + * @param object $post Post object that backs the block. + * @return bool Whether the block can be read. + */ + public function check_read_permission( $post ) { + // Ensure that the user is logged in and has the read_blocks capability. + $post_type = get_post_type_object( $post->post_type ); + if ( ! current_user_can( $post_type->cap->read_post, $post->ID ) ) { + return false; + } + + return parent::check_read_permission( $post ); + } + + /** + * Handle a DELETE request. + * + * @since 1.10.0 + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function delete_item( $request ) { + // Always hard-delete a block. + $request->set_param( 'force', true ); + + return parent::delete_item( $request ); + } + /** * Given an update or create request, build the post object that is saved to * the database. @@ -25,33 +58,22 @@ class WP_REST_Blocks_Controller extends WP_REST_Posts_Controller { * @param WP_REST_Request $request Request object. * @return stdClass|WP_Error Post object or WP_Error. */ - protected function prepare_item_for_database( $request ) { - $prepared_post = new stdClass; - - if ( isset( $request['id'] ) ) { - $existing_post = $this->get_post( $request['id'] ); - if ( is_wp_error( $existing_post ) ) { - return $existing_post; - } - - $prepared_post->ID = $existing_post->ID; - } + public function prepare_item_for_database( $request ) { + $prepared_post = parent::prepare_item_for_database( $request ); - $prepared_post->post_title = $request['title']; - $prepared_post->post_content = $request['content']; - $prepared_post->post_type = $this->post_type; - $prepared_post->post_status = 'publish'; + // Force blocks to always be published. + $prepared_post->post_status = 'publish'; - return apply_filters( "rest_pre_insert_{$this->post_type}", $prepared_post, $request ); + return $prepared_post; } /** - * Given a post from the database, build the array that is returned from an + * Given a block from the database, build the array that is returned from an * API response. * * @since 1.10.0 * - * @param WP_Post $post Post object. + * @param WP_Post $post Post object that backs the block. * @param WP_REST_Request $request Request object. * @return WP_REST_Response Response object. */ @@ -67,21 +89,6 @@ public function prepare_item_for_response( $post, $request ) { return apply_filters( "rest_prepare_{$this->post_type}", $response, $post, $request ); } - /** - * Handle a DELETE request. - * - * @since 1.10.0 - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. - */ - public function delete_item( $request ) { - // Always hard-delete a block. - $request->set_param( 'force', true ); - - return parent::delete_item( $request ); - } - /** * Builds the block's schema, conforming to JSON Schema. * diff --git a/lib/client-assets.php b/lib/client-assets.php index 341cd1debbf9ba..0cf66a5e4c71f0 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -79,26 +79,39 @@ function gutenberg_register_scripts_and_styles() { wp_register_script( 'wp-data', gutenberg_url( 'data/build/index.js' ), - array( 'wp-element', 'wp-utils' ), - filemtime( gutenberg_dir_path() . 'data/build/index.js' ) + array( 'wp-element', 'wp-utils', 'lodash' ), + filemtime( gutenberg_dir_path() . 'data/build/index.js' ), + true + ); + wp_register_script( + 'wp-core-data', + gutenberg_url( 'core-data/build/index.js' ), + array( 'wp-data', 'wp-api-request', 'lodash' ), + filemtime( gutenberg_dir_path() . 'core-data/build/index.js' ), + true ); wp_register_script( 'wp-utils', gutenberg_url( 'utils/build/index.js' ), - array(), - filemtime( gutenberg_dir_path() . 'utils/build/index.js' ) + array( 'tinymce-latest', 'lodash' ), + filemtime( gutenberg_dir_path() . 'utils/build/index.js' ), + true ); + wp_add_inline_script( 'wp-utils', 'var originalUtils = window.wp && window.wp.utils ? window.wp.utils : {};', 'before' ); + wp_add_inline_script( 'wp-utils', 'for ( var key in originalUtils ) wp.utils[ key ] = originalUtils[ key ];' ); wp_register_script( 'wp-hooks', gutenberg_url( 'hooks/build/index.js' ), array(), - filemtime( gutenberg_dir_path() . 'hooks/build/index.js' ) + filemtime( gutenberg_dir_path() . 'hooks/build/index.js' ), + true ); wp_register_script( 'wp-date', gutenberg_url( 'date/build/index.js' ), array( 'moment' ), - filemtime( gutenberg_dir_path() . 'date/build/index.js' ) + filemtime( gutenberg_dir_path() . 'date/build/index.js' ), + true ); global $wp_locale; wp_add_inline_script( 'wp-date', 'window._wpDateSettings = ' . wp_json_encode( array( @@ -130,35 +143,45 @@ function gutenberg_register_scripts_and_styles() { 'wp-i18n', gutenberg_url( 'i18n/build/index.js' ), array(), - filemtime( gutenberg_dir_path() . 'i18n/build/index.js' ) + filemtime( gutenberg_dir_path() . 'i18n/build/index.js' ), + true ); wp_register_script( 'wp-element', gutenberg_url( 'element/build/index.js' ), - array( 'react', 'react-dom', 'react-dom-server' ), - filemtime( gutenberg_dir_path() . 'element/build/index.js' ) + array( 'react', 'react-dom', 'wp-utils', 'lodash' ), + filemtime( gutenberg_dir_path() . 'element/build/index.js' ), + true ); wp_register_script( 'wp-components', gutenberg_url( 'components/build/index.js' ), - array( 'wp-element', 'wp-i18n', 'wp-utils', 'wp-hooks', 'wp-api-request', 'moment' ), - filemtime( gutenberg_dir_path() . 'components/build/index.js' ) + array( 'wp-element', 'wp-i18n', 'wp-utils', 'wp-hooks', 'wp-api-request', 'moment', 'lodash' ), + filemtime( gutenberg_dir_path() . 'components/build/index.js' ), + true ); wp_register_script( 'wp-blocks', gutenberg_url( 'blocks/build/index.js' ), - array( 'wp-element', 'wp-components', 'wp-utils', 'wp-hooks', 'wp-i18n', 'tinymce-latest', 'tinymce-latest-lists', 'tinymce-latest-paste', 'tinymce-latest-table', 'media-views', 'media-models', 'shortcode', 'editor' ), - filemtime( gutenberg_dir_path() . 'blocks/build/index.js' ) + array( 'wp-element', 'wp-components', 'wp-utils', 'wp-hooks', 'wp-i18n', 'tinymce-latest', 'tinymce-latest-lists', 'tinymce-latest-paste', 'tinymce-latest-table', 'shortcode', 'editor', 'wp-core-data', 'lodash' ), + filemtime( gutenberg_dir_path() . 'blocks/build/index.js' ), + true ); wp_add_inline_script( 'wp-blocks', gutenberg_get_script_polyfill( array( '\'Promise\' in window' => 'promise', '\'fetch\' in window' => 'fetch', - '\'WeakMap\' in window' => 'WeakMap', ) ), 'before' ); + wp_register_script( + 'wp-viewport', + gutenberg_url( 'viewport/build/index.js' ), + array( 'wp-element', 'wp-data', 'wp-components', 'lodash' ), + filemtime( gutenberg_dir_path() . 'viewport/build/index.js' ), + true + ); // Loading the old editor and its config to ensure the classic block works as expected. wp_add_inline_script( 'wp-blocks', 'window.wp.oldEditor = window.wp.editor;', 'before' @@ -231,17 +254,40 @@ function gutenberg_register_scripts_and_styles() { wp_register_script( 'wp-editor', gutenberg_url( 'editor/build/index.js' ), - array( 'postbox', 'jquery', 'wp-api', 'wp-data', 'wp-date', 'wp-i18n', 'wp-blocks', 'wp-element', 'wp-components', 'wp-utils', 'word-count', 'editor' ), - filemtime( gutenberg_dir_path() . 'editor/build/index.js' ) + array( + 'postbox', + 'jquery', + 'wp-api', + 'wp-data', + 'wp-date', + 'wp-i18n', + 'wp-blocks', + 'wp-element', + 'wp-components', + 'wp-utils', + 'wp-viewport', + 'wp-plugins', + 'wp-core-data', + 'word-count', + 'editor', + 'lodash', + ), + filemtime( gutenberg_dir_path() . 'editor/build/index.js' ), + true ); wp_register_script( 'wp-edit-post', gutenberg_url( 'edit-post/build/index.js' ), - array( 'jquery', 'heartbeat', 'wp-element', 'wp-components', 'wp-editor', 'wp-i18n', 'wp-date', 'wp-utils', 'wp-data', 'wp-embed' ), + array( 'jquery', 'media-views', 'media-models', 'wp-element', 'wp-components', 'wp-editor', 'wp-i18n', 'wp-date', 'wp-utils', 'wp-data', 'wp-embed', 'wp-viewport', 'wp-plugins', 'lodash' ), filemtime( gutenberg_dir_path() . 'edit-post/build/index.js' ), true ); + wp_add_inline_script( + 'wp-edit-post', + gutenberg_get_script_polyfill( array( 'window.FormData && window.FormData.prototype.keys' => 'formdata' ) ), + 'before' + ); // Editor Styles. wp_register_style( @@ -288,6 +334,13 @@ function gutenberg_register_scripts_and_styles() { filemtime( gutenberg_dir_path() . 'blocks/build/edit-blocks.css' ) ); wp_style_add_data( 'wp-edit-blocks', 'rtl', 'replace' ); + + wp_register_script( + 'wp-plugins', + gutenberg_url( 'plugins/build/index.js' ), + array( 'wp-element', 'wp-components', 'wp-utils', 'wp-data' ), + filemtime( gutenberg_dir_path() . 'plugins/build/index.js' ) + ); } add_action( 'wp_enqueue_scripts', 'gutenberg_register_scripts_and_styles', 5 ); add_action( 'admin_enqueue_scripts', 'gutenberg_register_scripts_and_styles', 5 ); @@ -345,23 +398,18 @@ function gutenberg_register_vendor_scripts() { gutenberg_register_vendor_script( 'react', - 'https://unpkg.com/react@16.2.0/umd/react' . $react_suffix . '.js' + 'https://unpkg.com/react@16.3.0/umd/react' . $react_suffix . '.js' ); gutenberg_register_vendor_script( 'react-dom', - 'https://unpkg.com/react-dom@16.2.0/umd/react-dom' . $react_suffix . '.js', - array( 'react' ) - ); - gutenberg_register_vendor_script( - 'react-dom-server', - 'https://unpkg.com/react-dom@16.2.0/umd/react-dom-server.browser' . $react_suffix . '.js', + 'https://unpkg.com/react-dom@16.3.0/umd/react-dom' . $react_suffix . '.js', array( 'react' ) ); $moment_script = SCRIPT_DEBUG ? 'moment.js' : 'min/moment.min.js'; gutenberg_register_vendor_script( 'moment', - 'https://unpkg.com/moment@2.18.1/' . $moment_script, - array( 'react' ) + 'https://unpkg.com/moment@2.21.0/' . $moment_script, + array() ); $tinymce_version = '4.7.2'; gutenberg_register_vendor_script( @@ -383,6 +431,11 @@ function gutenberg_register_vendor_scripts() { 'https://unpkg.com/tinymce@' . $tinymce_version . '/plugins/table/plugin' . $suffix . '.js', array( 'tinymce-latest' ) ); + gutenberg_register_vendor_script( + 'lodash', + 'https://unpkg.com/lodash@4.17.5/lodash' . $suffix . '.js' + ); + wp_add_inline_script( 'lodash', 'window.lodash = _.noConflict();' ); gutenberg_register_vendor_script( 'fetch', 'https://unpkg.com/whatwg-fetch/fetch.js' @@ -391,6 +444,10 @@ function gutenberg_register_vendor_scripts() { 'promise', 'https://unpkg.com/promise-polyfill@7.0.0/dist/promise' . $suffix . '.js' ); + gutenberg_register_vendor_script( + 'formdata', + 'https://unpkg.com/formdata-polyfill@3.0.9/formdata.min.js' + ); } /** @@ -518,24 +575,19 @@ function gutenberg_register_vendor_script( $handle, $src, $deps = array() ) { } fclose( $f ); $response = wp_remote_get( $src ); - if ( wp_remote_retrieve_response_code( $response ) !== 200 ) { - // The request failed; just enqueue the script directly from the - // URL. This will probably fail too, but surfacing the error to - // the browser is probably the best we can do. + if ( wp_remote_retrieve_response_code( $response ) === 200 ) { + $f = fopen( $full_path, 'w' ); + fwrite( $f, wp_remote_retrieve_body( $response ) ); + fclose( $f ); + } elseif ( ! filesize( $full_path ) ) { + // The request failed. If the file is already cached, continue to + // use this file. If not, then unlink the 0 byte file, and enqueue + // the script directly from the URL. wp_register_script( $handle, $src, $deps, null ); - // If our file was newly created above, it will have a size of - // zero, and we need to delete it so that we don't think it's - // already cached on the next request. - if ( ! filesize( $full_path ) ) { - unlink( $full_path ); - } + unlink( $full_path ); return; } - $f = fopen( $full_path, 'w' ); - fwrite( $f, wp_remote_retrieve_body( $response ) ); - fclose( $f ); } - wp_register_script( $handle, gutenberg_url( 'vendor/' . $filename ), @@ -569,23 +621,11 @@ function gutenberg_extend_wp_api_backbone_client() { $script = sprintf( 'wp.api.postTypeRestBaseMapping = %s;', wp_json_encode( $post_type_rest_base_mapping ) ); $script .= sprintf( 'wp.api.taxonomyRestBaseMapping = %s;', wp_json_encode( $taxonomy_rest_base_mapping ) ); $script .= <<<JS - wp.api.getPostTypeModel = function( postType ) { - var route = '/' + wpApiSettings.versionString + this.postTypeRestBaseMapping[ postType ] + '/(?P<id>[\\\\d]+)'; - return _.find( wp.api.models, function( model ) { - return model.prototype.route && route === model.prototype.route.index; - } ); + wp.api.getPostTypeRoute = function( postType ) { + return wp.api.postTypeRestBaseMapping[ postType ]; }; - wp.api.getTaxonomyModel = function( taxonomy ) { - var route = '/' + wpApiSettings.versionString + this.taxonomyRestBaseMapping[ taxonomy ] + '/(?P<id>[\\\\d]+)'; - return _.find( wp.api.models, function( model ) { - return model.prototype.route && route === model.prototype.route.index; - } ); - }; - wp.api.getTaxonomyCollection = function( taxonomy ) { - var route = '/' + wpApiSettings.versionString + this.taxonomyRestBaseMapping[ taxonomy ]; - return _.find( wp.api.collections, function( model ) { - return model.prototype.route && route === model.prototype.route.index; - } ); + wp.api.getTaxonomyRoute = function( taxonomy ) { + return wp.api.taxonomyRestBaseMapping[ taxonomy ]; }; JS; wp_add_inline_script( 'wp-api', $script ); @@ -598,12 +638,6 @@ function gutenberg_extend_wp_api_backbone_client() { wp_json_encode( $schema_response->get_data() ) ), 'before' ); } - - /* - * For API requests to happen over HTTP/1.0 methods, - * as HTTP/1.1 methods are blocked in a variety of situations. - */ - wp_add_inline_script( 'wp-api', 'Backbone.emulateHTTP = true;', 'before' ); } /** @@ -740,6 +774,32 @@ function gutenberg_enqueue_registered_block_scripts_and_styles() { add_action( 'enqueue_block_assets', 'gutenberg_enqueue_registered_block_scripts_and_styles' ); add_action( 'enqueue_block_editor_assets', 'gutenberg_enqueue_registered_block_scripts_and_styles' ); +/** + * The code editor settings that were last captured by + * gutenberg_capture_code_editor_settings(). + * + * @var array|false + */ +$gutenberg_captured_code_editor_settings = false; + +/** + * When passed to the wp_code_editor_settings filter, this function captures + * the code editor settings given to it and then prevents + * wp_enqueue_code_editor() from enqueuing any assets. + * + * This is a workaround until e.g. code_editor_settings() is added to Core. + * + * @since 2.1.0 + * + * @param array $settings Code editor settings. + * @return false + */ +function gutenberg_capture_code_editor_settings( $settings ) { + global $gutenberg_captured_code_editor_settings; + $gutenberg_captured_code_editor_settings = $settings; + return false; +} + /** * Scripts & Styles. * @@ -755,12 +815,18 @@ function gutenberg_editor_scripts_and_styles( $hook ) { gutenberg_extend_wp_api_backbone_client(); - wp_enqueue_script( 'wp-edit-post' ); + // Enqueue heartbeat separately as an "optional" dependency of the editor. + // Heartbeat is used for automatic nonce refreshing, but some hosts choose + // to disable it outright. + wp_enqueue_script( 'heartbeat' ); - // Register `wp-utils` as a dependency of `word-count` to ensure that - // `wp-utils` doesn't clobbber `word-count`. See WordPress/gutenberg#1569. - $word_count_script = wp_scripts()->query( 'word-count' ); - array_push( $word_count_script->deps, 'wp-utils' ); + // 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' ); global $post; // Generate API-prepared post. @@ -784,6 +850,7 @@ function gutenberg_editor_scripts_and_styles( $hook ) { // Preload common data. $preload_paths = array( + sprintf( '/wp/v2/types/%s?context=edit', $post_type ), sprintf( '/wp/v2/users/me?post_type=%s&context=edit', $post_type ), '/wp/v2/taxonomies?context=edit', gutenberg_get_rest_link( $post_to_edit, 'about', 'edit' ), @@ -818,9 +885,8 @@ function gutenberg_editor_scripts_and_styles( $hook ) { // Prepare Jed locale data. $locale_data = gutenberg_get_jed_locale_data( 'gutenberg' ); wp_add_inline_script( - 'wp-edit-post', - 'wp.i18n.setLocaleData( ' . json_encode( $locale_data ) . ' );', - 'before' + 'wp-i18n', + 'wp.i18n.setLocaleData( ' . json_encode( $locale_data ) . ' );' ); // Preload server-registered block schemas. @@ -836,6 +902,16 @@ function gutenberg_editor_scripts_and_styles( $hook ) { ), $meta_box_url ); wp_localize_script( 'wp-editor', '_wpMetaBoxUrl', $meta_box_url ); + // Populate default code editor settings by short-circuiting wp_enqueue_code_editor. + global $gutenberg_captured_code_editor_settings; + add_filter( 'wp_code_editor_settings', 'gutenberg_capture_code_editor_settings' ); + wp_enqueue_code_editor( array( 'type' => 'text/html' ) ); + remove_filter( 'wp_code_editor_settings', 'gutenberg_capture_code_editor_settings' ); + wp_add_inline_script( 'wp-editor', sprintf( + 'window._wpGutenbergCodeEditorSettings = %s;', + wp_json_encode( $gutenberg_captured_code_editor_settings ) + ) ); + // Initialize the editor. $gutenberg_theme_support = get_theme_support( 'gutenberg' ); $align_wide = get_theme_support( 'align-wide' ); @@ -867,9 +943,11 @@ function gutenberg_editor_scripts_and_styles( $hook ) { $editor_settings = array( 'alignWide' => $align_wide || ! empty( $gutenberg_theme_support[0]['wide-images'] ), // Backcompat. Use `align-wide` outside of `gutenberg` array. 'availableTemplates' => wp_get_theme()->get_page_templates( get_post( $post_to_edit['id'] ) ), - 'blockTypes' => $allowed_block_types, + 'allowedBlockTypes' => $allowed_block_types, 'disableCustomColors' => get_theme_support( 'disable-custom-colors' ), + 'disablePostFormats' => ! current_theme_supports( 'post-formats' ), 'titlePlaceholder' => apply_filters( 'enter_title_here', __( 'Add title', 'gutenberg' ), $post ), + 'bodyPlaceholder' => apply_filters( 'write_your_story', __( 'Write your story', 'gutenberg' ), $post ), ); if ( ! empty( $color_palette ) ) { @@ -904,7 +982,6 @@ function gutenberg_editor_scripts_and_styles( $hook ) { /** * Styles */ - wp_enqueue_style( 'wp-edit-post' ); /** diff --git a/lib/compat.php b/lib/compat.php index 7cd79b0c361f4d..c53ad6847e1a0b 100644 --- a/lib/compat.php +++ b/lib/compat.php @@ -69,6 +69,123 @@ function _gutenberg_utf8_split( $str ) { return $chars; } +/** + * Shims fix for apiRequest on sites configured to use plain permalinks and add Preloading support. + * + * @see https://core.trac.wordpress.org/ticket/42382 + * + * @param WP_Scripts $scripts WP_Scripts instance (passed by reference). + */ +function gutenberg_shim_api_request( $scripts ) { + $api_request_fix = <<<JS +( function( wp, wpApiSettings ) { + + // Fix plain permalinks sites + var buildAjaxOptions; + if ( 'string' === typeof wpApiSettings.root && -1 !== wpApiSettings.root.indexOf( '?' ) ) { + buildAjaxOptions = wp.apiRequest.buildAjaxOptions; + wp.apiRequest.buildAjaxOptions = function( options ) { + if ( 'string' === typeof options.path ) { + options.path = options.path.replace( '?', '&' ); + } + + return buildAjaxOptions.call( wp.apiRequest, options ); + }; + } + + function getStablePath( path ) { + var splitted = path.split( '?' ); + var query = splitted[ 1 ]; + var base = splitted[ 0 ]; + if ( ! query ) { + return base; + } + + // 'b=1&c=2&a=5' + return base + '?' + query + // [ 'b=1', 'c=2', 'a=5' ] + .split( '&' ) + // [ [ 'b, '1' ], [ 'c', '2' ], [ 'a', '5' ] ] + .map( function ( entry ) { + return entry.split( '=' ); + } ) + // [ [ 'a', '5' ], [ 'b, '1' ], [ 'c', '2' ] ] + .sort( function ( a, b ) { + return a[ 0 ].localeCompare( b[ 0 ] ); + } ) + // [ 'a=5', 'b=1', 'c=2' ] + .map( function ( pair ) { + return pair.join( '=' ); + } ) + // 'a=5&b=1&c=2' + .join( '&' ); + }; + + // Add preloading support + var previousApiRequest = wp.apiRequest; + wp.apiRequest = function( request ) { + var method, path; + + if ( typeof request.path === 'string' ) { + method = request.method || 'GET'; + path = getStablePath( request.path ); + + if ( 'GET' === method && window._wpAPIDataPreload[ path ] ) { + var deferred = jQuery.Deferred(); + deferred.resolve( window._wpAPIDataPreload[ path ].body ); + return deferred.promise(); + } + } + + return previousApiRequest.call( previousApiRequest, request ); + } + for ( var name in previousApiRequest ) { + if ( previousApiRequest.hasOwnProperty( name ) ) { + wp.apiRequest[ name ] = previousApiRequest[ name ]; + } + } + +} )( window.wp, window.wpApiSettings ); +JS; + + $scripts->add_inline_script( 'wp-api-request', $api_request_fix, 'after' ); +} +add_action( 'wp_default_scripts', 'gutenberg_shim_api_request' ); + +/** + * Shims support for emulating HTTP/1.0 requests in wp.apiRequest + * + * @see https://core.trac.wordpress.org/ticket/43605 + * + * @param WP_Scripts $scripts WP_Scripts instance (passed by reference). + */ +function gutenberg_shim_api_request_emulate_http( $scripts ) { + $api_request_fix = <<<JS +( function( wp ) { + var oldApiRequest = wp.apiRequest; + wp.apiRequest = function ( options ) { + if ( options.method ) { + if ( [ 'PATCH', 'PUT', 'DELETE' ].indexOf( options.method.toUpperCase() ) >= 0 ) { + if ( ! options.headers ) { + options.headers = {}; + } + options.headers['X-HTTP-Method-Override'] = options.method; + options.method = 'POST'; + + options.contentType = 'application/json'; + options.data = JSON.stringify( options.data ); + } + } + + return oldApiRequest( options ); + } +} )( window.wp ); +JS; + + $scripts->add_inline_script( 'wp-api-request', $api_request_fix, 'after' ); +} +add_action( 'wp_default_scripts', 'gutenberg_shim_api_request_emulate_http' ); + /** * Disables wpautop behavior in classic editor when post contains blocks, to * prevent removep from invalidating paragraph blocks. @@ -243,3 +360,80 @@ function gutenberg_filter_oembed_result( $response, $handler, $request ) { return $response; } add_filter( 'rest_request_after_callbacks', 'gutenberg_filter_oembed_result', 10, 3 ); + +/** + * Add additional 'visibility' rest api field to taxonomies. + * + * Used so private taxonomies are not displayed in the UI. + * + * @see https://core.trac.wordpress.org/ticket/42707 + */ +function gutenberg_add_taxonomy_visibility_field() { + register_rest_field( + 'taxonomy', + 'visibility', + array( + 'get_callback' => 'gutenberg_get_taxonomy_visibility_data', + 'schema' => array( + 'description' => __( 'The visibility settings for the taxonomy.', 'gutenberg' ), + 'type' => 'object', + 'context' => array( 'edit' ), + 'readonly' => true, + 'properties' => array( + 'public' => array( + 'description' => __( 'Whether a taxonomy is intended for use publicly either via the admin interface or by front-end users.', 'gutenberg' ), + 'type' => 'boolean', + ), + 'publicly_queryable' => array( + 'description' => __( 'Whether the taxonomy is publicly queryable.', 'gutenberg' ), + 'type' => 'boolean', + ), + 'show_ui' => array( + 'description' => __( 'Whether to generate a default UI for managing this taxonomy.', 'gutenberg' ), + 'type' => 'boolean', + ), + 'show_admin_column' => array( + 'description' => __( 'Whether to allow automatic creation of taxonomy columns on associated post-types table.', 'gutenberg' ), + 'type' => 'boolean', + ), + 'show_in_nav_menus' => array( + 'description' => __( 'Whether to make the taxonomy available for selection in navigation menus.', 'gutenberg' ), + 'type' => 'boolean', + ), + 'show_in_quick_edit' => array( + 'description' => __( 'Whether to show the taxonomy in the quick/bulk edit panel.', 'gutenberg' ), + 'type' => 'boolean', + ), + ), + ), + ) + ); +} + +/** + * Gets taxonomy visibility property data. + * + * @see https://core.trac.wordpress.org/ticket/42707 + * + * @param array $object Taxonomy data from REST API. + * @return array Array of taxonomy visibility data. + */ +function gutenberg_get_taxonomy_visibility_data( $object ) { + // Just return existing data for up-to-date Core. + if ( isset( $object['visibility'] ) ) { + return $object['visibility']; + } + + $taxonomy = get_taxonomy( $object['slug'] ); + + return array( + 'public' => $taxonomy->public, + 'publicly_queryable' => $taxonomy->publicly_queryable, + 'show_ui' => $taxonomy->show_ui, + 'show_admin_column' => $taxonomy->show_admin_column, + 'show_in_nav_menus' => $taxonomy->show_in_nav_menus, + 'show_in_quick_edit' => $taxonomy->show_ui, + ); +} + +add_action( 'rest_api_init', 'gutenberg_add_taxonomy_visibility_field' ); diff --git a/lib/i18n.php b/lib/i18n.php index 4d570ec83baffb..ab347c8334285b 100644 --- a/lib/i18n.php +++ b/lib/i18n.php @@ -22,23 +22,18 @@ function gutenberg_get_jed_locale_data( $domain ) { $translations = get_translations_for_domain( $domain ); $locale = array( - 'domain' => $domain, - 'locale_data' => array( - $domain => array( - '' => array( - 'domain' => $domain, - 'lang' => is_admin() ? get_user_locale() : get_locale(), - ), - ), + '' => array( + 'domain' => $domain, + 'lang' => is_admin() ? get_user_locale() : get_locale(), ), ); if ( ! empty( $translations->headers['Plural-Forms'] ) ) { - $locale['locale_data'][ $domain ]['']['plural_forms'] = $translations->headers['Plural-Forms']; + $locale['']['plural_forms'] = $translations->headers['Plural-Forms']; } foreach ( $translations->entries as $msgid => $entry ) { - $locale['locale_data'][ $domain ][ $msgid ] = $entry->translations; + $locale[ $msgid ] = $entry->translations; } return $locale; diff --git a/lib/meta-box-partial-page.php b/lib/meta-box-partial-page.php index 3e6e80db6c9fa4..bb4076fa1deb39 100644 --- a/lib/meta-box-partial-page.php +++ b/lib/meta-box-partial-page.php @@ -90,12 +90,25 @@ function gutenberg_filter_meta_boxes( $meta_boxes ) { $core_side_meta_boxes = array( 'submitdiv', 'formatdiv', - 'categorydiv', - 'tagsdiv-post_tag', 'pageparentdiv', 'postimagediv', ); + $custom_taxonomies = get_taxonomies( + array( + 'show_ui' => true, + ), + 'objects' + ); + + // Following the same logic as meta box generation in: + // https://github.com/WordPress/wordpress-develop/blob/c896326/src/wp-admin/edit-form-advanced.php#L288-L292. + foreach ( $custom_taxonomies as $custom_taxonomy ) { + $core_side_meta_boxes [] = $custom_taxonomy->hierarchical ? + $custom_taxonomy->name . 'div' : + 'tagsdiv-' . $custom_taxonomy->name; + } + $core_normal_meta_boxes = array( 'revisionsdiv', 'postexcerpt', @@ -137,32 +150,6 @@ function gutenberg_filter_meta_boxes( $meta_boxes ) { return $meta_boxes; } -/** - * Check whether a meta box is empty. - * - * @since 1.5.0 - * - * @param array $meta_boxes Meta box data. - * @param string $context Location of meta box, one of side, advanced, normal. - * @param string $post_type Post type to investigate. - * @return boolean Whether the meta box is empty. - */ -function gutenberg_is_meta_box_empty( $meta_boxes, $context, $post_type ) { - $page = $post_type; - - if ( ! isset( $meta_boxes[ $page ][ $context ] ) ) { - return true; - } - - foreach ( $meta_boxes[ $page ][ $context ] as $priority => $boxes ) { - if ( ! empty( $boxes ) ) { - return false; - } - } - - return true; -} - add_filter( 'filter_gutenberg_meta_boxes', 'gutenberg_filter_meta_boxes' ); /** @@ -304,7 +291,7 @@ function the_gutenberg_metaboxes() { */ $wp_meta_boxes = apply_filters( 'filter_gutenberg_meta_boxes', $wp_meta_boxes ); $locations = array( 'side', 'normal', 'advanced' ); - + $meta_box_data = array(); // Render meta boxes. ?> <form class="metabox-base-form"> @@ -315,11 +302,13 @@ function the_gutenberg_metaboxes() { <div id="poststuff" class="sidebar-open"> <div id="postbox-container-2" class="postbox-container"> <?php - do_meta_boxes( + $number_metaboxes = do_meta_boxes( $current_screen, $location, $post ); + + $meta_box_data[ $location ] = $number_metaboxes > 0; ?> </div> </div> @@ -327,6 +316,19 @@ function the_gutenberg_metaboxes() { <?php endforeach; ?> <?php + /** + * Sadly we probably can not add this data directly into editor settings. + * + * ACF and other meta boxes need admin_head to fire for meta box registry. + * admin_head fires after admin_enqueue_scripts which is where we create our + * editor instance. If a cleaner solution can be imagined, please change + * this, and try to get this data to load directly into the editor settings. + */ + wp_add_inline_script( + 'wp-edit-post', + 'window._wpLoadGutenbergEditor.then( function( editor ) { editor.initializeMetaBoxes( ' . wp_json_encode( $meta_box_data ) . ' ) } );' + ); + // Reset meta box data. $wp_meta_boxes = $_original_meta_boxes; } @@ -354,7 +356,6 @@ function gutenberg_meta_box_post_form_hidden_fields( $post ) { <input type="hidden" id="user-id" name="user_ID" value="<?php echo (int) $user_id; ?>" /> <input type="hidden" id="hiddenaction" name="action" value="<?php echo esc_attr( $form_action ); ?>" /> <input type="hidden" id="originalaction" name="originalaction" value="<?php echo esc_attr( $form_action ); ?>" /> - <input type="hidden" id="post_author" name="post_author" value="<?php echo esc_attr( $post->post_author ); ?>" /> <input type="hidden" id="post_type" name="post_type" value="<?php echo esc_attr( $post->post_type ); ?>" /> <input type="hidden" id="original_post_status" name="original_post_status" value="<?php echo esc_attr( $post->post_status ); ?>" /> <input type="hidden" id="referredby" name="referredby" value="<?php echo $referer ? esc_url( $referer ) : ''; ?>" /> diff --git a/lib/plugin-compat.php b/lib/plugin-compat.php index d056fb212b53ce..43ca01c3fcd8c5 100644 --- a/lib/plugin-compat.php +++ b/lib/plugin-compat.php @@ -26,9 +26,9 @@ * @return array $post Post object. */ function gutenberg_remove_wpcom_markdown_support( $post ) { - if ( gutenberg_content_has_blocks( $post->post_content ) ) { - remove_post_type_support( 'post', 'wpcom-markdown' ); + if ( class_exists( 'WPCom_Markdown' ) && gutenberg_content_has_blocks( $post['post_content'] ) ) { + WPCom_Markdown::get_instance()->unload_markdown_for_posts(); } return $post; } -add_filter( 'rest_pre_insert_post', 'gutenberg_remove_wpcom_markdown_support' ); +add_filter( 'wp_insert_post_data', 'gutenberg_remove_wpcom_markdown_support', 9 ); diff --git a/lib/register.php b/lib/register.php index 35ab50821d9753..8ede8f89a64778 100644 --- a/lib/register.php +++ b/lib/register.php @@ -32,8 +32,7 @@ function gutenberg_trick_plugins_into_registering_meta_boxes() { /** * Collect information about meta_boxes registered for the current post. * - * This is used to tell React and Redux whether the meta box location has - * meta boxes. + * Redirects to classic editor if a meta box is incompatible. * * @since 1.5.0 */ @@ -46,12 +45,20 @@ function gutenberg_collect_meta_box_data() { if ( isset( $_REQUEST['post'] ) ) { $post = get_post( absint( $_REQUEST['post'] ) ); $typenow = $post->post_type; + + if ( ! gutenberg_can_edit_post( $post->ID ) ) { + return; + } } else { // Eventually add handling for creating new posts of different types in Gutenberg. } $post_type = $post->post_type; $post_type_object = get_post_type_object( $post_type ); + if ( ! gutenberg_can_edit_post_type( $post_type ) ) { + return; + } + $thumbnail_support = current_theme_supports( 'post-thumbnails', $post_type ) && post_type_supports( $post_type, 'thumbnail' ); if ( ! $thumbnail_support && 'attachment' === $post_type && $post->post_mime_type ) { if ( wp_attachment_is( 'audio', $post ) ) { @@ -210,57 +217,36 @@ function gutenberg_collect_meta_box_data() { $meta_box_data = array(); - // If the meta box should be empty set to false. + // Redirect to classic editor if a meta box is incompatible. foreach ( $locations as $location ) { - if ( gutenberg_is_meta_box_empty( $_meta_boxes_copy, $location, $post->post_type ) ) { - $meta_box_data[ $location ] = false; - } else { - $meta_box_data[ $location ] = true; - $incompatible_meta_box = false; - // Check if we have a meta box that has declared itself incompatible with the block editor. - foreach ( $_meta_boxes_copy[ $post->post_type ][ $location ] as $boxes ) { - foreach ( $boxes as $box ) { - /* - * If __block_editor_compatible_meta_box is declared as a false-y value, - * the meta box is not compatible with the block editor. - */ - if ( is_array( $box['args'] ) - && isset( $box['args']['__block_editor_compatible_meta_box'] ) - && ! $box['args']['__block_editor_compatible_meta_box'] ) { - $incompatible_meta_box = true; - break 2; - } + if ( ! isset( $_meta_boxes_copy[ $post->post_type ][ $location ] ) ) { + continue; + } + // Check if we have a meta box that has declared itself incompatible with the block editor. + foreach ( $_meta_boxes_copy[ $post->post_type ][ $location ] as $boxes ) { + foreach ( $boxes as $box ) { + /* + * If __block_editor_compatible_meta_box is declared as a false-y value, + * the meta box is not compatible with the block editor. + */ + if ( is_array( $box['args'] ) + && isset( $box['args']['__block_editor_compatible_meta_box'] ) + && ! $box['args']['__block_editor_compatible_meta_box'] ) { + $incompatible_meta_box = true; + ?> + <script type="text/javascript"> + var joiner = '?'; + if ( window.location.search ) { + joiner = '&'; + } + window.location.href += joiner + 'classic-editor'; + </script> + <?php + exit; } } - - // Incompatible meta boxes require an immediate redirect to the classic editor. - if ( $incompatible_meta_box ) { - ?> - <script type="text/javascript"> - var joiner = '?'; - if ( window.location.search ) { - joiner = '&'; - } - window.location.href += joiner + 'classic-editor'; - </script> - <?php - exit; - } } } - - /** - * Sadly we probably can not add this data directly into editor settings. - * - * ACF and other meta boxes need admin_head to fire for meta box registry. - * admin_head fires after admin_enqueue_scripts which is where we create our - * editor instance. If a cleaner solution can be imagined, please change - * this, and try to get this data to load directly into the editor settings. - */ - wp_add_inline_script( - 'wp-edit-post', - 'window._wpLoadGutenbergEditor.then( function( editor ) { editor.initializeMetaBoxes( ' . wp_json_encode( $meta_box_data ) . ' ) } );' - ); } /** @@ -268,11 +254,12 @@ function gutenberg_collect_meta_box_data() { * * @since 0.5.0 * - * @param int|WP_Post $post_id Post. + * @param int|WP_Post $post Post ID or WP_Post object. * @return bool Whether the post can be edited with Gutenberg. */ -function gutenberg_can_edit_post( $post_id ) { - $post = get_post( $post_id ); +function gutenberg_can_edit_post( $post ) { + $post = get_post( $post ); + if ( ! $post ) { return false; } @@ -285,7 +272,7 @@ function gutenberg_can_edit_post( $post_id ) { return false; } - return current_user_can( 'edit_post', $post_id ); + return current_user_can( 'edit_post', $post->ID ); } /** @@ -387,11 +374,62 @@ function gutenberg_register_post_types() { 'singular_name' => 'Block', ), 'public' => false, - 'capability_type' => 'post', 'show_in_rest' => true, 'rest_base' => 'blocks', 'rest_controller_class' => 'WP_REST_Blocks_Controller', + 'capability_type' => 'block', + 'capabilities' => array( + 'read' => 'read_blocks', + 'create_posts' => 'create_blocks', + ), + 'map_meta_cap' => true, ) ); + + $editor_caps = array( + 'edit_blocks', + 'edit_others_blocks', + 'publish_blocks', + 'read_private_blocks', + 'read_blocks', + 'delete_blocks', + 'delete_private_blocks', + 'delete_published_blocks', + 'delete_others_blocks', + 'edit_private_blocks', + 'edit_published_blocks', + 'create_blocks', + ); + + $caps_map = array( + 'administrator' => $editor_caps, + 'editor' => $editor_caps, + 'author' => array( + 'edit_blocks', + 'publish_blocks', + 'read_blocks', + 'delete_blocks', + 'delete_published_blocks', + 'edit_published_blocks', + 'create_blocks', + ), + 'contributor' => array( + 'read_blocks', + ), + ); + + foreach ( $caps_map as $role_name => $caps ) { + $role = get_role( $role_name ); + + if ( empty( $role ) ) { + continue; + } + + foreach ( $caps as $cap ) { + if ( ! $role->has_cap( $cap ) ) { + $role->add_cap( $cap ); + } + } + } } add_action( 'init', 'gutenberg_register_post_types' ); diff --git a/package-lock.json b/package-lock.json index 3ce2b1b4a3eda7..fce43d9d7a554c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12732 +1,15731 @@ { - "name": "gutenberg", - "version": "2.2.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "@babel/code-frame": { - "version": "7.0.0-beta.40", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0-beta.40.tgz", - "integrity": "sha512-eVXQSbu/RimU6OKcK2/gDJVTFcxXJI4sHbIqw2mhwMZeQ2as/8AhS9DGkEDoHMBBNJZ5B0US63lF56x+KDcxiA==", - "dev": true, - "requires": { - "@babel/highlight": "7.0.0-beta.40" - } - }, - "@babel/helper-function-name": { - "version": "7.0.0-beta.31", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.0.0-beta.31.tgz", - "integrity": "sha512-c+DAyp8LMm2nzSs2uXEuxp4LYGSUYEyHtU3fU57avFChjsnTmmpWmXj2dv0yUxHTEydgVAv5fIzA+4KJwoqWDA==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "7.0.0-beta.31", - "@babel/template": "7.0.0-beta.31", - "@babel/traverse": "7.0.0-beta.31", - "@babel/types": "7.0.0-beta.31" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.0.0-beta.31", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0-beta.31.tgz", - "integrity": "sha512-m7rVVX/dMLbbB9NCzKYRrrFb0qZxgpmQ4Wv6y7zEsB6skoJHRuXVeb/hAFze79vXBbuD63ci7AVHXzAdZSk9KQ==", - "dev": true, - "requires": { - "@babel/types": "7.0.0-beta.31" - } - }, - "@babel/highlight": { - "version": "7.0.0-beta.40", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0-beta.40.tgz", - "integrity": "sha512-mOhhTrzieV6VO7odgzFGFapiwRK0ei8RZRhfzHhb6cpX3QM8XXuCLXWjN8qBB7JReDdUR80V3LFfFrGUYevhNg==", - "dev": true, - "requires": { - "chalk": "2.3.1", - "esutils": "2.0.2", - "js-tokens": "3.0.2" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", - "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", - "dev": true, - "requires": { - "color-convert": "1.9.1" - } - }, - "chalk": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.1.tgz", - "integrity": "sha512-QUU4ofkDoMIVO7hcx1iPTISs88wsO8jA92RQIm4JAwZvFGGAV2hSAA1NX7oVj2Ej2Q6NDTcRDjPTFrMCRZoJ6g==", - "dev": true, - "requires": { - "ansi-styles": "3.2.0", - "escape-string-regexp": "1.0.5", - "supports-color": "5.2.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "supports-color": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.2.0.tgz", - "integrity": "sha512-F39vS48la4YvTZUPVeTqsjsFNrvcMwrV3RLZINsmHo+7djCvuUzSIeXOnZ5hmjef4bajL1dNccN+tg5XAliO5Q==", - "dev": true, - "requires": { - "has-flag": "3.0.0" - } - } - } - }, - "@babel/template": { - "version": "7.0.0-beta.31", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.0.0-beta.31.tgz", - "integrity": "sha512-97IRmLvoDhIDSQkqklVt3UCxJsv0LUEVb/0DzXWtc8Lgiyxj567qZkmTG9aR21CmcJVVIvq2Y/moZj4oEpl5AA==", - "dev": true, - "requires": { - "@babel/code-frame": "7.0.0-beta.31", - "@babel/types": "7.0.0-beta.31", - "babylon": "7.0.0-beta.31", - "lodash": "4.17.4" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.0.0-beta.31", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0-beta.31.tgz", - "integrity": "sha512-yd7CkUughvHQoEahQqcMdrZw6o/6PwUxiRkfZuVDVHCDe77mysD/suoNyk5mK6phTnRW1kyIbPHyCJgxw++LXg==", - "dev": true, - "requires": { - "chalk": "2.3.0", - "esutils": "2.0.2", - "js-tokens": "3.0.2" - } - }, - "ansi-styles": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", - "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", - "dev": true, - "requires": { - "color-convert": "1.9.1" - } - }, - "babylon": { - "version": "7.0.0-beta.31", - "resolved": "https://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.31.tgz", - "integrity": "sha512-6lm2mV3S51yEnKmQQNnswoABL1U1H1KHoCCVwdwI3hvIv+W7ya4ki7Aw4o4KxtUHjNKkK5WpZb22rrMMOcJXJQ==", - "dev": true - }, - "chalk": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", - "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", - "dev": true, - "requires": { - "ansi-styles": "3.2.0", - "escape-string-regexp": "1.0.5", - "supports-color": "4.5.0" - } - }, - "supports-color": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", - "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", - "dev": true, - "requires": { - "has-flag": "2.0.0" - } - } - } - }, - "@babel/traverse": { - "version": "7.0.0-beta.31", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.0.0-beta.31.tgz", - "integrity": "sha512-3N+VJW+KlezEjFBG7WSYeMyC5kIqVLPb/PGSzCDPFcJrnArluD1GIl7Y3xC7cjKiTq2/JohaLWHVPjJWHlo9Gg==", - "dev": true, - "requires": { - "@babel/code-frame": "7.0.0-beta.31", - "@babel/helper-function-name": "7.0.0-beta.31", - "@babel/types": "7.0.0-beta.31", - "babylon": "7.0.0-beta.31", - "debug": "3.1.0", - "globals": "10.4.0", - "invariant": "2.2.2", - "lodash": "4.17.4" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.0.0-beta.31", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0-beta.31.tgz", - "integrity": "sha512-yd7CkUughvHQoEahQqcMdrZw6o/6PwUxiRkfZuVDVHCDe77mysD/suoNyk5mK6phTnRW1kyIbPHyCJgxw++LXg==", - "dev": true, - "requires": { - "chalk": "2.3.0", - "esutils": "2.0.2", - "js-tokens": "3.0.2" - } - }, - "ansi-styles": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", - "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", - "dev": true, - "requires": { - "color-convert": "1.9.1" - } - }, - "babylon": { - "version": "7.0.0-beta.31", - "resolved": "https://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.31.tgz", - "integrity": "sha512-6lm2mV3S51yEnKmQQNnswoABL1U1H1KHoCCVwdwI3hvIv+W7ya4ki7Aw4o4KxtUHjNKkK5WpZb22rrMMOcJXJQ==", - "dev": true - }, - "chalk": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", - "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", - "dev": true, - "requires": { - "ansi-styles": "3.2.0", - "escape-string-regexp": "1.0.5", - "supports-color": "4.5.0" - } - }, - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "globals": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-10.4.0.tgz", - "integrity": "sha512-uNUtxIZpGyuaq+5BqGGQHsL4wUlJAXRqOm6g3Y48/CWNGTLONgBibI0lh6lGxjR2HljFYUfszb+mk4WkgMntsA==", - "dev": true - }, - "supports-color": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", - "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", - "dev": true, - "requires": { - "has-flag": "2.0.0" - } - } - } - }, - "@babel/types": { - "version": "7.0.0-beta.31", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.0.0-beta.31.tgz", - "integrity": "sha512-exAHB+NeFGxkfQ5dSUD03xl3zYGneeSk2Mw2ldTt/nTvYxuDiuSp3DlxgUBgzbdTFG4fbwPk0WtKWOoTXCmNGg==", - "dev": true, - "requires": { - "esutils": "2.0.2", - "lodash": "4.17.4", - "to-fast-properties": "2.0.0" - } - }, - "@cypress/listr-verbose-renderer": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@cypress/listr-verbose-renderer/-/listr-verbose-renderer-0.4.1.tgz", - "integrity": "sha1-p3SS9LEdzHxEajSz4ochr9M8ZCo=", - "dev": true, - "requires": { - "chalk": "1.1.3", - "cli-cursor": "1.0.2", - "date-fns": "1.29.0", - "figures": "1.7.0" - } - }, - "@cypress/xvfb": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.1.3.tgz", - "integrity": "sha512-EfRzw+wgI0Zdb4ZlhSvjh3q7I+oenqEYPXvr7oH/2RnzQqGDrPr7IU1Pi2yzGwoXmkNUQbo6qvntnItvQj0F4Q==", - "dev": true, - "requires": { - "lodash.once": "4.1.1" - } - }, - "@romainberger/css-diff": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@romainberger/css-diff/-/css-diff-1.0.3.tgz", - "integrity": "sha1-ztOHU11PQqQqwf4TwJ3pf1rhNEw=", - "dev": true, - "requires": { - "lodash.merge": "4.6.1", - "postcss": "5.2.18" - } - }, - "@types/autosize": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/autosize/-/autosize-3.0.6.tgz", - "integrity": "sha512-gpfmXswGISLSWNOOdF2PDK96SfkaZdNtNixWJbYH10xn3Hqdt4VyS1GmoutuwOshWyCLuJw2jGhF0zkK7PUhrg==", - "requires": { - "@types/jquery": "3.3.0" - } - }, - "@types/blob-util": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@types/blob-util/-/blob-util-1.3.3.tgz", - "integrity": "sha512-4ahcL/QDnpjWA2Qs16ZMQif7HjGP2cw3AGjHabybjw7Vm1EKu+cfQN1D78BaZbS1WJNa1opSMF5HNMztx7lR0w==", - "dev": true - }, - "@types/bluebird": { - "version": "3.5.18", - "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.18.tgz", - "integrity": "sha512-OTPWHmsyW18BhrnG5x8F7PzeZ2nFxmHGb42bZn79P9hl+GI5cMzyPgQTwNjbem0lJhoru/8vtjAFCUOu3+gE2w==", - "dev": true - }, - "@types/chai": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.0.8.tgz", - "integrity": "sha512-m812CONwdZn/dMzkIJEY0yAs4apyTkTORgfB2UsMOxgkUbC205AHnm4T8I0I5gPg9MHrFc1dJ35iS75c0CJkjg==", - "dev": true - }, - "@types/chai-jquery": { - "version": "1.1.35", - "resolved": "https://registry.npmjs.org/@types/chai-jquery/-/chai-jquery-1.1.35.tgz", - "integrity": "sha512-7aIt9QMRdxuagLLI48dPz96YJdhu64p6FCa6n4qkGN5DQLHnrIjZpD9bXCvV2G0NwgZ1FAmfP214dxc5zNCfgQ==", - "dev": true, - "requires": { - "@types/chai": "4.0.8", - "@types/jquery": "3.3.0" - } - }, - "@types/jquery": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.3.0.tgz", - "integrity": "sha512-szaKV2OQgwxYTGTY6qd9eeBfGGCaP7n2OGit4JdbOcfGgc9VWjfhMhnu5AVNhIAu8WWDIB36q9dfPVba1fGeIQ==" - }, - "@types/lodash": { - "version": "4.14.87", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.87.tgz", - "integrity": "sha512-AqRC+aEF4N0LuNHtcjKtvF9OTfqZI0iaBoe3dA6m/W+/YZJBZjBmW/QIZ8fBeXC6cnytSY9tBoFBqZ9uSCeVsw==", - "dev": true - }, - "@types/minimatch": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.1.tgz", - "integrity": "sha512-rUO/jz10KRSyA9SHoCWQ8WX9BICyj5jZYu1/ucKEJKb4KzLZCKMURdYbadP157Q6Zl1x0vHsrU+Z/O0XlhYQDw==", - "dev": true - }, - "@types/mocha": { - "version": "2.2.44", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-2.2.44.tgz", - "integrity": "sha512-k2tWTQU8G4+iSMvqKi0Q9IIsWAp/n8xzdZS4Q4YVIltApoMA00wFBFdlJnmoaK1/z7B0Cy0yPe6GgXteSmdUNw==", - "dev": true - }, - "@types/node": { - "version": "9.4.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-9.4.6.tgz", - "integrity": "sha512-CTUtLb6WqCCgp6P59QintjHWqzf4VL1uPA27bipLAPxFqrtK1gEYllePzTICGqQ8rYsCbpnsNypXjjDzGAAjEQ==", - "dev": true - }, - "@types/prop-types": { - "version": "15.5.2", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.5.2.tgz", - "integrity": "sha512-pQRkAVoxiuUrLq8+CDwiQX4pTCep/PmmNgBbjIwnnsd/HoYjGpR81+FFPE030lvNXgR0haaAU6eoRtztWDE4Xw==" - }, - "@types/react": { - "version": "15.6.7", - "resolved": "https://registry.npmjs.org/@types/react/-/react-15.6.7.tgz", - "integrity": "sha512-HMfRuwiTp7/MfjPOsVlvlduouJH3haDzjc0oXqZy3ZMn3OTl3i4gGgbxsqzA/u9gNyl/oKkwOrU2oVR6vG5SAw==" - }, - "@types/sinon": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-4.0.0.tgz", - "integrity": "sha512-cuK4xM8Lg2wd8cxshcQa8RG4IK/xfyB6TNE6tNVvkrShR4xdrYgsV04q6Dp6v1Lp6biEFdzD8k8zg/ujQeiw+A==", - "dev": true - }, - "@types/sinon-chai": { - "version": "2.7.29", - "resolved": "https://registry.npmjs.org/@types/sinon-chai/-/sinon-chai-2.7.29.tgz", - "integrity": "sha512-EkI/ZvJT4hglWo7Ipf9SX+J+R9htNOMjW8xiOhce7+0csqvgoF5IXqY5Ae1GqRgNtWCuaywR5HjVa1snkTqpOw==", - "dev": true, - "requires": { - "@types/chai": "4.0.8", - "@types/sinon": "4.0.0" - } - }, - "@wordpress/a11y": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@wordpress/a11y/-/a11y-1.0.6.tgz", - "integrity": "sha512-IyL7KzYGzMEg+FFyTrQzD/CUfABYCXOvmnm29vBZBA3JMER1ep3/+NFDe6CpWVEEMCw94oj2gUOSQ4YKVgDjUQ==", - "requires": { - "@wordpress/dom-ready": "1.0.3" - } - }, - "@wordpress/autop": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@wordpress/autop/-/autop-1.0.4.tgz", - "integrity": "sha512-nqm/gP+ipeUMvEngh4Sp4k5umph8SPqfc5aCd9Ge03mz4JSWAIE4z36pPQwId0a1B3hGqri5Wo40O1hQ851ZnA==" - }, - "@wordpress/babel-preset-default": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@wordpress/babel-preset-default/-/babel-preset-default-1.1.1.tgz", - "integrity": "sha512-47/4U2NjA/1qETBk0QKkDAPq8sRtQMYOCOi6XdHTaP4MhCXviRvonuC+2J5uufIT0+CuinlRa1K2cgvY+BtU6A==", - "dev": true, - "requires": { - "@wordpress/browserslist-config": "2.1.1", - "babel-plugin-lodash": "3.3.2", - "babel-plugin-transform-object-rest-spread": "6.26.0", - "babel-plugin-transform-react-jsx": "6.24.1", - "babel-plugin-transform-runtime": "6.23.0", - "babel-preset-env": "1.6.1" - } - }, - "@wordpress/browserslist-config": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@wordpress/browserslist-config/-/browserslist-config-2.1.1.tgz", - "integrity": "sha512-qYfKfxrksXWSfrmU+JGixKskVytdKWXv5vIYxzXojl2V6OOs/j2Crspi2Hona0Iv5LTUdg02WX7X1rPlmQHmLg==", - "dev": true - }, - "@wordpress/dom-ready": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@wordpress/dom-ready/-/dom-ready-1.0.3.tgz", - "integrity": "sha512-1tmJLO2NDc45wxnUWv6F/4q8/00qSgqLvBXBKl7IayLvJbG25vt7lMQkdSfiY2gTwsujAqOgOuJdO5VOGuqQNg==" - }, - "@wordpress/hooks": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@wordpress/hooks/-/hooks-1.1.4.tgz", - "integrity": "sha512-V8HVPBKsEUh2hPfO21/i0TNUYduofIo4emuk/JfVyTT7+hSJZsj6Fn6oWt7Hs2mf7JTAi4DUVusX4gSDspzM6w==" - }, - "@wordpress/jest-console": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@wordpress/jest-console/-/jest-console-1.0.5.tgz", - "integrity": "sha512-PwhDL2H4EI6adnGyQo0v4p8zRokjNu4DJ3EDZpH9dmNK0/G9hKuuIAwGN2e9RGyAiqipddCkt5y4qzH1mx8PJw==", - "dev": true, - "requires": { - "jest-matcher-utils": "22.4.0", - "lodash": "4.17.4" - } - }, - "@wordpress/jest-preset-default": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@wordpress/jest-preset-default/-/jest-preset-default-1.0.3.tgz", - "integrity": "sha512-1+uREUyhMWBanX4qceevrFEuaFm/gRKIKiDb36Wc5X02ODEaUaQ6qjIvrD6fariYtvxQpjC8TBjKapsvSaqHHw==", - "dev": true, - "requires": { - "@wordpress/jest-console": "1.0.5", - "babel-jest": "22.4.0", - "enzyme": "3.3.0", - "enzyme-adapter-react-16": "1.1.1", - "jest-enzyme": "4.2.0", - "pegjs": "0.10.0" - } - }, - "@wordpress/scripts": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@wordpress/scripts/-/scripts-1.1.0.tgz", - "integrity": "sha512-gbtpV6i4SKi7Pya8qeB6N9FGyWAMBtyAsRnFg6Lv6Ejh0Pk9R2Aa71GjX4ZAMzq1LFuVNdhciIybdxDPSsP8sQ==", - "dev": true, - "requires": { - "@wordpress/babel-preset-default": "1.1.1", - "@wordpress/jest-preset-default": "1.0.3", - "cross-spawn": "5.1.0", - "jest": "22.4.0", - "read-pkg-up": "3.0.0" - }, - "dependencies": { - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "requires": { - "locate-path": "2.0.0" - } - }, - "load-json-file": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", - "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", - "dev": true, - "requires": { - "graceful-fs": "4.1.11", - "parse-json": "4.0.0", - "pify": "3.0.0", - "strip-bom": "3.0.0" - } - }, - "parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", - "dev": true, - "requires": { - "error-ex": "1.3.1", - "json-parse-better-errors": "1.0.1" - } - }, - "path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", - "dev": true, - "requires": { - "pify": "3.0.0" - } - }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - }, - "read-pkg": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", - "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", - "dev": true, - "requires": { - "load-json-file": "4.0.0", - "normalize-package-data": "2.4.0", - "path-type": "3.0.0" - } - }, - "read-pkg-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", - "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", - "dev": true, - "requires": { - "find-up": "2.1.0", - "read-pkg": "3.0.0" - } - } - } - }, - "@wordpress/url": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@wordpress/url/-/url-1.0.3.tgz", - "integrity": "sha512-0nqf62SWS0DiFnSD5miszPuAey01OrBsdqBFzOCYChjtB7AZm+8Q06qpeV02rpLY5FHaUxVgL+2JRljYIAFlpA==" - }, - "abab": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz", - "integrity": "sha1-X6rZwsB/YN12dw9xzwJbYqY8/U4=", - "dev": true - }, - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" - }, - "acorn": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.4.1.tgz", - "integrity": "sha512-XLmq3H/BVvW6/GbxKryGxWORz1ebilSsUDlyC27bXhWGWAZWkGwS6FLHjOlwFXNFoWFQEO/Df4u0YYd0K3BQgQ==", - "dev": true - }, - "acorn-dynamic-import": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-2.0.2.tgz", - "integrity": "sha1-x1K9IQvvZ5UBtsbLf8hPj0cVjMQ=", - "dev": true, - "requires": { - "acorn": "4.0.13" - }, - "dependencies": { - "acorn": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", - "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=", - "dev": true - } - } - }, - "acorn-globals": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.1.0.tgz", - "integrity": "sha512-KjZwU26uG3u6eZcfGbTULzFcsoz6pegNKtHPksZPOUsiKo5bUmiBPa38FuHZ/Eun+XYh/JCCkS9AS3Lu4McQOQ==", - "dev": true, - "requires": { - "acorn": "5.4.1" - } - }, - "acorn-jsx": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", - "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", - "dev": true, - "requires": { - "acorn": "3.3.0" - }, - "dependencies": { - "acorn": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", - "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=", - "dev": true - } - } - }, - "ajv": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", - "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", - "dev": true, - "requires": { - "co": "4.6.0", - "fast-deep-equal": "1.0.0", - "fast-json-stable-stringify": "2.0.0", - "json-schema-traverse": "0.3.1" - } - }, - "ajv-keywords": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-2.1.1.tgz", - "integrity": "sha1-YXmX/F9gV2iUxDX5QNgZ4TW4B2I=", - "dev": true - }, - "align-text": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", - "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", - "dev": true, - "requires": { - "kind-of": "3.2.2", - "longest": "1.0.1", - "repeat-string": "1.6.1" - } - }, - "alphanum-sort": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz", - "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=", - "dev": true - }, - "amdefine": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", - "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", - "dev": true - }, - "ansi-escapes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.0.0.tgz", - "integrity": "sha512-O/klc27mWNUigtv0F8NJWbLF00OcegQalkqKURWdosW08YZKi4m6CnSUSvIZG1otNJbTWhN01Hhz389DW7mvDQ==", - "dev": true - }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "any-observable": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/any-observable/-/any-observable-0.2.0.tgz", - "integrity": "sha1-xnhwBYADV5AJCD9UrAq6+1wz0kI=", - "dev": true - }, - "anymatch": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz", - "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==", - "dev": true, - "requires": { - "micromatch": "2.3.11", - "normalize-path": "2.1.1" - } - }, - "app-root-path": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-2.0.1.tgz", - "integrity": "sha1-zWLc+OT9WkF+/GZNLlsQZTxlG0Y=", - "dev": true - }, - "append-transform": { - "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": "1.0.0" - } - }, - "aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "dev": true - }, - "are-we-there-yet": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz", - "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=", - "dev": true, - "requires": { - "delegates": "1.0.0", - "readable-stream": "2.3.3" - } - }, - "argparse": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz", - "integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=", - "dev": true, - "requires": { - "sprintf-js": "1.0.3" - }, - "dependencies": { - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true - } - } - }, - "argv": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/argv/-/argv-0.0.2.tgz", - "integrity": "sha1-7L0W+JSbFXGDcRsb2jNPN4QBhas=", - "dev": true - }, - "aria-query": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-0.7.1.tgz", - "integrity": "sha1-Jsu1r/ZBRLCoJb4YRuCxbPoAsR4=", - "dev": true, - "requires": { - "ast-types-flow": "0.0.7", - "commander": "2.14.1" - } - }, - "arr-diff": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", - "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", - "dev": true, - "requires": { - "arr-flatten": "1.1.0" - } - }, - "arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", - "dev": true - }, - "array-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", - "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=", - "dev": true - }, - "array-find-index": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", - "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", - "dev": true - }, - "array-includes": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.0.3.tgz", - "integrity": "sha1-GEtI9i2S10UrsxsyMWXH+L0CJm0=", - "dev": true, - "requires": { - "define-properties": "1.1.2", - "es-abstract": "1.10.0" - } - }, - "array-union": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", - "dev": true, - "requires": { - "array-uniq": "1.0.3" - } - }, - "array-uniq": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", - "dev": true - }, - "array-unique": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", - "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", - "dev": true - }, - "arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", - "dev": true - }, - "asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" - }, - "asn1": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", - "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=", - "dev": true - }, - "asn1.js": { - "version": "4.9.2", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.9.2.tgz", - "integrity": "sha512-b/OsSjvWEo8Pi8H0zsDd2P6Uqo2TK2pH8gNLSJtNLM2Db0v2QaAZ0pBQJXVjAn4gBuugeVDr7s63ZogpUIwWDg==", - "dev": true, - "requires": { - "bn.js": "4.11.8", - "inherits": "2.0.3", - "minimalistic-assert": "1.0.0" - } - }, - "assert": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz", - "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=", - "dev": true, - "requires": { - "util": "0.10.3" - } - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true - }, - "ast-types-flow": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", - "integrity": "sha1-9wtzXGvKGlycItmCw+Oef+ujva0=", - "dev": true - }, - "astral-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", - "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", - "dev": true - }, - "async": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.0.tgz", - "integrity": "sha512-xAfGg1/NTLBBKlHFmnd7PlmUW9KhVQIUuSrYem9xzFUZy13ScvtyGGejaae9iAVRiRq9+Cx7DPFaAAhCpyxyPw==", - "dev": true, - "requires": { - "lodash": "4.17.4" - } - }, - "async-each": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz", - "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=", - "dev": true - }, - "async-foreach": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz", - "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=", - "dev": true - }, - "async-limiter": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", - "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==", - "dev": true - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true - }, - "autoprefixer": { - "version": "6.7.7", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-6.7.7.tgz", - "integrity": "sha1-Hb0cg1ZY41zj+ZhAmdsAWFx4IBQ=", - "dev": true, - "requires": { - "browserslist": "1.7.7", - "caniuse-db": "1.0.30000804", - "normalize-range": "0.1.2", - "num2fraction": "1.2.2", - "postcss": "5.2.18", - "postcss-value-parser": "3.3.0" - }, - "dependencies": { - "browserslist": { - "version": "1.7.7", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-1.7.7.tgz", - "integrity": "sha1-C9dnBCWL6CmyOYu1Dkti0aFmsLk=", - "dev": true, - "requires": { - "caniuse-db": "1.0.30000804", - "electron-to-chromium": "1.3.33" - } - } - } - }, - "autosize": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/autosize/-/autosize-4.0.0.tgz", - "integrity": "sha1-egWZsbqE1zvXWJsNnaOHAVLGkjc=" - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", - "dev": true - }, - "aws4": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", - "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=", - "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", - "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", - "dev": true, - "requires": { - "chalk": "1.1.3", - "esutils": "2.0.2", - "js-tokens": "3.0.2" - } - }, - "babel-core": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.0.tgz", - "integrity": "sha1-rzL3izGm/O8RnIew/Y2XU/A6C7g=", - "dev": true, - "requires": { - "babel-code-frame": "6.26.0", - "babel-generator": "6.26.1", - "babel-helpers": "6.24.1", - "babel-messages": "6.23.0", - "babel-register": "6.26.0", - "babel-runtime": "6.26.0", - "babel-template": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0", - "babylon": "6.18.0", - "convert-source-map": "1.5.1", - "debug": "2.6.9", - "json5": "0.5.1", - "lodash": "4.17.4", - "minimatch": "3.0.4", - "path-is-absolute": "1.0.1", - "private": "0.1.8", - "slash": "1.0.0", - "source-map": "0.5.7" - } - }, - "babel-eslint": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-8.0.3.tgz", - "integrity": "sha512-7D4iUpylEiKJPGbeSAlNddGcmA41PadgZ6UAb6JVyh003h3d0EbZusYFBR/+nBgqtaVJM2J2zUVa3N0hrpMH6g==", - "dev": true, - "requires": { - "@babel/code-frame": "7.0.0-beta.31", - "@babel/traverse": "7.0.0-beta.31", - "@babel/types": "7.0.0-beta.31", - "babylon": "7.0.0-beta.31" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.0.0-beta.31", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0-beta.31.tgz", - "integrity": "sha512-yd7CkUughvHQoEahQqcMdrZw6o/6PwUxiRkfZuVDVHCDe77mysD/suoNyk5mK6phTnRW1kyIbPHyCJgxw++LXg==", - "dev": true, - "requires": { - "chalk": "2.3.0", - "esutils": "2.0.2", - "js-tokens": "3.0.2" - } - }, - "ansi-styles": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", - "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", - "dev": true, - "requires": { - "color-convert": "1.9.1" - } - }, - "babylon": { - "version": "7.0.0-beta.31", - "resolved": "https://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.31.tgz", - "integrity": "sha512-6lm2mV3S51yEnKmQQNnswoABL1U1H1KHoCCVwdwI3hvIv+W7ya4ki7Aw4o4KxtUHjNKkK5WpZb22rrMMOcJXJQ==", - "dev": true - }, - "chalk": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", - "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", - "dev": true, - "requires": { - "ansi-styles": "3.2.0", - "escape-string-regexp": "1.0.5", - "supports-color": "4.5.0" - } - }, - "supports-color": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", - "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", - "dev": true, - "requires": { - "has-flag": "2.0.0" - } - } - } - }, - "babel-generator": { - "version": "6.26.1", - "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", - "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==", - "dev": true, - "requires": { - "babel-messages": "6.23.0", - "babel-runtime": "6.26.0", - "babel-types": "6.26.0", - "detect-indent": "4.0.0", - "jsesc": "1.3.0", - "lodash": "4.17.4", - "source-map": "0.5.7", - "trim-right": "1.0.1" - }, - "dependencies": { - "jsesc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", - "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=", - "dev": true - } - } - }, - "babel-helper-builder-binary-assignment-operator-visitor": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz", - "integrity": "sha1-zORReto1b0IgvK6KAsKzRvmlZmQ=", - "dev": true, - "requires": { - "babel-helper-explode-assignable-expression": "6.24.1", - "babel-runtime": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-helper-builder-react-jsx": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-helper-builder-react-jsx/-/babel-helper-builder-react-jsx-6.26.0.tgz", - "integrity": "sha1-Of+DE7dci2Xc7/HzHTg+D/KkCKA=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0", - "babel-types": "6.26.0", - "esutils": "2.0.2" - } - }, - "babel-helper-call-delegate": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz", - "integrity": "sha1-7Oaqzdx25Bw0YfiL/Fdb0Nqi340=", - "dev": true, - "requires": { - "babel-helper-hoist-variables": "6.24.1", - "babel-runtime": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-helper-define-map": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz", - "integrity": "sha1-pfVtq0GiX5fstJjH66ypgZ+Vvl8=", - "dev": true, - "requires": { - "babel-helper-function-name": "6.24.1", - "babel-runtime": "6.26.0", - "babel-types": "6.26.0", - "lodash": "4.17.4" - } - }, - "babel-helper-explode-assignable-expression": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz", - "integrity": "sha1-8luCz33BBDPFX3BZLVdGQArCLKo=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-helper-function-name": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz", - "integrity": "sha1-00dbjAPtmCQqJbSDUasYOZ01gKk=", - "dev": true, - "requires": { - "babel-helper-get-function-arity": "6.24.1", - "babel-runtime": "6.26.0", - "babel-template": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-helper-get-function-arity": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz", - "integrity": "sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-helper-hoist-variables": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz", - "integrity": "sha1-HssnaJydJVE+rbyZFKc/VAi+enY=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-helper-module-imports": { - "version": "7.0.0-beta.3", - "resolved": "https://registry.npmjs.org/babel-helper-module-imports/-/babel-helper-module-imports-7.0.0-beta.3.tgz", - "integrity": "sha512-bdPrIXbUTYfREhRhjbN8SstwQaj0S4+rW4PKi1f2Wc5fizSh0hGYkfXUdiSSOgyTydm956tAyz4FrG61bqdQyw==", - "dev": true, - "requires": { - "babel-types": "7.0.0-beta.3", - "lodash": "4.17.4" - }, - "dependencies": { - "babel-types": { - "version": "7.0.0-beta.3", - "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-7.0.0-beta.3.tgz", - "integrity": "sha512-36k8J+byAe181OmCMawGhw+DtKO7AwexPVtsPXoMfAkjtZgoCX3bEuHWfdE5sYxRM8dojvtG/+O08M0Z/YDC6w==", - "dev": true, - "requires": { - "esutils": "2.0.2", - "lodash": "4.17.4", - "to-fast-properties": "2.0.0" - } - } - } - }, - "babel-helper-optimise-call-expression": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz", - "integrity": "sha1-96E0J7qfc/j0+pk8VKl4gtEkQlc=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-helper-regex": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz", - "integrity": "sha1-MlxZ+QL4LyS3T6zu0DY5VPZJXnI=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0", - "babel-types": "6.26.0", - "lodash": "4.17.4" - } - }, - "babel-helper-remap-async-to-generator": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz", - "integrity": "sha1-XsWBgnrXI/7N04HxySg5BnbkVRs=", - "dev": true, - "requires": { - "babel-helper-function-name": "6.24.1", - "babel-runtime": "6.26.0", - "babel-template": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-helper-replace-supers": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz", - "integrity": "sha1-v22/5Dk40XNpohPKiov3S2qQqxo=", - "dev": true, - "requires": { - "babel-helper-optimise-call-expression": "6.24.1", - "babel-messages": "6.23.0", - "babel-runtime": "6.26.0", - "babel-template": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-helpers": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz", - "integrity": "sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0", - "babel-template": "6.26.0" - } - }, - "babel-jest": { - "version": "22.4.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-22.4.0.tgz", - "integrity": "sha512-A/safCd5jSf1D98XoHCN3YYuGurtUPntuPh8b7UxsLNfEp/QC8UwdL+VEGSLN5Fk3+tS/Jdbf5NK/T2it8RGYw==", - "dev": true, - "requires": { - "babel-plugin-istanbul": "4.1.5", - "babel-preset-jest": "22.2.0" - } - }, - "babel-loader": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-7.1.2.tgz", - "integrity": "sha512-jRwlFbINAeyDStqK6Dd5YuY0k5YuzQUvlz2ZamuXrXmxav3pNqe9vfJ402+2G+OmlJSXxCOpB6Uz0INM7RQe2A==", - "dev": true, - "requires": { - "find-cache-dir": "1.0.0", - "loader-utils": "1.1.0", - "mkdirp": "0.5.1" - } - }, - "babel-messages": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", - "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-check-es2015-constants": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz", - "integrity": "sha1-NRV7EBQm/S/9PaP3XH0ekYNbv4o=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-istanbul": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.5.tgz", - "integrity": "sha1-Z2DN2Xf0EdPhdbsGTyvDJ9mbK24=", - "dev": true, - "requires": { - "find-up": "2.1.0", - "istanbul-lib-instrument": "1.9.2", - "test-exclude": "4.2.0" - }, - "dependencies": { - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "requires": { - "locate-path": "2.0.0" - } - } - } - }, - "babel-plugin-jest-hoist": { - "version": "22.2.0", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-22.2.0.tgz", - "integrity": "sha512-NwicD5n1YQaj6sM3PVULdPBDk1XdlWvh8xBeUJg3nqZwp79Vofb8Q7GOVeWoZZ/RMlMuJMMrEAgSQl/p392nLA==", - "dev": true - }, - "babel-plugin-lodash": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/babel-plugin-lodash/-/babel-plugin-lodash-3.3.2.tgz", - "integrity": "sha512-lNsptTRfc0FTdW56O087EiKEADVEjJo2frDQ97olMjCKbRZfZPu7MvdyxnZLOoDpuTCtavN8/4Zk65x4gT+C3Q==", - "dev": true, - "requires": { - "babel-helper-module-imports": "7.0.0-beta.3", - "babel-types": "6.26.0", - "glob": "7.1.2", - "lodash": "4.17.4", - "require-package-name": "2.0.1" - } - }, - "babel-plugin-syntax-async-functions": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz", - "integrity": "sha1-ytnK0RkbWtY0vzCuCHI5HgZHvpU=", - "dev": true - }, - "babel-plugin-syntax-exponentiation-operator": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz", - "integrity": "sha1-nufoM3KQ2pUoggGmpX9BcDF4MN4=", - "dev": true - }, - "babel-plugin-syntax-jsx": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", - "integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY=", - "dev": true - }, - "babel-plugin-syntax-object-rest-spread": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz", - "integrity": "sha1-/WU28rzhODb/o6VFjEkDpZe7O/U=", - "dev": true - }, - "babel-plugin-syntax-trailing-function-commas": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz", - "integrity": "sha1-ugNgk3+NBuQBgKQ/4NVhb/9TLPM=", - "dev": true - }, - "babel-plugin-transform-async-to-generator": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz", - "integrity": "sha1-ZTbjeK/2yx1VF6wOQOs+n8jQh2E=", - "dev": true, - "requires": { - "babel-helper-remap-async-to-generator": "6.24.1", - "babel-plugin-syntax-async-functions": "6.13.0", - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-es2015-arrow-functions": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz", - "integrity": "sha1-RSaSy3EdX3ncf4XkQM5BufJE0iE=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-es2015-block-scoped-functions": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz", - "integrity": "sha1-u8UbSflk1wy42OC5ToICRs46YUE=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-es2015-block-scoping": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz", - "integrity": "sha1-1w9SmcEwjQXBL0Y4E7CgnnOxiV8=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0", - "babel-template": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0", - "lodash": "4.17.4" - } - }, - "babel-plugin-transform-es2015-classes": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz", - "integrity": "sha1-WkxYpQyclGHlZLSyo7+ryXolhNs=", - "dev": true, - "requires": { - "babel-helper-define-map": "6.26.0", - "babel-helper-function-name": "6.24.1", - "babel-helper-optimise-call-expression": "6.24.1", - "babel-helper-replace-supers": "6.24.1", - "babel-messages": "6.23.0", - "babel-runtime": "6.26.0", - "babel-template": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-plugin-transform-es2015-computed-properties": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz", - "integrity": "sha1-b+Ko0WiV1WNPTNmZttNICjCBWbM=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0", - "babel-template": "6.26.0" - } - }, - "babel-plugin-transform-es2015-destructuring": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz", - "integrity": "sha1-mXux8auWf2gtKwh2/jWNYOdlxW0=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-es2015-duplicate-keys": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz", - "integrity": "sha1-c+s9MQypaePvnskcU3QabxV2Qj4=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-plugin-transform-es2015-for-of": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz", - "integrity": "sha1-9HyVsrYT3x0+zC/bdXNiPHUkhpE=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-es2015-function-name": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz", - "integrity": "sha1-g0yJhTvDaxrw86TF26qU/Y6sqos=", - "dev": true, - "requires": { - "babel-helper-function-name": "6.24.1", - "babel-runtime": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-plugin-transform-es2015-literals": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz", - "integrity": "sha1-T1SgLWzWbPkVKAAZox0xklN3yi4=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-es2015-modules-amd": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz", - "integrity": "sha1-Oz5UAXI5hC1tGcMBHEvS8AoA0VQ=", - "dev": true, - "requires": { - "babel-plugin-transform-es2015-modules-commonjs": "6.26.0", - "babel-runtime": "6.26.0", - "babel-template": "6.26.0" - } - }, - "babel-plugin-transform-es2015-modules-commonjs": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.0.tgz", - "integrity": "sha1-DYOUApt9xqvhqX7xgeAHWN0uXYo=", - "dev": true, - "requires": { - "babel-plugin-transform-strict-mode": "6.24.1", - "babel-runtime": "6.26.0", - "babel-template": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-plugin-transform-es2015-modules-systemjs": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz", - "integrity": "sha1-/4mhQrkRmpBhlfXxBuzzBdlAfSM=", - "dev": true, - "requires": { - "babel-helper-hoist-variables": "6.24.1", - "babel-runtime": "6.26.0", - "babel-template": "6.26.0" - } - }, - "babel-plugin-transform-es2015-modules-umd": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz", - "integrity": "sha1-rJl+YoXNGO1hdq22B9YCNErThGg=", - "dev": true, - "requires": { - "babel-plugin-transform-es2015-modules-amd": "6.24.1", - "babel-runtime": "6.26.0", - "babel-template": "6.26.0" - } - }, - "babel-plugin-transform-es2015-object-super": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz", - "integrity": "sha1-JM72muIcuDp/hgPa0CH1cusnj40=", - "dev": true, - "requires": { - "babel-helper-replace-supers": "6.24.1", - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-es2015-parameters": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz", - "integrity": "sha1-V6w1GrScrxSpfNE7CfZv3wpiXys=", - "dev": true, - "requires": { - "babel-helper-call-delegate": "6.24.1", - "babel-helper-get-function-arity": "6.24.1", - "babel-runtime": "6.26.0", - "babel-template": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-plugin-transform-es2015-shorthand-properties": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz", - "integrity": "sha1-JPh11nIch2YbvZmkYi5R8U3jiqA=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-plugin-transform-es2015-spread": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz", - "integrity": "sha1-1taKmfia7cRTbIGlQujdnxdG+NE=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-es2015-sticky-regex": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz", - "integrity": "sha1-AMHNsaynERLN8M9hJsLta0V8zbw=", - "dev": true, - "requires": { - "babel-helper-regex": "6.26.0", - "babel-runtime": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-plugin-transform-es2015-template-literals": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz", - "integrity": "sha1-qEs0UPfp+PH2g51taH2oS7EjbY0=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-es2015-typeof-symbol": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz", - "integrity": "sha1-3sCfHN3/lLUqxz1QXITfWdzOs3I=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-es2015-unicode-regex": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz", - "integrity": "sha1-04sS9C6nMj9yk4fxinxa4frrNek=", - "dev": true, - "requires": { - "babel-helper-regex": "6.26.0", - "babel-runtime": "6.26.0", - "regexpu-core": "2.0.0" - } - }, - "babel-plugin-transform-exponentiation-operator": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz", - "integrity": "sha1-KrDJx/MJj6SJB3cruBP+QejeOg4=", - "dev": true, - "requires": { - "babel-helper-builder-binary-assignment-operator-visitor": "6.24.1", - "babel-plugin-syntax-exponentiation-operator": "6.13.0", - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-object-rest-spread": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz", - "integrity": "sha1-DzZpLVD+9rfi1LOsFHgTepY7ewY=", - "dev": true, - "requires": { - "babel-plugin-syntax-object-rest-spread": "6.13.0", - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-react-jsx": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx/-/babel-plugin-transform-react-jsx-6.24.1.tgz", - "integrity": "sha1-hAoCjn30YN/DotKfDA2R9jduZqM=", - "dev": true, - "requires": { - "babel-helper-builder-react-jsx": "6.26.0", - "babel-plugin-syntax-jsx": "6.18.0", - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-regenerator": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz", - "integrity": "sha1-4HA2lvveJ/Cj78rPi03KL3s6jy8=", - "dev": true, - "requires": { - "regenerator-transform": "0.10.1" - } - }, - "babel-plugin-transform-runtime": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-runtime/-/babel-plugin-transform-runtime-6.23.0.tgz", - "integrity": "sha1-iEkNRGUC6puOfvsP4J7E2ZR5se4=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-strict-mode": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz", - "integrity": "sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-preset-env": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/babel-preset-env/-/babel-preset-env-1.6.1.tgz", - "integrity": "sha512-W6VIyA6Ch9ePMI7VptNn2wBM6dbG0eSz25HEiL40nQXCsXGTGZSTZu1Iap+cj3Q0S5a7T9+529l/5Bkvd+afNA==", - "dev": true, - "requires": { - "babel-plugin-check-es2015-constants": "6.22.0", - "babel-plugin-syntax-trailing-function-commas": "6.22.0", - "babel-plugin-transform-async-to-generator": "6.24.1", - "babel-plugin-transform-es2015-arrow-functions": "6.22.0", - "babel-plugin-transform-es2015-block-scoped-functions": "6.22.0", - "babel-plugin-transform-es2015-block-scoping": "6.26.0", - "babel-plugin-transform-es2015-classes": "6.24.1", - "babel-plugin-transform-es2015-computed-properties": "6.24.1", - "babel-plugin-transform-es2015-destructuring": "6.23.0", - "babel-plugin-transform-es2015-duplicate-keys": "6.24.1", - "babel-plugin-transform-es2015-for-of": "6.23.0", - "babel-plugin-transform-es2015-function-name": "6.24.1", - "babel-plugin-transform-es2015-literals": "6.22.0", - "babel-plugin-transform-es2015-modules-amd": "6.24.1", - "babel-plugin-transform-es2015-modules-commonjs": "6.26.0", - "babel-plugin-transform-es2015-modules-systemjs": "6.24.1", - "babel-plugin-transform-es2015-modules-umd": "6.24.1", - "babel-plugin-transform-es2015-object-super": "6.24.1", - "babel-plugin-transform-es2015-parameters": "6.24.1", - "babel-plugin-transform-es2015-shorthand-properties": "6.24.1", - "babel-plugin-transform-es2015-spread": "6.22.0", - "babel-plugin-transform-es2015-sticky-regex": "6.24.1", - "babel-plugin-transform-es2015-template-literals": "6.22.0", - "babel-plugin-transform-es2015-typeof-symbol": "6.23.0", - "babel-plugin-transform-es2015-unicode-regex": "6.24.1", - "babel-plugin-transform-exponentiation-operator": "6.24.1", - "babel-plugin-transform-regenerator": "6.26.0", - "browserslist": "2.11.3", - "invariant": "2.2.2", - "semver": "5.3.0" - } - }, - "babel-preset-jest": { - "version": "22.2.0", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-22.2.0.tgz", - "integrity": "sha512-p61cPMGYlSgfNScn1yQuVnLguWE4bjhB/br4KQDMbYZG+v6ryE5Ch7TKukjA6mRuIQj1zhyou7Sbpqrh4/N6Pg==", - "dev": true, - "requires": { - "babel-plugin-jest-hoist": "22.2.0", - "babel-plugin-syntax-object-rest-spread": "6.13.0" - } - }, - "babel-register": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.26.0.tgz", - "integrity": "sha1-btAhFz4vy0htestFxgCahW9kcHE=", - "dev": true, - "requires": { - "babel-core": "6.26.0", - "babel-runtime": "6.26.0", - "core-js": "2.5.3", - "home-or-tmp": "2.0.0", - "lodash": "4.17.4", - "mkdirp": "0.5.1", - "source-map-support": "0.4.18" - }, - "dependencies": { - "core-js": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.3.tgz", - "integrity": "sha1-isw4NFgk8W2DZbfJtCWRaOjtYD4=", - "dev": true - }, - "source-map-support": { - "version": "0.4.18", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", - "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", - "dev": true, - "requires": { - "source-map": "0.5.7" - } - } - } - }, - "babel-runtime": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", - "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", - "dev": true, - "requires": { - "core-js": "2.5.3", - "regenerator-runtime": "0.11.1" - }, - "dependencies": { - "core-js": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.3.tgz", - "integrity": "sha1-isw4NFgk8W2DZbfJtCWRaOjtYD4=", - "dev": true - } - } - }, - "babel-template": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", - "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0", - "babylon": "6.18.0", - "lodash": "4.17.4" - } - }, - "babel-traverse": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", - "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", - "dev": true, - "requires": { - "babel-code-frame": "6.26.0", - "babel-messages": "6.23.0", - "babel-runtime": "6.26.0", - "babel-types": "6.26.0", - "babylon": "6.18.0", - "debug": "2.6.9", - "globals": "9.18.0", - "invariant": "2.2.2", - "lodash": "4.17.4" - } - }, - "babel-types": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", - "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0", - "esutils": "2.0.2", - "lodash": "4.17.4", - "to-fast-properties": "1.0.3" - }, - "dependencies": { - "to-fast-properties": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", - "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=", - "dev": true - } - } - }, - "babylon": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", - "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", - "dev": true - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" - }, - "base64-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.1.tgz", - "integrity": "sha512-dwVUVIXsBZXwTuwnXI9RK8sBmgq09NDHzyR9SAph9eqk76gKK2JSQmZARC2zRC81JC2QTtxD0ARU5qTS25gIGw==", - "dev": true - }, - "bcrypt-pbkdf": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", - "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", - "dev": true, - "optional": true, - "requires": { - "tweetnacl": "0.14.5" - } - }, - "big.js": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz", - "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==", - "dev": true - }, - "binary-extensions": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.11.0.tgz", - "integrity": "sha1-RqoXUftqL5PuXmibsQh9SxTGwgU=", - "dev": true - }, - "block-stream": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", - "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", - "dev": true, - "requires": { - "inherits": "2.0.3" - } - }, - "bluebird": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", - "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==" - }, - "bn.js": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", - "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", - "dev": true - }, - "boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", - "dev": true - }, - "boom": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", - "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", - "dev": true, - "requires": { - "hoek": "4.2.0" - } - }, - "brace-expansion": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", - "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", - "requires": { - "balanced-match": "1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", - "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", - "dev": true, - "requires": { - "expand-range": "1.8.2", - "preserve": "0.2.0", - "repeat-element": "1.1.2" - } - }, - "brorand": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", - "dev": true - }, - "browser-process-hrtime": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-0.1.2.tgz", - "integrity": "sha1-Ql1opY00R/AqBKqJQYf86K+Le44=", - "dev": true - }, - "browser-resolve": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.2.tgz", - "integrity": "sha1-j/CbCixCFxihBRwmCzLkj0QpOM4=", - "dev": true, - "requires": { - "resolve": "1.1.7" - }, - "dependencies": { - "resolve": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", - "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", - "dev": true - } - } - }, - "browserify-aes": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.1.1.tgz", - "integrity": "sha512-UGnTYAnB2a3YuYKIRy1/4FB2HdM866E0qC46JXvVTYKlBlZlnvfpSfY6OKfXZAkv70eJ2a1SqzpAo5CRhZGDFg==", - "dev": true, - "requires": { - "buffer-xor": "1.0.3", - "cipher-base": "1.0.4", - "create-hash": "1.1.3", - "evp_bytestokey": "1.0.3", - "inherits": "2.0.3", - "safe-buffer": "5.1.1" - } - }, - "browserify-cipher": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.0.tgz", - "integrity": "sha1-mYgkSHS/XtTijalWZtzWasj8Njo=", - "dev": true, - "requires": { - "browserify-aes": "1.1.1", - "browserify-des": "1.0.0", - "evp_bytestokey": "1.0.3" - } - }, - "browserify-des": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.0.tgz", - "integrity": "sha1-2qJ3cXRwki7S/hhZQRihdUOXId0=", - "dev": true, - "requires": { - "cipher-base": "1.0.4", - "des.js": "1.0.0", - "inherits": "2.0.3" - } - }, - "browserify-rsa": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", - "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", - "dev": true, - "requires": { - "bn.js": "4.11.8", - "randombytes": "2.0.6" - } - }, - "browserify-sign": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", - "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", - "dev": true, - "requires": { - "bn.js": "4.11.8", - "browserify-rsa": "4.0.1", - "create-hash": "1.1.3", - "create-hmac": "1.1.6", - "elliptic": "6.4.0", - "inherits": "2.0.3", - "parse-asn1": "5.1.0" - } - }, - "browserify-zlib": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", - "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", - "dev": true, - "requires": { - "pako": "1.0.6" - } - }, - "browserslist": { - "version": "2.11.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-2.11.3.tgz", - "integrity": "sha512-yWu5cXT7Av6mVwzWc8lMsJMHWn4xyjSuGYi4IozbVTLUOEYPSagUB8kiMDUHA1fS3zjr8nkxkn9jdvug4BBRmA==", - "dev": true, - "requires": { - "caniuse-lite": "1.0.30000810", - "electron-to-chromium": "1.3.33" - } - }, - "bser": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.0.0.tgz", - "integrity": "sha1-mseNPtXZFYBP2HrLFYvHlxR6Fxk=", - "dev": true, - "requires": { - "node-int64": "0.4.0" - } - }, - "buffer": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", - "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", - "dev": true, - "requires": { - "base64-js": "1.2.1", - "ieee754": "1.1.8", - "isarray": "1.0.0" - } - }, - "buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", - "dev": true - }, - "buffer-xor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", - "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", - "dev": true - }, - "builtin-modules": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", - "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=" - }, - "builtin-status-codes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", - "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", - "dev": true - }, - "caller-path": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", - "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", - "dev": true, - "requires": { - "callsites": "0.2.0" - }, - "dependencies": { - "callsites": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", - "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", - "dev": true - } - } - }, - "callsites": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", - "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=", - "dev": true - }, - "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" - }, - "camelcase-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", - "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", - "dev": true, - "requires": { - "camelcase": "2.1.1", - "map-obj": "1.0.1" - }, - "dependencies": { - "camelcase": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", - "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", - "dev": true - } - } - }, - "caniuse-api": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-1.6.1.tgz", - "integrity": "sha1-tTTnxzTE+B7F++isoq0kNUuWLGw=", - "dev": true, - "requires": { - "browserslist": "1.7.7", - "caniuse-db": "1.0.30000804", - "lodash.memoize": "4.1.2", - "lodash.uniq": "4.5.0" - }, - "dependencies": { - "browserslist": { - "version": "1.7.7", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-1.7.7.tgz", - "integrity": "sha1-C9dnBCWL6CmyOYu1Dkti0aFmsLk=", - "dev": true, - "requires": { - "caniuse-db": "1.0.30000804", - "electron-to-chromium": "1.3.33" - } - } - } - }, - "caniuse-db": { - "version": "1.0.30000804", - "resolved": "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30000804.tgz", - "integrity": "sha1-hP60IBj8ZM9q/2Nx5DEV8pLAAXk=", - "dev": true - }, - "caniuse-lite": { - "version": "1.0.30000810", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000810.tgz", - "integrity": "sha512-/0Q00Oie9C72P8zQHtFvzmkrMC3oOFUnMWjCy5F2+BE8lzICm91hQPhh0+XIsAFPKOe2Dh3pKgbRmU3EKxfldA==", - "dev": true - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", - "dev": true - }, - "center-align": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", - "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", - "dev": true, - "requires": { - "align-text": "0.1.4", - "lazy-cache": "1.0.4" - } - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "2.2.1", - "escape-string-regexp": "1.0.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" - } - }, - "chardet": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz", - "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=", - "dev": true - }, - "check-more-types": { - "version": "2.24.0", - "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz", - "integrity": "sha1-FCD/sQ/URNz8ebQ4kbv//TKoRgA=", - "dev": true - }, - "check-node-version": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/check-node-version/-/check-node-version-3.1.1.tgz", - "integrity": "sha512-52fHDe/0pbidY3InI33Beyb/oarySfLANlXxLGBl9lLVrLIW88XWIwu4jGJrQ1imuWzX5ukNGWXUyCgmgVUD8A==", - "dev": true, - "requires": { - "chalk": "2.3.0", - "map-values": "1.0.1", - "minimist": "1.2.0", - "object-filter": "1.0.2", - "object.assign": "4.1.0", - "run-parallel": "1.1.6", - "semver": "5.3.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", - "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", - "dev": true, - "requires": { - "color-convert": "1.9.1" - } - }, - "chalk": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", - "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", - "dev": true, - "requires": { - "ansi-styles": "3.2.0", - "escape-string-regexp": "1.0.5", - "supports-color": "4.5.0" - } - }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - }, - "supports-color": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", - "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", - "dev": true, - "requires": { - "has-flag": "2.0.0" - } - } - } - }, - "cheerio": { - "version": "1.0.0-rc.2", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.2.tgz", - "integrity": "sha1-S59TqBsn5NXawxwP/Qz6A8xoMNs=", - "dev": true, - "requires": { - "css-select": "1.2.0", - "dom-serializer": "0.1.0", - "entities": "1.1.1", - "htmlparser2": "3.9.2", - "lodash": "4.17.4", - "parse5": "3.0.3" - } - }, - "chokidar": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", - "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=", - "dev": true, - "requires": { - "anymatch": "1.3.2", - "async-each": "1.0.1", - "fsevents": "1.1.3", - "glob-parent": "2.0.0", - "inherits": "2.0.3", - "is-binary-path": "1.0.1", - "is-glob": "2.0.1", - "path-is-absolute": "1.0.1", - "readdirp": "2.1.0" - } - }, - "ci-info": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.1.2.tgz", - "integrity": "sha512-uTGIPNx/nSpBdsF6xnseRXLLtfr9VLqkz8ZqHXr3Y7b6SftyRxBGjwMtJj1OhNbmlc1wZzLNAlAcvyIiE8a6ZA==", - "dev": true - }, - "cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", - "dev": true, - "requires": { - "inherits": "2.0.3", - "safe-buffer": "5.1.1" - } - }, - "circular-json": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", - "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", - "dev": true - }, - "circular-json-es6": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/circular-json-es6/-/circular-json-es6-2.0.2.tgz", - "integrity": "sha512-ODYONMMNb3p658Zv+Pp+/XPa5s6q7afhz3Tzyvo+VRh9WIrJ64J76ZC4GQxnlye/NesTn09jvOiuE8+xxfpwhQ==", - "dev": true - }, - "clap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/clap/-/clap-1.2.3.tgz", - "integrity": "sha512-4CoL/A3hf90V3VIEjeuhSvlGFEHKzOz+Wfc2IVZc+FaUgU0ZQafJTP49fvnULipOPcAfqhyI2duwQyns6xqjYA==", - "dev": true, - "requires": { - "chalk": "1.1.3" - } - }, - "classnames": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.5.tgz", - "integrity": "sha1-+zgB1FNGdknvNgPH1hoCvRKb3m0=" - }, - "cli-cursor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", - "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", - "dev": true, - "requires": { - "restore-cursor": "1.0.1" - } - }, - "cli-spinners": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-0.1.2.tgz", - "integrity": "sha1-u3ZNiOGF+54eaiofGXcjGPYF4xw=", - "dev": true - }, - "cli-truncate": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-0.2.1.tgz", - "integrity": "sha1-nxXPuwcFAFNpIWxiasfQWrkN1XQ=", - "dev": true, - "requires": { - "slice-ansi": "0.0.4", - "string-width": "1.0.2" - }, - "dependencies": { - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - } - } - }, - "cli-width": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", - "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", - "dev": true - }, - "clipboard": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-1.7.1.tgz", - "integrity": "sha1-Ng1taUbpmnof7zleQrqStem1oWs=", - "requires": { - "good-listener": "1.2.2", - "select": "1.1.2", - "tiny-emitter": "2.0.2" - } - }, - "cliui": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", - "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", - "requires": { - "string-width": "1.0.2", - "strip-ansi": "3.0.1", - "wrap-ansi": "2.1.0" - }, - "dependencies": { - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - } - } - }, - "clone": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.3.tgz", - "integrity": "sha1-KY1+IjFmD0DAA8LtMUDezz9TCF8=", - "dev": true - }, - "clone-deep": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-0.3.0.tgz", - "integrity": "sha1-NIxhrpzb4O3+BT2R/0zFIdeQ7eg=", - "dev": true, - "requires": { - "for-own": "1.0.0", - "is-plain-object": "2.0.4", - "kind-of": "3.2.2", - "shallow-clone": "0.1.2" - }, - "dependencies": { - "for-own": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", - "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", - "dev": true, - "requires": { - "for-in": "1.0.2" - } - } - } - }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", - "dev": true - }, - "coa": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/coa/-/coa-1.0.4.tgz", - "integrity": "sha1-qe8VNmDWqGqL3sAomlxoTSF0Mv0=", - "dev": true, - "requires": { - "q": "1.5.1" - } - }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" - }, - "codecov": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/codecov/-/codecov-3.0.0.tgz", - "integrity": "sha1-wnO4xPEpRXI+jcnSWAPYk0Pl8o4=", - "dev": true, - "requires": { - "argv": "0.0.2", - "request": "2.81.0", - "urlgrey": "0.4.4" - }, - "dependencies": { - "ajv": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", - "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", - "dev": true, - "requires": { - "co": "4.6.0", - "json-stable-stringify": "1.0.1" - } - }, - "assert-plus": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", - "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=", - "dev": true - }, - "aws-sign2": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", - "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=", - "dev": true - }, - "boom": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", - "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", - "dev": true, - "requires": { - "hoek": "2.16.3" - } - }, - "cryptiles": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", - "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", - "dev": true, - "requires": { - "boom": "2.10.1" - } - }, - "form-data": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", - "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", - "dev": true, - "requires": { - "asynckit": "0.4.0", - "combined-stream": "1.0.5", - "mime-types": "2.1.17" - } - }, - "har-schema": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz", - "integrity": "sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4=", - "dev": true - }, - "har-validator": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz", - "integrity": "sha1-M0gdDxu/9gDdID11gSpqX7oALio=", - "dev": true, - "requires": { - "ajv": "4.11.8", - "har-schema": "1.0.5" - } - }, - "hawk": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", - "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", - "dev": true, - "requires": { - "boom": "2.10.1", - "cryptiles": "2.0.5", - "hoek": "2.16.3", - "sntp": "1.0.9" - } - }, - "hoek": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", - "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=", - "dev": true - }, - "http-signature": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", - "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", - "dev": true, - "requires": { - "assert-plus": "0.2.0", - "jsprim": "1.4.1", - "sshpk": "1.13.1" - } - }, - "performance-now": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", - "integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=", - "dev": true - }, - "qs": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", - "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=", - "dev": true - }, - "request": { - "version": "2.81.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.81.0.tgz", - "integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=", - "dev": true, - "requires": { - "aws-sign2": "0.6.0", - "aws4": "1.6.0", - "caseless": "0.12.0", - "combined-stream": "1.0.5", - "extend": "3.0.1", - "forever-agent": "0.6.1", - "form-data": "2.1.4", - "har-validator": "4.2.1", - "hawk": "3.1.3", - "http-signature": "1.1.1", - "is-typedarray": "1.0.0", - "isstream": "0.1.2", - "json-stringify-safe": "5.0.1", - "mime-types": "2.1.17", - "oauth-sign": "0.8.2", - "performance-now": "0.2.0", - "qs": "6.4.0", - "safe-buffer": "5.1.1", - "stringstream": "0.0.5", - "tough-cookie": "2.3.3", - "tunnel-agent": "0.6.0", - "uuid": "3.1.0" - } - }, - "sntp": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", - "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", - "dev": true, - "requires": { - "hoek": "2.16.3" - } - } - } - }, - "color": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/color/-/color-0.11.4.tgz", - "integrity": "sha1-bXtcdPtl6EHNSHkq0e1eB7kE12Q=", - "dev": true, - "requires": { - "clone": "1.0.3", - "color-convert": "1.9.1", - "color-string": "0.3.0" - } - }, - "color-convert": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", - "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, - "color-string": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-0.3.0.tgz", - "integrity": "sha1-J9RvtnAlxcL6JZk7+/V55HhBuZE=", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "colormin": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/colormin/-/colormin-1.1.2.tgz", - "integrity": "sha1-6i90IKcrlogaOKrlnsEkpvcpgTM=", - "dev": true, - "requires": { - "color": "0.11.4", - "css-color-names": "0.0.4", - "has": "1.0.1" - } - }, - "colors": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/colors/-/colors-0.5.1.tgz", - "integrity": "sha1-fQAj6usVTo7p/Oddy5I9DtFmd3Q=", - "dev": true - }, - "combined-stream": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", - "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", - "dev": true, - "requires": { - "delayed-stream": "1.0.0" - } - }, - "commander": { - "version": "2.14.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.14.1.tgz", - "integrity": "sha512-+YR16o3rK53SmWHU3rEM3tPAh2rwb1yPcQX5irVn7mb0gXbwuCCrnkbV5+PBfETdfg1vui07nM6PCG1zndcjQw==" - }, - "comment-parser": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-0.4.2.tgz", - "integrity": "sha1-+lo/eAEwcBFIZtx7jpzzF6ljX3Q=", - "requires": { - "readable-stream": "2.3.3" - } - }, - "common-tags": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.4.0.tgz", - "integrity": "sha1-EYe+Tz1M8MBCfUP3Tu8fc1AWFMA=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0" - } - }, - "commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", - "dev": true - }, - "computed-style": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/computed-style/-/computed-style-0.1.4.tgz", - "integrity": "sha1-fzRP2FhLLkJb7cpKGvwOMAuwXXQ=" - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "concat-stream": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.0.tgz", - "integrity": "sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc=", - "dev": true, - "requires": { - "inherits": "2.0.3", - "readable-stream": "2.3.3", - "typedarray": "0.0.6" - } - }, - "concurrently": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-3.5.0.tgz", - "integrity": "sha1-jPG3cHppFqeKT/W3e7BN7FSzebI=", - "dev": true, - "requires": { - "chalk": "0.5.1", - "commander": "2.6.0", - "date-fns": "1.29.0", - "lodash": "4.17.4", - "rx": "2.3.24", - "spawn-command": "0.0.2-1", - "supports-color": "3.2.3", - "tree-kill": "1.2.0" - }, - "dependencies": { - "ansi-regex": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz", - "integrity": "sha1-DY6UaWej2BQ/k+JOKYUl/BsiNfk=", - "dev": true - }, - "ansi-styles": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.1.0.tgz", - "integrity": "sha1-6uy/Zs1waIJ2Cy9GkVgrj1XXp94=", - "dev": true - }, - "chalk": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.5.1.tgz", - "integrity": "sha1-Zjs6ZItotV0EaQ1JFnqoN4WPIXQ=", - "dev": true, - "requires": { - "ansi-styles": "1.1.0", - "escape-string-regexp": "1.0.5", - "has-ansi": "0.1.0", - "strip-ansi": "0.3.0", - "supports-color": "0.2.0" - }, - "dependencies": { - "supports-color": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-0.2.0.tgz", - "integrity": "sha1-2S3iaU6z9nMjlz1649i1W0wiGQo=", - "dev": true - } - } - }, - "commander": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.6.0.tgz", - "integrity": "sha1-nfflL7Kgyw+4kFjugMMQQiXzfh0=", - "dev": true - }, - "has-ansi": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-0.1.0.tgz", - "integrity": "sha1-hPJlqujA5qiKEtcCKJS3VoiUxi4=", - "dev": true, - "requires": { - "ansi-regex": "0.2.1" - } - }, - "has-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", - "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", - "dev": true - }, - "strip-ansi": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.3.0.tgz", - "integrity": "sha1-JfSOoiynkYfzF0pNuHWTR7sSYiA=", - "dev": true, - "requires": { - "ansi-regex": "0.2.1" - } - }, - "supports-color": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", - "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", - "dev": true, - "requires": { - "has-flag": "1.0.0" - } - } - } - }, - "config-chain": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.11.tgz", - "integrity": "sha1-q6CXR9++TD5w52am5BWG4YWfxvI=", - "requires": { - "ini": "1.3.5", - "proto-list": "1.2.4" - } - }, - "console-browserify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", - "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", - "dev": true, - "requires": { - "date-now": "0.1.4" - } - }, - "console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "dev": true - }, - "constants-browserify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", - "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", - "dev": true - }, - "contains-path": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", - "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=" - }, - "content-type-parser": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/content-type-parser/-/content-type-parser-1.0.2.tgz", - "integrity": "sha512-lM4l4CnMEwOLHAHr/P6MEZwZFPJFtAAKgL6pogbXmVZggIqXhdB6RbBtPOTsw2FcXwYhehRGERJmRrjOiIB8pQ==", - "dev": true - }, - "convert-source-map": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.1.tgz", - "integrity": "sha1-uCeAl7m8IpNl3lxiz1/K7YtVmeU=", - "dev": true - }, - "core-js": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", - "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=" - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "cosmiconfig": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-2.2.2.tgz", - "integrity": "sha512-GiNXLwAFPYHy25XmTPpafYvn3CLAkJ8FLsscq78MQd1Kh0OU6Yzhn4eV2MVF4G9WEQZoWEGltatdR+ntGPMl5A==", - "dev": true, - "requires": { - "is-directory": "0.3.1", - "js-yaml": "3.10.0", - "minimist": "1.2.0", - "object-assign": "4.1.1", - "os-homedir": "1.0.2", - "parse-json": "2.2.0", - "require-from-string": "1.2.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - } - } - }, - "create-ecdh": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.0.tgz", - "integrity": "sha1-iIxyNZbN92EvZJgjPuvXo1MBc30=", - "dev": true, - "requires": { - "bn.js": "4.11.8", - "elliptic": "6.4.0" - } - }, - "create-hash": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.1.3.tgz", - "integrity": "sha1-YGBCrIuSYnUPSDyt2rD1gZFy2P0=", - "dev": true, - "requires": { - "cipher-base": "1.0.4", - "inherits": "2.0.3", - "ripemd160": "2.0.1", - "sha.js": "2.4.10" - } - }, - "create-hmac": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.6.tgz", - "integrity": "sha1-rLniIaThe9sHbpBlfEK5PjcmzwY=", - "dev": true, - "requires": { - "cipher-base": "1.0.4", - "create-hash": "1.1.3", - "inherits": "2.0.3", - "ripemd160": "2.0.1", - "safe-buffer": "5.1.1", - "sha.js": "2.4.10" - } - }, - "cross-env": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-3.2.4.tgz", - "integrity": "sha1-ngWF8neGTtQhznVvgamA/w1piro=", - "dev": true, - "requires": { - "cross-spawn": "5.1.0", - "is-windows": "1.0.1" - } - }, - "cross-spawn": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", - "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", - "requires": { - "lru-cache": "4.1.1", - "shebang-command": "1.2.0", - "which": "1.3.0" - }, - "dependencies": { - "lru-cache": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.1.tgz", - "integrity": "sha512-q4spe4KTfsAS1SUHLO0wz8Qiyf1+vMIAgpRYioFYDMNqKfHQbg+AVDH3i4fvpl71/P1L0dBl+fQi+P37UYf0ew==", - "requires": { - "pseudomap": "1.0.2", - "yallist": "2.1.2" - } - } - } - }, - "cryptiles": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", - "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", - "dev": true, - "requires": { - "boom": "5.2.0" - }, - "dependencies": { - "boom": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", - "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", - "dev": true, - "requires": { - "hoek": "4.2.0" - } - } - } - }, - "crypto-browserify": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", - "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", - "dev": true, - "requires": { - "browserify-cipher": "1.0.0", - "browserify-sign": "4.0.4", - "create-ecdh": "4.0.0", - "create-hash": "1.1.3", - "create-hmac": "1.1.6", - "diffie-hellman": "5.0.2", - "inherits": "2.0.3", - "pbkdf2": "3.0.14", - "public-encrypt": "4.0.0", - "randombytes": "2.0.6", - "randomfill": "1.0.3" - } - }, - "css-color-names": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", - "integrity": "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=", - "dev": true - }, - "css-select": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", - "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", - "dev": true, - "requires": { - "boolbase": "1.0.0", - "css-what": "2.1.0", - "domutils": "1.5.1", - "nth-check": "1.0.1" - } - }, - "css-what": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.0.tgz", - "integrity": "sha1-lGfQMsOM+u+58teVASUwYvh/ob0=", - "dev": true - }, - "cssnano": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-3.10.0.tgz", - "integrity": "sha1-Tzj2zqK5sX+gFJDyPx3GjqZcHDg=", - "dev": true, - "requires": { - "autoprefixer": "6.7.7", - "decamelize": "1.2.0", - "defined": "1.0.0", - "has": "1.0.1", - "object-assign": "4.1.1", - "postcss": "5.2.18", - "postcss-calc": "5.3.1", - "postcss-colormin": "2.2.2", - "postcss-convert-values": "2.6.1", - "postcss-discard-comments": "2.0.4", - "postcss-discard-duplicates": "2.1.0", - "postcss-discard-empty": "2.1.0", - "postcss-discard-overridden": "0.1.1", - "postcss-discard-unused": "2.2.3", - "postcss-filter-plugins": "2.0.2", - "postcss-merge-idents": "2.1.7", - "postcss-merge-longhand": "2.0.2", - "postcss-merge-rules": "2.1.2", - "postcss-minify-font-values": "1.0.5", - "postcss-minify-gradients": "1.0.5", - "postcss-minify-params": "1.2.2", - "postcss-minify-selectors": "2.1.1", - "postcss-normalize-charset": "1.1.1", - "postcss-normalize-url": "3.0.8", - "postcss-ordered-values": "2.2.3", - "postcss-reduce-idents": "2.4.0", - "postcss-reduce-initial": "1.0.1", - "postcss-reduce-transforms": "1.0.4", - "postcss-svgo": "2.1.6", - "postcss-unique-selectors": "2.0.2", - "postcss-value-parser": "3.3.0", - "postcss-zindex": "2.2.0" - } - }, - "csso": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/csso/-/csso-2.3.2.tgz", - "integrity": "sha1-3dUsWHAz9J6Utx/FVWnyUuj/X4U=", - "dev": true, - "requires": { - "clap": "1.2.3", - "source-map": "0.5.7" - } - }, - "cssom": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.2.tgz", - "integrity": "sha1-uANhcMefB6kP8vFuIihAJ6JDhIs=", - "dev": true - }, - "cssstyle": { - "version": "0.2.37", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-0.2.37.tgz", - "integrity": "sha1-VBCXI0yyUTyDzu06zdwn/yeYfVQ=", - "dev": true, - "requires": { - "cssom": "0.3.2" - } - }, - "currently-unhandled": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", - "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", - "dev": true, - "requires": { - "array-find-index": "1.0.2" - } - }, - "cypress": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-1.4.1.tgz", - "integrity": "sha1-YvQHSgDm8S4t/jiKf06BaknOsD8=", - "dev": true, - "requires": { - "@cypress/listr-verbose-renderer": "0.4.1", - "@cypress/xvfb": "1.1.3", - "@types/blob-util": "1.3.3", - "@types/bluebird": "3.5.18", - "@types/chai": "4.0.8", - "@types/chai-jquery": "1.1.35", - "@types/jquery": "3.2.16", - "@types/lodash": "4.14.87", - "@types/minimatch": "3.0.1", - "@types/mocha": "2.2.44", - "@types/sinon": "4.0.0", - "@types/sinon-chai": "2.7.29", - "bluebird": "3.5.0", - "chalk": "2.1.0", - "check-more-types": "2.24.0", - "commander": "2.11.0", - "common-tags": "1.4.0", - "debug": "3.1.0", - "extract-zip": "1.6.6", - "fs-extra": "4.0.1", - "getos": "2.8.4", - "glob": "7.1.2", - "is-ci": "1.0.10", - "is-installed-globally": "0.1.0", - "lazy-ass": "1.6.0", - "listr": "0.12.0", - "lodash": "4.17.4", - "minimist": "1.2.0", - "progress": "1.1.8", - "ramda": "0.24.1", - "request": "2.81.0", - "request-progress": "0.3.1", - "supports-color": "5.1.0", - "tmp": "0.0.31", - "url": "0.11.0", - "yauzl": "2.8.0" - }, - "dependencies": { - "@types/jquery": { - "version": "3.2.16", - "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.2.16.tgz", - "integrity": "sha512-q2WC02YxQoX2nY1HRKlYGHpGP1saPmD7GN0pwCDlTz35a4eOtJG+aHRlXyjCuXokUukSrR2aXyBhSW3j+jPc0A==", - "dev": true - }, - "ajv": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", - "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", - "dev": true, - "requires": { - "co": "4.6.0", - "json-stable-stringify": "1.0.1" - } - }, - "ansi-styles": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", - "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", - "dev": true, - "requires": { - "color-convert": "1.9.1" - } - }, - "assert-plus": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", - "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=", - "dev": true - }, - "aws-sign2": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", - "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=", - "dev": true - }, - "bluebird": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.0.tgz", - "integrity": "sha1-eRQg1/VR7qKJdFOop3ZT+WYG1nw=", - "dev": true - }, - "boom": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", - "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", - "dev": true, - "requires": { - "hoek": "2.16.3" - } - }, - "chalk": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.1.0.tgz", - "integrity": "sha512-LUHGS/dge4ujbXMJrnihYMcL4AoOweGnw9Tp3kQuqy1Kx5c1qKjqvMJZ6nVJPMWJtKCTN72ZogH3oeSO9g9rXQ==", - "dev": true, - "requires": { - "ansi-styles": "3.2.0", - "escape-string-regexp": "1.0.5", - "supports-color": "4.5.0" - }, - "dependencies": { - "supports-color": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", - "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", - "dev": true, - "requires": { - "has-flag": "2.0.0" - } - } - } - }, - "commander": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", - "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==", - "dev": true - }, - "cryptiles": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", - "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", - "dev": true, - "requires": { - "boom": "2.10.1" - } - }, - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "form-data": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", - "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", - "dev": true, - "requires": { - "asynckit": "0.4.0", - "combined-stream": "1.0.5", - "mime-types": "2.1.17" - } - }, - "har-schema": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz", - "integrity": "sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4=", - "dev": true - }, - "har-validator": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz", - "integrity": "sha1-M0gdDxu/9gDdID11gSpqX7oALio=", - "dev": true, - "requires": { - "ajv": "4.11.8", - "har-schema": "1.0.5" - } - }, - "hawk": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", - "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", - "dev": true, - "requires": { - "boom": "2.10.1", - "cryptiles": "2.0.5", - "hoek": "2.16.3", - "sntp": "1.0.9" - } - }, - "hoek": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", - "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=", - "dev": true - }, - "http-signature": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", - "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", - "dev": true, - "requires": { - "assert-plus": "0.2.0", - "jsprim": "1.4.1", - "sshpk": "1.13.1" - } - }, - "is-ci": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.0.10.tgz", - "integrity": "sha1-9zkzayYyNlBhqdSCcM1WrjNpMY4=", - "dev": true, - "requires": { - "ci-info": "1.1.2" - } - }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - }, - "performance-now": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", - "integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=", - "dev": true - }, - "qs": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", - "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=", - "dev": true - }, - "request": { - "version": "2.81.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.81.0.tgz", - "integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=", - "dev": true, - "requires": { - "aws-sign2": "0.6.0", - "aws4": "1.6.0", - "caseless": "0.12.0", - "combined-stream": "1.0.5", - "extend": "3.0.1", - "forever-agent": "0.6.1", - "form-data": "2.1.4", - "har-validator": "4.2.1", - "hawk": "3.1.3", - "http-signature": "1.1.1", - "is-typedarray": "1.0.0", - "isstream": "0.1.2", - "json-stringify-safe": "5.0.1", - "mime-types": "2.1.17", - "oauth-sign": "0.8.2", - "performance-now": "0.2.0", - "qs": "6.4.0", - "safe-buffer": "5.1.1", - "stringstream": "0.0.5", - "tough-cookie": "2.3.3", - "tunnel-agent": "0.6.0", - "uuid": "3.1.0" - } - }, - "sntp": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", - "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", - "dev": true, - "requires": { - "hoek": "2.16.3" - } - }, - "supports-color": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.1.0.tgz", - "integrity": "sha512-Ry0AwkoKjDpVKK4sV4h6o3UJmNRbjYm2uXhwfj3J56lMVdvnUNqzQVRztOOMGQ++w1K/TjNDFvpJk0F/LoeBCQ==", - "dev": true, - "requires": { - "has-flag": "2.0.0" - } - } - } - }, - "d": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", - "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", - "dev": true, - "requires": { - "es5-ext": "0.10.38" - } - }, - "damerau-levenshtein": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.4.tgz", - "integrity": "sha1-AxkcQyy27qFou3fzpV/9zLiXhRQ=", - "dev": true - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "dev": true, - "requires": { - "assert-plus": "1.0.0" - } - }, - "date-fns": { - "version": "1.29.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.29.0.tgz", - "integrity": "sha512-lbTXWZ6M20cWH8N9S6afb0SBm6tMk+uUg6z3MqHPKE9atmsY3kJkTm8vKe93izJ2B2+q5MV990sM2CHgtAZaOw==", - "dev": true - }, - "date-now": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", - "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=", - "dev": true - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" - }, - "dedent": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", - "integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=", - "dev": true - }, - "deep-equal-ident": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/deep-equal-ident/-/deep-equal-ident-1.1.1.tgz", - "integrity": "sha1-BvS4nlNxDNbOpKd4HHqVZkLejck=", - "dev": true, - "requires": { - "lodash.isequal": "3.0.4" - } - }, - "deep-freeze": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/deep-freeze/-/deep-freeze-0.0.1.tgz", - "integrity": "sha1-OgsABd4YZygZ39OM0x+RF5yJPoQ=", - "dev": true - }, - "deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", - "dev": true - }, - "default-require-extensions": { - "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": "2.0.0" - }, - "dependencies": { - "strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", - "dev": true, - "requires": { - "is-utf8": "0.2.1" - } - } - } - }, - "define-properties": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.2.tgz", - "integrity": "sha1-g6c/L+pWmJj7c3GTyPhzyvbUXJQ=", - "dev": true, - "requires": { - "foreach": "2.0.5", - "object-keys": "1.0.11" - } - }, - "defined": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", - "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=", - "dev": true - }, - "del": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", - "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=", - "dev": true, - "requires": { - "globby": "5.0.0", - "is-path-cwd": "1.0.0", - "is-path-in-cwd": "1.0.0", - "object-assign": "4.1.1", - "pify": "2.3.0", - "pinkie-promise": "2.0.1", - "rimraf": "2.6.2" - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true - }, - "delegate": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz", - "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==" - }, - "delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", - "dev": true - }, - "des.js": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", - "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=", - "dev": true, - "requires": { - "inherits": "2.0.3", - "minimalistic-assert": "1.0.0" - } - }, - "detect-indent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", - "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", - "dev": true, - "requires": { - "repeating": "2.0.1" - } - }, - "detect-newline": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", - "integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=", - "dev": true - }, - "diff": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.4.0.tgz", - "integrity": "sha512-QpVuMTEoJMF7cKzi6bvWhRulU1fZqZnvyVQgNhPaxxuTYwyjn/j1v9falseQ/uXWwPnO56RBfwtg4h/EQXmucA==", - "dev": true - }, - "diffie-hellman": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.2.tgz", - "integrity": "sha1-tYNXOScM/ias9jIJn97SoH8gnl4=", - "dev": true, - "requires": { - "bn.js": "4.11.8", - "miller-rabin": "4.0.1", - "randombytes": "2.0.6" - } - }, - "discontinuous-range": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz", - "integrity": "sha1-44Mx8IRLukm5qctxx3FYWqsbxlo=", - "dev": true - }, - "doctrine": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", - "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", - "requires": { - "esutils": "2.0.2", - "isarray": "1.0.0" - } - }, - "dom-react": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/dom-react/-/dom-react-2.2.0.tgz", - "integrity": "sha1-3GJwYI7VbL35DJo+w1U/m1oL17M=" - }, - "dom-scroll-into-view": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/dom-scroll-into-view/-/dom-scroll-into-view-1.2.1.tgz", - "integrity": "sha1-6PNnMt0ImwIBqI14Fdw/iObWbH4=" - }, - "dom-serializer": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz", - "integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=", - "dev": true, - "requires": { - "domelementtype": "1.1.3", - "entities": "1.1.1" - }, - "dependencies": { - "domelementtype": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz", - "integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs=", - "dev": true - } - } - }, - "domain-browser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", - "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", - "dev": true - }, - "domelementtype": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz", - "integrity": "sha1-sXrtguirWeUt2cGbF1bg/BhyBMI=", - "dev": true - }, - "domexception": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", - "integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==", - "dev": true, - "requires": { - "webidl-conversions": "4.0.2" - } - }, - "domhandler": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.1.tgz", - "integrity": "sha1-iS5HAAqZvlW783dP/qBWHYh5wlk=", - "dev": true, - "requires": { - "domelementtype": "1.3.0" - } - }, - "domutils": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", - "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", - "dev": true, - "requires": { - "dom-serializer": "0.1.0", - "domelementtype": "1.3.0" - } - }, - "ecc-jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", - "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", - "dev": true, - "optional": true, - "requires": { - "jsbn": "0.1.1" - } - }, - "editorconfig": { - "version": "0.13.3", - "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.13.3.tgz", - "integrity": "sha512-WkjsUNVCu+ITKDj73QDvi0trvpdDWdkDyHybDGSXPfekLCqwmpD7CP7iPbvBgosNuLcI96XTDwNa75JyFl7tEQ==", - "requires": { - "bluebird": "3.5.1", - "commander": "2.14.1", - "lru-cache": "3.2.0", - "semver": "5.3.0", - "sigmund": "1.0.1" - } - }, - "electron-to-chromium": { - "version": "1.3.33", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.33.tgz", - "integrity": "sha1-vwBwPWKnxlI4E2V4w1LWxcBCpUU=", - "dev": true - }, - "elegant-spinner": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/elegant-spinner/-/elegant-spinner-1.0.1.tgz", - "integrity": "sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4=", - "dev": true - }, - "element-closest": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/element-closest/-/element-closest-2.0.2.tgz", - "integrity": "sha1-cqdAoQdFM4LijfnOXbtajfD5Zuw=" - }, - "elliptic": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.0.tgz", - "integrity": "sha1-ysmvh2LIWDYYcAPI3+GT5eLq5d8=", - "dev": true, - "requires": { - "bn.js": "4.11.8", - "brorand": "1.1.0", - "hash.js": "1.1.3", - "hmac-drbg": "1.0.1", - "inherits": "2.0.3", - "minimalistic-assert": "1.0.0", - "minimalistic-crypto-utils": "1.0.1" - } - }, - "emoji-regex": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-6.5.1.tgz", - "integrity": "sha512-PAHp6TxrCy7MGMFidro8uikr+zlJJKJ/Q6mm2ExZ7HwkyR9lSVFfE3kt36qcwa24BQL7y0G9axycGjK1A/0uNQ==", - "dev": true - }, - "emojis-list": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", - "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=", - "dev": true - }, - "encoding": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", - "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", - "requires": { - "iconv-lite": "0.4.19" - } - }, - "enhanced-resolve": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-3.4.1.tgz", - "integrity": "sha1-BCHjOf1xQZs9oT0Smzl5BAIwR24=", - "dev": true, - "requires": { - "graceful-fs": "4.1.11", - "memory-fs": "0.4.1", - "object-assign": "4.1.1", - "tapable": "0.2.8" - } - }, - "entities": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz", - "integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA=", - "dev": true - }, - "enzyme": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/enzyme/-/enzyme-3.3.0.tgz", - "integrity": "sha512-l8csyPyLmtxskTz6pX9W8eDOyH1ckEtDttXk/vlFWCjv00SkjTjtoUrogqp4yEvMyneU9dUJoOLnqFoiHb8IHA==", - "dev": true, - "requires": { - "cheerio": "1.0.0-rc.2", - "function.prototype.name": "1.1.0", - "has": "1.0.1", - "is-boolean-object": "1.0.0", - "is-callable": "1.1.3", - "is-number-object": "1.0.3", - "is-string": "1.0.4", - "is-subset": "0.1.1", - "lodash": "4.17.4", - "object-inspect": "1.5.0", - "object-is": "1.0.1", - "object.assign": "4.1.0", - "object.entries": "1.0.4", - "object.values": "1.0.4", - "raf": "3.4.0", - "rst-selector-parser": "2.2.3" - } - }, - "enzyme-adapter-react-16": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.1.1.tgz", - "integrity": "sha512-kC8pAtU2Jk3OJ0EG8Y2813dg9Ol0TXi7UNxHzHiWs30Jo/hj7alc//G1YpKUsPP1oKl9X+Lkx+WlGJpPYA+nvw==", - "dev": true, - "requires": { - "enzyme-adapter-utils": "1.3.0", - "lodash": "4.17.4", - "object.assign": "4.1.0", - "object.values": "1.0.4", - "prop-types": "15.6.0", - "react-reconciler": "0.7.0", - "react-test-renderer": "16.0.0" - }, - "dependencies": { - "prop-types": { - "version": "15.6.0", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.0.tgz", - "integrity": "sha1-zq8IMCL8RrSjX2nhPvda7Q1jmFY=", - "dev": true, - "requires": { - "fbjs": "0.8.16", - "loose-envify": "1.3.1", - "object-assign": "4.1.1" - } - } - } - }, - "enzyme-adapter-utils": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/enzyme-adapter-utils/-/enzyme-adapter-utils-1.3.0.tgz", - "integrity": "sha512-vVXSt6uDv230DIv+ebCG66T1Pm36Kv+m74L1TrF4kaE7e1V7Q/LcxO0QRkajk5cA6R3uu9wJf5h13wOTezTbjA==", - "dev": true, - "requires": { - "lodash": "4.17.4", - "object.assign": "4.1.0", - "prop-types": "15.6.0" - }, - "dependencies": { - "prop-types": { - "version": "15.6.0", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.0.tgz", - "integrity": "sha1-zq8IMCL8RrSjX2nhPvda7Q1jmFY=", - "dev": true, - "requires": { - "fbjs": "0.8.16", - "loose-envify": "1.3.1", - "object-assign": "4.1.1" - } - } - } - }, - "enzyme-matchers": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/enzyme-matchers/-/enzyme-matchers-4.2.0.tgz", - "integrity": "sha512-5Gf/mAVYx6KPAUuxuDhAGt/gu9ndPd6duFcVnH2rbEad2clgTpHZL4Df49FHFukrjEEubX9rhfeAKx0/sbfVkQ==", - "dev": true, - "requires": { - "circular-json-es6": "2.0.2", - "deep-equal-ident": "1.1.1" - } - }, - "enzyme-to-json": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/enzyme-to-json/-/enzyme-to-json-3.3.1.tgz", - "integrity": "sha512-PrgRyZAgEwOrh5/8BtBWrwGcv1mC7yNohytIciAX6SUqDaXg1BlU8CepYQ9BgnDP1i1jTB65qJJITMMCph+T6A==", - "dev": true, - "requires": { - "lodash": "4.17.4" - } - }, - "errno": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.6.tgz", - "integrity": "sha512-IsORQDpaaSwcDP4ZZnHxgE85werpo34VYn1Ud3mq+eUsF593faR8oCZNXrROVkpFu2TsbrNhHin0aUrTsQ9vNw==", - "dev": true, - "requires": { - "prr": "1.0.1" - } - }, - "error-ex": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", - "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", - "requires": { - "is-arrayish": "0.2.1" - } - }, - "es-abstract": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.10.0.tgz", - "integrity": "sha512-/uh/DhdqIOSkAWifU+8nG78vlQxdLckUdI/sPgy0VhuXi2qJ7T8czBmqIYtLQVpCIFYafChnsRsB5pyb1JdmCQ==", - "dev": true, - "requires": { - "es-to-primitive": "1.1.1", - "function-bind": "1.1.1", - "has": "1.0.1", - "is-callable": "1.1.3", - "is-regex": "1.0.4" - } - }, - "es-to-primitive": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.1.1.tgz", - "integrity": "sha1-RTVSSKiJeQNLZ5Lhm7gfK3l13Q0=", - "dev": true, - "requires": { - "is-callable": "1.1.3", - "is-date-object": "1.0.1", - "is-symbol": "1.0.1" - } - }, - "es5-ext": { - "version": "0.10.38", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.38.tgz", - "integrity": "sha512-jCMyePo7AXbUESwbl8Qi01VSH2piY9s/a3rSU/5w/MlTIx8HPL1xn2InGN8ejt/xulcJgnTO7vqNtOAxzYd2Kg==", - "dev": true, - "requires": { - "es6-iterator": "2.0.3", - "es6-symbol": "3.1.1" - } - }, - "es6-iterator": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", - "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", - "dev": true, - "requires": { - "d": "1.0.0", - "es5-ext": "0.10.38", - "es6-symbol": "3.1.1" - } - }, - "es6-map": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.5.tgz", - "integrity": "sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA=", - "dev": true, - "requires": { - "d": "1.0.0", - "es5-ext": "0.10.38", - "es6-iterator": "2.0.3", - "es6-set": "0.1.5", - "es6-symbol": "3.1.1", - "event-emitter": "0.3.5" - } - }, - "es6-set": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz", - "integrity": "sha1-0rPsXU2ADO2BjbU40ol02wpzzLE=", - "dev": true, - "requires": { - "d": "1.0.0", - "es5-ext": "0.10.38", - "es6-iterator": "2.0.3", - "es6-symbol": "3.1.1", - "event-emitter": "0.3.5" - } - }, - "es6-symbol": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", - "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", - "dev": true, - "requires": { - "d": "1.0.0", - "es5-ext": "0.10.38" - } - }, - "es6-weak-map": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.2.tgz", - "integrity": "sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8=", - "dev": true, - "requires": { - "d": "1.0.0", - "es5-ext": "0.10.38", - "es6-iterator": "2.0.3", - "es6-symbol": "3.1.1" - } - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" - }, - "escodegen": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.9.0.tgz", - "integrity": "sha512-v0MYvNQ32bzwoG2OSFzWAkuahDQHK92JBN0pTAALJ4RIxEZe766QJPDR8Hqy7XNUy5K3fnVL76OqYAdc4TZEIw==", - "dev": true, - "requires": { - "esprima": "3.1.3", - "estraverse": "4.2.0", - "esutils": "2.0.2", - "optionator": "0.8.2", - "source-map": "0.5.7" - }, - "dependencies": { - "esprima": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", - "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", - "dev": true - } - } - }, - "escope": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/escope/-/escope-3.6.0.tgz", - "integrity": "sha1-4Bl16BJ4GhY6ba392AOY3GTIicM=", - "dev": true, - "requires": { - "es6-map": "0.1.5", - "es6-weak-map": "2.0.2", - "esrecurse": "4.2.0", - "estraverse": "4.2.0" - } - }, - "eslint": { - "version": "4.16.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-4.16.0.tgz", - "integrity": "sha512-YVXV4bDhNoHHcv0qzU4Meof7/P26B4EuaktMi5L1Tnt52Aov85KmYA8c5D+xyZr/BkhvwUqr011jDSD/QTULxg==", - "dev": true, - "requires": { - "ajv": "5.5.2", - "babel-code-frame": "6.26.0", - "chalk": "2.3.0", - "concat-stream": "1.6.0", - "cross-spawn": "5.1.0", - "debug": "3.1.0", - "doctrine": "2.1.0", - "eslint-scope": "3.7.1", - "eslint-visitor-keys": "1.0.0", - "espree": "3.5.3", - "esquery": "1.0.0", - "esutils": "2.0.2", - "file-entry-cache": "2.0.0", - "functional-red-black-tree": "1.0.1", - "glob": "7.1.2", - "globals": "11.3.0", - "ignore": "3.3.7", - "imurmurhash": "0.1.4", - "inquirer": "3.3.0", - "is-resolvable": "1.1.0", - "js-yaml": "3.10.0", - "json-stable-stringify-without-jsonify": "1.0.1", - "levn": "0.3.0", - "lodash": "4.17.4", - "minimatch": "3.0.4", - "mkdirp": "0.5.1", - "natural-compare": "1.4.0", - "optionator": "0.8.2", - "path-is-inside": "1.0.2", - "pluralize": "7.0.0", - "progress": "2.0.0", - "require-uncached": "1.0.3", - "semver": "5.3.0", - "strip-ansi": "4.0.0", - "strip-json-comments": "2.0.1", - "table": "4.0.2", - "text-table": "0.2.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 - }, - "ansi-styles": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", - "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", - "dev": true, - "requires": { - "color-convert": "1.9.1" - } - }, - "chalk": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", - "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", - "dev": true, - "requires": { - "ansi-styles": "3.2.0", - "escape-string-regexp": "1.0.5", - "supports-color": "4.5.0" - } - }, - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "requires": { - "esutils": "2.0.2" - } - }, - "globals": { - "version": "11.3.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.3.0.tgz", - "integrity": "sha512-kkpcKNlmQan9Z5ZmgqKH/SMbSmjxQ7QjyNqfXVc8VJcoBV2UEg+sxQD15GQofGRh2hfpwUb70VC31DR7Rq5Hdw==", - "dev": true - }, - "progress": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.0.tgz", - "integrity": "sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8=", - "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" - } - }, - "supports-color": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", - "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", - "dev": true, - "requires": { - "has-flag": "2.0.0" - } - } - } - }, - "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-import-resolver-node": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz", - "integrity": "sha512-sfmTqJfPSizWu4aymbPr4Iidp5yKm8yDkHp+Ir3YiTHiiDfxh69mOUsmiqW6RZ9zRXFaF64GtYmN7e+8GHBv6Q==", - "requires": { - "debug": "2.6.9", - "resolve": "1.5.0" - } - }, - "eslint-module-utils": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.1.1.tgz", - "integrity": "sha512-jDI/X5l/6D1rRD/3T43q8Qgbls2nq5km5KSqiwlyUbGo5+04fXhMKdCPhjwbqAa6HXWaMxj8Q4hQDIh7IadJQw==", - "requires": { - "debug": "2.6.9", - "pkg-dir": "1.0.0" - } - }, - "eslint-plugin-i18n": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-i18n/-/eslint-plugin-i18n-1.1.0.tgz", - "integrity": "sha1-q+48yBH2PD3JVIgQHf0hetYnUDI=" - }, - "eslint-plugin-import": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.8.0.tgz", - "integrity": "sha512-Rf7dfKJxZ16QuTgVv1OYNxkZcsu/hULFnC+e+w0Gzi6jMC3guQoWQgxYxc54IDRinlb6/0v5z/PxxIKmVctN+g==", - "requires": { - "builtin-modules": "1.1.1", - "contains-path": "0.1.0", - "debug": "2.6.9", - "doctrine": "1.5.0", - "eslint-import-resolver-node": "0.3.2", - "eslint-module-utils": "2.1.1", - "has": "1.0.1", - "lodash.cond": "4.5.2", - "minimatch": "3.0.4", - "read-pkg-up": "2.0.0" - } - }, - "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.1.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-3.1.3.tgz", - "integrity": "sha512-ujXBhNQz57tLP0bs99QTDPiCX54EypczVhgg9CMJVD9iwfDeFZk5LkQHk+iPfKlV5tk8+dMm+Soxq8QmQK99ZA==", - "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", - "integrity": "sha1-ZZJ3p1iwNsMFp+ShMFfDAc075z8=", - "dev": true, - "requires": { - "aria-query": "0.7.1", - "array-includes": "3.0.3", - "ast-types-flow": "0.0.7", - "axobject-query": "0.1.0", - "damerau-levenshtein": "1.0.4", - "emoji-regex": "6.5.1", - "jsx-ast-utils": "1.4.1" - } - }, - "eslint-plugin-node": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-5.1.1.tgz", - "integrity": "sha512-3xdoEbPyyQNyGhhqttjgSO3cU/non8QDBJF8ttGaHM2h8CaY5zFIngtqW6ZbLEIvhpoFPDVwiQg61b8zanx5zQ==", - "requires": { - "ignore": "3.3.7", - "minimatch": "3.0.4", - "resolve": "1.5.0", - "semver": "5.3.0" - } - }, - "eslint-plugin-react": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.5.1.tgz", - "integrity": "sha512-YGSjB9Qu6QbVTroUZi66pYky3DfoIPLdHQ/wmrBGyBRnwxQsBXAov9j2rpXt/55i8nyMv6IRWJv2s4d4YnduzQ==", - "dev": true, - "requires": { - "doctrine": "2.1.0", - "has": "1.0.1", - "jsx-ast-utils": "2.0.1", - "prop-types": "15.6.0" - }, - "dependencies": { - "doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "requires": { - "esutils": "2.0.2" - } - }, - "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.0", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.0.tgz", - "integrity": "sha1-zq8IMCL8RrSjX2nhPvda7Q1jmFY=", - "dev": true, - "requires": { - "fbjs": "0.8.16", - "loose-envify": "1.3.1", - "object-assign": "4.1.1" - } - } - } - }, - "eslint-plugin-wordpress": { - "version": "git://github.com/WordPress-Coding-Standards/eslint-plugin-wordpress.git#327b6bdec434177a6e841bd3210e87627ccfcecb", - "requires": { - "eslint-plugin-i18n": "1.1.0", - "eslint-plugin-jsdoc": "3.1.3", - "eslint-plugin-node": "5.1.1", - "eslint-plugin-wpcalypso": "3.4.1", - "merge": "1.2.0" - } - }, - "eslint-plugin-wpcalypso": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-wpcalypso/-/eslint-plugin-wpcalypso-3.4.1.tgz", - "integrity": "sha512-rHbCINm3qJmCgASUDKdmRiulwt06EcJTy9Hd+MpZMS4o9eFfS23Q1z1bBYVsJ4nFexvWswqcfCsgRQnFPtT5pQ==", - "requires": { - "requireindex": "1.1.0" - } - }, - "eslint-scope": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.1.tgz", - "integrity": "sha1-PWPD7f2gLgbgGkUq2IyqzHzctug=", - "dev": true, - "requires": { - "esrecurse": "4.2.0", - "estraverse": "4.2.0" - } - }, - "eslint-visitor-keys": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", - "integrity": "sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==", - "dev": true - }, - "espree": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.3.tgz", - "integrity": "sha512-Zy3tAJDORxQZLl2baguiRU1syPERAIg0L+JB2MWorORgTu/CplzvxS9WWA7Xh4+Q+eOQihNs/1o1Xep8cvCxWQ==", - "dev": true, - "requires": { - "acorn": "5.4.1", - "acorn-jsx": "3.0.1" - } - }, - "esprima": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", - "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==", - "dev": true - }, - "esquery": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.0.tgz", - "integrity": "sha1-z7qLV9f7qT8XKYqKAGoEzaE9gPo=", - "dev": true, - "requires": { - "estraverse": "4.2.0" - } - }, - "esrecurse": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.0.tgz", - "integrity": "sha1-+pVo2Y04I/mkHZHpAtyrnqblsWM=", - "dev": true, - "requires": { - "estraverse": "4.2.0", - "object-assign": "4.1.1" - } - }, - "estraverse": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", - "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", - "dev": true - }, - "esutils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=" - }, - "event-emitter": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", - "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", - "dev": true, - "requires": { - "d": "1.0.0", - "es5-ext": "0.10.38" - } - }, - "events": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", - "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=", - "dev": true - }, - "evp_bytestokey": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", - "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", - "dev": true, - "requires": { - "md5.js": "1.3.4", - "safe-buffer": "5.1.1" - } - }, - "exec-sh": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.2.1.tgz", - "integrity": "sha512-aLt95pexaugVtQerpmE51+4QfWrNc304uez7jvj6fWnN8GeEHpttB8F36n8N7uVhUMbH/1enbxQ9HImZ4w/9qg==", - "dev": true, - "requires": { - "merge": "1.2.0" - } - }, - "execa": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", - "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", - "requires": { - "cross-spawn": "5.1.0", - "get-stream": "3.0.0", - "is-stream": "1.1.0", - "npm-run-path": "2.0.2", - "p-finally": "1.0.0", - "signal-exit": "3.0.2", - "strip-eof": "1.0.0" - } - }, - "exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", - "dev": true - }, - "exit-hook": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz", - "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=", - "dev": true - }, - "expand-brackets": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", - "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", - "dev": true, - "requires": { - "is-posix-bracket": "0.1.1" - } - }, - "expand-range": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", - "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", - "dev": true, - "requires": { - "fill-range": "2.2.3" - } - }, - "expect": { - "version": "22.4.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-22.4.0.tgz", - "integrity": "sha512-Fiy862jT3qc70hwIHwwCBNISmaqBrfWKKrtqyMJ6iwZr+6KXtcnHojZFtd63TPRvRl8EQTJ+YXYy2lK6/6u+Hw==", - "dev": true, - "requires": { - "ansi-styles": "3.2.0", - "jest-diff": "22.4.0", - "jest-get-type": "22.1.0", - "jest-matcher-utils": "22.4.0", - "jest-message-util": "22.4.0", - "jest-regex-util": "22.1.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", - "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", - "dev": true, - "requires": { - "color-convert": "1.9.1" - } - } - } - }, - "expose-loader": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/expose-loader/-/expose-loader-0.7.3.tgz", - "integrity": "sha1-NfvTZZeJ5PqoH1nei36fw55GbVE=", - "dev": true - }, - "extend": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", - "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=", - "dev": true - }, - "external-editor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.1.0.tgz", - "integrity": "sha512-E44iT5QVOUJBKij4IIV3uvxuNlbKS38Tw1HiupxEIHPv9qtC2PrDYohbXV5U+1jnfIXttny8gUhj+oZvflFlzA==", - "dev": true, - "requires": { - "chardet": "0.4.2", - "iconv-lite": "0.4.19", - "tmp": "0.0.33" - }, - "dependencies": { - "tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, - "requires": { - "os-tmpdir": "1.0.2" - } - } - } - }, - "extglob": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", - "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", - "dev": true, - "requires": { - "is-extglob": "1.0.0" - } - }, - "extract-text-webpack-plugin": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/extract-text-webpack-plugin/-/extract-text-webpack-plugin-3.0.0.tgz", - "integrity": "sha1-kMqnkHvESfM1AF46x1MrQbAN5hI=", - "dev": true, - "requires": { - "async": "2.6.0", - "loader-utils": "1.1.0", - "schema-utils": "0.3.0", - "webpack-sources": "1.1.0" - } - }, - "extract-zip": { - "version": "1.6.6", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.6.6.tgz", - "integrity": "sha1-EpDt6NINCHK0Kf0/NRyhKOxe+Fw=", - "dev": true, - "requires": { - "concat-stream": "1.6.0", - "debug": "2.6.9", - "mkdirp": "0.5.0", - "yauzl": "2.4.1" - }, - "dependencies": { - "mkdirp": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.0.tgz", - "integrity": "sha1-HXMHam35hs2TROFecfzAWkyavxI=", - "dev": true, - "requires": { - "minimist": "0.0.8" - } - }, - "yauzl": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz", - "integrity": "sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=", - "dev": true, - "requires": { - "fd-slicer": "1.0.1" - } - } - } - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", - "dev": true - }, - "fast-deep-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz", - "integrity": "sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8=", - "dev": true - }, - "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true - }, - "fb-watchman": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.0.tgz", - "integrity": "sha1-VOmr99+i8mzZsWNsWIwa/AXeXVg=", - "dev": true, - "requires": { - "bser": "2.0.0" - } - }, - "fbjs": { - "version": "0.8.16", - "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.16.tgz", - "integrity": "sha1-XmdDL1UNxBtXK/VYR7ispk5TN9s=", - "requires": { - "core-js": "1.2.7", - "isomorphic-fetch": "2.2.1", - "loose-envify": "1.3.1", - "object-assign": "4.1.1", - "promise": "7.3.1", - "setimmediate": "1.0.5", - "ua-parser-js": "0.7.17" - } - }, - "fd-slicer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz", - "integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=", - "dev": true, - "requires": { - "pend": "1.2.0" - } - }, - "figures": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", - "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", - "dev": true, - "requires": { - "escape-string-regexp": "1.0.5", - "object-assign": "4.1.1" - } - }, - "file-entry-cache": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz", - "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=", - "dev": true, - "requires": { - "flat-cache": "1.3.0", - "object-assign": "4.1.1" - } - }, - "filename-regex": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", - "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=", - "dev": true - }, - "fileset": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/fileset/-/fileset-2.0.3.tgz", - "integrity": "sha1-jnVIqW08wjJ+5eZ0FocjozO7oqA=", - "dev": true, - "requires": { - "glob": "7.1.2", - "minimatch": "3.0.4" - } - }, - "fill-range": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.3.tgz", - "integrity": "sha1-ULd9/X5Gm8dJJHCWNpn+eoSFpyM=", - "dev": true, - "requires": { - "is-number": "2.1.0", - "isobject": "2.1.0", - "randomatic": "1.1.7", - "repeat-element": "1.1.2", - "repeat-string": "1.6.1" - } - }, - "find-cache-dir": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-1.0.0.tgz", - "integrity": "sha1-kojj6ePMN0hxfTnq3hfPcfww7m8=", - "dev": true, - "requires": { - "commondir": "1.0.1", - "make-dir": "1.1.0", - "pkg-dir": "2.0.0" - }, - "dependencies": { - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "requires": { - "locate-path": "2.0.0" - } - }, - "pkg-dir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", - "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", - "dev": true, - "requires": { - "find-up": "2.1.0" - } - } - } - }, - "find-parent-dir": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/find-parent-dir/-/find-parent-dir-0.3.0.tgz", - "integrity": "sha1-M8RLQpqysvBkYpnF+fcY83b/jVQ=", - "dev": true - }, - "find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", - "requires": { - "path-exists": "2.1.0", - "pinkie-promise": "2.0.1" - } - }, - "findup": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/findup/-/findup-0.1.5.tgz", - "integrity": "sha1-itkpozk7rGJ5V6fl3kYjsGsOLOs=", - "dev": true, - "requires": { - "colors": "0.6.2", - "commander": "2.1.0" - }, - "dependencies": { - "colors": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz", - "integrity": "sha1-JCP+ZnisDF2uiFLl0OW+CMmXq8w=", - "dev": true - }, - "commander": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.1.0.tgz", - "integrity": "sha1-0SG7roYNmZKj1Re6lvVliOR8Z4E=", - "dev": true - } - } - }, - "flat-cache": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.0.tgz", - "integrity": "sha1-0wMLMrOBVPTjt+nHCfSQ9++XxIE=", - "dev": true, - "requires": { - "circular-json": "0.3.3", - "del": "2.2.2", - "graceful-fs": "4.1.11", - "write": "0.2.1" - } - }, - "flatten": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/flatten/-/flatten-1.0.2.tgz", - "integrity": "sha1-2uRqnXj74lKSJYzB54CkHZXAN4I=", - "dev": true - }, - "for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", - "dev": true - }, - "for-own": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", - "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", - "dev": true, - "requires": { - "for-in": "1.0.2" - } - }, - "foreach": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", - "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=", - "dev": true - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", - "dev": true - }, - "form-data": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.1.tgz", - "integrity": "sha1-b7lPvXGIUwbXPRXMSX/kzE7NRL8=", - "dev": true, - "requires": { - "asynckit": "0.4.0", - "combined-stream": "1.0.5", - "mime-types": "2.1.17" - } - }, - "fs-extra": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.1.tgz", - "integrity": "sha1-f8DGyJV/mD9X8waiTlud3Y0N2IA=", - "dev": true, - "requires": { - "graceful-fs": "4.1.11", - "jsonfile": "3.0.1", - "universalify": "0.1.1" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "fsevents": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.1.3.tgz", - "integrity": "sha512-WIr7iDkdmdbxu/Gh6eKEZJL6KPE74/5MEsf2whTOFNxbIoIixogroLdKYqB6FDav4Wavh/lZdzzd3b2KxIXC5Q==", - "dev": true, - "optional": true, - "requires": { - "nan": "2.8.0", - "node-pre-gyp": "0.6.39" - }, - "dependencies": { - "abbrev": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.0.tgz", - "integrity": "sha1-0FVMIlZjbi9W58LlrRg/hZQo2B8=", - "dev": true, - "optional": true - }, - "ajv": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", - "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", - "dev": true, - "optional": true, - "requires": { - "co": "4.6.0", - "json-stable-stringify": "1.0.1" - } - }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "aproba": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.1.1.tgz", - "integrity": "sha1-ldNgDwdxCqDpKYxyatXs8urLq6s=", - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz", - "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=", - "dev": true, - "optional": true, - "requires": { - "delegates": "1.0.0", - "readable-stream": "2.2.9" - } - }, - "asn1": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", - "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=", - "dev": true, - "optional": true - }, - "assert-plus": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", - "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=", - "dev": true, - "optional": true - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true, - "optional": true - }, - "aws-sign2": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", - "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=", - "dev": true, - "optional": true - }, - "aws4": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", - "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=", - "dev": true, - "optional": true - }, - "balanced-match": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", - "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=", - "dev": true - }, - "bcrypt-pbkdf": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", - "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", - "dev": true, - "optional": true, - "requires": { - "tweetnacl": "0.14.5" - } - }, - "block-stream": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", - "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", - "dev": true, - "requires": { - "inherits": "2.0.3" - } - }, - "boom": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", - "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", - "dev": true, - "requires": { - "hoek": "2.16.3" - } - }, - "brace-expansion": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.7.tgz", - "integrity": "sha1-Pv/DxQ4ABTH7cg6v+A8K6O8jz1k=", - "dev": true, - "requires": { - "balanced-match": "0.4.2", - "concat-map": "0.0.1" - } - }, - "buffer-shims": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz", - "integrity": "sha1-mXjOMXOIxkmth5MCjDR37wRKi1E=", - "dev": true - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", - "dev": true, - "optional": true - }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true - }, - "combined-stream": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", - "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", - "dev": true, - "requires": { - "delayed-stream": "1.0.0" - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true - }, - "cryptiles": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", - "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", - "dev": true, - "requires": { - "boom": "2.10.1" - } - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "dev": true, - "optional": true, - "requires": { - "assert-plus": "1.0.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true, - "optional": true - } - } - }, - "debug": { - "version": "2.6.8", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", - "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=", - "dev": true, - "optional": true, - "requires": { - "ms": "2.0.0" - } - }, - "deep-extend": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.2.tgz", - "integrity": "sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8=", - "dev": true, - "optional": true - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true - }, - "delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", - "dev": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.2.tgz", - "integrity": "sha1-ca1dIEvxempsqPRQxhRUBm70YeE=", - "dev": true, - "optional": true - }, - "ecc-jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", - "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", - "dev": true, - "optional": true, - "requires": { - "jsbn": "0.1.1" - } - }, - "extend": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", - "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=", - "dev": true, - "optional": true - }, - "extsprintf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.2.tgz", - "integrity": "sha1-4QgOBljjALBilJkMxw4VAiNf1VA=", - "dev": true - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", - "dev": true, - "optional": true - }, - "form-data": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", - "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", - "dev": true, - "optional": true, - "requires": { - "asynckit": "0.4.0", - "combined-stream": "1.0.5", - "mime-types": "2.1.15" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "fstream": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", - "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", - "dev": true, - "requires": { - "graceful-fs": "4.1.11", - "inherits": "2.0.3", - "mkdirp": "0.5.1", - "rimraf": "2.6.1" - } - }, - "fstream-ignore": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/fstream-ignore/-/fstream-ignore-1.0.5.tgz", - "integrity": "sha1-nDHa40dnAY/h0kmyTa2mfQktoQU=", - "dev": true, - "optional": true, - "requires": { - "fstream": "1.0.11", - "inherits": "2.0.3", - "minimatch": "3.0.4" - } - }, - "gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "dev": true, - "optional": true, - "requires": { - "aproba": "1.1.1", - "console-control-strings": "1.1.0", - "has-unicode": "2.0.1", - "object-assign": "4.1.1", - "signal-exit": "3.0.2", - "string-width": "1.0.2", - "strip-ansi": "3.0.1", - "wide-align": "1.1.2" - } - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "dev": true, - "optional": true, - "requires": { - "assert-plus": "1.0.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true, - "optional": true - } - } - }, - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "dev": true, - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "graceful-fs": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", - "dev": true - }, - "har-schema": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz", - "integrity": "sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4=", - "dev": true, - "optional": true - }, - "har-validator": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz", - "integrity": "sha1-M0gdDxu/9gDdID11gSpqX7oALio=", - "dev": true, - "optional": true, - "requires": { - "ajv": "4.11.8", - "har-schema": "1.0.5" - } - }, - "has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", - "dev": true, - "optional": true - }, - "hawk": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", - "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", - "dev": true, - "requires": { - "boom": "2.10.1", - "cryptiles": "2.0.5", - "hoek": "2.16.3", - "sntp": "1.0.9" - } - }, - "hoek": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", - "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=", - "dev": true - }, - "http-signature": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", - "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", - "dev": true, - "optional": true, - "requires": { - "assert-plus": "0.2.0", - "jsprim": "1.4.0", - "sshpk": "1.13.0" - } - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - }, - "ini": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.4.tgz", - "integrity": "sha1-BTfLedr1m1mhpRff9wbIbsA5Fi4=", - "dev": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "1.0.1" - } - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "dev": true, - "optional": true - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", - "dev": true, - "optional": true - }, - "jodid25519": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/jodid25519/-/jodid25519-1.0.2.tgz", - "integrity": "sha1-BtSRIlUJNBlHfUJWM2BuDpB4KWc=", - "dev": true, - "optional": true, - "requires": { - "jsbn": "0.1.1" - } - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "dev": true, - "optional": true - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", - "dev": true, - "optional": true - }, - "json-stable-stringify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", - "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", - "dev": true, - "optional": true, - "requires": { - "jsonify": "0.0.0" - } - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", - "dev": true, - "optional": true - }, - "jsonify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", - "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", - "dev": true, - "optional": true - }, - "jsprim": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.0.tgz", - "integrity": "sha1-o7h+QCmNjDgFUtjMdiigu5WiKRg=", - "dev": true, - "optional": true, - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.0.2", - "json-schema": "0.2.3", - "verror": "1.3.6" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true, - "optional": true - } - } - }, - "mime-db": { - "version": "1.27.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.27.0.tgz", - "integrity": "sha1-gg9XIpa70g7CXtVeW13oaeVDbrE=", - "dev": true - }, - "mime-types": { - "version": "2.1.15", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.15.tgz", - "integrity": "sha1-pOv1BkCUVpI3uM9wBGd20J/JKu0=", - "dev": true, - "requires": { - "mime-db": "1.27.0" - } - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "1.1.7" - } - }, - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true, - "optional": true - }, - "node-pre-gyp": { - "version": "0.6.39", - "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.6.39.tgz", - "integrity": "sha512-OsJV74qxnvz/AMGgcfZoDaeDXKD3oY3QVIbBmwszTFkRisTSXbMQyn4UWzUMOtA5SVhrBZOTp0wcoSBgfMfMmQ==", - "dev": true, - "optional": true, - "requires": { - "detect-libc": "1.0.2", - "hawk": "3.1.3", - "mkdirp": "0.5.1", - "nopt": "4.0.1", - "npmlog": "4.1.0", - "rc": "1.2.1", - "request": "2.81.0", - "rimraf": "2.6.1", - "semver": "5.3.0", - "tar": "2.2.1", - "tar-pack": "3.4.0" - } - }, - "nopt": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", - "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", - "dev": true, - "optional": true, - "requires": { - "abbrev": "1.1.0", - "osenv": "0.1.4" - } - }, - "npmlog": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.0.tgz", - "integrity": "sha512-ocolIkZYZt8UveuiDS0yAkkIjid1o7lPG8cYm05yNYzBn8ykQtaiPMEGp8fY9tKdDgm8okpdKzkvu1y9hUYugA==", - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "1.1.4", - "console-control-strings": "1.1.0", - "gauge": "2.7.4", - "set-blocking": "2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true - }, - "oauth-sign": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", - "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=", - "dev": true, - "optional": true - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1.0.2" - } - }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "dev": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "dev": true, - "optional": true - }, - "osenv": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.4.tgz", - "integrity": "sha1-Qv5tWVPfBsgGS+bxdsPQWqqjRkQ=", - "dev": true, - "optional": true, - "requires": { - "os-homedir": "1.0.2", - "os-tmpdir": "1.0.2" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - }, - "performance-now": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", - "integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=", - "dev": true, - "optional": true - }, - "process-nextick-args": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", - "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", - "dev": true - }, - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", - "dev": true, - "optional": true - }, - "qs": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", - "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=", - "dev": true, - "optional": true - }, - "rc": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.1.tgz", - "integrity": "sha1-LgPo5C7kULjLPc5lvhv4l04d/ZU=", - "dev": true, - "optional": true, - "requires": { - "deep-extend": "0.4.2", - "ini": "1.3.4", - "minimist": "1.2.0", - "strip-json-comments": "2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.2.9", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.9.tgz", - "integrity": "sha1-z3jsb0ptHrQ9JkiMrJfwQudLf8g=", - "dev": true, - "requires": { - "buffer-shims": "1.0.0", - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "1.0.7", - "string_decoder": "1.0.1", - "util-deprecate": "1.0.2" - } - }, - "request": { - "version": "2.81.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.81.0.tgz", - "integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=", - "dev": true, - "optional": true, - "requires": { - "aws-sign2": "0.6.0", - "aws4": "1.6.0", - "caseless": "0.12.0", - "combined-stream": "1.0.5", - "extend": "3.0.1", - "forever-agent": "0.6.1", - "form-data": "2.1.4", - "har-validator": "4.2.1", - "hawk": "3.1.3", - "http-signature": "1.1.1", - "is-typedarray": "1.0.0", - "isstream": "0.1.2", - "json-stringify-safe": "5.0.1", - "mime-types": "2.1.15", - "oauth-sign": "0.8.2", - "performance-now": "0.2.0", - "qs": "6.4.0", - "safe-buffer": "5.0.1", - "stringstream": "0.0.5", - "tough-cookie": "2.3.2", - "tunnel-agent": "0.6.0", - "uuid": "3.0.1" - } - }, - "rimraf": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.1.tgz", - "integrity": "sha1-wjOOxkPfeht/5cVPqG9XQopV8z0=", - "dev": true, - "requires": { - "glob": "7.1.2" - } - }, - "safe-buffer": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.0.1.tgz", - "integrity": "sha1-0mPKVGls2KMGtcplUekt5XkY++c=", - "dev": true - }, - "semver": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", - "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", - "dev": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", - "dev": true, - "optional": true - }, - "sntp": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", - "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", - "dev": true, - "requires": { - "hoek": "2.16.3" - } - }, - "sshpk": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.0.tgz", - "integrity": "sha1-/yo+T9BEl1Vf7Zezmg/YL6+zozw=", - "dev": true, - "optional": true, - "requires": { - "asn1": "0.2.3", - "assert-plus": "1.0.0", - "bcrypt-pbkdf": "1.0.1", - "dashdash": "1.14.1", - "ecc-jsbn": "0.1.1", - "getpass": "0.1.7", - "jodid25519": "1.0.2", - "jsbn": "0.1.1", - "tweetnacl": "0.14.5" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true, - "optional": true - } - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - }, - "string_decoder": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.1.tgz", - "integrity": "sha1-YuIA8DmVWmgQ2N8KM//A8BNmLZg=", - "dev": true, - "requires": { - "safe-buffer": "5.0.1" - } - }, - "stringstream": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", - "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=", - "dev": true, - "optional": true - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "2.1.1" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "dev": true, - "optional": true - }, - "tar": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", - "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", - "dev": true, - "requires": { - "block-stream": "0.0.9", - "fstream": "1.0.11", - "inherits": "2.0.3" - } - }, - "tar-pack": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/tar-pack/-/tar-pack-3.4.0.tgz", - "integrity": "sha1-I74tf2cagzk3bL2wuP4/3r8xeYQ=", - "dev": true, - "optional": true, - "requires": { - "debug": "2.6.8", - "fstream": "1.0.11", - "fstream-ignore": "1.0.5", - "once": "1.4.0", - "readable-stream": "2.2.9", - "rimraf": "2.6.1", - "tar": "2.2.1", - "uid-number": "0.0.6" - } - }, - "tough-cookie": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.2.tgz", - "integrity": "sha1-8IH3bkyFcg5sN6X6ztc3FQ2EByo=", - "dev": true, - "optional": true, - "requires": { - "punycode": "1.4.1" - } - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "dev": true, - "optional": true - }, - "uid-number": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz", - "integrity": "sha1-DqEOgDXo61uOREnwbaHHMGY7qoE=", - "dev": true, - "optional": true - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true - }, - "uuid": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.0.1.tgz", - "integrity": "sha1-ZUS7ot/ajBzxfmKaOjBeK7H+5sE=", - "dev": true, - "optional": true - }, - "verror": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz", - "integrity": "sha1-z/XfEpRtKX0rqu+qJoniW+AcAFw=", - "dev": true, - "optional": true, - "requires": { - "extsprintf": "1.0.2" - } - }, - "wide-align": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz", - "integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==", - "dev": true, - "optional": true, - "requires": { - "string-width": "1.0.2" - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - } - } - }, - "fstream": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", - "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", - "dev": true, - "requires": { - "graceful-fs": "4.1.11", - "inherits": "2.0.3", - "mkdirp": "0.5.1", - "rimraf": "2.6.2" - } - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "function.prototype.name": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.0.tgz", - "integrity": "sha512-Bs0VRrTz4ghD8pTmbJQD1mZ8A/mN0ur/jGz+A6FBxPDUPkm1tNfF6bhTYPA7i7aF4lZJVr+OXTNNrnnIl58Wfg==", - "dev": true, - "requires": { - "define-properties": "1.1.2", - "function-bind": "1.1.1", - "is-callable": "1.1.3" - } - }, - "functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", - "dev": true - }, - "gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "dev": true, - "requires": { - "aproba": "1.2.0", - "console-control-strings": "1.1.0", - "has-unicode": "2.0.1", - "object-assign": "4.1.1", - "signal-exit": "3.0.2", - "string-width": "1.0.2", - "strip-ansi": "3.0.1", - "wide-align": "1.1.2" - }, - "dependencies": { - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - } - } - }, - "gaze": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.2.tgz", - "integrity": "sha1-hHIkZ3rbiHDWeSV+0ziP22HkAQU=", - "dev": true, - "requires": { - "globule": "1.2.0" - } - }, - "generate-function": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", - "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=", - "dev": true - }, - "generate-object-property": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", - "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", - "dev": true, - "requires": { - "is-property": "1.0.2" - } - }, - "get-caller-file": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz", - "integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U=" - }, - "get-own-enumerable-property-symbols": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-2.0.1.tgz", - "integrity": "sha512-TtY/sbOemiMKPRUDDanGCSgBYe7Mf0vbRsWnBZ+9yghpZ1MvcpSpuZFjHdEeY/LZjZy0vdLjS77L6HosisFiug==", - "dev": true - }, - "get-stdin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", - "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", - "dev": true - }, - "get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" - }, - "getos": { - "version": "2.8.4", - "resolved": "https://registry.npmjs.org/getos/-/getos-2.8.4.tgz", - "integrity": "sha1-e4YD02GcKOOMsP56T2PDrLgNUWM=", - "dev": true, - "requires": { - "async": "2.1.4" - }, - "dependencies": { - "async": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/async/-/async-2.1.4.tgz", - "integrity": "sha1-LSFgx3iAMuTdbL4lAvH5osj2zeQ=", - "dev": true, - "requires": { - "lodash": "4.17.4" - } - } - } - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "dev": true, - "requires": { - "assert-plus": "1.0.0" - } - }, - "gettext-parser": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/gettext-parser/-/gettext-parser-1.3.0.tgz", - "integrity": "sha512-iloxjcw+uTPnQ8DrGICWtqkHNgk3mAiDI77pLmXQCnhM+BxFQXstzTA4zj3EpIYMysRQnnNzHyHzBUEazz80Sw==", - "dev": true, - "requires": { - "encoding": "0.1.12", - "safe-buffer": "5.1.1" - } - }, - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "dev": true, - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "glob-base": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", - "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", - "dev": true, - "requires": { - "glob-parent": "2.0.0", - "is-glob": "2.0.1" - } - }, - "glob-parent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", - "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", - "dev": true, - "requires": { - "is-glob": "2.0.1" - } - }, - "global-dirs": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", - "integrity": "sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=", - "dev": true, - "requires": { - "ini": "1.3.5" - } - }, - "globals": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", - "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", - "dev": true - }, - "globby": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", - "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", - "dev": true, - "requires": { - "array-union": "1.0.2", - "arrify": "1.0.1", - "glob": "7.1.2", - "object-assign": "4.1.1", - "pify": "2.3.0", - "pinkie-promise": "2.0.1" - } - }, - "globule": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.0.tgz", - "integrity": "sha1-HcScaCLdnoovoAuiopUAboZkvQk=", - "dev": true, - "requires": { - "glob": "7.1.2", - "lodash": "4.17.4", - "minimatch": "3.0.4" - } - }, - "good-listener": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz", - "integrity": "sha1-1TswzfkxPf+33JoNR3CWqm0UXFA=", - "requires": { - "delegate": "3.2.0" - } - }, - "graceful-fs": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" - }, - "growly": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", - "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", - "dev": true - }, - "handlebars": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.11.tgz", - "integrity": "sha1-Ywo13+ApS8KB7a5v/F0yn8eYLcw=", - "dev": true, - "requires": { - "async": "1.5.2", - "optimist": "0.6.1", - "source-map": "0.4.4", - "uglify-js": "2.8.29" - }, - "dependencies": { - "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", - "dev": true - }, - "source-map": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", - "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", - "dev": true, - "requires": { - "amdefine": "1.0.1" - } - } - } - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", - "dev": true - }, - "har-validator": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", - "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", - "dev": true, - "requires": { - "ajv": "5.5.2", - "har-schema": "2.0.0" - } - }, - "has": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.1.tgz", - "integrity": "sha1-hGFzP1OLCDfJNh45qauelwTcLyg=", - "requires": { - "function-bind": "1.1.1" - } - }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "dev": true, - "requires": { - "ansi-regex": "2.1.1" - } - }, - "has-flag": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", - "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", - "dev": true - }, - "has-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", - "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", - "dev": true - }, - "has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", - "dev": true - }, - "hash-base": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-2.0.2.tgz", - "integrity": "sha1-ZuodhW206KVHDK32/OI65SRO8uE=", - "dev": true, - "requires": { - "inherits": "2.0.3" - } - }, - "hash.js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.3.tgz", - "integrity": "sha512-/UETyP0W22QILqS+6HowevwhEFJ3MBJnwTf75Qob9Wz9t0DPuisL8kW8YZMK62dHAKE1c1p+gY1TtOLY+USEHA==", - "dev": true, - "requires": { - "inherits": "2.0.3", - "minimalistic-assert": "1.0.0" - } - }, - "hawk": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", - "integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==", - "dev": true, - "requires": { - "boom": "4.3.1", - "cryptiles": "3.1.2", - "hoek": "4.2.0", - "sntp": "2.1.0" - } - }, - "hmac-drbg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", - "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", - "dev": true, - "requires": { - "hash.js": "1.1.3", - "minimalistic-assert": "1.0.0", - "minimalistic-crypto-utils": "1.0.1" - } - }, - "hoek": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz", - "integrity": "sha512-v0XCLxICi9nPfYrS9RL8HbYnXi9obYAeLbSP00BmnZwCK9+Ih9WOjoZ8YoHCoav2csqn4FOz4Orldsy2dmDwmQ==", - "dev": true - }, - "hoist-non-react-statics": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz", - "integrity": "sha1-qkSM8JhtVcxAdzsXF0t90GbLfPs=" - }, - "home-or-tmp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", - "integrity": "sha1-42w/LSyufXRqhX440Y1fMqeILbg=", - "dev": true, - "requires": { - "os-homedir": "1.0.2", - "os-tmpdir": "1.0.2" - } - }, - "hosted-git-info": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.5.0.tgz", - "integrity": "sha512-pNgbURSuab90KbTqvRPsseaTxOJCZBD0a7t+haSN33piP9cCM4l0CqdzAif2hUqm716UovKB2ROmiabGAKVXyg==" - }, - "hpq": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/hpq/-/hpq-1.2.0.tgz", - "integrity": "sha1-nGGLI5YqLXPW6Cugh0l4vLNov6I=" - }, - "html-comment-regex": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/html-comment-regex/-/html-comment-regex-1.1.1.tgz", - "integrity": "sha1-ZouTd26q5V696POtRkswekljYl4=", - "dev": true - }, - "html-encoding-sniffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz", - "integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==", - "dev": true, - "requires": { - "whatwg-encoding": "1.0.3" - } - }, - "htmlparser2": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.9.2.tgz", - "integrity": "sha1-G9+HrMoPP55T+k/M6w9LTLsAszg=", - "dev": true, - "requires": { - "domelementtype": "1.3.0", - "domhandler": "2.4.1", - "domutils": "1.5.1", - "entities": "1.1.1", - "inherits": "2.0.3", - "readable-stream": "2.3.3" - } - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "dev": true, - "requires": { - "assert-plus": "1.0.0", - "jsprim": "1.4.1", - "sshpk": "1.13.1" - } - }, - "https-browserify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", - "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", - "dev": true - }, - "husky": { - "version": "0.14.3", - "resolved": "https://registry.npmjs.org/husky/-/husky-0.14.3.tgz", - "integrity": "sha512-e21wivqHpstpoiWA/Yi8eFti8E+sQDSS53cpJsPptPs295QTOQR0ZwnHo2TXy1XOpZFD9rPOd3NpmqTK6uMLJA==", - "dev": true, - "requires": { - "is-ci": "1.1.0", - "normalize-path": "1.0.0", - "strip-indent": "2.0.0" - }, - "dependencies": { - "normalize-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-1.0.0.tgz", - "integrity": "sha1-MtDkcvkf80VwHBWoMRAY07CpA3k=", - "dev": true - }, - "strip-indent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz", - "integrity": "sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=", - "dev": true - } - } - }, - "iconv-lite": { - "version": "0.4.19", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", - "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" - }, - "ieee754": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz", - "integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=", - "dev": true - }, - "ignore": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.7.tgz", - "integrity": "sha512-YGG3ejvBNHRqu0559EOxxNFihD0AjpvHlC/pdGKd3X3ofe+CoJkYazwNJYTNebqpPKN+VVQbh4ZFn1DivMNuHA==" - }, - "import-local": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-1.0.0.tgz", - "integrity": "sha512-vAaZHieK9qjGo58agRBg+bhHX3hoTZU/Oa3GESWLz7t1U62fk63aHuDJJEteXoDeTCcPmUT+z38gkHPZkkmpmQ==", - "dev": true, - "requires": { - "pkg-dir": "2.0.0", - "resolve-cwd": "2.0.0" - }, - "dependencies": { - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "requires": { - "locate-path": "2.0.0" - } - }, - "pkg-dir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", - "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", - "dev": true, - "requires": { - "find-up": "2.1.0" - } - } - } - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true - }, - "in-publish": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.0.tgz", - "integrity": "sha1-4g/146KvwmkDILbcVSaCqcf631E=", - "dev": true - }, - "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.1" - } - }, - "indexes-of": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", - "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=", - "dev": true - }, - "indexof": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", - "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" - }, - "inquirer": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.3.0.tgz", - "integrity": "sha512-h+xtnyk4EwKvFWHrUYsWErEVR+igKtLdchu+o0Z1RL7VU/jVMFbYir2bp6bAj8efFNxWqHX0dIss6fJQ+/+qeQ==", - "dev": true, - "requires": { - "ansi-escapes": "3.0.0", - "chalk": "2.3.0", - "cli-cursor": "2.1.0", - "cli-width": "2.2.0", - "external-editor": "2.1.0", - "figures": "2.0.0", - "lodash": "4.17.4", - "mute-stream": "0.0.7", - "run-async": "2.3.0", - "rx-lite": "4.0.8", - "rx-lite-aggregates": "4.0.8", - "string-width": "2.1.1", - "strip-ansi": "4.0.0", - "through": "2.3.8" - }, - "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 - }, - "ansi-styles": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", - "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", - "dev": true, - "requires": { - "color-convert": "1.9.1" - } - }, - "chalk": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", - "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", - "dev": true, - "requires": { - "ansi-styles": "3.2.0", - "escape-string-regexp": "1.0.5", - "supports-color": "4.5.0" - } - }, - "cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", - "dev": true, - "requires": { - "restore-cursor": "2.0.0" - } - }, - "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" - } - }, - "onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", - "dev": true, - "requires": { - "mimic-fn": "1.2.0" - } - }, - "restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", - "dev": true, - "requires": { - "onetime": "2.0.1", - "signal-exit": "3.0.2" - } - }, - "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" - } - }, - "supports-color": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", - "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", - "dev": true, - "requires": { - "has-flag": "2.0.0" - } - } - } - }, - "interpret": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz", - "integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=", - "dev": true - }, - "invariant": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.2.tgz", - "integrity": "sha1-nh9WrArNtr8wMwbzOL47IErmA2A=", - "requires": { - "loose-envify": "1.3.1" - } - }, - "invert-kv": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", - "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=" - }, - "is-absolute-url": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-2.1.0.tgz", - "integrity": "sha1-UFMN+4T8yap9vnhS6Do3uTufKqY=", - "dev": true - }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" - }, - "is-binary-path": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", - "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", - "dev": true, - "requires": { - "binary-extensions": "1.11.0" - } - }, - "is-boolean-object": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.0.0.tgz", - "integrity": "sha1-mPiygDBoQhmpXzdc+9iM40Bd/5M=", - "dev": true - }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "is-builtin-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", - "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", - "requires": { - "builtin-modules": "1.1.1" - } - }, - "is-callable": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.3.tgz", - "integrity": "sha1-hut1OSgF3cM69xySoO7fdO52BLI=", - "dev": true - }, - "is-ci": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.1.0.tgz", - "integrity": "sha512-c7TnwxLePuqIlxHgr7xtxzycJPegNHFuIrBkwbf8hc58//+Op1CqFkyS+xnIMkwn9UsJIwc174BIjkyBmSpjKg==", - "dev": true, - "requires": { - "ci-info": "1.1.2" - } - }, - "is-date-object": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", - "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", - "dev": true - }, - "is-directory": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", - "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=", - "dev": true - }, - "is-dotfile": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", - "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=", - "dev": true - }, - "is-equal-shallow": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", - "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", - "requires": { - "is-primitive": "2.0.0" - } - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", - "dev": true - }, - "is-extglob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", - "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", - "dev": true - }, - "is-finite": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", - "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", - "dev": true, - "requires": { - "number-is-nan": "1.0.1" - } - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "requires": { - "number-is-nan": "1.0.1" - } - }, - "is-generator-fn": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-1.0.0.tgz", - "integrity": "sha1-lp1J4bszKfa7fwkIm+JleLLd1Go=", - "dev": true - }, - "is-glob": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", - "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", - "dev": true, - "requires": { - "is-extglob": "1.0.0" - } - }, - "is-installed-globally": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.1.0.tgz", - "integrity": "sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA=", - "dev": true, - "requires": { - "global-dirs": "0.1.1", - "is-path-inside": "1.0.1" - } - }, - "is-my-json-valid": { - "version": "2.17.1", - "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.17.1.tgz", - "integrity": "sha512-Q2khNw+oBlWuaYvEEHtKSw/pCxD2L5Rc1C+UQme9X6JdRDh7m5D7HkozA0qa3DUkQ6VzCnEm8mVIQPyIRkI5sQ==", - "dev": true, - "requires": { - "generate-function": "2.0.0", - "generate-object-property": "1.2.0", - "jsonpointer": "4.0.1", - "xtend": "4.0.1" - } - }, - "is-number": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", - "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", - "dev": true, - "requires": { - "kind-of": "3.2.2" - } - }, - "is-number-object": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.3.tgz", - "integrity": "sha1-8mWrian0RQNO9q/xWo8AsA9VF5k=", - "dev": true - }, - "is-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", - "dev": true - }, - "is-observable": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/is-observable/-/is-observable-0.2.0.tgz", - "integrity": "sha1-s2ExHYPG5dcmyr9eJQsCNxBvWuI=", - "dev": true, - "requires": { - "symbol-observable": "0.2.4" - }, - "dependencies": { - "symbol-observable": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-0.2.4.tgz", - "integrity": "sha1-lag9smGG1q9+ehjb2XYKL4bQj0A=", - "dev": true - } - } - }, - "is-path-cwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", - "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", - "dev": true - }, - "is-path-in-cwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz", - "integrity": "sha1-ZHdYK4IU1gI0YJRWcAO+ip6sBNw=", - "dev": true, - "requires": { - "is-path-inside": "1.0.1" - } - }, - "is-path-inside": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", - "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", - "dev": true, - "requires": { - "path-is-inside": "1.0.2" - } - }, - "is-plain-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", - "dev": true - }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "requires": { - "isobject": "3.0.1" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "is-posix-bracket": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", - "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=", - "dev": true - }, - "is-primitive": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", - "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=" - }, - "is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", - "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", - "dev": true - }, - "is-property": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", - "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", - "dev": true - }, - "is-regex": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", - "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", - "dev": true, - "requires": { - "has": "1.0.1" - } - }, - "is-regexp": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", - "integrity": "sha1-/S2INUXEa6xaYz57mgnof6LLUGk=", - "dev": true - }, - "is-resolvable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", - "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==", - "dev": true - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" - }, - "is-string": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.4.tgz", - "integrity": "sha1-zDqbaYV9Yh6WNyWiTK7shzuCbmQ=", - "dev": true - }, - "is-subset": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz", - "integrity": "sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY=", - "dev": true - }, - "is-svg": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-svg/-/is-svg-2.1.0.tgz", - "integrity": "sha1-z2EJDaDZ77yrhyLeum8DIgjbsOk=", - "dev": true, - "requires": { - "html-comment-regex": "1.1.1" - } - }, - "is-symbol": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.1.tgz", - "integrity": "sha1-PMWfAAJRlLarLjjbrmaJJWtmBXI=", - "dev": true - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "dev": true - }, - "is-utf8": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", - "dev": true - }, - "is-windows": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.1.tgz", - "integrity": "sha1-MQ23D3QtJZoWo2kgK1GvhCMzENk=", - "dev": true - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" - }, - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "dev": true, - "requires": { - "isarray": "1.0.0" - } - }, - "isomorphic-fetch": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", - "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=", - "requires": { - "node-fetch": "1.7.3", - "whatwg-fetch": "2.0.3" - } - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", - "dev": true - }, - "istanbul-api": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/istanbul-api/-/istanbul-api-1.2.2.tgz", - "integrity": "sha512-kH5YRdqdbs5hiH4/Rr1Q0cSAGgjh3jTtg8vu9NLebBAoK3adVO4jk81J+TYOkTr2+Q4NLeb1ACvmEt65iG/Vbw==", - "dev": true, - "requires": { - "async": "2.6.0", - "fileset": "2.0.3", - "istanbul-lib-coverage": "1.1.2", - "istanbul-lib-hook": "1.1.0", - "istanbul-lib-instrument": "1.9.2", - "istanbul-lib-report": "1.1.3", - "istanbul-lib-source-maps": "1.2.3", - "istanbul-reports": "1.1.4", - "js-yaml": "3.10.0", - "mkdirp": "0.5.1", - "once": "1.4.0" - } - }, - "istanbul-lib-coverage": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-1.1.2.tgz", - "integrity": "sha512-tZYA0v5A7qBSsOzcebJJ/z3lk3oSzH62puG78DbBA1+zupipX2CakDyiPV3pOb8He+jBwVimuwB0dTnh38hX0w==", - "dev": true - }, - "istanbul-lib-hook": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-1.1.0.tgz", - "integrity": "sha512-U3qEgwVDUerZ0bt8cfl3dSP3S6opBoOtk3ROO5f2EfBr/SRiD9FQqzwaZBqFORu8W7O0EXpai+k7kxHK13beRg==", - "dev": true, - "requires": { - "append-transform": "0.4.0" - } - }, - "istanbul-lib-instrument": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-1.9.2.tgz", - "integrity": "sha512-nz8t4HQ2206a/3AXi+NHFWEa844DMpPsgbcUteJbt1j8LX1xg56H9rOMnhvcvVvPbW60qAIyrSk44H8ZDqaSSA==", - "dev": true, - "requires": { - "babel-generator": "6.26.1", - "babel-template": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0", - "babylon": "6.18.0", - "istanbul-lib-coverage": "1.1.2", - "semver": "5.3.0" - } - }, - "istanbul-lib-report": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-1.1.3.tgz", - "integrity": "sha512-D4jVbMDtT2dPmloPJS/rmeP626N5Pr3Rp+SovrPn1+zPChGHcggd/0sL29jnbm4oK9W0wHjCRsdch9oLd7cm6g==", - "dev": true, - "requires": { - "istanbul-lib-coverage": "1.1.2", - "mkdirp": "0.5.1", - "path-parse": "1.0.5", - "supports-color": "3.2.3" - }, - "dependencies": { - "has-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", - "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", - "dev": true - }, - "supports-color": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", - "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", - "dev": true, - "requires": { - "has-flag": "1.0.0" - } - } - } - }, - "istanbul-lib-source-maps": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.3.tgz", - "integrity": "sha512-fDa0hwU/5sDXwAklXgAoCJCOsFsBplVQ6WBldz5UwaqOzmDhUK4nfuR7/G//G2lERlblUNJB8P6e8cXq3a7MlA==", - "dev": true, - "requires": { - "debug": "3.1.0", - "istanbul-lib-coverage": "1.1.2", - "mkdirp": "0.5.1", - "rimraf": "2.6.2", - "source-map": "0.5.7" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - } - } - }, - "istanbul-reports": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-1.1.4.tgz", - "integrity": "sha512-DfSTVOTkuO+kRmbO8Gk650Wqm1WRGr6lrdi2EwDK1vxpS71vdlLd613EpzOKdIFioB5f/scJTjeWBnvd1FWejg==", - "dev": true, - "requires": { - "handlebars": "4.0.11" - } - }, - "jed": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/jed/-/jed-1.1.1.tgz", - "integrity": "sha1-elSbvZ/+FYWwzQoZHiAwVb7ldLQ=" - }, - "jest": { - "version": "22.4.0", - "resolved": "https://registry.npmjs.org/jest/-/jest-22.4.0.tgz", - "integrity": "sha512-eze1JLbBDkrbZMnE6xIlBxHkqPAmuHbz4GQbED8qRVtnpea3o6Tt/Dc3SBs3qnlTo7svema8Ho5bqLfdHyabyQ==", - "dev": true, - "requires": { - "import-local": "1.0.0", - "jest-cli": "22.4.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 - }, - "ansi-styles": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", - "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", - "dev": true, - "requires": { - "color-convert": "1.9.1" - } - }, - "chalk": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.1.tgz", - "integrity": "sha512-QUU4ofkDoMIVO7hcx1iPTISs88wsO8jA92RQIm4JAwZvFGGAV2hSAA1NX7oVj2Ej2Q6NDTcRDjPTFrMCRZoJ6g==", - "dev": true, - "requires": { - "ansi-styles": "3.2.0", - "escape-string-regexp": "1.0.5", - "supports-color": "5.2.0" - } - }, - "cliui": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.0.0.tgz", - "integrity": "sha512-nY3W5Gu2racvdDk//ELReY+dHjb9PlIcVDFXP72nVIhq2Gy3LuVXYwJoPVudwQnv1shtohpgkdCKT2YaKY0CKw==", - "dev": true, - "requires": { - "string-width": "2.1.1", - "strip-ansi": "4.0.0", - "wrap-ansi": "2.1.0" - } - }, - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "requires": { - "locate-path": "2.0.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "jest-cli": { - "version": "22.4.0", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-22.4.0.tgz", - "integrity": "sha512-0JlBb/PvHGQZR2I9GZwsycHgWHhriBmvBWPaaPYUT186oiIIDY4ezDxFOFt2Ts0yNTRg3iY9mTyHsfWbT5VRWA==", - "dev": true, - "requires": { - "ansi-escapes": "3.0.0", - "chalk": "2.3.1", - "exit": "0.1.2", - "glob": "7.1.2", - "graceful-fs": "4.1.11", - "import-local": "1.0.0", - "is-ci": "1.1.0", - "istanbul-api": "1.2.2", - "istanbul-lib-coverage": "1.1.2", - "istanbul-lib-instrument": "1.9.2", - "istanbul-lib-source-maps": "1.2.3", - "jest-changed-files": "22.2.0", - "jest-config": "22.4.0", - "jest-environment-jsdom": "22.4.0", - "jest-get-type": "22.1.0", - "jest-haste-map": "22.4.0", - "jest-message-util": "22.4.0", - "jest-regex-util": "22.1.0", - "jest-resolve-dependencies": "22.1.0", - "jest-runner": "22.4.0", - "jest-runtime": "22.4.0", - "jest-snapshot": "22.4.0", - "jest-util": "22.4.0", - "jest-validate": "22.4.0", - "jest-worker": "22.2.2", - "micromatch": "2.3.11", - "node-notifier": "5.2.1", - "realpath-native": "1.0.0", - "rimraf": "2.6.2", - "slash": "1.0.0", - "string-length": "2.0.0", - "strip-ansi": "4.0.0", - "which": "1.3.0", - "yargs": "10.1.2" - } - }, - "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" - } - }, - "supports-color": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.2.0.tgz", - "integrity": "sha512-F39vS48la4YvTZUPVeTqsjsFNrvcMwrV3RLZINsmHo+7djCvuUzSIeXOnZ5hmjef4bajL1dNccN+tg5XAliO5Q==", - "dev": true, - "requires": { - "has-flag": "3.0.0" - } - }, - "yargs": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-10.1.2.tgz", - "integrity": "sha512-ivSoxqBGYOqQVruxD35+EyCFDYNEFL/Uo6FcOnz+9xZdZzK0Zzw4r4KhbrME1Oo2gOggwJod2MnsdamSG7H9ig==", - "dev": true, - "requires": { - "cliui": "4.0.0", - "decamelize": "1.2.0", - "find-up": "2.1.0", - "get-caller-file": "1.0.2", - "os-locale": "2.1.0", - "require-directory": "2.1.1", - "require-main-filename": "1.0.1", - "set-blocking": "2.0.0", - "string-width": "2.1.1", - "which-module": "2.0.0", - "y18n": "3.2.1", - "yargs-parser": "8.1.0" - } - }, - "yargs-parser": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-8.1.0.tgz", - "integrity": "sha512-yP+6QqN8BmrgW2ggLtTbdrOyBNSI7zBa4IykmiV5R1wl1JWNxQvWhMfMdmzIYtKU7oP3OOInY/tl2ov3BDjnJQ==", - "dev": true, - "requires": { - "camelcase": "4.1.0" - } - } - } - }, - "jest-changed-files": { - "version": "22.2.0", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-22.2.0.tgz", - "integrity": "sha512-SzqOvoPMrXB0NPvDrSPeKETpoUNCtNDOsFbCzAGWxqWVvNyrIMLpUjVExT3u3LfdVrENlrNGCfh5YoFd8+ZeXg==", - "dev": true, - "requires": { - "throat": "4.1.0" - } - }, - "jest-config": { - "version": "22.4.0", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-22.4.0.tgz", - "integrity": "sha512-hZs8qHjCybOpqni0Kwt40eAavYN/3KnJJwYxSJsBRedJ98IgGSiI18SjybCSccKayA7eHgw1A+dLkHcfI4LItQ==", - "dev": true, - "requires": { - "chalk": "2.3.1", - "glob": "7.1.2", - "jest-environment-jsdom": "22.4.0", - "jest-environment-node": "22.4.0", - "jest-get-type": "22.1.0", - "jest-jasmine2": "22.4.0", - "jest-regex-util": "22.1.0", - "jest-resolve": "22.4.0", - "jest-util": "22.4.0", - "jest-validate": "22.4.0", - "pretty-format": "22.4.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", - "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", - "dev": true, - "requires": { - "color-convert": "1.9.1" - } - }, - "chalk": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.1.tgz", - "integrity": "sha512-QUU4ofkDoMIVO7hcx1iPTISs88wsO8jA92RQIm4JAwZvFGGAV2hSAA1NX7oVj2Ej2Q6NDTcRDjPTFrMCRZoJ6g==", - "dev": true, - "requires": { - "ansi-styles": "3.2.0", - "escape-string-regexp": "1.0.5", - "supports-color": "5.2.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "supports-color": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.2.0.tgz", - "integrity": "sha512-F39vS48la4YvTZUPVeTqsjsFNrvcMwrV3RLZINsmHo+7djCvuUzSIeXOnZ5hmjef4bajL1dNccN+tg5XAliO5Q==", - "dev": true, - "requires": { - "has-flag": "3.0.0" - } - } - } - }, - "jest-diff": { - "version": "22.4.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-22.4.0.tgz", - "integrity": "sha512-+/t20WmnkOkB8MOaGaPziI8zWKxquMvYw4Ub+wOzi7AUhmpFXz43buWSxVoZo4J5RnCozpGbX3/FssjJ5KV9Nw==", - "dev": true, - "requires": { - "chalk": "2.3.1", - "diff": "3.4.0", - "jest-get-type": "22.1.0", - "pretty-format": "22.4.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", - "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", - "dev": true, - "requires": { - "color-convert": "1.9.1" - } - }, - "chalk": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.1.tgz", - "integrity": "sha512-QUU4ofkDoMIVO7hcx1iPTISs88wsO8jA92RQIm4JAwZvFGGAV2hSAA1NX7oVj2Ej2Q6NDTcRDjPTFrMCRZoJ6g==", - "dev": true, - "requires": { - "ansi-styles": "3.2.0", - "escape-string-regexp": "1.0.5", - "supports-color": "5.2.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "supports-color": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.2.0.tgz", - "integrity": "sha512-F39vS48la4YvTZUPVeTqsjsFNrvcMwrV3RLZINsmHo+7djCvuUzSIeXOnZ5hmjef4bajL1dNccN+tg5XAliO5Q==", - "dev": true, - "requires": { - "has-flag": "3.0.0" - } - } - } - }, - "jest-docblock": { - "version": "22.4.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-22.4.0.tgz", - "integrity": "sha512-lDY7GZ+/CJb02oULYLBDj7Hs5shBhVpDYpIm8LUyqw9X2J22QRsM19gmGQwIFqGSJmpc/LRrSYudeSrG510xlQ==", - "dev": true, - "requires": { - "detect-newline": "2.1.0" - } - }, - "jest-environment-jsdom": { - "version": "22.4.0", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-22.4.0.tgz", - "integrity": "sha512-SAUCte4KFLaD2YhYwHFVEI2GkR4BHqHJsnbFgmQMGgHnZ2CfjSZE8Bnb+jlarbxIG4GXl31+2e9rjBpzbY9gKQ==", - "dev": true, - "requires": { - "jest-mock": "22.2.0", - "jest-util": "22.4.0", - "jsdom": "11.6.2" - } - }, - "jest-environment-node": { - "version": "22.4.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-22.4.0.tgz", - "integrity": "sha512-ihSKa2MU5jkAhmRJ17FU4nisbbfW6spvl6Jtwmm5W9kmTVa2sa9UoHWbOWAb7HXuLi3PGGjzTfEt5o3uIzisnQ==", - "dev": true, - "requires": { - "jest-mock": "22.2.0", - "jest-util": "22.4.0" - } - }, - "jest-enzyme": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/jest-enzyme/-/jest-enzyme-4.2.0.tgz", - "integrity": "sha512-nna99NnU6sDbWqVX0153c81RUuxI/spTgw4Xobh049NcKihu0OAtAawbuSzZUnlCqdZOoXlKMudfjUPm0sCTsg==", - "dev": true, - "requires": { - "enzyme-matchers": "4.2.0", - "enzyme-to-json": "3.3.1" - } - }, - "jest-get-type": { - "version": "22.1.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-22.1.0.tgz", - "integrity": "sha512-nD97IVOlNP6fjIN5i7j5XRH+hFsHL7VlauBbzRvueaaUe70uohrkz7pL/N8lx/IAwZRTJ//wOdVgh85OgM7g3w==", - "dev": true - }, - "jest-haste-map": { - "version": "22.4.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-22.4.0.tgz", - "integrity": "sha512-znYomZ+GaRcuFLQz7hmwQOfLkHY2Y2Aoyd29ZcXLrwBEWts5U/c7lFsqo54XUJUlMhrM5M2IOaAUWjZ1CRqAOQ==", - "dev": true, - "requires": { - "fb-watchman": "2.0.0", - "graceful-fs": "4.1.11", - "jest-docblock": "22.4.0", - "jest-serializer": "22.4.0", - "jest-worker": "22.2.2", - "micromatch": "2.3.11", - "sane": "2.4.1" - } - }, - "jest-jasmine2": { - "version": "22.4.0", - "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-22.4.0.tgz", - "integrity": "sha512-oL7bNLfEL9jPVjmiwqQuwrAJ/5ddmKHSpns0kCpAmv1uQ47Q5aC9zBTXZbDWP5GVbVHj2hbYtNbkwTiXJr0e8w==", - "dev": true, - "requires": { - "callsites": "2.0.0", - "chalk": "2.3.1", - "co": "4.6.0", - "expect": "22.4.0", - "graceful-fs": "4.1.11", - "is-generator-fn": "1.0.0", - "jest-diff": "22.4.0", - "jest-matcher-utils": "22.4.0", - "jest-message-util": "22.4.0", - "jest-snapshot": "22.4.0", - "source-map-support": "0.5.3" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", - "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", - "dev": true, - "requires": { - "color-convert": "1.9.1" - } - }, - "chalk": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.1.tgz", - "integrity": "sha512-QUU4ofkDoMIVO7hcx1iPTISs88wsO8jA92RQIm4JAwZvFGGAV2hSAA1NX7oVj2Ej2Q6NDTcRDjPTFrMCRZoJ6g==", - "dev": true, - "requires": { - "ansi-styles": "3.2.0", - "escape-string-regexp": "1.0.5", - "supports-color": "5.2.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "supports-color": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.2.0.tgz", - "integrity": "sha512-F39vS48la4YvTZUPVeTqsjsFNrvcMwrV3RLZINsmHo+7djCvuUzSIeXOnZ5hmjef4bajL1dNccN+tg5XAliO5Q==", - "dev": true, - "requires": { - "has-flag": "3.0.0" - } - } - } - }, - "jest-leak-detector": { - "version": "22.4.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-22.4.0.tgz", - "integrity": "sha512-r3NEIVNh4X3fEeJtUIrKXWKhNokwUM2ILp5LD8w1KrEanPsFtZmYjmyZYjDTX2dXYr33TW65OvbRE3hWFAyq6g==", - "dev": true, - "requires": { - "pretty-format": "22.4.0" - } - }, - "jest-matcher-utils": { - "version": "22.4.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-22.4.0.tgz", - "integrity": "sha512-03m3issxUXpWMwDYTfmL8hRNewUB0yCRTeXPm+eq058rZxLHD9f5NtSSO98CWHqe4UyISIxd9Ao9iDVjHWd2qg==", - "dev": true, - "requires": { - "chalk": "2.3.1", - "jest-get-type": "22.1.0", - "pretty-format": "22.4.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", - "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", - "dev": true, - "requires": { - "color-convert": "1.9.1" - } - }, - "chalk": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.1.tgz", - "integrity": "sha512-QUU4ofkDoMIVO7hcx1iPTISs88wsO8jA92RQIm4JAwZvFGGAV2hSAA1NX7oVj2Ej2Q6NDTcRDjPTFrMCRZoJ6g==", - "dev": true, - "requires": { - "ansi-styles": "3.2.0", - "escape-string-regexp": "1.0.5", - "supports-color": "5.2.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "supports-color": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.2.0.tgz", - "integrity": "sha512-F39vS48la4YvTZUPVeTqsjsFNrvcMwrV3RLZINsmHo+7djCvuUzSIeXOnZ5hmjef4bajL1dNccN+tg5XAliO5Q==", - "dev": true, - "requires": { - "has-flag": "3.0.0" - } - } - } - }, - "jest-message-util": { - "version": "22.4.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-22.4.0.tgz", - "integrity": "sha512-eyCJB0T3hrlpFF2FqQoIB093OulP+1qvATQmD3IOgJgMGqPL6eYw8TbC5P/VCWPqKhGL51xvjIIhow5eZ2wHFw==", - "dev": true, - "requires": { - "@babel/code-frame": "7.0.0-beta.40", - "chalk": "2.3.1", - "micromatch": "2.3.11", - "slash": "1.0.0", - "stack-utils": "1.0.1" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", - "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", - "dev": true, - "requires": { - "color-convert": "1.9.1" - } - }, - "chalk": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.1.tgz", - "integrity": "sha512-QUU4ofkDoMIVO7hcx1iPTISs88wsO8jA92RQIm4JAwZvFGGAV2hSAA1NX7oVj2Ej2Q6NDTcRDjPTFrMCRZoJ6g==", - "dev": true, - "requires": { - "ansi-styles": "3.2.0", - "escape-string-regexp": "1.0.5", - "supports-color": "5.2.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "supports-color": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.2.0.tgz", - "integrity": "sha512-F39vS48la4YvTZUPVeTqsjsFNrvcMwrV3RLZINsmHo+7djCvuUzSIeXOnZ5hmjef4bajL1dNccN+tg5XAliO5Q==", - "dev": true, - "requires": { - "has-flag": "3.0.0" - } - } - } - }, - "jest-mock": { - "version": "22.2.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-22.2.0.tgz", - "integrity": "sha512-eOfoUYLOB/JlxChOFkh/bzpWGqUXb9I+oOpkprHHs9L7nUNfL8Rk28h1ycWrqzWCEQ/jZBg/xIv7VdQkfAkOhw==", - "dev": true - }, - "jest-regex-util": { - "version": "22.1.0", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-22.1.0.tgz", - "integrity": "sha512-on0LqVS6Xeh69sw3d1RukVnur+lVOl3zkmb0Q54FHj9wHoq6dbtWqb3TSlnVUyx36hqjJhjgs/QLqs07Bzu72Q==", - "dev": true - }, - "jest-resolve": { - "version": "22.4.0", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-22.4.0.tgz", - "integrity": "sha512-Vs/5VeJEHLpB0ubpYuU9QpBjcCUZRHoHnoV58ZC+N3EXyMJr/MgoqUNpo4OHGQERWlUpvl4YLAAO5uxSMF2VIg==", - "dev": true, - "requires": { - "browser-resolve": "1.11.2", - "chalk": "2.3.1" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", - "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", - "dev": true, - "requires": { - "color-convert": "1.9.1" - } - }, - "chalk": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.1.tgz", - "integrity": "sha512-QUU4ofkDoMIVO7hcx1iPTISs88wsO8jA92RQIm4JAwZvFGGAV2hSAA1NX7oVj2Ej2Q6NDTcRDjPTFrMCRZoJ6g==", - "dev": true, - "requires": { - "ansi-styles": "3.2.0", - "escape-string-regexp": "1.0.5", - "supports-color": "5.2.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "supports-color": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.2.0.tgz", - "integrity": "sha512-F39vS48la4YvTZUPVeTqsjsFNrvcMwrV3RLZINsmHo+7djCvuUzSIeXOnZ5hmjef4bajL1dNccN+tg5XAliO5Q==", - "dev": true, - "requires": { - "has-flag": "3.0.0" - } - } - } - }, - "jest-resolve-dependencies": { - "version": "22.1.0", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-22.1.0.tgz", - "integrity": "sha512-76Ll61bD/Sus8wK8d+lw891EtiBJGJkWG8OuVDTEX0z3z2+jPujvQqSB2eQ+kCHyCsRwJ2PSjhn3UHqae/oEtA==", - "dev": true, - "requires": { - "jest-regex-util": "22.1.0" - } - }, - "jest-runner": { - "version": "22.4.0", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-22.4.0.tgz", - "integrity": "sha512-x5QJQrSQs/oaZq2UxtKJxCjGq3fNF7guKRLxAIS39QIaRSAynS4agniMyvHMnLaYsBh6yzUea2SDeNHayQh+TQ==", - "dev": true, - "requires": { - "exit": "0.1.2", - "jest-config": "22.4.0", - "jest-docblock": "22.4.0", - "jest-haste-map": "22.4.0", - "jest-jasmine2": "22.4.0", - "jest-leak-detector": "22.4.0", - "jest-message-util": "22.4.0", - "jest-runtime": "22.4.0", - "jest-util": "22.4.0", - "jest-worker": "22.2.2", - "throat": "4.1.0" - } - }, - "jest-runtime": { - "version": "22.4.0", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-22.4.0.tgz", - "integrity": "sha512-aixL2DIXoFQ2ubnurzK4kbNXLl3+m0m7wIBb5VWaJdl1/3nV1UCSjZ9/dJZzpWGGfXsoGw2RZd8sS0nS5s+tdw==", - "dev": true, - "requires": { - "babel-core": "6.26.0", - "babel-jest": "22.4.0", - "babel-plugin-istanbul": "4.1.5", - "chalk": "2.3.1", - "convert-source-map": "1.5.1", - "exit": "0.1.2", - "graceful-fs": "4.1.11", - "jest-config": "22.4.0", - "jest-haste-map": "22.4.0", - "jest-regex-util": "22.1.0", - "jest-resolve": "22.4.0", - "jest-util": "22.4.0", - "jest-validate": "22.4.0", - "json-stable-stringify": "1.0.1", - "micromatch": "2.3.11", - "realpath-native": "1.0.0", - "slash": "1.0.0", - "strip-bom": "3.0.0", - "write-file-atomic": "2.3.0", - "yargs": "10.1.2" - }, - "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 - }, - "ansi-styles": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", - "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", - "dev": true, - "requires": { - "color-convert": "1.9.1" - } - }, - "chalk": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.1.tgz", - "integrity": "sha512-QUU4ofkDoMIVO7hcx1iPTISs88wsO8jA92RQIm4JAwZvFGGAV2hSAA1NX7oVj2Ej2Q6NDTcRDjPTFrMCRZoJ6g==", - "dev": true, - "requires": { - "ansi-styles": "3.2.0", - "escape-string-regexp": "1.0.5", - "supports-color": "5.2.0" - } - }, - "cliui": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.0.0.tgz", - "integrity": "sha512-nY3W5Gu2racvdDk//ELReY+dHjb9PlIcVDFXP72nVIhq2Gy3LuVXYwJoPVudwQnv1shtohpgkdCKT2YaKY0CKw==", - "dev": true, - "requires": { - "string-width": "2.1.1", - "strip-ansi": "4.0.0", - "wrap-ansi": "2.1.0" - } - }, - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "requires": { - "locate-path": "2.0.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "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" - } - }, - "supports-color": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.2.0.tgz", - "integrity": "sha512-F39vS48la4YvTZUPVeTqsjsFNrvcMwrV3RLZINsmHo+7djCvuUzSIeXOnZ5hmjef4bajL1dNccN+tg5XAliO5Q==", - "dev": true, - "requires": { - "has-flag": "3.0.0" - } - }, - "yargs": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-10.1.2.tgz", - "integrity": "sha512-ivSoxqBGYOqQVruxD35+EyCFDYNEFL/Uo6FcOnz+9xZdZzK0Zzw4r4KhbrME1Oo2gOggwJod2MnsdamSG7H9ig==", - "dev": true, - "requires": { - "cliui": "4.0.0", - "decamelize": "1.2.0", - "find-up": "2.1.0", - "get-caller-file": "1.0.2", - "os-locale": "2.1.0", - "require-directory": "2.1.1", - "require-main-filename": "1.0.1", - "set-blocking": "2.0.0", - "string-width": "2.1.1", - "which-module": "2.0.0", - "y18n": "3.2.1", - "yargs-parser": "8.1.0" - } - }, - "yargs-parser": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-8.1.0.tgz", - "integrity": "sha512-yP+6QqN8BmrgW2ggLtTbdrOyBNSI7zBa4IykmiV5R1wl1JWNxQvWhMfMdmzIYtKU7oP3OOInY/tl2ov3BDjnJQ==", - "dev": true, - "requires": { - "camelcase": "4.1.0" - } - } - } - }, - "jest-serializer": { - "version": "22.4.0", - "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-22.4.0.tgz", - "integrity": "sha512-dnqde95MiYfdc1ZJpjEiHCRvRGGJHPsZQARJFucEGIaOzxqqS9/tt2WzD/OUSGT6kxaEGLQE92faVJGdoCu+Rw==", - "dev": true - }, - "jest-snapshot": { - "version": "22.4.0", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-22.4.0.tgz", - "integrity": "sha512-6Zz4F9G1Nbr93kfm5h3A2+OkE+WGpgJlskYE4iSNN2uYfoTL5b9W6aB9Orpx+ueReHyqmy7HET7Z3EmYlL3hKw==", - "dev": true, - "requires": { - "chalk": "2.3.1", - "jest-diff": "22.4.0", - "jest-matcher-utils": "22.4.0", - "mkdirp": "0.5.1", - "natural-compare": "1.4.0", - "pretty-format": "22.4.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", - "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", - "dev": true, - "requires": { - "color-convert": "1.9.1" - } - }, - "chalk": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.1.tgz", - "integrity": "sha512-QUU4ofkDoMIVO7hcx1iPTISs88wsO8jA92RQIm4JAwZvFGGAV2hSAA1NX7oVj2Ej2Q6NDTcRDjPTFrMCRZoJ6g==", - "dev": true, - "requires": { - "ansi-styles": "3.2.0", - "escape-string-regexp": "1.0.5", - "supports-color": "5.2.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "supports-color": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.2.0.tgz", - "integrity": "sha512-F39vS48la4YvTZUPVeTqsjsFNrvcMwrV3RLZINsmHo+7djCvuUzSIeXOnZ5hmjef4bajL1dNccN+tg5XAliO5Q==", - "dev": true, - "requires": { - "has-flag": "3.0.0" - } - } - } - }, - "jest-util": { - "version": "22.4.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-22.4.0.tgz", - "integrity": "sha512-652EArz3XScAGAUMhbny7FrFGlmJkp+56CO+9RTrKPtGfbtVDF2WB2D8G+6D6zorDmDW5hNtKNIGNdGfG2kj1g==", - "dev": true, - "requires": { - "callsites": "2.0.0", - "chalk": "2.3.1", - "graceful-fs": "4.1.11", - "is-ci": "1.1.0", - "jest-message-util": "22.4.0", - "mkdirp": "0.5.1" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", - "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", - "dev": true, - "requires": { - "color-convert": "1.9.1" - } - }, - "chalk": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.1.tgz", - "integrity": "sha512-QUU4ofkDoMIVO7hcx1iPTISs88wsO8jA92RQIm4JAwZvFGGAV2hSAA1NX7oVj2Ej2Q6NDTcRDjPTFrMCRZoJ6g==", - "dev": true, - "requires": { - "ansi-styles": "3.2.0", - "escape-string-regexp": "1.0.5", - "supports-color": "5.2.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "supports-color": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.2.0.tgz", - "integrity": "sha512-F39vS48la4YvTZUPVeTqsjsFNrvcMwrV3RLZINsmHo+7djCvuUzSIeXOnZ5hmjef4bajL1dNccN+tg5XAliO5Q==", - "dev": true, - "requires": { - "has-flag": "3.0.0" - } - } - } - }, - "jest-validate": { - "version": "22.4.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-22.4.0.tgz", - "integrity": "sha512-l5JwbIAso8jGp/5/Dy86BCVjOra/Rb81wyXcFTGa4VxbtIh4AEOp2WixgprHLwp+YlUrHugZwaGyuagjB+iB+A==", - "dev": true, - "requires": { - "chalk": "2.3.1", - "jest-config": "22.4.0", - "jest-get-type": "22.1.0", - "leven": "2.1.0", - "pretty-format": "22.4.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", - "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", - "dev": true, - "requires": { - "color-convert": "1.9.1" - } - }, - "chalk": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.1.tgz", - "integrity": "sha512-QUU4ofkDoMIVO7hcx1iPTISs88wsO8jA92RQIm4JAwZvFGGAV2hSAA1NX7oVj2Ej2Q6NDTcRDjPTFrMCRZoJ6g==", - "dev": true, - "requires": { - "ansi-styles": "3.2.0", - "escape-string-regexp": "1.0.5", - "supports-color": "5.2.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "supports-color": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.2.0.tgz", - "integrity": "sha512-F39vS48la4YvTZUPVeTqsjsFNrvcMwrV3RLZINsmHo+7djCvuUzSIeXOnZ5hmjef4bajL1dNccN+tg5XAliO5Q==", - "dev": true, - "requires": { - "has-flag": "3.0.0" - } - } - } - }, - "jest-worker": { - "version": "22.2.2", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-22.2.2.tgz", - "integrity": "sha512-ZylDXjrFNt/OP6cUxwJFWwDgazP7hRjtCQbocFHyiwov+04Wm1x5PYzMGNJT53s4nwr0oo9ocYTImS09xOlUnw==", - "dev": true, - "requires": { - "merge-stream": "1.0.1" - } - }, - "jquery": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.2.1.tgz", - "integrity": "sha1-XE2d5lKvbNCncBVKYxu6ErAVx4c=" - }, - "js-base64": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.4.3.tgz", - "integrity": "sha512-H7ErYLM34CvDMto3GbD6xD0JLUGYXR3QTcH6B/tr4Hi/QpSThnCsIp+Sy5FRTw3B0d6py4HcNkW7nO/wdtGWEw==", - "dev": true - }, - "js-beautify": { - "version": "1.6.14", - "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.6.14.tgz", - "integrity": "sha1-07j3Mi0CuSd9WL0jgmTDJ+WARM0=", - "requires": { - "config-chain": "1.1.11", - "editorconfig": "0.13.3", - "mkdirp": "0.5.1", - "nopt": "3.0.6" - } - }, - "js-tokens": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", - "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=" - }, - "js-yaml": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.10.0.tgz", - "integrity": "sha512-O2v52ffjLa9VeM43J4XocZE//WT9N0IiwDa3KSHH7Tu8CtH+1qM8SIZvnsTh6v+4yFy5KUY3BHUVwjpfAWsjIA==", - "dev": true, - "requires": { - "argparse": "1.0.9", - "esprima": "4.0.0" - } - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "dev": true, - "optional": true - }, - "jsdom": { - "version": "11.6.2", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-11.6.2.tgz", - "integrity": "sha512-pAeZhpbSlUp5yQcS6cBQJwkbzmv4tWFaYxHbFVSxzXefqjvtRA851Z5N2P+TguVG9YeUDcgb8pdeVQRJh0XR3Q==", - "dev": true, - "requires": { - "abab": "1.0.4", - "acorn": "5.4.1", - "acorn-globals": "4.1.0", - "array-equal": "1.0.0", - "browser-process-hrtime": "0.1.2", - "content-type-parser": "1.0.2", - "cssom": "0.3.2", - "cssstyle": "0.2.37", - "domexception": "1.0.1", - "escodegen": "1.9.0", - "html-encoding-sniffer": "1.0.2", - "left-pad": "1.2.0", - "nwmatcher": "1.4.3", - "parse5": "4.0.0", - "pn": "1.1.0", - "request": "2.83.0", - "request-promise-native": "1.0.5", - "sax": "1.2.4", - "symbol-tree": "3.2.2", - "tough-cookie": "2.3.3", - "w3c-hr-time": "1.0.1", - "webidl-conversions": "4.0.2", - "whatwg-encoding": "1.0.3", - "whatwg-url": "6.4.0", - "ws": "4.0.0", - "xml-name-validator": "3.0.0" - }, - "dependencies": { - "parse5": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz", - "integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==", - "dev": true - } - } - }, - "jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", - "dev": true - }, - "json-loader": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/json-loader/-/json-loader-0.5.7.tgz", - "integrity": "sha512-QLPs8Dj7lnf3e3QYS1zkCo+4ZwqOiF9d/nZnYozTISxXWCfNs9yuky5rJw4/W34s7POaNlbZmQGaB5NiXCbP4w==", - "dev": true - }, - "json-parse-better-errors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.1.tgz", - "integrity": "sha512-xyQpxeWWMKyJps9CuGJYeng6ssI5bpqS9ltQpdVQ90t4ql6NdnxFKh95JcRt2cun/DjMVNrdjniLPuMA69xmCw==", - "dev": true - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", - "dev": true - }, - "json-schema-traverse": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", - "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", - "dev": true - }, - "json-stable-stringify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", - "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", - "dev": true, - "requires": { - "jsonify": "0.0.0" - } - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", - "dev": true - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", - "dev": true - }, - "json5": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", - "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", - "dev": true - }, - "jsonfile": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz", - "integrity": "sha1-pezG9l9T9mLEQVx2daAzHQmS7GY=", - "dev": true, - "requires": { - "graceful-fs": "4.1.11" - } - }, - "jsonify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", - "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", - "dev": true - }, - "jsonpointer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", - "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=", - "dev": true - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "dev": true, - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - } - }, - "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 - }, - "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.6" - } - }, - "lazy-ass": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz", - "integrity": "sha1-eZllXoZGwX8In90YfRUNMyTVRRM=", - "dev": true - }, - "lazy-cache": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", - "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", - "dev": true - }, - "lcid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", - "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", - "requires": { - "invert-kv": "1.0.0" - } - }, - "left-pad": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.2.0.tgz", - "integrity": "sha1-0wpzxrggHY99jnlWupYWCHpo4O4=", - "dev": true - }, - "leven": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", - "integrity": "sha1-wuep93IJTe6dNCAq6KzORoeHVYA=", - "dev": true - }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "dev": true, - "requires": { - "prelude-ls": "1.1.2", - "type-check": "0.3.2" - } - }, - "line-height": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/line-height/-/line-height-0.3.1.tgz", - "integrity": "sha1-SxIF7d4YKHKl76PI9iCzGHqcVMk=", - "requires": { - "computed-style": "0.1.4" - } - }, - "lint-staged": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-6.1.0.tgz", - "integrity": "sha512-RMB6BUd2bEKaPnj06F7j8RRB8OHM+UP4fQS2LT8lF+X9BjSaezw1oVB5hc4elLhYvzlFCkhAaatzYz+x53YHgw==", - "dev": true, - "requires": { - "app-root-path": "2.0.1", - "chalk": "2.3.0", - "commander": "2.14.1", - "cosmiconfig": "4.0.0", - "debug": "3.1.0", - "dedent": "0.7.0", - "execa": "0.8.0", - "find-parent-dir": "0.3.0", - "is-glob": "4.0.0", - "jest-validate": "21.2.1", - "listr": "0.13.0", - "lodash": "4.17.4", - "log-symbols": "2.2.0", - "minimatch": "3.0.4", - "npm-which": "3.0.1", - "p-map": "1.2.0", - "path-is-inside": "1.0.2", - "pify": "3.0.0", - "staged-git-files": "0.0.4", - "stringify-object": "3.2.2" - }, - "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 - }, - "ansi-styles": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", - "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", - "dev": true, - "requires": { - "color-convert": "1.9.1" - } - }, - "chalk": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", - "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", - "dev": true, - "requires": { - "ansi-styles": "3.2.0", - "escape-string-regexp": "1.0.5", - "supports-color": "4.5.0" - } - }, - "cosmiconfig": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-4.0.0.tgz", - "integrity": "sha512-6e5vDdrXZD+t5v0L8CrurPeybg4Fmf+FCSYxXKYVAqLUtyCSbuyqE059d0kDthTNRzKVjL7QMgNpEUlsoYH3iQ==", - "dev": true, - "requires": { - "is-directory": "0.3.1", - "js-yaml": "3.10.0", - "parse-json": "4.0.0", - "require-from-string": "2.0.1" - } - }, - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "execa": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.8.0.tgz", - "integrity": "sha1-2NdrvBtVIX7RkP1t1J08d07PyNo=", - "dev": true, - "requires": { - "cross-spawn": "5.1.0", - "get-stream": "3.0.0", - "is-stream": "1.1.0", - "npm-run-path": "2.0.2", - "p-finally": "1.0.0", - "signal-exit": "3.0.2", - "strip-eof": "1.0.0" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-glob": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz", - "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=", - "dev": true, - "requires": { - "is-extglob": "2.1.1" - } - }, - "jest-get-type": { - "version": "21.2.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-21.2.0.tgz", - "integrity": "sha512-y2fFw3C+D0yjNSDp7ab1kcd6NUYfy3waPTlD8yWkAtiocJdBRQqNoRqVfMNxgj+IjT0V5cBIHJO0z9vuSSZ43Q==", - "dev": true - }, - "jest-validate": { - "version": "21.2.1", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-21.2.1.tgz", - "integrity": "sha512-k4HLI1rZQjlU+EC682RlQ6oZvLrE5SCh3brseQc24vbZTxzT/k/3urar5QMCVgjadmSO7lECeGdc6YxnM3yEGg==", - "dev": true, - "requires": { - "chalk": "2.3.0", - "jest-get-type": "21.2.0", - "leven": "2.1.0", - "pretty-format": "21.2.1" - } - }, - "listr": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/listr/-/listr-0.13.0.tgz", - "integrity": "sha1-ILsLowuuZg7oTMBQPfS+PVYjiH0=", - "dev": true, - "requires": { - "chalk": "1.1.3", - "cli-truncate": "0.2.1", - "figures": "1.7.0", - "indent-string": "2.1.0", - "is-observable": "0.2.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.1", - "log-symbols": "1.0.2", - "log-update": "1.0.2", - "ora": "0.2.3", - "p-map": "1.2.0", - "rxjs": "5.5.6", - "stream-to-observable": "0.2.0", - "strip-ansi": "3.0.1" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "2.2.1", - "escape-string-regexp": "1.0.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" - } - }, - "log-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-1.0.2.tgz", - "integrity": "sha1-N2/3tY6jCGoPCfrMdGF+ylAeGhg=", - "dev": true, - "requires": { - "chalk": "1.1.3" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - } - } - }, - "listr-update-renderer": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/listr-update-renderer/-/listr-update-renderer-0.4.0.tgz", - "integrity": "sha1-NE2YDaLKLosUW6MFkI8yrj9MyKc=", - "dev": true, - "requires": { - "chalk": "1.1.3", - "cli-truncate": "0.2.1", - "elegant-spinner": "1.0.1", - "figures": "1.7.0", - "indent-string": "3.2.0", - "log-symbols": "1.0.2", - "log-update": "1.0.2", - "strip-ansi": "3.0.1" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "2.2.1", - "escape-string-regexp": "1.0.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" - } - }, - "indent-string": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", - "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=", - "dev": true - }, - "log-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-1.0.2.tgz", - "integrity": "sha1-N2/3tY6jCGoPCfrMdGF+ylAeGhg=", - "dev": true, - "requires": { - "chalk": "1.1.3" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - } - } - }, - "log-symbols": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", - "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", - "dev": true, - "requires": { - "chalk": "2.3.0" - } - }, - "parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", - "dev": true, - "requires": { - "error-ex": "1.3.1", - "json-parse-better-errors": "1.0.1" - } - }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - }, - "pretty-format": { - "version": "21.2.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-21.2.1.tgz", - "integrity": "sha512-ZdWPGYAnYfcVP8yKA3zFjCn8s4/17TeYH28MXuC8vTp0o21eXjbFGcOAXZEaDaOFJjc3h2qa7HQNHNshhvoh2A==", - "dev": true, - "requires": { - "ansi-regex": "3.0.0", - "ansi-styles": "3.2.0" - } - }, - "require-from-string": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.1.tgz", - "integrity": "sha1-xUUjPp19pmFunVmt+zn8n1iGdv8=", - "dev": true - }, - "stream-to-observable": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/stream-to-observable/-/stream-to-observable-0.2.0.tgz", - "integrity": "sha1-WdbqOT2HwsDdrBCqDVYbxrpvDhA=", - "dev": true, - "requires": { - "any-observable": "0.2.0" - } - }, - "supports-color": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", - "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", - "dev": true, - "requires": { - "has-flag": "2.0.0" - } - } - } - }, - "listr": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/listr/-/listr-0.12.0.tgz", - "integrity": "sha1-a84sD1YD+klYDqF81qAMwOX6RRo=", - "dev": true, - "requires": { - "chalk": "1.1.3", - "cli-truncate": "0.2.1", - "figures": "1.7.0", - "indent-string": "2.1.0", - "is-promise": "2.1.0", - "is-stream": "1.1.0", - "listr-silent-renderer": "1.1.1", - "listr-update-renderer": "0.2.0", - "listr-verbose-renderer": "0.4.1", - "log-symbols": "1.0.2", - "log-update": "1.0.2", - "ora": "0.2.3", - "p-map": "1.2.0", - "rxjs": "5.5.6", - "stream-to-observable": "0.1.0", - "strip-ansi": "3.0.1" - } - }, - "listr-silent-renderer": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz", - "integrity": "sha1-kktaN1cVN3C/Go4/v3S4u/P5JC4=", - "dev": true - }, - "listr-update-renderer": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/listr-update-renderer/-/listr-update-renderer-0.2.0.tgz", - "integrity": "sha1-yoDhd5tOcCZoB+ju0a1qvjmFUPk=", - "dev": true, - "requires": { - "chalk": "1.1.3", - "cli-truncate": "0.2.1", - "elegant-spinner": "1.0.1", - "figures": "1.7.0", - "indent-string": "3.2.0", - "log-symbols": "1.0.2", - "log-update": "1.0.2", - "strip-ansi": "3.0.1" - }, - "dependencies": { - "indent-string": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", - "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=", - "dev": true - } - } - }, - "listr-verbose-renderer": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/listr-verbose-renderer/-/listr-verbose-renderer-0.4.1.tgz", - "integrity": "sha1-ggb0z21S3cWCfl/RSYng6WWTOjU=", - "dev": true, - "requires": { - "chalk": "1.1.3", - "cli-cursor": "1.0.2", - "date-fns": "1.29.0", - "figures": "1.7.0" - } - }, - "load-json-file": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", - "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", - "requires": { - "graceful-fs": "4.1.11", - "parse-json": "2.2.0", - "pify": "2.3.0", - "strip-bom": "3.0.0" - } - }, - "loader-runner": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.3.0.tgz", - "integrity": "sha1-9IKuqC1UPgeSFwDVpG7yb9rGuKI=", - "dev": true - }, - "loader-utils": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.1.0.tgz", - "integrity": "sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=", - "dev": true, - "requires": { - "big.js": "3.2.0", - "emojis-list": "2.1.0", - "json5": "0.5.1" - } - }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "requires": { - "p-locate": "2.0.0", - "path-exists": "3.0.0" - }, - "dependencies": { - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" - } - } - }, - "lodash": { - "version": "4.17.4", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", - "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" - }, - "lodash-es": { - "version": "4.17.5", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.5.tgz", - "integrity": "sha512-Ez3ONp3TK9gX1HYKp6IhetcVybD+2F+Yp6GS9dfH8ue6EOCEzQtQEh4K0FYWBP9qLv+lzeQAYXw+3ySfxyZqkw==" - }, - "lodash._baseisequal": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/lodash._baseisequal/-/lodash._baseisequal-3.0.7.tgz", - "integrity": "sha1-2AJfdjOdKTQnZ9zIh85cuVpbUfE=", - "dev": true, - "requires": { - "lodash.isarray": "3.0.4", - "lodash.istypedarray": "3.0.6", - "lodash.keys": "3.1.2" - } - }, - "lodash._bindcallback": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz", - "integrity": "sha1-5THCdkTPi1epnhftlbNcdIeJOS4=", - "dev": true - }, - "lodash._getnative": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", - "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=", - "dev": true - }, - "lodash.assign": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", - "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=", - "dev": true - }, - "lodash.clonedeep": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", - "dev": true - }, - "lodash.cond": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/lodash.cond/-/lodash.cond-4.5.2.tgz", - "integrity": "sha1-9HGh2khr5g9quVXRcRVSPdHSVdU=" - }, - "lodash.flattendeep": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", - "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", - "dev": true - }, - "lodash.isarguments": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", - "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=", - "dev": true - }, - "lodash.isarray": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", - "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", - "dev": true - }, - "lodash.isequal": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-3.0.4.tgz", - "integrity": "sha1-HDXrO27wzR/1F0Pj6jz3/f/ay2Q=", - "dev": true, - "requires": { - "lodash._baseisequal": "3.0.7", - "lodash._bindcallback": "3.0.1" - } - }, - "lodash.istypedarray": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/lodash.istypedarray/-/lodash.istypedarray-3.0.6.tgz", - "integrity": "sha1-yaR3SYYHUB2OhJTSg7h8OSgc72I=", - "dev": true - }, - "lodash.keys": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", - "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", - "dev": true, - "requires": { - "lodash._getnative": "3.9.1", - "lodash.isarguments": "3.1.0", - "lodash.isarray": "3.0.4" - } - }, - "lodash.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", - "dev": true - }, - "lodash.merge": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.1.tgz", - "integrity": "sha512-AOYza4+Hf5z1/0Hztxpm2/xiPZgi/cjMqdnKTUWTBSKchJlxXXuUSxCCl8rJlf4g6yww/j6mA8nC8Hw/EZWxKQ==", - "dev": true - }, - "lodash.mergewith": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz", - "integrity": "sha512-eWw5r+PYICtEBgrBE5hhlT6aAa75f411bgDz/ZL2KZqYV03USvucsxcHUIlGTDTECs1eunpI7HOV7U+WLDvNdQ==", - "dev": true - }, - "lodash.once": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=", - "dev": true - }, - "lodash.sortby": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", - "dev": true - }, - "lodash.tail": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.tail/-/lodash.tail-4.1.1.tgz", - "integrity": "sha1-0jM6NtnncXyK0vfKyv7HwytERmQ=", - "dev": true - }, - "lodash.uniq": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", - "dev": true - }, - "log-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-1.0.2.tgz", - "integrity": "sha1-N2/3tY6jCGoPCfrMdGF+ylAeGhg=", - "dev": true, - "requires": { - "chalk": "1.1.3" - } - }, - "log-update": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-1.0.2.tgz", - "integrity": "sha1-GZKfZMQJPS0ucHWh2tivWcKWuNE=", - "dev": true, - "requires": { - "ansi-escapes": "1.4.0", - "cli-cursor": "1.0.2" - }, - "dependencies": { - "ansi-escapes": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", - "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=", - "dev": true - } - } - }, - "longest": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", - "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", - "dev": true - }, - "loose-envify": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", - "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=", - "requires": { - "js-tokens": "3.0.2" - } - }, - "loud-rejection": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", - "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", - "dev": true, - "requires": { - "currently-unhandled": "0.4.1", - "signal-exit": "3.0.2" - } - }, - "lru-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-3.2.0.tgz", - "integrity": "sha1-cXibO39Tmb7IVl3aOKow0qCX7+4=", - "requires": { - "pseudomap": "1.0.2" - } - }, - "macaddress": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/macaddress/-/macaddress-0.2.8.tgz", - "integrity": "sha1-WQTcU3w57G2+/q6QIycTX6hRHxI=", - "dev": true - }, - "make-dir": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.1.0.tgz", - "integrity": "sha512-0Pkui4wLJ7rxvmfUvs87skoEaxmu0hCUApF8nonzpl7q//FWp9zu8W61Scz4sd/kUiqDxvUhtoam2efDyiBzcA==", - "dev": true, - "requires": { - "pify": "3.0.0" - }, - "dependencies": { - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - } - } - }, - "makeerror": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz", - "integrity": "sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw=", - "dev": true, - "requires": { - "tmpl": "1.0.4" - } - }, - "map-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", - "dev": true - }, - "map-values": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/map-values/-/map-values-1.0.1.tgz", - "integrity": "sha1-douOecAJvytk/ugG4ip7HEGQyZA=", - "dev": true - }, - "material-colors": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.5.tgz", - "integrity": "sha1-UpJZPmdUyxvMK5gDDk4Najr8nqE=" - }, - "math-expression-evaluator": { - "version": "1.2.17", - "resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz", - "integrity": "sha1-3oGf282E3M2PrlnGrreWFbnSZqw=", - "dev": true - }, - "md5.js": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz", - "integrity": "sha1-6b296UogpawYsENA/Fdk1bCdkB0=", - "dev": true, - "requires": { - "hash-base": "3.0.4", - "inherits": "2.0.3" - }, - "dependencies": { - "hash-base": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", - "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", - "dev": true, - "requires": { - "inherits": "2.0.3", - "safe-buffer": "5.1.1" - } - } - } - }, - "mem": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz", - "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=", - "requires": { - "mimic-fn": "1.2.0" - } - }, - "memize": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/memize/-/memize-1.0.5.tgz", - "integrity": "sha512-Dm8Jhb5kiC4+ynYsVR4QDXKt+o2dfqGuY4hE2x+XlXZkdndlT80bJxfcMv5QGp/FCy6MhG7f5ElpmKPFKOSEpg==" - }, - "memory-fs": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", - "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", - "dev": true, - "requires": { - "errno": "0.1.6", - "readable-stream": "2.3.3" - } - }, - "meow": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", - "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", - "dev": true, - "requires": { - "camelcase-keys": "2.1.0", - "decamelize": "1.2.0", - "loud-rejection": "1.6.0", - "map-obj": "1.0.1", - "minimist": "1.2.0", - "normalize-package-data": "2.4.0", - "object-assign": "4.1.1", - "read-pkg-up": "1.0.1", - "redent": "1.0.0", - "trim-newlines": "1.0.0" - }, - "dependencies": { - "load-json-file": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", - "dev": true, - "requires": { - "graceful-fs": "4.1.11", - "parse-json": "2.2.0", - "pify": "2.3.0", - "pinkie-promise": "2.0.1", - "strip-bom": "2.0.0" - } - }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - }, - "path-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", - "dev": true, - "requires": { - "graceful-fs": "4.1.11", - "pify": "2.3.0", - "pinkie-promise": "2.0.1" - } - }, - "read-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", - "dev": true, - "requires": { - "load-json-file": "1.1.0", - "normalize-package-data": "2.4.0", - "path-type": "1.1.0" - } - }, - "read-pkg-up": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", - "dev": true, - "requires": { - "find-up": "1.1.2", - "read-pkg": "1.1.0" - } - }, - "strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", - "dev": true, - "requires": { - "is-utf8": "0.2.1" - } - } - } - }, - "merge": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/merge/-/merge-1.2.0.tgz", - "integrity": "sha1-dTHjnUlJwoGma4xabgJl6LBYlNo=" - }, - "merge-stream": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-1.0.1.tgz", - "integrity": "sha1-QEEgLVCKNCugAXQAjfDCUbjBNeE=", - "dev": true, - "requires": { - "readable-stream": "2.3.3" - } - }, - "micromatch": { - "version": "2.3.11", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", - "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", - "dev": true, - "requires": { - "arr-diff": "2.0.0", - "array-unique": "0.2.1", - "braces": "1.8.5", - "expand-brackets": "0.1.5", - "extglob": "0.3.2", - "filename-regex": "2.0.1", - "is-extglob": "1.0.0", - "is-glob": "2.0.1", - "kind-of": "3.2.2", - "normalize-path": "2.1.1", - "object.omit": "2.0.1", - "parse-glob": "3.0.4", - "regex-cache": "0.4.4" - } - }, - "miller-rabin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", - "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", - "dev": true, - "requires": { - "bn.js": "4.11.8", - "brorand": "1.1.0" - } - }, - "mime-db": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz", - "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=", - "dev": true - }, - "mime-types": { - "version": "2.1.17", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", - "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", - "dev": true, - "requires": { - "mime-db": "1.30.0" - } - }, - "mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==" - }, - "minimalistic-assert": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz", - "integrity": "sha1-cCvi3aazf0g2vLP121ZkG2Sh09M=", - "dev": true - }, - "minimalistic-crypto-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", - "dev": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "requires": { - "brace-expansion": "1.1.8" - } - }, - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" - }, - "mixin-object": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mixin-object/-/mixin-object-2.0.1.tgz", - "integrity": "sha1-T7lJRB2rGCVA8f4DW6YOGUel5X4=", - "dev": true, - "requires": { - "for-in": "0.1.8", - "is-extendable": "0.1.1" - }, - "dependencies": { - "for-in": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-0.1.8.tgz", - "integrity": "sha1-2Hc5COMSVhCZUrH9ubP6hn0ndeE=", - "dev": true - } - } - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "requires": { - "minimist": "0.0.8" - } - }, - "moment": { - "version": "2.18.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.18.1.tgz", - "integrity": "sha1-w2GT3Tzhwu7SrbfIAtu8d6gbHA8=" - }, - "moment-timezone": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.13.tgz", - "integrity": "sha1-mc5cfYJyYusPH3AgRBd/YHRde5A=", - "requires": { - "moment": "2.18.1" - } - }, - "mousetrap": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/mousetrap/-/mousetrap-1.6.1.tgz", - "integrity": "sha1-KghfXHUSlMdefoH27CVFspy/Qtk=" - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "mute-stream": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", - "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", - "dev": true - }, - "nan": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.8.0.tgz", - "integrity": "sha1-7XFfP+neArV6XmJS2QqWZ14fCFo=", - "dev": true - }, - "natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", - "dev": true - }, - "nearley": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.11.1.tgz", - "integrity": "sha512-1azpqq1JvHKZNPEixS1jNEXf4kDilhFtr8AIZIGjP8N0TcAcUhKgi354niI5pM4JoOsMQ+H6vzCYWQa95LQjcw==", - "dev": true, - "requires": { - "nomnom": "1.6.2", - "railroad-diagrams": "1.0.0", - "randexp": "0.4.6", - "semver": "5.5.0" - }, - "dependencies": { - "semver": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", - "dev": true - } - } - }, - "node-fetch": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", - "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==", - "requires": { - "encoding": "0.1.12", - "is-stream": "1.1.0" - } - }, - "node-gyp": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.6.2.tgz", - "integrity": "sha1-m/vlRWIoYoSDjnUOrAUpWFP6HGA=", - "dev": true, - "requires": { - "fstream": "1.0.11", - "glob": "7.1.2", - "graceful-fs": "4.1.11", - "minimatch": "3.0.4", - "mkdirp": "0.5.1", - "nopt": "3.0.6", - "npmlog": "4.1.2", - "osenv": "0.1.4", - "request": "2.83.0", - "rimraf": "2.6.2", - "semver": "5.3.0", - "tar": "2.2.1", - "which": "1.3.0" - } - }, - "node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=", - "dev": true - }, - "node-libs-browser": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.1.0.tgz", - "integrity": "sha512-5AzFzdoIMb89hBGMZglEegffzgRg+ZFoUmisQ8HI4j1KDdpx13J0taNp2y9xPbur6W61gepGDDotGBVQ7mfUCg==", - "dev": true, - "requires": { - "assert": "1.4.1", - "browserify-zlib": "0.2.0", - "buffer": "4.9.1", - "console-browserify": "1.1.0", - "constants-browserify": "1.0.0", - "crypto-browserify": "3.12.0", - "domain-browser": "1.2.0", - "events": "1.1.1", - "https-browserify": "1.0.0", - "os-browserify": "0.3.0", - "path-browserify": "0.0.0", - "process": "0.11.10", - "punycode": "1.4.1", - "querystring-es3": "0.2.1", - "readable-stream": "2.3.3", - "stream-browserify": "2.0.1", - "stream-http": "2.8.0", - "string_decoder": "1.0.3", - "timers-browserify": "2.0.6", - "tty-browserify": "0.0.0", - "url": "0.11.0", - "util": "0.10.3", - "vm-browserify": "0.0.4" - } - }, - "node-notifier": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.2.1.tgz", - "integrity": "sha512-MIBs+AAd6dJ2SklbbE8RUDRlIVhU8MaNLh1A9SUZDUHPiZkWLFde6UNwG41yQHZEToHgJMXqyVZ9UcS/ReOVTg==", - "dev": true, - "requires": { - "growly": "1.3.0", - "semver": "5.5.0", - "shellwords": "0.1.1", - "which": "1.3.0" - }, - "dependencies": { - "semver": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", - "dev": true - } - } - }, - "node-sass": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.7.2.tgz", - "integrity": "sha512-CaV+wLqZ7//Jdom5aUFCpGNoECd7BbNhjuwdsX/LkXBrHl8eb1Wjw4HvWqcFvhr5KuNgAk8i/myf/MQ1YYeroA==", - "dev": true, - "requires": { - "async-foreach": "0.1.3", - "chalk": "1.1.3", - "cross-spawn": "3.0.1", - "gaze": "1.1.2", - "get-stdin": "4.0.1", - "glob": "7.1.2", - "in-publish": "2.0.0", - "lodash.assign": "4.2.0", - "lodash.clonedeep": "4.5.0", - "lodash.mergewith": "4.6.1", - "meow": "3.7.0", - "mkdirp": "0.5.1", - "nan": "2.8.0", - "node-gyp": "3.6.2", - "npmlog": "4.1.2", - "request": "2.79.0", - "sass-graph": "2.2.4", - "stdout-stream": "1.4.0", - "true-case-path": "1.0.2" - }, - "dependencies": { - "assert-plus": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", - "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=", - "dev": true - }, - "aws-sign2": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", - "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=", - "dev": true - }, - "boom": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", - "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", - "dev": true, - "requires": { - "hoek": "2.16.3" - } - }, - "caseless": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz", - "integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=", - "dev": true - }, - "cross-spawn": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz", - "integrity": "sha1-ElYDfsufDF9549bvE14wdwGEuYI=", - "dev": true, - "requires": { - "lru-cache": "4.1.1", - "which": "1.3.0" - } - }, - "cryptiles": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", - "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", - "dev": true, - "requires": { - "boom": "2.10.1" - } - }, - "form-data": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", - "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", - "dev": true, - "requires": { - "asynckit": "0.4.0", - "combined-stream": "1.0.5", - "mime-types": "2.1.17" - } - }, - "har-validator": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz", - "integrity": "sha1-zcvAgYgmWtEZtqWnyKtw7s+10n0=", - "dev": true, - "requires": { - "chalk": "1.1.3", - "commander": "2.14.1", - "is-my-json-valid": "2.17.1", - "pinkie-promise": "2.0.1" - } - }, - "hawk": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", - "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", - "dev": true, - "requires": { - "boom": "2.10.1", - "cryptiles": "2.0.5", - "hoek": "2.16.3", - "sntp": "1.0.9" - } - }, - "hoek": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", - "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=", - "dev": true - }, - "http-signature": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", - "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", - "dev": true, - "requires": { - "assert-plus": "0.2.0", - "jsprim": "1.4.1", - "sshpk": "1.13.1" - } - }, - "lru-cache": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.1.tgz", - "integrity": "sha512-q4spe4KTfsAS1SUHLO0wz8Qiyf1+vMIAgpRYioFYDMNqKfHQbg+AVDH3i4fvpl71/P1L0dBl+fQi+P37UYf0ew==", - "dev": true, - "requires": { - "pseudomap": "1.0.2", - "yallist": "2.1.2" - } - }, - "qs": { - "version": "6.3.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.3.2.tgz", - "integrity": "sha1-51vV9uJoEioqDgvaYwslUMFmUCw=", - "dev": true - }, - "request": { - "version": "2.79.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.79.0.tgz", - "integrity": "sha1-Tf5b9r6LjNw3/Pk+BLZVd3InEN4=", - "dev": true, - "requires": { - "aws-sign2": "0.6.0", - "aws4": "1.6.0", - "caseless": "0.11.0", - "combined-stream": "1.0.5", - "extend": "3.0.1", - "forever-agent": "0.6.1", - "form-data": "2.1.4", - "har-validator": "2.0.6", - "hawk": "3.1.3", - "http-signature": "1.1.1", - "is-typedarray": "1.0.0", - "isstream": "0.1.2", - "json-stringify-safe": "5.0.1", - "mime-types": "2.1.17", - "oauth-sign": "0.8.2", - "qs": "6.3.2", - "stringstream": "0.0.5", - "tough-cookie": "2.3.3", - "tunnel-agent": "0.4.3", - "uuid": "3.1.0" - } - }, - "sntp": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", - "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", - "dev": true, - "requires": { - "hoek": "2.16.3" - } - }, - "tunnel-agent": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz", - "integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=", - "dev": true - } - } - }, - "nomnom": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/nomnom/-/nomnom-1.6.2.tgz", - "integrity": "sha1-hKZqJgF0QI/Ft3oY+IjszET7aXE=", - "dev": true, - "requires": { - "colors": "0.5.1", - "underscore": "1.4.4" - } - }, - "nopt": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", - "requires": { - "abbrev": "1.1.1" - } - }, - "normalize-package-data": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", - "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", - "requires": { - "hosted-git-info": "2.5.0", - "is-builtin-module": "1.0.0", - "semver": "5.3.0", - "validate-npm-package-license": "3.0.1" - } - }, - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "dev": true, - "requires": { - "remove-trailing-separator": "1.1.0" - } - }, - "normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", - "dev": true - }, - "normalize-url": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-1.9.1.tgz", - "integrity": "sha1-LMDWazHqIwNkWENuNiDYWVTGbDw=", - "dev": true, - "requires": { - "object-assign": "4.1.1", - "prepend-http": "1.0.4", - "query-string": "4.3.4", - "sort-keys": "1.1.2" - } - }, - "npm-path": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/npm-path/-/npm-path-2.0.4.tgz", - "integrity": "sha512-IFsj0R9C7ZdR5cP+ET342q77uSRdtWOlWpih5eC+lu29tIDbNEgDbzgVJ5UFvYHWhxDZ5TFkJafFioO0pPQjCw==", - "dev": true, - "requires": { - "which": "1.3.0" - } - }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "requires": { - "path-key": "2.0.1" - } - }, - "npm-which": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/npm-which/-/npm-which-3.0.1.tgz", - "integrity": "sha1-kiXybsOihcIJyuZ8OxGmtKtxQKo=", - "dev": true, - "requires": { - "commander": "2.14.1", - "npm-path": "2.0.4", - "which": "1.3.0" - } - }, - "npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "dev": true, - "requires": { - "are-we-there-yet": "1.1.4", - "console-control-strings": "1.1.0", - "gauge": "2.7.4", - "set-blocking": "2.0.0" - } - }, - "nth-check": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.1.tgz", - "integrity": "sha1-mSms32KPwsQQmN6rgqxYDPFJquQ=", - "dev": true, - "requires": { - "boolbase": "1.0.0" - } - }, - "num2fraction": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", - "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=", - "dev": true - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" - }, - "nwmatcher": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/nwmatcher/-/nwmatcher-1.4.3.tgz", - "integrity": "sha512-IKdSTiDWCarf2JTS5e9e2+5tPZGdkRJ79XjYV0pzK8Q9BpsFyBq1RGKxzs7Q8UBushGw7m6TzVKz6fcY99iSWw==", - "dev": true - }, - "oauth-sign": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", - "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=", - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" - }, - "object-filter": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/object-filter/-/object-filter-1.0.2.tgz", - "integrity": "sha1-rwt5f/6+r4pSxmN87b6IFs/sG8g=", - "dev": true - }, - "object-inspect": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.5.0.tgz", - "integrity": "sha512-UmOFbHbwvv+XHj7BerrhVq+knjceBdkvU5AriwLMvhv2qi+e7DJzxfBeFpILEjVzCp+xA+W/pIf06RGPWlZNfw==", - "dev": true - }, - "object-is": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.0.1.tgz", - "integrity": "sha1-CqYOyZiaCz7Xlc9NBvYs8a1lObY=", - "dev": true - }, - "object-keys": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.11.tgz", - "integrity": "sha1-xUYBd4rVYPEULODgG8yotW0TQm0=", - "dev": true - }, - "object.assign": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", - "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", - "dev": true, - "requires": { - "define-properties": "1.1.2", - "function-bind": "1.1.1", - "has-symbols": "1.0.0", - "object-keys": "1.0.11" - } - }, - "object.entries": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.0.4.tgz", - "integrity": "sha1-G/mk3SKI9bM/Opk9JXZh8F0WGl8=", - "dev": true, - "requires": { - "define-properties": "1.1.2", - "es-abstract": "1.10.0", - "function-bind": "1.1.1", - "has": "1.0.1" - } - }, - "object.getownpropertydescriptors": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", - "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", - "dev": true, - "requires": { - "define-properties": "1.1.2", - "es-abstract": "1.10.0" - } - }, - "object.omit": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", - "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", - "dev": true, - "requires": { - "for-own": "0.1.5", - "is-extendable": "0.1.1" - } - }, - "object.values": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.0.4.tgz", - "integrity": "sha1-5STaCbT2b/Bd9FdUbscqyZ8TBpo=", - "dev": true, - "requires": { - "define-properties": "1.1.2", - "es-abstract": "1.10.0", - "function-bind": "1.1.1", - "has": "1.0.1" - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1.0.2" - } - }, - "onetime": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", - "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", - "dev": true - }, - "optimist": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", - "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", - "dev": true, - "requires": { - "minimist": "0.0.8", - "wordwrap": "0.0.3" - } - }, - "optionator": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", - "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", - "dev": true, - "requires": { - "deep-is": "0.1.3", - "fast-levenshtein": "2.0.6", - "levn": "0.3.0", - "prelude-ls": "1.1.2", - "type-check": "0.3.2", - "wordwrap": "1.0.0" - }, - "dependencies": { - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", - "dev": true - } - } - }, - "ora": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/ora/-/ora-0.2.3.tgz", - "integrity": "sha1-N1J9Igrc1Tw5tzVx11QVbV22V6Q=", - "dev": true, - "requires": { - "chalk": "1.1.3", - "cli-cursor": "1.0.2", - "cli-spinners": "0.1.2", - "object-assign": "4.1.1" - } - }, - "os-browserify": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", - "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", - "dev": true - }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "dev": true - }, - "os-locale": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz", - "integrity": "sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==", - "requires": { - "execa": "0.7.0", - "lcid": "1.0.0", - "mem": "1.1.0" - } - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "dev": true - }, - "osenv": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.4.tgz", - "integrity": "sha1-Qv5tWVPfBsgGS+bxdsPQWqqjRkQ=", - "dev": true, - "requires": { - "os-homedir": "1.0.2", - "os-tmpdir": "1.0.2" - } - }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" - }, - "p-limit": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.2.0.tgz", - "integrity": "sha512-Y/OtIaXtUPr4/YpMv1pCL5L5ed0rumAaAeBSj12F+bSlMdys7i8oQF/GUJmfpTS/QoaRrS/k6pma29haJpsMng==", - "requires": { - "p-try": "1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "requires": { - "p-limit": "1.2.0" - } - }, - "p-map": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-1.2.0.tgz", - "integrity": "sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA==", - "dev": true - }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=" - }, - "pako": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.6.tgz", - "integrity": "sha512-lQe48YPsMJAig+yngZ87Lus+NF+3mtu7DVOBu6b/gHO1YpKwIj5AWjZ/TOS7i46HD/UixzWb1zeWDZfGZ3iYcg==", - "dev": true - }, - "parse-asn1": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.0.tgz", - "integrity": "sha1-N8T5t+06tlx0gXtfJICTf7+XxxI=", - "dev": true, - "requires": { - "asn1.js": "4.9.2", - "browserify-aes": "1.1.1", - "create-hash": "1.1.3", - "evp_bytestokey": "1.0.3", - "pbkdf2": "3.0.14" - } - }, - "parse-glob": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", - "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", - "dev": true, - "requires": { - "glob-base": "0.3.0", - "is-dotfile": "1.0.3", - "is-extglob": "1.0.0", - "is-glob": "2.0.1" - } - }, - "parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", - "requires": { - "error-ex": "1.3.1" - } - }, - "parse5": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-3.0.3.tgz", - "integrity": "sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA==", - "dev": true, - "requires": { - "@types/node": "9.4.6" - } - }, - "path-browserify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz", - "integrity": "sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo=", - "dev": true - }, - "path-exists": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", - "requires": { - "pinkie-promise": "2.0.1" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - }, - "path-is-inside": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", - "dev": true - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" - }, - "path-parse": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", - "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=" - }, - "path-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", - "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", - "requires": { - "pify": "2.3.0" - } - }, - "pbkdf2": { - "version": "3.0.14", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.14.tgz", - "integrity": "sha512-gjsZW9O34fm0R7PaLHRJmLLVfSoesxztjPjE9o6R+qtVJij90ltg1joIovN9GKrRW3t1PzhDDG3UMEMFfZ+1wA==", - "dev": true, - "requires": { - "create-hash": "1.1.3", - "create-hmac": "1.1.6", - "ripemd160": "2.0.1", - "safe-buffer": "5.1.1", - "sha.js": "2.4.10" - } - }, - "pegjs": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/pegjs/-/pegjs-0.10.0.tgz", - "integrity": "sha1-z4uvrm7d/0tafvsYUmnqr0YQ3b0=", - "dev": true - }, - "pegjs-loader": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/pegjs-loader/-/pegjs-loader-0.5.4.tgz", - "integrity": "sha512-ViH8WwUkc/N8H59zuarORrgCi7uxn+gDIq+Ydriw1GFJi/oUg2xvhsgDDujO6dAxRsxXMgqWESx6TKYIqHorqA==", - "dev": true, - "requires": { - "loader-utils": "0.2.17" - }, - "dependencies": { - "loader-utils": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz", - "integrity": "sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=", - "dev": true, - "requires": { - "big.js": "3.2.0", - "emojis-list": "2.1.0", - "json5": "0.5.1", - "object-assign": "4.1.1" - } - } - } - }, - "pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", - "dev": true - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", - "dev": true - }, - "phpegjs": { - "version": "1.0.0-beta7", - "resolved": "https://registry.npmjs.org/phpegjs/-/phpegjs-1.0.0-beta7.tgz", - "integrity": "sha1-uLbthQGYB//Q7+ID4AKj5e2LTZQ=", - "dev": true - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" - }, - "pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" - }, - "pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", - "requires": { - "pinkie": "2.0.4" - } - }, - "pkg-dir": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-1.0.0.tgz", - "integrity": "sha1-ektQio1bstYp1EcFb/TpyTFM89Q=", - "requires": { - "find-up": "1.1.2" - } - }, - "pluralize": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-7.0.0.tgz", - "integrity": "sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow==", - "dev": true - }, - "pn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", - "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==", - "dev": true - }, - "popper.js": { - "version": "1.12.9", - "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.12.9.tgz", - "integrity": "sha1-DfvC3/lsRRuzMu3Pz6r1ZtMx1bM=" - }, - "postcss": { - "version": "5.2.18", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", - "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", - "dev": true, - "requires": { - "chalk": "1.1.3", - "js-base64": "2.4.3", - "source-map": "0.5.7", - "supports-color": "3.2.3" - }, - "dependencies": { - "has-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", - "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", - "dev": true - }, - "supports-color": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", - "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", - "dev": true, - "requires": { - "has-flag": "1.0.0" - } - } - } - }, - "postcss-calc": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-5.3.1.tgz", - "integrity": "sha1-d7rnypKK2FcW4v2kLyYb98HWW14=", - "dev": true, - "requires": { - "postcss": "5.2.18", - "postcss-message-helpers": "2.0.0", - "reduce-css-calc": "1.3.0" - } - }, - "postcss-colormin": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-2.2.2.tgz", - "integrity": "sha1-ZjFBfV8OkJo9fsJrJMio0eT5bks=", - "dev": true, - "requires": { - "colormin": "1.1.2", - "postcss": "5.2.18", - "postcss-value-parser": "3.3.0" - } - }, - "postcss-convert-values": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-2.6.1.tgz", - "integrity": "sha1-u9hZPFwf0uPRwyK7kl3K6Nrk1i0=", - "dev": true, - "requires": { - "postcss": "5.2.18", - "postcss-value-parser": "3.3.0" - } - }, - "postcss-discard-comments": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-2.0.4.tgz", - "integrity": "sha1-vv6J+v1bPazlzM5Rt2uBUUvgDj0=", - "dev": true, - "requires": { - "postcss": "5.2.18" - } - }, - "postcss-discard-duplicates": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-2.1.0.tgz", - "integrity": "sha1-uavye4isGIFYpesSq8riAmO5GTI=", - "dev": true, - "requires": { - "postcss": "5.2.18" - } - }, - "postcss-discard-empty": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-2.1.0.tgz", - "integrity": "sha1-0rS9nVztXr2Nyt52QMfXzX9PkrU=", - "dev": true, - "requires": { - "postcss": "5.2.18" - } - }, - "postcss-discard-overridden": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-0.1.1.tgz", - "integrity": "sha1-ix6vVU9ob7KIzYdMVWZ7CqNmjVg=", - "dev": true, - "requires": { - "postcss": "5.2.18" - } - }, - "postcss-discard-unused": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/postcss-discard-unused/-/postcss-discard-unused-2.2.3.tgz", - "integrity": "sha1-vOMLLMWR/8Y0Mitfs0ZLbZNPRDM=", - "dev": true, - "requires": { - "postcss": "5.2.18", - "uniqs": "2.0.0" - } - }, - "postcss-filter-plugins": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/postcss-filter-plugins/-/postcss-filter-plugins-2.0.2.tgz", - "integrity": "sha1-bYWGJTTXNaxCDkqFgG4fXUKG2Ew=", - "dev": true, - "requires": { - "postcss": "5.2.18", - "uniqid": "4.1.1" - } - }, - "postcss-load-config": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-1.2.0.tgz", - "integrity": "sha1-U56a/J3chiASHr+djDZz4M5Q0oo=", - "dev": true, - "requires": { - "cosmiconfig": "2.2.2", - "object-assign": "4.1.1", - "postcss-load-options": "1.2.0", - "postcss-load-plugins": "2.3.0" - } - }, - "postcss-load-options": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/postcss-load-options/-/postcss-load-options-1.2.0.tgz", - "integrity": "sha1-sJixVZ3awt8EvAuzdfmaXP4rbYw=", - "dev": true, - "requires": { - "cosmiconfig": "2.2.2", - "object-assign": "4.1.1" - } - }, - "postcss-load-plugins": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/postcss-load-plugins/-/postcss-load-plugins-2.3.0.tgz", - "integrity": "sha1-dFdoEWWZrKLwCfrUJrABdQSdjZI=", - "dev": true, - "requires": { - "cosmiconfig": "2.2.2", - "object-assign": "4.1.1" - } - }, - "postcss-loader": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-2.0.6.tgz", - "integrity": "sha512-HIq7yy1hh9KI472Y38iSRV4WupZUNy6zObkxQM/ZuInoaE2+PyX4NcO6jjP5HG5mXL7j5kcNEl0fAG4Kva7O9w==", - "dev": true, - "requires": { - "loader-utils": "1.1.0", - "postcss": "6.0.17", - "postcss-load-config": "1.2.0", - "schema-utils": "0.3.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", - "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", - "dev": true, - "requires": { - "color-convert": "1.9.1" - } - }, - "chalk": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", - "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", - "dev": true, - "requires": { - "ansi-styles": "3.2.0", - "escape-string-regexp": "1.0.5", - "supports-color": "4.5.0" - }, - "dependencies": { - "supports-color": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", - "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", - "dev": true, - "requires": { - "has-flag": "2.0.0" - } - } - } - }, - "postcss": { - "version": "6.0.17", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.17.tgz", - "integrity": "sha512-Bl1nybsSzWYbP8O4gAVD8JIjZIul9hLNOPTGBIlVmZNUnNAGL+W0cpYWzVwfImZOwumct4c1SDvSbncVWKtXUw==", - "dev": true, - "requires": { - "chalk": "2.3.0", - "source-map": "0.6.1", - "supports-color": "5.1.0" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "supports-color": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.1.0.tgz", - "integrity": "sha512-Ry0AwkoKjDpVKK4sV4h6o3UJmNRbjYm2uXhwfj3J56lMVdvnUNqzQVRztOOMGQ++w1K/TjNDFvpJk0F/LoeBCQ==", - "dev": true, - "requires": { - "has-flag": "2.0.0" - } - } - } - }, - "postcss-merge-idents": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/postcss-merge-idents/-/postcss-merge-idents-2.1.7.tgz", - "integrity": "sha1-TFUwMTwI4dWzu/PSu8dH4njuonA=", - "dev": true, - "requires": { - "has": "1.0.1", - "postcss": "5.2.18", - "postcss-value-parser": "3.3.0" - } - }, - "postcss-merge-longhand": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-2.0.2.tgz", - "integrity": "sha1-I9kM0Sewp3mUkVMyc5A0oaTz1lg=", - "dev": true, - "requires": { - "postcss": "5.2.18" - } - }, - "postcss-merge-rules": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-2.1.2.tgz", - "integrity": "sha1-0d9d+qexrMO+VT8OnhDofGG19yE=", - "dev": true, - "requires": { - "browserslist": "1.7.7", - "caniuse-api": "1.6.1", - "postcss": "5.2.18", - "postcss-selector-parser": "2.2.3", - "vendors": "1.0.1" - }, - "dependencies": { - "browserslist": { - "version": "1.7.7", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-1.7.7.tgz", - "integrity": "sha1-C9dnBCWL6CmyOYu1Dkti0aFmsLk=", - "dev": true, - "requires": { - "caniuse-db": "1.0.30000804", - "electron-to-chromium": "1.3.33" - } - } - } - }, - "postcss-message-helpers": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postcss-message-helpers/-/postcss-message-helpers-2.0.0.tgz", - "integrity": "sha1-pPL0+rbk/gAvCu0ABHjN9S+bpg4=", - "dev": true - }, - "postcss-minify-font-values": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-1.0.5.tgz", - "integrity": "sha1-S1jttWZB66fIR0qzUmyv17vey2k=", - "dev": true, - "requires": { - "object-assign": "4.1.1", - "postcss": "5.2.18", - "postcss-value-parser": "3.3.0" - } - }, - "postcss-minify-gradients": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-1.0.5.tgz", - "integrity": "sha1-Xb2hE3NwP4PPtKPqOIHY11/15uE=", - "dev": true, - "requires": { - "postcss": "5.2.18", - "postcss-value-parser": "3.3.0" - } - }, - "postcss-minify-params": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-1.2.2.tgz", - "integrity": "sha1-rSzgcTc7lDs9kwo/pZo1jCjW8fM=", - "dev": true, - "requires": { - "alphanum-sort": "1.0.2", - "postcss": "5.2.18", - "postcss-value-parser": "3.3.0", - "uniqs": "2.0.0" - } - }, - "postcss-minify-selectors": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-2.1.1.tgz", - "integrity": "sha1-ssapjAByz5G5MtGkllCBFDEXNb8=", - "dev": true, - "requires": { - "alphanum-sort": "1.0.2", - "has": "1.0.1", - "postcss": "5.2.18", - "postcss-selector-parser": "2.2.3" - } - }, - "postcss-normalize-charset": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-1.1.1.tgz", - "integrity": "sha1-757nEhLX/nWceO0WL2HtYrXLk/E=", - "dev": true, - "requires": { - "postcss": "5.2.18" - } - }, - "postcss-normalize-url": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-3.0.8.tgz", - "integrity": "sha1-EI90s/L82viRov+j6kWSJ5/HgiI=", - "dev": true, - "requires": { - "is-absolute-url": "2.1.0", - "normalize-url": "1.9.1", - "postcss": "5.2.18", - "postcss-value-parser": "3.3.0" - } - }, - "postcss-ordered-values": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-2.2.3.tgz", - "integrity": "sha1-7sbCpntsQSqNsgQud/6NpD+VwR0=", - "dev": true, - "requires": { - "postcss": "5.2.18", - "postcss-value-parser": "3.3.0" - } - }, - "postcss-reduce-idents": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/postcss-reduce-idents/-/postcss-reduce-idents-2.4.0.tgz", - "integrity": "sha1-wsbSDMlYKE9qv75j92Cb9AkFmtM=", - "dev": true, - "requires": { - "postcss": "5.2.18", - "postcss-value-parser": "3.3.0" - } - }, - "postcss-reduce-initial": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-1.0.1.tgz", - "integrity": "sha1-aPgGlfBF0IJjqHmtJA343WT2ROo=", - "dev": true, - "requires": { - "postcss": "5.2.18" - } - }, - "postcss-reduce-transforms": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-1.0.4.tgz", - "integrity": "sha1-/3b02CEkN7McKYpC0uFEQCV3GuE=", - "dev": true, - "requires": { - "has": "1.0.1", - "postcss": "5.2.18", - "postcss-value-parser": "3.3.0" - } - }, - "postcss-selector-parser": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-2.2.3.tgz", - "integrity": "sha1-+UN3iGBsPJrO4W/+jYsWKX8nu5A=", - "dev": true, - "requires": { - "flatten": "1.0.2", - "indexes-of": "1.0.1", - "uniq": "1.0.1" - } - }, - "postcss-svgo": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-2.1.6.tgz", - "integrity": "sha1-tt8YqmE7Zm4TPwittSGcJoSsEI0=", - "dev": true, - "requires": { - "is-svg": "2.1.0", - "postcss": "5.2.18", - "postcss-value-parser": "3.3.0", - "svgo": "0.7.2" - } - }, - "postcss-unique-selectors": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-2.0.2.tgz", - "integrity": "sha1-mB1X0p3csz57Hf4f1DuGSfkzyh0=", - "dev": true, - "requires": { - "alphanum-sort": "1.0.2", - "postcss": "5.2.18", - "uniqs": "2.0.0" - } - }, - "postcss-value-parser": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz", - "integrity": "sha1-h/OPnxj3dKSrTIojL1xc6IcqnRU=", - "dev": true - }, - "postcss-zindex": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/postcss-zindex/-/postcss-zindex-2.2.0.tgz", - "integrity": "sha1-0hCd3AVbka9n/EyzsCWUZjnSryI=", - "dev": true, - "requires": { - "has": "1.0.1", - "postcss": "5.2.18", - "uniqs": "2.0.0" - } - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", - "dev": true - }, - "prepend-http": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", - "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", - "dev": true - }, - "preserve": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", - "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=", - "dev": true - }, - "pretty-format": { - "version": "22.4.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-22.4.0.tgz", - "integrity": "sha512-pvCxP2iODIIk9adXlo4S3GRj0BrJiil68kByAa1PrgG97c1tClh9dLMgp3Z6cHFZrclaABt0UH8PIhwHuFLqYA==", - "dev": true, - "requires": { - "ansi-regex": "3.0.0", - "ansi-styles": "3.2.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 - }, - "ansi-styles": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", - "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", - "dev": true, - "requires": { - "color-convert": "1.9.1" - } - } - } - }, - "prismjs": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.6.0.tgz", - "integrity": "sha1-EY2V+3pm26InLjQ7NF9SNmWds2U=", - "dev": true, - "requires": { - "clipboard": "1.7.1" - } - }, - "private": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", - "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==", - "dev": true - }, - "process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", - "dev": true - }, - "process-nextick-args": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", - "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" - }, - "progress": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz", - "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=", - "dev": true - }, - "promise": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", - "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", - "requires": { - "asap": "2.0.6" - } - }, - "prop-types": { - "version": "15.5.10", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.5.10.tgz", - "integrity": "sha1-J5ffwxJhguOpXj37suiT3ddFYVQ=", - "requires": { - "fbjs": "0.8.16", - "loose-envify": "1.3.1" - } - }, - "proto-list": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", - "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=" - }, - "prr": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", - "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", - "dev": true - }, - "pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" - }, - "public-encrypt": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.0.tgz", - "integrity": "sha1-OfaZ86RlYN1eusvKaTyvfGXBjMY=", - "dev": true, - "requires": { - "bn.js": "4.11.8", - "browserify-rsa": "4.0.1", - "create-hash": "1.1.3", - "parse-asn1": "5.1.0", - "randombytes": "2.0.6" - } - }, - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", - "dev": true - }, - "q": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", - "dev": true - }, - "qs": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", - "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==", - "dev": true - }, - "query-string": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", - "integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=", - "dev": true, - "requires": { - "object-assign": "4.1.1", - "strict-uri-encode": "1.1.0" - } - }, - "querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", - "dev": true - }, - "querystring-es3": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", - "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=" - }, - "raf": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.0.tgz", - "integrity": "sha512-pDP/NMRAXoTfrhCfyfSEwJAKLaxBU9eApMeBPB1TkDouZmvPerIClV8lTAd+uF8ZiTaVl69e1FCxQrAd/VTjGw==", - "dev": true, - "requires": { - "performance-now": "2.1.0" - } - }, - "railroad-diagrams": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz", - "integrity": "sha1-635iZ1SN3t+4mcG5Dlc3RVnN234=", - "dev": true - }, - "ramda": { - "version": "0.24.1", - "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.24.1.tgz", - "integrity": "sha1-w7d1UZfzW43DUCIoJixMkd22uFc=", - "dev": true - }, - "randexp": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz", - "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==", - "dev": true, - "requires": { - "discontinuous-range": "1.0.0", - "ret": "0.1.15" - } - }, - "randomatic": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz", - "integrity": "sha512-D5JUjPyJbaJDkuAazpVnSfVkLlpeO3wDlPROTMLGKG1zMFNFRgrciKo1ltz/AzNTkqE0HzDx655QOL51N06how==", - "dev": true, - "requires": { - "is-number": "3.0.0", - "kind-of": "4.0.0" - }, - "dependencies": { - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "3.2.2" - }, - "dependencies": { - "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.6" - } - } - } - }, - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "dev": true, - "requires": { - "is-buffer": "1.1.6" - } - } - } - }, - "randombytes": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.6.tgz", - "integrity": "sha512-CIQ5OFxf4Jou6uOKe9t1AOgqpeU5fd70A8NPdHSGeYXqXsPe6peOwI0cUl88RWZ6sP1vPMV3avd/R6cZ5/sP1A==", - "dev": true, - "requires": { - "safe-buffer": "5.1.1" - } - }, - "randomfill": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.3.tgz", - "integrity": "sha512-YL6GrhrWoic0Eq8rXVbMptH7dAxCs0J+mh5Y0euNekPPYaxEmdVGim6GdoxoRzKW2yJoU8tueifS7mYxvcFDEQ==", - "dev": true, - "requires": { - "randombytes": "2.0.6", - "safe-buffer": "5.1.1" - } - }, - "raw-loader": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-0.5.1.tgz", - "integrity": "sha1-DD0L6u2KAclm2Xh793goElKpeao=", - "dev": true - }, - "re-resizable": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/re-resizable/-/re-resizable-4.0.3.tgz", - "integrity": "sha512-6YpsC4JFT7zVG8/8gIXxdnrlHdz64/H7dpjHgXNCy5kwy2DIG1dVbdgASNUTwy4AaHgI8rvjJJzr2BxZAVu/3Q==" - }, - "react": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-16.2.0.tgz", - "integrity": "sha512-ZmIomM7EE1DvPEnSFAHZn9Vs9zJl5A9H7el0EGTE6ZbW9FKe/14IYAlPbC8iH25YarEQxZL+E8VW7Mi7kfQrDQ==", - "requires": { - "fbjs": "0.8.16", - "loose-envify": "1.3.1", - "object-assign": "4.1.1", - "prop-types": "15.6.0" - }, - "dependencies": { - "prop-types": { - "version": "15.6.0", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.0.tgz", - "integrity": "sha1-zq8IMCL8RrSjX2nhPvda7Q1jmFY=", - "requires": { - "fbjs": "0.8.16", - "loose-envify": "1.3.1", - "object-assign": "4.1.1" - } - } - } - }, - "react-autosize-textarea": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/react-autosize-textarea/-/react-autosize-textarea-2.0.0.tgz", - "integrity": "sha1-fDDSqktzic4WvkLtJj4utGwysiE=", - "requires": { - "@types/autosize": "3.0.6", - "@types/prop-types": "15.5.2", - "@types/react": "15.6.7", - "autosize": "4.0.0", - "line-height": "0.3.1", - "prop-types": "15.5.10" - } - }, - "react-click-outside": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/react-click-outside/-/react-click-outside-2.3.1.tgz", - "integrity": "sha1-MYc3698IGko7zUaCVmNnTL6YNus=", - "requires": { - "hoist-non-react-statics": "1.2.0" - } - }, - "react-color": { - "version": "2.13.4", - "resolved": "https://registry.npmjs.org/react-color/-/react-color-2.13.4.tgz", - "integrity": "sha512-rNJTTxMPTImI1NpFaKLggDIvHgKOYRXj0krVh8c+Mo1YNsrLko8O94yiFqqdnSQgtIPteiAcGEJgBo9V5+uqaw==", - "requires": { - "lodash": "4.17.4", - "material-colors": "1.2.5", - "prop-types": "15.5.10", - "reactcss": "1.2.3", - "tinycolor2": "1.4.1" - } - }, - "react-datepicker": { - "version": "0.61.0", - "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-0.61.0.tgz", - "integrity": "sha512-FJnqJCbYaU1ca7Jn9LaJ7iKTYHKAskVkVHCsOBDQd8vJZrESLAu3rtPbj3T6IB++dQO7qb0IlcCXtmC0geIAGA==", - "requires": { - "classnames": "2.2.5", - "eslint-plugin-import": "2.8.0", - "eslint-plugin-node": "5.2.1", - "moment": "2.20.1", - "prop-types": "15.6.0", - "react-onclickoutside": "6.7.1", - "react-popper": "0.7.5" - }, - "dependencies": { - "eslint-plugin-node": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-5.2.1.tgz", - "integrity": "sha512-xhPXrh0Vl/b7870uEbaumb2Q+LxaEcOQ3kS1jtIXanBAwpMre1l5q/l2l/hESYJGEFKuI78bp6Uw50hlpr7B+g==", - "requires": { - "ignore": "3.3.7", - "minimatch": "3.0.4", - "resolve": "1.5.0", - "semver": "5.3.0" - } - }, - "moment": { - "version": "2.20.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.20.1.tgz", - "integrity": "sha512-Yh9y73JRljxW5QxN08Fner68eFLxM5ynNOAw2LbIB1YAGeQzZT8QFSUvkAz609Zf+IHhhaUxqZK8dG3W/+HEvg==" - }, - "prop-types": { - "version": "15.6.0", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.0.tgz", - "integrity": "sha1-zq8IMCL8RrSjX2nhPvda7Q1jmFY=", - "requires": { - "fbjs": "0.8.16", - "loose-envify": "1.3.1", - "object-assign": "4.1.1" - } - } - } - }, - "react-dom": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.2.0.tgz", - "integrity": "sha512-zpGAdwHVn9K0091d+hr+R0qrjoJ84cIBFL2uU60KvWBPfZ7LPSrfqviTxGHWN0sjPZb2hxWzMexwrvJdKePvjg==", - "requires": { - "fbjs": "0.8.16", - "loose-envify": "1.3.1", - "object-assign": "4.1.1", - "prop-types": "15.6.0" - }, - "dependencies": { - "prop-types": { - "version": "15.6.0", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.0.tgz", - "integrity": "sha1-zq8IMCL8RrSjX2nhPvda7Q1jmFY=", - "requires": { - "fbjs": "0.8.16", - "loose-envify": "1.3.1", - "object-assign": "4.1.1" - } - } - } - }, - "react-onclickoutside": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.7.1.tgz", - "integrity": "sha512-p84kBqGaMoa7VYT0vZ/aOYRfJB+gw34yjpda1Z5KeLflg70HipZOT+MXQenEhdkPAABuE2Astq4zEPdMqUQxcg==" - }, - "react-popper": { - "version": "0.7.5", - "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-0.7.5.tgz", - "integrity": "sha512-ya9dhhGCf74JTOB2uyksEHhIGw7w9tNZRUJF73lEq2h4H5JT6MBa4PdT4G+sx6fZwq+xKZAL/sVNAIuojPn7Dg==", - "requires": { - "popper.js": "1.12.9", - "prop-types": "15.5.10" - } - }, - "react-reconciler": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.7.0.tgz", - "integrity": "sha512-50JwZ3yNyMS8fchN+jjWEJOH3Oze7UmhxeoJLn2j6f3NjpfCRbcmih83XTWmzqtar/ivd5f7tvQhvvhism2fgg==", - "dev": true, - "requires": { - "fbjs": "0.8.16", - "loose-envify": "1.3.1", - "object-assign": "4.1.1", - "prop-types": "15.6.0" - }, - "dependencies": { - "prop-types": { - "version": "15.6.0", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.0.tgz", - "integrity": "sha1-zq8IMCL8RrSjX2nhPvda7Q1jmFY=", - "dev": true, - "requires": { - "fbjs": "0.8.16", - "loose-envify": "1.3.1", - "object-assign": "4.1.1" - } - } - } - }, - "react-redux": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-5.0.6.tgz", - "integrity": "sha512-8taaaGu+J7PMJQDJrk/xiWEYQmdo3mkXw6wPr3K3LxvXis3Fymiq7c13S+Tpls/AyNUAsoONkU81AP0RA6y6Vw==", - "requires": { - "hoist-non-react-statics": "2.3.1", - "invariant": "2.2.2", - "lodash": "4.17.4", - "lodash-es": "4.17.5", - "loose-envify": "1.3.1", - "prop-types": "15.5.10" - }, - "dependencies": { - "hoist-non-react-statics": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.3.1.tgz", - "integrity": "sha1-ND24TGAYxlB3iJgkATWhQg7iLOA=" - } - } - }, - "react-test-renderer": { - "version": "16.0.0", - "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.0.0.tgz", - "integrity": "sha1-n+e4MI8vcfKfw1bUECCG8THJyxU=", - "dev": true, - "requires": { - "fbjs": "0.8.16", - "object-assign": "4.1.1" - } - }, - "reactcss": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz", - "integrity": "sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A==", - "requires": { - "lodash": "4.17.4" - } - }, - "read-pkg": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", - "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", - "requires": { - "load-json-file": "2.0.0", - "normalize-package-data": "2.4.0", - "path-type": "2.0.0" - } - }, - "read-pkg-up": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", - "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", - "requires": { - "find-up": "2.1.0", - "read-pkg": "2.0.0" - }, - "dependencies": { - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "requires": { - "locate-path": "2.0.0" - } - } - } - }, - "readable-stream": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", - "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "1.0.7", - "safe-buffer": "5.1.1", - "string_decoder": "1.0.3", - "util-deprecate": "1.0.2" - } - }, - "readdirp": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz", - "integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=", - "dev": true, - "requires": { - "graceful-fs": "4.1.11", - "minimatch": "3.0.4", - "readable-stream": "2.3.3", - "set-immediate-shim": "1.0.1" - } - }, - "realpath-native": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/realpath-native/-/realpath-native-1.0.0.tgz", - "integrity": "sha512-XJtlRJ9jf0E1H1SLeJyQ9PGzQD7S65h1pRXEcAeK48doKOnKxcgPeNohJvD5u/2sI9J1oke6E8bZHS/fmW1UiQ==", - "dev": true, - "requires": { - "util.promisify": "1.0.0" - } - }, - "redent": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", - "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", - "dev": true, - "requires": { - "indent-string": "2.1.0", - "strip-indent": "1.0.1" - } - }, - "reduce-css-calc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz", - "integrity": "sha1-dHyRTgSWFKTJz7umKYca0dKSdxY=", - "dev": true, - "requires": { - "balanced-match": "0.4.2", - "math-expression-evaluator": "1.2.17", - "reduce-function-call": "1.0.2" - }, - "dependencies": { - "balanced-match": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", - "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=", - "dev": true - } - } - }, - "reduce-function-call": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/reduce-function-call/-/reduce-function-call-1.0.2.tgz", - "integrity": "sha1-WiAL+S4ON3UXUv5FsKszD9S2vpk=", - "dev": true, - "requires": { - "balanced-match": "0.4.2" - }, - "dependencies": { - "balanced-match": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", - "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=", - "dev": true - } - } - }, - "redux": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/redux/-/redux-3.7.2.tgz", - "integrity": "sha512-pNqnf9q1hI5HHZRBkj3bAngGZW/JMCmexDlOxw4XagXY2o1327nHH54LoTjiPJ0gizoqPDRqWyX/00g0hD6w+A==", - "requires": { - "lodash": "4.17.4", - "lodash-es": "4.17.5", - "loose-envify": "1.3.1", - "symbol-observable": "1.2.0" - } - }, - "redux-multi": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/redux-multi/-/redux-multi-0.1.12.tgz", - "integrity": "sha1-KOH+XklnLLxb2KB/Cyrq8O+DVcI=" - }, - "redux-optimist": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/redux-optimist/-/redux-optimist-0.0.2.tgz", - "integrity": "sha1-cNoX6GPFOoYE1YHKIKJpCL2haX4=" - }, - "refx": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/refx/-/refx-3.0.0.tgz", - "integrity": "sha512-qmd73YvYiVWfKPECtE90ujmPwwtAnmtEOkBKgfNEuqJ4trTeKbqFV2UY878yFvHBvU7BBu4/w/Q8pk/t0zDpYA==" - }, - "regenerate": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.3.3.tgz", - "integrity": "sha512-jVpo1GadrDAK59t/0jRx5VxYWQEDkkEKi6+HjE3joFVLfDOh9Xrdh0dF1eSq+BI/SwvTQ44gSscJ8N5zYL61sg==", - "dev": true - }, - "regenerator-runtime": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", - "dev": true - }, - "regenerator-transform": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.10.1.tgz", - "integrity": "sha512-PJepbvDbuK1xgIgnau7Y90cwaAmO/LCLMI2mPvaXq2heGMR3aWW5/BQvYrhJ8jgmQjXewXvBjzfqKcVOmhjZ6Q==", - "dev": true, - "requires": { - "babel-runtime": "6.26.0", - "babel-types": "6.26.0", - "private": "0.1.8" - } - }, - "regex-cache": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", - "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", - "dev": true, - "requires": { - "is-equal-shallow": "0.1.3" - } - }, - "regexpu-core": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-2.0.0.tgz", - "integrity": "sha1-SdA4g3uNz4v6W5pCE5k45uoq4kA=", - "dev": true, - "requires": { - "regenerate": "1.3.3", - "regjsgen": "0.2.0", - "regjsparser": "0.1.5" - } - }, - "regjsgen": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", - "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=", - "dev": true - }, - "regjsparser": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", - "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=", - "dev": true, - "requires": { - "jsesc": "0.5.0" - } - }, - "rememo": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/rememo/-/rememo-2.4.0.tgz", - "integrity": "sha512-4rqlLATPcha9lfdvylUWqSbceiTlYiBJvEJAyUiT/68cYPlNG1zXf7ABeve7s4YPrT6o3Q6zfN6n38ecAL71lw==", - "requires": { - "shallow-equal": "1.0.0" - } - }, - "remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", - "dev": true - }, - "repeat-element": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", - "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=", - "dev": true - }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", - "dev": true - }, - "repeating": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", - "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", - "dev": true, - "requires": { - "is-finite": "1.0.2" - } - }, - "request": { - "version": "2.83.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.83.0.tgz", - "integrity": "sha512-lR3gD69osqm6EYLk9wB/G1W/laGWjzH90t1vEa2xuxHD5KUrSzp9pUSfTm+YC5Nxt2T8nMPEvKlhbQayU7bgFw==", - "dev": true, - "requires": { - "aws-sign2": "0.7.0", - "aws4": "1.6.0", - "caseless": "0.12.0", - "combined-stream": "1.0.5", - "extend": "3.0.1", - "forever-agent": "0.6.1", - "form-data": "2.3.1", - "har-validator": "5.0.3", - "hawk": "6.0.2", - "http-signature": "1.2.0", - "is-typedarray": "1.0.0", - "isstream": "0.1.2", - "json-stringify-safe": "5.0.1", - "mime-types": "2.1.17", - "oauth-sign": "0.8.2", - "performance-now": "2.1.0", - "qs": "6.5.1", - "safe-buffer": "5.1.1", - "stringstream": "0.0.5", - "tough-cookie": "2.3.3", - "tunnel-agent": "0.6.0", - "uuid": "3.1.0" - } - }, - "request-progress": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-0.3.1.tgz", - "integrity": "sha1-ByHBBdipasayzossia4tXs/Pazo=", - "dev": true, - "requires": { - "throttleit": "0.0.2" - } - }, - "request-promise-core": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.1.tgz", - "integrity": "sha1-Pu4AssWqgyOc+wTFcA2jb4HNCLY=", - "dev": true, - "requires": { - "lodash": "4.17.4" - } - }, - "request-promise-native": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.5.tgz", - "integrity": "sha1-UoF3D2jgyXGeUWP9P6tIIhX0/aU=", - "dev": true, - "requires": { - "request-promise-core": "1.1.1", - "stealthy-require": "1.1.1", - "tough-cookie": "2.3.3" - } - }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" - }, - "require-from-string": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-1.2.1.tgz", - "integrity": "sha1-UpyczvJzgK3+yaL5ZbZJu+5jZBg=", - "dev": true - }, - "require-main-filename": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=" - }, - "require-package-name": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/require-package-name/-/require-package-name-2.0.1.tgz", - "integrity": "sha1-wR6XJ2tluOKSP3Xav1+y7ww4Qbk=", - "dev": true - }, - "require-uncached": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", - "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", - "dev": true, - "requires": { - "caller-path": "0.1.0", - "resolve-from": "1.0.1" - }, - "dependencies": { - "resolve-from": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", - "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", - "dev": true - } - } - }, - "requireindex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.1.0.tgz", - "integrity": "sha1-5UBLgVV+91225JxacgBIk/4D4WI=" - }, - "resolve": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.5.0.tgz", - "integrity": "sha512-hgoSGrc3pjzAPHNBg+KnFcK2HwlHTs/YrAGUr6qgTVUZmXv1UEXXl0bZNBKMA9fud6lRYFdPGz0xXxycPzmmiw==", - "requires": { - "path-parse": "1.0.5" - } - }, - "resolve-cwd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", - "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", - "dev": true, - "requires": { - "resolve-from": "3.0.0" - } - }, - "resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", - "dev": true - }, - "restore-cursor": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", - "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", - "dev": true, - "requires": { - "exit-hook": "1.1.1", - "onetime": "1.1.0" - } - }, - "ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", - "dev": true - }, - "right-align": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", - "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", - "dev": true, - "requires": { - "align-text": "0.1.4" - } - }, - "rimraf": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", - "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", - "dev": true, - "requires": { - "glob": "7.1.2" - } - }, - "ripemd160": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.1.tgz", - "integrity": "sha1-D0WEKVxTo2KK9+bXmsohzlfRxuc=", - "dev": true, - "requires": { - "hash-base": "2.0.2", - "inherits": "2.0.3" - } - }, - "rst-selector-parser": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz", - "integrity": "sha1-gbIw6i/MYGbInjRy3nlChdmwPZE=", - "dev": true, - "requires": { - "lodash.flattendeep": "4.4.0", - "nearley": "2.11.1" - } - }, - "rtlcss": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-2.2.1.tgz", - "integrity": "sha512-JjQ5DlrmwiItAjlmhoxrJq5ihgZcE0wMFxt7S17bIrt4Lw0WwKKFk+viRhvodB/0falyG/5fiO043ZDh6/aqTw==", - "dev": true, - "requires": { - "chalk": "2.3.0", - "findup": "0.1.5", - "mkdirp": "0.5.1", - "postcss": "6.0.17", - "strip-json-comments": "2.0.1" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", - "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", - "dev": true, - "requires": { - "color-convert": "1.9.1" - } - }, - "chalk": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", - "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", - "dev": true, - "requires": { - "ansi-styles": "3.2.0", - "escape-string-regexp": "1.0.5", - "supports-color": "4.5.0" - } - }, - "postcss": { - "version": "6.0.17", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.17.tgz", - "integrity": "sha512-Bl1nybsSzWYbP8O4gAVD8JIjZIul9hLNOPTGBIlVmZNUnNAGL+W0cpYWzVwfImZOwumct4c1SDvSbncVWKtXUw==", - "dev": true, - "requires": { - "chalk": "2.3.0", - "source-map": "0.6.1", - "supports-color": "5.1.0" - }, - "dependencies": { - "supports-color": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.1.0.tgz", - "integrity": "sha512-Ry0AwkoKjDpVKK4sV4h6o3UJmNRbjYm2uXhwfj3J56lMVdvnUNqzQVRztOOMGQ++w1K/TjNDFvpJk0F/LoeBCQ==", - "dev": true, - "requires": { - "has-flag": "2.0.0" - } - } - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "supports-color": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", - "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", - "dev": true, - "requires": { - "has-flag": "2.0.0" - } - } - } - }, - "run-async": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", - "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", - "dev": true, - "requires": { - "is-promise": "2.1.0" - } - }, - "run-parallel": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.6.tgz", - "integrity": "sha1-KQA8miFj4B4tLfyQV18sbB1hoDk=", - "dev": true - }, - "rx": { - "version": "2.3.24", - "resolved": "https://registry.npmjs.org/rx/-/rx-2.3.24.tgz", - "integrity": "sha1-FPlQpCF9fjXapxu8vljv9o6ksrc=", - "dev": true - }, - "rx-lite": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-4.0.8.tgz", - "integrity": "sha1-Cx4Rr4vESDbwSmQH6S2kJGe3lEQ=", - "dev": true - }, - "rx-lite-aggregates": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz", - "integrity": "sha1-dTuHqJoRyVRnxKwWJsTvxOBcZ74=", - "dev": true, - "requires": { - "rx-lite": "4.0.8" - } - }, - "rxjs": { - "version": "5.5.6", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.6.tgz", - "integrity": "sha512-v4Q5HDC0FHAQ7zcBX7T2IL6O5ltl1a2GX4ENjPXg6SjDY69Cmx9v4113C99a4wGF16ClPv5Z8mghuYorVkg/kg==", - "dev": true, - "requires": { - "symbol-observable": "1.0.1" - }, - "dependencies": { - "symbol-observable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.1.tgz", - "integrity": "sha1-g0D8RwLDEi310iKI+IKD9RPT/dQ=", - "dev": true - } - } - }, - "safe-buffer": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" - }, - "sane": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/sane/-/sane-2.4.1.tgz", - "integrity": "sha512-fW9svvNd81XzHDZyis9/tEY1bZikDGryy8Hi1BErPyNPYv47CdLseUN+tI5FBHWXEENRtj1SWtX/jBnggLaP0w==", - "dev": true, - "requires": { - "anymatch": "1.3.2", - "exec-sh": "0.2.1", - "fb-watchman": "2.0.0", - "fsevents": "1.1.3", - "minimatch": "3.0.4", - "minimist": "1.2.0", - "walker": "1.0.7", - "watch": "0.18.0" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - } - } - }, - "sass-graph": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.4.tgz", - "integrity": "sha1-E/vWPNHK8JCLn9k0dq1DpR0eC0k=", - "dev": true, - "requires": { - "glob": "7.1.2", - "lodash": "4.17.4", - "scss-tokenizer": "0.2.3", - "yargs": "7.1.0" - }, - "dependencies": { - "camelcase": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", - "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", - "dev": true - }, - "load-json-file": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", - "dev": true, - "requires": { - "graceful-fs": "4.1.11", - "parse-json": "2.2.0", - "pify": "2.3.0", - "pinkie-promise": "2.0.1", - "strip-bom": "2.0.0" - } - }, - "os-locale": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", - "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", - "dev": true, - "requires": { - "lcid": "1.0.0" - } - }, - "path-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", - "dev": true, - "requires": { - "graceful-fs": "4.1.11", - "pify": "2.3.0", - "pinkie-promise": "2.0.1" - } - }, - "read-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", - "dev": true, - "requires": { - "load-json-file": "1.1.0", - "normalize-package-data": "2.4.0", - "path-type": "1.1.0" - } - }, - "read-pkg-up": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", - "dev": true, - "requires": { - "find-up": "1.1.2", - "read-pkg": "1.1.0" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - }, - "strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", - "dev": true, - "requires": { - "is-utf8": "0.2.1" - } - }, - "which-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", - "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=", - "dev": true - }, - "yargs": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz", - "integrity": "sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=", - "dev": true, - "requires": { - "camelcase": "3.0.0", - "cliui": "3.2.0", - "decamelize": "1.2.0", - "get-caller-file": "1.0.2", - "os-locale": "1.4.0", - "read-pkg-up": "1.0.1", - "require-directory": "2.1.1", - "require-main-filename": "1.0.1", - "set-blocking": "2.0.0", - "string-width": "1.0.2", - "which-module": "1.0.0", - "y18n": "3.2.1", - "yargs-parser": "5.0.0" - } - }, - "yargs-parser": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0.tgz", - "integrity": "sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo=", - "dev": true, - "requires": { - "camelcase": "3.0.0" - } - } - } - }, - "sass-loader": { - "version": "6.0.6", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-6.0.6.tgz", - "integrity": "sha512-c3/Zc+iW+qqDip6kXPYLEgsAu2lf4xz0EZDplB7EmSUMda12U1sGJPetH55B/j9eu0bTtKzKlNPWWyYC7wFNyQ==", - "dev": true, - "requires": { - "async": "2.6.0", - "clone-deep": "0.3.0", - "loader-utils": "1.1.0", - "lodash.tail": "4.1.1", - "pify": "3.0.0" - }, - "dependencies": { - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - } - } - }, - "sass-variables-loader": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/sass-variables-loader/-/sass-variables-loader-0.1.3.tgz", - "integrity": "sha1-TwwvYJzRVKobFmnct+/paYeGJDw=", - "dev": true - }, - "sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "dev": true - }, - "schema-utils": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.3.0.tgz", - "integrity": "sha1-9YdyIs4+kx7a4DnxfrNxbnE3+M8=", - "dev": true, - "requires": { - "ajv": "5.5.2" - } - }, - "scss-tokenizer": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz", - "integrity": "sha1-jrBtualyMzOCTT9VMGQRSYR85dE=", - "dev": true, - "requires": { - "js-base64": "2.4.3", - "source-map": "0.4.4" - }, - "dependencies": { - "source-map": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", - "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", - "dev": true, - "requires": { - "amdefine": "1.0.1" - } - } - } - }, - "select": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", - "integrity": "sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0=" - }, - "semver": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", - "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=" - }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" - }, - "set-immediate-shim": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", - "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=", - "dev": true - }, - "setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" - }, - "sha.js": { - "version": "2.4.10", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.10.tgz", - "integrity": "sha512-vnwmrFDlOExK4Nm16J2KMWHLrp14lBrjxMxBJpu++EnsuBmpiYaM/MEs46Vxxm/4FvdP5yTwuCTO9it5FSjrqA==", - "dev": true, - "requires": { - "inherits": "2.0.3", - "safe-buffer": "5.1.1" - } - }, - "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.7", - "mixin-object": "2.0.1" - }, - "dependencies": { - "kind-of": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-2.0.1.tgz", - "integrity": "sha1-AY7HpM5+OobLkUG+UZ0kyPqpgbU=", - "dev": true, - "requires": { - "is-buffer": "1.1.6" - } - }, - "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 - } - } - }, - "shallow-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.0.0.tgz", - "integrity": "sha1-UI0YOLPeWQq4dXsBGyXkMJAJRfc=" - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "requires": { - "shebang-regex": "1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" - }, - "shellwords": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", - "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", - "dev": true - }, - "showdown": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/showdown/-/showdown-1.7.4.tgz", - "integrity": "sha1-a7yd0s2x5f3XSeza3GpHuFZZSuA=", - "requires": { - "yargs": "8.0.2" - } - }, - "sigmund": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", - "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=" - }, - "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" - }, - "simple-html-tokenizer": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/simple-html-tokenizer/-/simple-html-tokenizer-0.4.1.tgz", - "integrity": "sha1-AomIu3/osuZkVnbYIFJYfUQLAtM=" - }, - "slash": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", - "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=", - "dev": true - }, - "slice-ansi": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", - "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=", - "dev": true - }, - "sntp": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz", - "integrity": "sha512-FL1b58BDrqS3A11lJ0zEdnJ3UOKqVxawAkF3k7F0CVN7VQ34aZrV+G8BZ1WC9ZL7NyrwsW0oviwsWDgRuVYtJg==", - "dev": true, - "requires": { - "hoek": "4.2.0" - } - }, - "sort-keys": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", - "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", - "dev": true, - "requires": { - "is-plain-obj": "1.1.0" - } - }, - "source-list-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.0.tgz", - "integrity": "sha512-I2UmuJSRr/T8jisiROLU3A3ltr+swpniSmNPI4Ml3ZCX6tVnDsuZzK7F2hl5jTqbZBWCEKlj5HRQiPExXLgE8A==", - "dev": true - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - }, - "source-map-support": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.3.tgz", - "integrity": "sha512-eKkTgWYeBOQqFGXRfKabMFdnWepo51vWqEdoeikaEPFiJC7MCU5j2h4+6Q8npkZTeLGbSyecZvRxiSoWl3rh+w==", - "dev": true, - "requires": { - "source-map": "0.6.1" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "spawn-command": { - "version": "0.0.2-1", - "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2-1.tgz", - "integrity": "sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A=", - "dev": true - }, - "spdx-correct": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-1.0.2.tgz", - "integrity": "sha1-SzBz2TP/UfORLwOsVRlJikFQ20A=", - "requires": { - "spdx-license-ids": "1.2.2" - } - }, - "spdx-expression-parse": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz", - "integrity": "sha1-m98vIOH0DtRH++JzJmGR/O1RYmw=" - }, - "spdx-license-ids": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz", - "integrity": "sha1-yd96NCRZSt5r0RkA1ZZpbcBrrFc=" - }, - "sprintf-js": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.1.tgz", - "integrity": "sha1-Nr54Mgr+WAH2zqPueLblqrlA6gw=", - "dev": true - }, - "sshpk": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", - "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", - "dev": true, - "requires": { - "asn1": "0.2.3", - "assert-plus": "1.0.0", - "bcrypt-pbkdf": "1.0.1", - "dashdash": "1.14.1", - "ecc-jsbn": "0.1.1", - "getpass": "0.1.7", - "jsbn": "0.1.1", - "tweetnacl": "0.14.5" - } - }, - "stack-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.1.tgz", - "integrity": "sha1-1PM6tU6OOHeLDKXP07OvsS22hiA=", - "dev": true - }, - "staged-git-files": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/staged-git-files/-/staged-git-files-0.0.4.tgz", - "integrity": "sha1-15fhtVHKemOd7AI33G60u5vhfTU=", - "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=", - "dev": true, - "requires": { - "readable-stream": "2.3.3" - } - }, - "stealthy-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", - "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", - "dev": true - }, - "stream-browserify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz", - "integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=", - "dev": true, - "requires": { - "inherits": "2.0.3", - "readable-stream": "2.3.3" - } - }, - "stream-http": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.0.tgz", - "integrity": "sha512-sZOFxI/5xw058XIRHl4dU3dZ+TTOIGJR78Dvo0oEAejIt4ou27k+3ne1zYmCV+v7UucbxIFQuOgnkTVHh8YPnw==", - "dev": true, - "requires": { - "builtin-status-codes": "3.0.0", - "inherits": "2.0.3", - "readable-stream": "2.3.3", - "to-arraybuffer": "1.0.1", - "xtend": "4.0.1" - } - }, - "stream-to-observable": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/stream-to-observable/-/stream-to-observable-0.1.0.tgz", - "integrity": "sha1-Rb8dny19wJvtgfHDB8Qw5ouEz/4=", - "dev": true - }, - "strict-uri-encode": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", - "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=", - "dev": true - }, - "string-length": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-2.0.0.tgz", - "integrity": "sha1-1A27aGo6zpYMHP/KVivyxF+DY+0=", - "dev": true, - "requires": { - "astral-regex": "1.0.0", - "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" - } - } - } - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "requires": { - "is-fullwidth-code-point": "2.0.0", - "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=" - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - }, - "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" - } - } - } - }, - "string_decoder": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", - "requires": { - "safe-buffer": "5.1.1" - } - }, - "stringify-object": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.2.2.tgz", - "integrity": "sha512-O696NF21oLiDy8PhpWu8AEqoZHw++QW6mUv0UvKZe8gWSdSvMXkiLufK7OmnP27Dro4GU5kb9U7JIO0mBuCRQg==", - "dev": true, - "requires": { - "get-own-enumerable-property-symbols": "2.0.1", - "is-obj": "1.0.1", - "is-regexp": "1.0.0" - } - }, - "stringstream": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", - "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=", - "dev": true - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "requires": { - "ansi-regex": "2.1.1" - } - }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=" - }, - "strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" - }, - "strip-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", - "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", - "dev": true, - "requires": { - "get-stdin": "4.0.1" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "dev": true - }, - "style-loader": { - "version": "0.18.2", - "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-0.18.2.tgz", - "integrity": "sha512-WPpJPZGUxWYHWIUMNNOYqql7zh85zGmr84FdTVWq52WTIkqlW9xSxD3QYWi/T31cqn9UNSsietVEgGn2aaSCzw==", - "dev": true, - "requires": { - "loader-utils": "1.1.0", - "schema-utils": "0.3.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - }, - "svgo": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-0.7.2.tgz", - "integrity": "sha1-n1dyQTlSE1xv779Ar+ak+qiLS7U=", - "dev": true, - "requires": { - "coa": "1.0.4", - "colors": "1.1.2", - "csso": "2.3.2", - "js-yaml": "3.7.0", - "mkdirp": "0.5.1", - "sax": "1.2.4", - "whet.extend": "0.9.9" - }, - "dependencies": { - "colors": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", - "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", - "dev": true - }, - "esprima": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", - "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", - "dev": true - }, - "js-yaml": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.7.0.tgz", - "integrity": "sha1-XJZ93YN6m/3KXy3oQlOr6KHAO4A=", - "dev": true, - "requires": { - "argparse": "1.0.9", - "esprima": "2.7.3" - } - } - } - }, - "symbol-observable": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", - "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" - }, - "symbol-tree": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.2.tgz", - "integrity": "sha1-rifbOPZgp64uHDt9G8KQgZuFGeY=", - "dev": true - }, - "table": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/table/-/table-4.0.2.tgz", - "integrity": "sha512-UUkEAPdSGxtRpiV9ozJ5cMTtYiqz7Ni1OGqLXRCynrvzdtR1p+cfOWe2RJLwvUG8hNanaSRjecIqwOjqeatDsA==", - "dev": true, - "requires": { - "ajv": "5.5.2", - "ajv-keywords": "2.1.1", - "chalk": "2.3.0", - "lodash": "4.17.4", - "slice-ansi": "1.0.0", - "string-width": "2.1.1" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", - "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", - "dev": true, - "requires": { - "color-convert": "1.9.1" - } - }, - "chalk": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", - "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", - "dev": true, - "requires": { - "ansi-styles": "3.2.0", - "escape-string-regexp": "1.0.5", - "supports-color": "4.5.0" - } - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "slice-ansi": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-1.0.0.tgz", - "integrity": "sha512-POqxBK6Lb3q6s047D/XsDVNPnF9Dl8JSaqe9h9lURl0OdNqy/ujDrOiIHtsqXMGbWWTIomRzAMaTyawAU//Reg==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "2.0.0" - } - }, - "supports-color": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", - "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", - "dev": true, - "requires": { - "has-flag": "2.0.0" - } - } - } - }, - "tapable": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-0.2.8.tgz", - "integrity": "sha1-mTcqXJmb8t8WCvwNdL7U9HlIzSI=", - "dev": true - }, - "tar": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", - "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", - "dev": true, - "requires": { - "block-stream": "0.0.9", - "fstream": "1.0.11", - "inherits": "2.0.3" - } - }, - "test-exclude": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-4.2.0.tgz", - "integrity": "sha512-8hMFzjxbPv6xSlwGhXSvOMJ/vTy3bkng+2pxmf6E1z6VF7I9nIyNfvHtaw+NBPgvz647gADBbMSbwLfZYppT/w==", - "dev": true, - "requires": { - "arrify": "1.0.1", - "micromatch": "2.3.11", - "object-assign": "4.1.1", - "read-pkg-up": "1.0.1", - "require-main-filename": "1.0.1" - }, - "dependencies": { - "load-json-file": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", - "dev": true, - "requires": { - "graceful-fs": "4.1.11", - "parse-json": "2.2.0", - "pify": "2.3.0", - "pinkie-promise": "2.0.1", - "strip-bom": "2.0.0" - } - }, - "path-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", - "dev": true, - "requires": { - "graceful-fs": "4.1.11", - "pify": "2.3.0", - "pinkie-promise": "2.0.1" - } - }, - "read-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", - "dev": true, - "requires": { - "load-json-file": "1.1.0", - "normalize-package-data": "2.4.0", - "path-type": "1.1.0" - } - }, - "read-pkg-up": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", - "dev": true, - "requires": { - "find-up": "1.1.2", - "read-pkg": "1.1.0" - } - }, - "strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", - "dev": true, - "requires": { - "is-utf8": "0.2.1" - } - } - } - }, - "text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", - "dev": true - }, - "throat": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/throat/-/throat-4.1.0.tgz", - "integrity": "sha1-iQN8vJLFarGJJua6TLsgDhVnKmo=", - "dev": true - }, - "throttleit": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-0.0.2.tgz", - "integrity": "sha1-z+34jmDADdlpe2H90qg0OptoDq8=", - "dev": true - }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", - "dev": true - }, - "timers-browserify": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.6.tgz", - "integrity": "sha512-HQ3nbYRAowdVd0ckGFvmJPPCOH/CHleFN/Y0YQCX1DVaB7t+KFvisuyN09fuP8Jtp1CpfSh8O8bMkHbdbPe6Pw==", - "dev": true, - "requires": { - "setimmediate": "1.0.5" - } - }, - "tiny-emitter": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.0.2.tgz", - "integrity": "sha512-2NM0auVBGft5tee/OxP4PI3d8WItkDM+fPnaRAVo6xTDI2knbz9eC5ArWGqtGlYqiH3RU5yMpdyTTO7MguC4ow==" - }, - "tinycolor2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.1.tgz", - "integrity": "sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g=" - }, - "tinymce": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/tinymce/-/tinymce-4.7.2.tgz", - "integrity": "sha1-JL9k/x0eaBkOFUYaZY3CV6Qmxe4=", - "dev": true - }, - "tmp": { - "version": "0.0.31", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.31.tgz", - "integrity": "sha1-jzirlDjhcxXl29izZX6L+yd65Kc=", - "dev": true, - "requires": { - "os-tmpdir": "1.0.2" - } - }, - "tmpl": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", - "integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=", - "dev": true - }, - "to-arraybuffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", - "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", - "dev": true - }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true - }, - "tough-cookie": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz", - "integrity": "sha1-C2GKVWW23qkL80JdBNVe3EdadWE=", - "dev": true, - "requires": { - "punycode": "1.4.1" - } - }, - "tr46": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", - "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", - "dev": true, - "requires": { - "punycode": "2.1.0" - }, - "dependencies": { - "punycode": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.0.tgz", - "integrity": "sha1-X4Y+3Im5bbCQdLrXlHvwkFbKTn0=", - "dev": true - } - } - }, - "tree-kill": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.0.tgz", - "integrity": "sha512-DlX6dR0lOIRDFxI0mjL9IYg6OTncLm/Zt+JiBhE5OlFcAR8yc9S7FFXU9so0oda47frdM/JFsk7UjNt9vscKcg==", - "dev": true - }, - "trim-newlines": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", - "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", - "dev": true - }, - "trim-right": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", - "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", - "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=", - "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.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - } - } - }, - "tty-browserify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", - "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", - "dev": true - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "dev": true, - "requires": { - "safe-buffer": "5.1.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "dev": true, - "optional": true - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "dev": true, - "requires": { - "prelude-ls": "1.1.2" - } - }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", - "dev": true - }, - "ua-parser-js": { - "version": "0.7.17", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.17.tgz", - "integrity": "sha512-uRdSdu1oA1rncCQL7sCj8vSyZkgtL7faaw9Tc9rZ3mGgraQ7+Pdx7w5mnOSF3gw9ZNG6oc+KXfkon3bKuROm0g==" - }, - "uglify-js": { - "version": "2.8.29", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", - "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", - "dev": true, - "requires": { - "source-map": "0.5.7", - "uglify-to-browserify": "1.0.2", - "yargs": "3.10.0" - }, - "dependencies": { - "camelcase": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", - "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", - "dev": true - }, - "cliui": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", - "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", - "dev": true, - "requires": { - "center-align": "0.1.3", - "right-align": "0.1.3", - "wordwrap": "0.0.2" - } - }, - "wordwrap": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", - "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", - "dev": true - }, - "yargs": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", - "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", - "dev": true, - "requires": { - "camelcase": "1.2.1", - "cliui": "2.1.0", - "decamelize": "1.2.0", - "window-size": "0.1.0" - } - } - } - }, - "uglify-to-browserify": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", - "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", - "dev": true, - "optional": true - }, - "uglifyjs-webpack-plugin": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-0.4.6.tgz", - "integrity": "sha1-uVH0q7a9YX5m9j64kUmOORdj4wk=", - "dev": true, - "requires": { - "source-map": "0.5.7", - "uglify-js": "2.8.29", - "webpack-sources": "1.1.0" - } - }, - "ultron": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", - "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==", - "dev": true - }, - "underscore": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.4.4.tgz", - "integrity": "sha1-YaajIBBiKvoHljvzJSA88SI51gQ=", - "dev": true - }, - "uniq": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", - "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=", - "dev": true - }, - "uniqid": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/uniqid/-/uniqid-4.1.1.tgz", - "integrity": "sha1-iSIN32t1GuUrX3JISGNShZa7hME=", - "dev": true, - "requires": { - "macaddress": "0.2.8" - } - }, - "uniqs": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/uniqs/-/uniqs-2.0.0.tgz", - "integrity": "sha1-/+3ks2slKQaW5uFl1KWe25mOawI=", - "dev": true - }, - "universalify": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz", - "integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc=", - "dev": true - }, - "url": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", - "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", - "dev": true, - "requires": { - "punycode": "1.3.2", - "querystring": "0.2.0" - }, - "dependencies": { - "punycode": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", - "dev": true - } - } - }, - "urlgrey": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/urlgrey/-/urlgrey-0.4.4.tgz", - "integrity": "sha1-iS/pWWCAXoVRnxzUOJ8stMu3ZS8=", - "dev": true - }, - "util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", - "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", - "dev": true, - "requires": { - "inherits": "2.0.1" - }, - "dependencies": { - "inherits": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", - "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", - "dev": true - } - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "util.promisify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", - "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", - "dev": true, - "requires": { - "define-properties": "1.1.2", - "object.getownpropertydescriptors": "2.0.3" - } - }, - "uuid": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", - "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==" - }, - "validate-npm-package-license": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz", - "integrity": "sha1-KAS6vnEq0zeUWaz74kdGqywwP7w=", - "requires": { - "spdx-correct": "1.0.2", - "spdx-expression-parse": "1.0.4" - } - }, - "vendors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.1.tgz", - "integrity": "sha1-N61zyO5Bf7PVgOeFMSMH0nSEfyI=", - "dev": true - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "dev": true, - "requires": { - "assert-plus": "1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "1.3.0" - } - }, - "vm-browserify": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz", - "integrity": "sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=", - "dev": true, - "requires": { - "indexof": "0.0.1" - } - }, - "w3c-hr-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz", - "integrity": "sha1-gqwr/2PZUOqeMYmlimViX+3xkEU=", - "dev": true, - "requires": { - "browser-process-hrtime": "0.1.2" - } - }, - "walker": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz", - "integrity": "sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=", - "dev": true, - "requires": { - "makeerror": "1.0.11" - } - }, - "watch": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/watch/-/watch-0.18.0.tgz", - "integrity": "sha1-KAlUdsbffJDJYxOJkMClQj60uYY=", - "dev": true, - "requires": { - "exec-sh": "0.2.1", - "minimist": "1.2.0" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - } - } - }, - "watchpack": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.4.0.tgz", - "integrity": "sha1-ShRyvLuVK9Cpu0A2gB+VTfs5+qw=", - "dev": true, - "requires": { - "async": "2.6.0", - "chokidar": "1.7.0", - "graceful-fs": "4.1.11" - } - }, - "webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", - "dev": true - }, - "webpack": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-3.10.0.tgz", - "integrity": "sha512-fxxKXoicjdXNUMY7LIdY89tkJJJ0m1Oo8PQutZ5rLgWbV5QVKI15Cn7+/IHnRTd3vfKfiwBx6SBqlorAuNA8LA==", - "dev": true, - "requires": { - "acorn": "5.4.1", - "acorn-dynamic-import": "2.0.2", - "ajv": "5.5.2", - "ajv-keywords": "2.1.1", - "async": "2.6.0", - "enhanced-resolve": "3.4.1", - "escope": "3.6.0", - "interpret": "1.1.0", - "json-loader": "0.5.7", - "json5": "0.5.1", - "loader-runner": "2.3.0", - "loader-utils": "1.1.0", - "memory-fs": "0.4.1", - "mkdirp": "0.5.1", - "node-libs-browser": "2.1.0", - "source-map": "0.5.7", - "supports-color": "4.5.0", - "tapable": "0.2.8", - "uglifyjs-webpack-plugin": "0.4.6", - "watchpack": "1.4.0", - "webpack-sources": "1.1.0", - "yargs": "8.0.2" - }, - "dependencies": { - "supports-color": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", - "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", - "dev": true, - "requires": { - "has-flag": "2.0.0" - } - } - } - }, - "webpack-rtl-plugin": { - "version": "github:yoavf/webpack-rtl-plugin#fc5a2f20dd99fde8f86f297844aefde601780fa3", - "dev": true, - "requires": { - "@romainberger/css-diff": "1.0.3", - "async": "2.6.0", - "cssnano": "3.10.0", - "postcss": "5.2.18", - "rtlcss": "2.2.1", - "webpack-sources": "0.1.5" - }, - "dependencies": { - "source-list-map": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-0.1.8.tgz", - "integrity": "sha1-xVCyq1Qn9rPyH1r+rYjE9Vh7IQY=", - "dev": true - }, - "webpack-sources": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-0.1.5.tgz", - "integrity": "sha1-qh86vw8NdNtxEcQOUAuE+WZkB1A=", - "dev": true, - "requires": { - "source-list-map": "0.1.8", - "source-map": "0.5.7" - } - } - } - }, - "webpack-sources": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.1.0.tgz", - "integrity": "sha512-aqYp18kPphgoO5c/+NaUvEeACtZjMESmDChuD3NBciVpah3XpMEU9VAAtIaB1BsfJWWTSdv8Vv1m3T0aRk2dUw==", - "dev": true, - "requires": { - "source-list-map": "2.0.0", - "source-map": "0.6.1" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "whatwg-encoding": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.3.tgz", - "integrity": "sha512-jLBwwKUhi8WtBfsMQlL4bUUcT8sMkAtQinscJAe/M4KHCkHuUJAF6vuB0tueNIw4c8ziO6AkRmgY+jL3a0iiPw==", - "dev": true, - "requires": { - "iconv-lite": "0.4.19" - } - }, - "whatwg-fetch": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz", - "integrity": "sha1-nITsLc9oGH/wC8ZOEnS0QhduHIQ=" - }, - "whatwg-url": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.4.0.tgz", - "integrity": "sha512-Z0CVh/YE217Foyb488eo+iBv+r7eAQ0wSTyApi9n06jhcA3z6Nidg/EGvl0UFkg7kMdKxfBzzr+o9JF+cevgMg==", - "dev": true, - "requires": { - "lodash.sortby": "4.7.0", - "tr46": "1.0.1", - "webidl-conversions": "4.0.2" - } - }, - "whet.extend": { - "version": "0.9.9", - "resolved": "https://registry.npmjs.org/whet.extend/-/whet.extend-0.9.9.tgz", - "integrity": "sha1-+HfVv2SMl+WqVC+twW1qJZucEaE=", - "dev": true - }, - "which": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", - "integrity": "sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==", - "requires": { - "isexe": "2.0.0" - } - }, - "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" - }, - "wide-align": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz", - "integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==", - "dev": true, - "requires": { - "string-width": "1.0.2" - }, - "dependencies": { - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - } - } - }, - "window-size": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", - "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", - "dev": true - }, - "wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", - "dev": true - }, - "wrap-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", - "requires": { - "string-width": "1.0.2", - "strip-ansi": "3.0.1" - }, - "dependencies": { - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - } - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - }, - "write": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", - "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", - "dev": true, - "requires": { - "mkdirp": "0.5.1" - } - }, - "write-file-atomic": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.3.0.tgz", - "integrity": "sha512-xuPeK4OdjWqtfi59ylvVL0Yn35SF3zgcAcv7rBPFHVaEapaDr4GdGgm3j7ckTwH9wHL7fGmgfAnb0+THrHb8tA==", - "dev": true, - "requires": { - "graceful-fs": "4.1.11", - "imurmurhash": "0.1.4", - "signal-exit": "3.0.2" - } - }, - "ws": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-4.0.0.tgz", - "integrity": "sha512-QYslsH44bH8O7/W2815u5DpnCpXWpEK44FmaHffNwgJI4JMaSZONgPBTOfrxJ29mXKbXak+LsJ2uAkDTYq2ptQ==", - "dev": true, - "requires": { - "async-limiter": "1.0.0", - "safe-buffer": "5.1.1", - "ultron": "1.1.1" - } - }, - "xml-name-validator": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", - "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", - "dev": true - }, - "xtend": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", - "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", - "dev": true - }, - "y18n": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", - "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=" - }, - "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" - }, - "yargs": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-8.0.2.tgz", - "integrity": "sha1-YpmpBVsc78lp/355wdkY3Osiw2A=", - "requires": { - "camelcase": "4.1.0", - "cliui": "3.2.0", - "decamelize": "1.2.0", - "get-caller-file": "1.0.2", - "os-locale": "2.1.0", - "read-pkg-up": "2.0.0", - "require-directory": "2.1.1", - "require-main-filename": "1.0.1", - "set-blocking": "2.0.0", - "string-width": "2.1.1", - "which-module": "2.0.0", - "y18n": "3.2.1", - "yargs-parser": "7.0.0" - } - }, - "yargs-parser": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-7.0.0.tgz", - "integrity": "sha1-jQrELxbqVd69MyyvTEA4s+P139k=", - "requires": { - "camelcase": "4.1.0" - } - }, - "yauzl": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.8.0.tgz", - "integrity": "sha1-eUUK/yKyqcWkHvVOAtuQfM+/nuI=", - "dev": true, - "requires": { - "buffer-crc32": "0.2.13", - "fd-slicer": "1.0.1" - } - } - } + "name": "gutenberg", + "version": "2.6.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@babel/code-frame": { + "version": "7.0.0-beta.40", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0-beta.40.tgz", + "integrity": "sha512-eVXQSbu/RimU6OKcK2/gDJVTFcxXJI4sHbIqw2mhwMZeQ2as/8AhS9DGkEDoHMBBNJZ5B0US63lF56x+KDcxiA==", + "dev": true, + "requires": { + "@babel/highlight": "7.0.0-beta.40" + } + }, + "@babel/helper-function-name": { + "version": "7.0.0-beta.31", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.0.0-beta.31.tgz", + "integrity": "sha512-c+DAyp8LMm2nzSs2uXEuxp4LYGSUYEyHtU3fU57avFChjsnTmmpWmXj2dv0yUxHTEydgVAv5fIzA+4KJwoqWDA==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "7.0.0-beta.31", + "@babel/template": "7.0.0-beta.31", + "@babel/traverse": "7.0.0-beta.31", + "@babel/types": "7.0.0-beta.31" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.0.0-beta.31", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0-beta.31.tgz", + "integrity": "sha512-m7rVVX/dMLbbB9NCzKYRrrFb0qZxgpmQ4Wv6y7zEsB6skoJHRuXVeb/hAFze79vXBbuD63ci7AVHXzAdZSk9KQ==", + "dev": true, + "requires": { + "@babel/types": "7.0.0-beta.31" + } + }, + "@babel/highlight": { + "version": "7.0.0-beta.40", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0-beta.40.tgz", + "integrity": "sha512-mOhhTrzieV6VO7odgzFGFapiwRK0ei8RZRhfzHhb6cpX3QM8XXuCLXWjN8qBB7JReDdUR80V3LFfFrGUYevhNg==", + "dev": true, + "requires": { + "chalk": "2.3.1", + "esutils": "2.0.2", + "js-tokens": "3.0.2" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + }, + "chalk": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.1.tgz", + "integrity": "sha512-QUU4ofkDoMIVO7hcx1iPTISs88wsO8jA92RQIm4JAwZvFGGAV2hSAA1NX7oVj2Ej2Q6NDTcRDjPTFrMCRZoJ6g==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "5.2.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.2.0.tgz", + "integrity": "sha512-F39vS48la4YvTZUPVeTqsjsFNrvcMwrV3RLZINsmHo+7djCvuUzSIeXOnZ5hmjef4bajL1dNccN+tg5XAliO5Q==", + "dev": true, + "requires": { + "has-flag": "3.0.0" + } + } + } + }, + "@babel/template": { + "version": "7.0.0-beta.31", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.0.0-beta.31.tgz", + "integrity": "sha512-97IRmLvoDhIDSQkqklVt3UCxJsv0LUEVb/0DzXWtc8Lgiyxj567qZkmTG9aR21CmcJVVIvq2Y/moZj4oEpl5AA==", + "dev": true, + "requires": { + "@babel/code-frame": "7.0.0-beta.31", + "@babel/types": "7.0.0-beta.31", + "babylon": "7.0.0-beta.31", + "lodash": "4.17.5" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.0.0-beta.31", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0-beta.31.tgz", + "integrity": "sha512-yd7CkUughvHQoEahQqcMdrZw6o/6PwUxiRkfZuVDVHCDe77mysD/suoNyk5mK6phTnRW1kyIbPHyCJgxw++LXg==", + "dev": true, + "requires": { + "chalk": "2.3.0", + "esutils": "2.0.2", + "js-tokens": "3.0.2" + } + }, + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + }, + "babylon": { + "version": "7.0.0-beta.31", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.31.tgz", + "integrity": "sha512-6lm2mV3S51yEnKmQQNnswoABL1U1H1KHoCCVwdwI3hvIv+W7ya4ki7Aw4o4KxtUHjNKkK5WpZb22rrMMOcJXJQ==", + "dev": true + }, + "chalk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", + "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "4.5.0" + } + }, + "supports-color": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", + "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", + "dev": true, + "requires": { + "has-flag": "2.0.0" + } + } + } + }, + "@babel/traverse": { + "version": "7.0.0-beta.31", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.0.0-beta.31.tgz", + "integrity": "sha512-3N+VJW+KlezEjFBG7WSYeMyC5kIqVLPb/PGSzCDPFcJrnArluD1GIl7Y3xC7cjKiTq2/JohaLWHVPjJWHlo9Gg==", + "dev": true, + "requires": { + "@babel/code-frame": "7.0.0-beta.31", + "@babel/helper-function-name": "7.0.0-beta.31", + "@babel/types": "7.0.0-beta.31", + "babylon": "7.0.0-beta.31", + "debug": "3.1.0", + "globals": "10.4.0", + "invariant": "2.2.2", + "lodash": "4.17.5" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.0.0-beta.31", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0-beta.31.tgz", + "integrity": "sha512-yd7CkUughvHQoEahQqcMdrZw6o/6PwUxiRkfZuVDVHCDe77mysD/suoNyk5mK6phTnRW1kyIbPHyCJgxw++LXg==", + "dev": true, + "requires": { + "chalk": "2.3.0", + "esutils": "2.0.2", + "js-tokens": "3.0.2" + } + }, + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + }, + "babylon": { + "version": "7.0.0-beta.31", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.31.tgz", + "integrity": "sha512-6lm2mV3S51yEnKmQQNnswoABL1U1H1KHoCCVwdwI3hvIv+W7ya4ki7Aw4o4KxtUHjNKkK5WpZb22rrMMOcJXJQ==", + "dev": true + }, + "chalk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", + "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "4.5.0" + } + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "globals": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-10.4.0.tgz", + "integrity": "sha512-uNUtxIZpGyuaq+5BqGGQHsL4wUlJAXRqOm6g3Y48/CWNGTLONgBibI0lh6lGxjR2HljFYUfszb+mk4WkgMntsA==", + "dev": true + }, + "supports-color": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", + "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", + "dev": true, + "requires": { + "has-flag": "2.0.0" + } + } + } + }, + "@babel/types": { + "version": "7.0.0-beta.31", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.0.0-beta.31.tgz", + "integrity": "sha512-exAHB+NeFGxkfQ5dSUD03xl3zYGneeSk2Mw2ldTt/nTvYxuDiuSp3DlxgUBgzbdTFG4fbwPk0WtKWOoTXCmNGg==", + "dev": true, + "requires": { + "esutils": "2.0.2", + "lodash": "4.17.5", + "to-fast-properties": "2.0.0" + } + }, + "@romainberger/css-diff": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@romainberger/css-diff/-/css-diff-1.0.3.tgz", + "integrity": "sha1-ztOHU11PQqQqwf4TwJ3pf1rhNEw=", + "dev": true, + "requires": { + "lodash.merge": "4.6.1", + "postcss": "5.2.18" + } + }, + "@sindresorhus/is": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.7.0.tgz", + "integrity": "sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==", + "dev": true + }, + "@types/node": { + "version": "9.4.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-9.4.6.tgz", + "integrity": "sha512-CTUtLb6WqCCgp6P59QintjHWqzf4VL1uPA27bipLAPxFqrtK1gEYllePzTICGqQ8rYsCbpnsNypXjjDzGAAjEQ==", + "dev": true + }, + "@wordpress/a11y": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@wordpress/a11y/-/a11y-1.0.6.tgz", + "integrity": "sha512-IyL7KzYGzMEg+FFyTrQzD/CUfABYCXOvmnm29vBZBA3JMER1ep3/+NFDe6CpWVEEMCw94oj2gUOSQ4YKVgDjUQ==", + "requires": { + "@wordpress/dom-ready": "1.0.3" + } + }, + "@wordpress/autop": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@wordpress/autop/-/autop-1.0.4.tgz", + "integrity": "sha512-nqm/gP+ipeUMvEngh4Sp4k5umph8SPqfc5aCd9Ge03mz4JSWAIE4z36pPQwId0a1B3hGqri5Wo40O1hQ851ZnA==" + }, + "@wordpress/babel-plugin-makepot": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@wordpress/babel-plugin-makepot/-/babel-plugin-makepot-1.0.0.tgz", + "integrity": "sha512-YNcXr33fUUHg9HncNhUNkG2jnCAtnvC5hN0ZdDdTVe7r7aR4l+gCTCZXvM/0SHqikrNhRNjSrE5ZM/3u2zsOyw==", + "dev": true, + "requires": { + "gettext-parser": "1.3.1", + "lodash": "4.17.5" + }, + "dependencies": { + "gettext-parser": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/gettext-parser/-/gettext-parser-1.3.1.tgz", + "integrity": "sha512-W4t55eB/c7WrH0gbCHFiHuaEnJ1WiPJVnbFFiNEoh2QkOmuSLxs0PmJDGAmCQuTJCU740Fmb6D+2D/2xECWZGQ==", + "dev": true, + "requires": { + "encoding": "0.1.12", + "safe-buffer": "5.1.1" + } + }, + "lodash": { + "version": "4.17.5", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.5.tgz", + "integrity": "sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw==", + "dev": true + } + } + }, + "@wordpress/babel-preset-default": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@wordpress/babel-preset-default/-/babel-preset-default-1.2.0.tgz", + "integrity": "sha512-DJ4P3gwj8gUQy94GeaVdHTh2JVKQ8eNFwCQi4GDhCcq78KDAJjRO497ZwoJWJR0MVq5a3XfsrJ6fVf7S8APfUw==", + "dev": true, + "requires": { + "@wordpress/browserslist-config": "2.1.3", + "babel-plugin-transform-object-rest-spread": "6.26.0", + "babel-plugin-transform-react-jsx": "6.24.1", + "babel-plugin-transform-runtime": "6.23.0", + "babel-preset-env": "1.6.1" + } + }, + "@wordpress/browserslist-config": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@wordpress/browserslist-config/-/browserslist-config-2.1.3.tgz", + "integrity": "sha512-I8xUK/oqxI0LYoTar2U/vvQS78pWnEXWqClyhzmQ53NfWWaGydYgNdH3X2+oCGZjNUtdEkXuhjnyAwSZ12h0HQ==", + "dev": true + }, + "@wordpress/custom-templated-path-webpack-plugin": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@wordpress/custom-templated-path-webpack-plugin/-/custom-templated-path-webpack-plugin-1.0.0.tgz", + "integrity": "sha512-7GDENg5juXusGye4JqKAjfAr1EcKNNmJ6GUnnUrdOkXgjnT5CoAw2ADJxvtALtl4vx6o/nqzJxp9YQ/bZi85Dg==", + "dev": true, + "requires": { + "escape-string-regexp": "1.0.5" + } + }, + "@wordpress/dom-ready": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@wordpress/dom-ready/-/dom-ready-1.0.3.tgz", + "integrity": "sha512-1tmJLO2NDc45wxnUWv6F/4q8/00qSgqLvBXBKl7IayLvJbG25vt7lMQkdSfiY2gTwsujAqOgOuJdO5VOGuqQNg==" + }, + "@wordpress/hooks": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@wordpress/hooks/-/hooks-1.1.6.tgz", + "integrity": "sha512-+7s5j296RTXRabaubvNK35ED/+WUYJgM8oeiHWP6RvPGd/2rkei3cI0SNwjBdaRrlNQ22vtzvCfhdDCyb9W1xQ==" + }, + "@wordpress/i18n": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-1.1.0.tgz", + "integrity": "sha512-zzpyhSaVOv5iLIwkJ4nrPt7FO+50xHlGDSJljfGdS+ypvFAnEHpCkkJ84F3NhHaYIIZqMEn5lC4k1edIaIqAbA==", + "requires": { + "gettext-parser": "1.3.1", + "jed": "1.1.1", + "lodash": "4.17.5", + "memize": "1.0.5" + }, + "dependencies": { + "lodash": { + "version": "4.17.5", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.5.tgz", + "integrity": "sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw==" + } + } + }, + "@wordpress/jest-console": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@wordpress/jest-console/-/jest-console-1.0.5.tgz", + "integrity": "sha512-PwhDL2H4EI6adnGyQo0v4p8zRokjNu4DJ3EDZpH9dmNK0/G9hKuuIAwGN2e9RGyAiqipddCkt5y4qzH1mx8PJw==", + "dev": true, + "requires": { + "jest-matcher-utils": "22.4.0", + "lodash": "4.17.5" + } + }, + "@wordpress/jest-preset-default": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@wordpress/jest-preset-default/-/jest-preset-default-1.0.3.tgz", + "integrity": "sha512-1+uREUyhMWBanX4qceevrFEuaFm/gRKIKiDb36Wc5X02ODEaUaQ6qjIvrD6fariYtvxQpjC8TBjKapsvSaqHHw==", + "dev": true, + "requires": { + "@wordpress/jest-console": "1.0.5", + "babel-jest": "22.4.0", + "enzyme": "3.3.0", + "enzyme-adapter-react-16": "1.1.1", + "jest-enzyme": "4.2.0", + "pegjs": "0.10.0" + } + }, + "@wordpress/scripts": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@wordpress/scripts/-/scripts-1.1.0.tgz", + "integrity": "sha512-gbtpV6i4SKi7Pya8qeB6N9FGyWAMBtyAsRnFg6Lv6Ejh0Pk9R2Aa71GjX4ZAMzq1LFuVNdhciIybdxDPSsP8sQ==", + "dev": true, + "requires": { + "@wordpress/babel-preset-default": "1.2.0", + "@wordpress/jest-preset-default": "1.0.3", + "cross-spawn": "5.1.0", + "jest": "22.4.0", + "read-pkg-up": "3.0.0" + }, + "dependencies": { + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "2.0.0" + } + }, + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "parse-json": "4.0.0", + "pify": "3.0.0", + "strip-bom": "3.0.0" + } + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "1.3.1", + "json-parse-better-errors": "1.0.1" + } + }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "requires": { + "pify": "3.0.0" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "dev": true, + "requires": { + "load-json-file": "4.0.0", + "normalize-package-data": "2.4.0", + "path-type": "3.0.0" + } + }, + "read-pkg-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", + "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", + "dev": true, + "requires": { + "find-up": "2.1.0", + "read-pkg": "3.0.0" + } + } + } + }, + "@wordpress/url": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@wordpress/url/-/url-1.0.3.tgz", + "integrity": "sha512-0nqf62SWS0DiFnSD5miszPuAey01OrBsdqBFzOCYChjtB7AZm+8Q06qpeV02rpLY5FHaUxVgL+2JRljYIAFlpA==" + }, + "abab": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz", + "integrity": "sha1-X6rZwsB/YN12dw9xzwJbYqY8/U4=", + "dev": true + }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, + "acorn": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.4.1.tgz", + "integrity": "sha512-XLmq3H/BVvW6/GbxKryGxWORz1ebilSsUDlyC27bXhWGWAZWkGwS6FLHjOlwFXNFoWFQEO/Df4u0YYd0K3BQgQ==", + "dev": true + }, + "acorn-dynamic-import": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-3.0.0.tgz", + "integrity": "sha512-zVWV8Z8lislJoOKKqdNMOB+s6+XV5WERty8MnKBeFgwA+19XJjJHs2RP5dzM57FftIs+jQnRToLiWazKr6sSWg==", + "dev": true, + "requires": { + "acorn": "5.4.1" + } + }, + "acorn-globals": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.1.0.tgz", + "integrity": "sha512-KjZwU26uG3u6eZcfGbTULzFcsoz6pegNKtHPksZPOUsiKo5bUmiBPa38FuHZ/Eun+XYh/JCCkS9AS3Lu4McQOQ==", + "dev": true, + "requires": { + "acorn": "5.4.1" + } + }, + "acorn-jsx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", + "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", + "dev": true, + "requires": { + "acorn": "3.3.0" + }, + "dependencies": { + "acorn": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", + "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=", + "dev": true + } + } + }, + "agent-base": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.0.tgz", + "integrity": "sha512-c+R/U5X+2zz2+UCrCFv6odQzJdoqI+YecuhnAJLa1zYaMc13zPfwMwZrr91Pd1DYNo/yPRbiM4WVf9whgwFsIg==", + "dev": true, + "requires": { + "es6-promisify": "5.0.0" + } + }, + "ajv": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "dev": true, + "requires": { + "co": "4.6.0", + "fast-deep-equal": "1.0.0", + "fast-json-stable-stringify": "2.0.0", + "json-schema-traverse": "0.3.1" + } + }, + "ajv-keywords": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-2.1.1.tgz", + "integrity": "sha1-YXmX/F9gV2iUxDX5QNgZ4TW4B2I=", + "dev": true + }, + "align-text": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", + "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", + "dev": true, + "requires": { + "kind-of": "3.2.2", + "longest": "1.0.1", + "repeat-string": "1.6.1" + } + }, + "alphanum-sort": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz", + "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=", + "dev": true + }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", + "dev": true + }, + "ansi-escapes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.0.0.tgz", + "integrity": "sha512-O/klc27mWNUigtv0F8NJWbLF00OcegQalkqKURWdosW08YZKi4m6CnSUSvIZG1otNJbTWhN01Hhz389DW7mvDQ==", + "dev": true + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "any-observable": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/any-observable/-/any-observable-0.2.0.tgz", + "integrity": "sha1-xnhwBYADV5AJCD9UrAq6+1wz0kI=", + "dev": true + }, + "anymatch": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz", + "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==", + "dev": true, + "requires": { + "micromatch": "2.3.11", + "normalize-path": "2.1.1" + } + }, + "append-transform": { + "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": "1.0.0" + } + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "dev": true + }, + "are-we-there-yet": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz", + "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=", + "dev": true, + "requires": { + "delegates": "1.0.0", + "readable-stream": "2.3.3" + } + }, + "argparse": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz", + "integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=", + "dev": true, + "requires": { + "sprintf-js": "1.0.3" + }, + "dependencies": { + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + } + } + }, + "argv": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/argv/-/argv-0.0.2.tgz", + "integrity": "sha1-7L0W+JSbFXGDcRsb2jNPN4QBhas=", + "dev": true + }, + "aria-query": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-0.7.1.tgz", + "integrity": "sha1-Jsu1r/ZBRLCoJb4YRuCxbPoAsR4=", + "dev": true, + "requires": { + "ast-types-flow": "0.0.7", + "commander": "2.14.1" + } + }, + "arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "dev": true, + "requires": { + "arr-flatten": "1.1.0" + } + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", + "dev": true + }, + "array-differ": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-1.0.0.tgz", + "integrity": "sha1-7/UuN1gknTO+QCuLuOVkuytdQDE=", + "dev": true + }, + "array-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", + "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=", + "dev": true + }, + "array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", + "dev": true + }, + "array-includes": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.0.3.tgz", + "integrity": "sha1-GEtI9i2S10UrsxsyMWXH+L0CJm0=", + "dev": true, + "requires": { + "define-properties": "1.1.2", + "es-abstract": "1.10.0" + } + }, + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "dev": true, + "requires": { + "array-uniq": "1.0.3" + } + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "dev": true + }, + "array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", + "dev": true + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true + }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" + }, + "asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=", + "dev": true + }, + "asn1.js": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", + "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "dev": true, + "requires": { + "bn.js": "4.11.8", + "inherits": "2.0.3", + "minimalistic-assert": "1.0.0" + } + }, + "assert": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz", + "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=", + "dev": true, + "requires": { + "util": "0.10.3" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "dev": true + }, + "ast-types": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.11.3.tgz", + "integrity": "sha512-XA5o5dsNw8MhyW0Q7MWXJWc4oOzZKbdsEJq45h7c8q/d9DwWZ5F2ugUc1PuMLPGsUnphCt/cNDHu8JeBbxf1qA==", + "dev": true + }, + "ast-types-flow": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", + "integrity": "sha1-9wtzXGvKGlycItmCw+Oef+ujva0=", + "dev": true + }, + "astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "dev": true + }, + "async": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.0.tgz", + "integrity": "sha512-xAfGg1/NTLBBKlHFmnd7PlmUW9KhVQIUuSrYem9xzFUZy13ScvtyGGejaae9iAVRiRq9+Cx7DPFaAAhCpyxyPw==", + "dev": true, + "requires": { + "lodash": "4.17.5" + } + }, + "async-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz", + "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=", + "dev": true + }, + "async-foreach": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz", + "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=", + "dev": true + }, + "async-limiter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", + "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "atob": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.0.tgz", + "integrity": "sha512-SuiKH8vbsOyCALjA/+EINmt/Kdl+TQPrtFgW7XZZcwtryFu9e5kQoX3bjCW6mIvGH1fbeAZZuvwGR5IlBRznGw==", + "dev": true + }, + "autoprefixer": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-8.2.0.tgz", + "integrity": "sha512-xBVQpGAcSNNS1PBnEfT+F9VF8ZJeoKZ121I3OVQ0n1F0SqVuj4oLI6yFeEviPV8Z/GjoqBRXcYis0oSS8zjNEg==", + "dev": true, + "requires": { + "browserslist": "3.2.4", + "caniuse-lite": "1.0.30000821", + "normalize-range": "0.1.2", + "num2fraction": "1.2.2", + "postcss": "6.0.21", + "postcss-value-parser": "3.3.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + }, + "browserslist": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-3.2.4.tgz", + "integrity": "sha512-Dwe62y/fNAcMfknzGJnkh7feISrrN0SmRvMFozb+Y2+qg7rfTIH5MS8yHzaIXcEWl8fPeIcdhZNQi1Lux+7dlg==", + "dev": true, + "requires": { + "caniuse-lite": "1.0.30000821", + "electron-to-chromium": "1.3.41" + } + }, + "caniuse-lite": { + "version": "1.0.30000821", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000821.tgz", + "integrity": "sha512-qyYay02wr/5k7PO86W+LKFaEUZfWIvT65PaXuPP16jkSpgZGIsSstHKiYAPVLjTj98j2WnWwZg8CjXPx7UIPYg==", + "dev": true + }, + "chalk": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.2.tgz", + "integrity": "sha512-ZM4j2/ld/YZDc3Ma8PgN7gyAk+kHMMMyzLNryCPGhWrsfAuDVeuid5bpRFTDgMH9JBK2lA4dyyAkkZYF/WcqDQ==", + "dev": true, + "requires": { + "ansi-styles": "3.2.1", + "escape-string-regexp": "1.0.5", + "supports-color": "5.3.0" + } + }, + "electron-to-chromium": { + "version": "1.3.41", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.41.tgz", + "integrity": "sha1-fjNkPgDNhe39F+BBlPbQDnNzcjU=", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "postcss": { + "version": "6.0.21", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.21.tgz", + "integrity": "sha512-y/bKfbQz2Nn/QBC08bwvYUxEFOVGfPIUOTsJ2CK5inzlXW9SdYR1x4pEsG9blRAF/PX+wRNdOah+gx/hv4q7dw==", + "dev": true, + "requires": { + "chalk": "2.3.2", + "source-map": "0.6.1", + "supports-color": "5.3.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.3.0.tgz", + "integrity": "sha512-0aP01LLIskjKs3lq52EC0aGBAJhLq7B2Rd8HC/DR/PtNNpcLilNmHC12O+hu0usQpo7wtHNRqtrhBwtDb0+dNg==", + "dev": true, + "requires": { + "has-flag": "3.0.0" + } + } + } + }, + "autosize": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/autosize/-/autosize-4.0.1.tgz", + "integrity": "sha512-Sapd3XwNqZin0VW0DFiAGfgr9s2d1Sudj2+hnE/jj6aJUKXvlaimkY+FFB2Xzm3nfD7tCaMSC2jo4sVB2EOqpw==" + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "dev": true + }, + "aws4": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", + "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=", + "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", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "esutils": "2.0.2", + "js-tokens": "3.0.2" + } + }, + "babel-core": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.0.tgz", + "integrity": "sha1-rzL3izGm/O8RnIew/Y2XU/A6C7g=", + "dev": true, + "requires": { + "babel-code-frame": "6.26.0", + "babel-generator": "6.26.1", + "babel-helpers": "6.24.1", + "babel-messages": "6.23.0", + "babel-register": "6.26.0", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "convert-source-map": "1.5.1", + "debug": "2.6.9", + "json5": "0.5.1", + "lodash": "4.17.5", + "minimatch": "3.0.4", + "path-is-absolute": "1.0.1", + "private": "0.1.8", + "slash": "1.0.0", + "source-map": "0.5.7" + } + }, + "babel-eslint": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-8.0.3.tgz", + "integrity": "sha512-7D4iUpylEiKJPGbeSAlNddGcmA41PadgZ6UAb6JVyh003h3d0EbZusYFBR/+nBgqtaVJM2J2zUVa3N0hrpMH6g==", + "dev": true, + "requires": { + "@babel/code-frame": "7.0.0-beta.31", + "@babel/traverse": "7.0.0-beta.31", + "@babel/types": "7.0.0-beta.31", + "babylon": "7.0.0-beta.31" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.0.0-beta.31", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0-beta.31.tgz", + "integrity": "sha512-yd7CkUughvHQoEahQqcMdrZw6o/6PwUxiRkfZuVDVHCDe77mysD/suoNyk5mK6phTnRW1kyIbPHyCJgxw++LXg==", + "dev": true, + "requires": { + "chalk": "2.3.0", + "esutils": "2.0.2", + "js-tokens": "3.0.2" + } + }, + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + }, + "babylon": { + "version": "7.0.0-beta.31", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.31.tgz", + "integrity": "sha512-6lm2mV3S51yEnKmQQNnswoABL1U1H1KHoCCVwdwI3hvIv+W7ya4ki7Aw4o4KxtUHjNKkK5WpZb22rrMMOcJXJQ==", + "dev": true + }, + "chalk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", + "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "4.5.0" + } + }, + "supports-color": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", + "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", + "dev": true, + "requires": { + "has-flag": "2.0.0" + } + } + } + }, + "babel-generator": { + "version": "6.26.1", + "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", + "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==", + "dev": true, + "requires": { + "babel-messages": "6.23.0", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "detect-indent": "4.0.0", + "jsesc": "1.3.0", + "lodash": "4.17.5", + "source-map": "0.5.7", + "trim-right": "1.0.1" + }, + "dependencies": { + "jsesc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", + "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=", + "dev": true + } + } + }, + "babel-helper-bindify-decorators": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-bindify-decorators/-/babel-helper-bindify-decorators-6.24.1.tgz", + "integrity": "sha1-FMGeXxQte0fxmlJDHlKxzLxAozA=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helper-builder-binary-assignment-operator-visitor": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz", + "integrity": "sha1-zORReto1b0IgvK6KAsKzRvmlZmQ=", + "dev": true, + "requires": { + "babel-helper-explode-assignable-expression": "6.24.1", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helper-builder-react-jsx": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-helper-builder-react-jsx/-/babel-helper-builder-react-jsx-6.26.0.tgz", + "integrity": "sha1-Of+DE7dci2Xc7/HzHTg+D/KkCKA=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "esutils": "2.0.2" + } + }, + "babel-helper-call-delegate": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz", + "integrity": "sha1-7Oaqzdx25Bw0YfiL/Fdb0Nqi340=", + "dev": true, + "requires": { + "babel-helper-hoist-variables": "6.24.1", + "babel-runtime": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helper-define-map": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz", + "integrity": "sha1-pfVtq0GiX5fstJjH66ypgZ+Vvl8=", + "dev": true, + "requires": { + "babel-helper-function-name": "6.24.1", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "lodash": "4.17.5" + } + }, + "babel-helper-explode-assignable-expression": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz", + "integrity": "sha1-8luCz33BBDPFX3BZLVdGQArCLKo=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helper-explode-class": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-explode-class/-/babel-helper-explode-class-6.24.1.tgz", + "integrity": "sha1-fcKjkQ3uAHBW4eMdZAztPVTqqes=", + "dev": true, + "requires": { + "babel-helper-bindify-decorators": "6.24.1", + "babel-runtime": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helper-function-name": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz", + "integrity": "sha1-00dbjAPtmCQqJbSDUasYOZ01gKk=", + "dev": true, + "requires": { + "babel-helper-get-function-arity": "6.24.1", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helper-get-function-arity": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz", + "integrity": "sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helper-hoist-variables": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz", + "integrity": "sha1-HssnaJydJVE+rbyZFKc/VAi+enY=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helper-optimise-call-expression": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz", + "integrity": "sha1-96E0J7qfc/j0+pk8VKl4gtEkQlc=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helper-regex": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz", + "integrity": "sha1-MlxZ+QL4LyS3T6zu0DY5VPZJXnI=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "lodash": "4.17.5" + } + }, + "babel-helper-remap-async-to-generator": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz", + "integrity": "sha1-XsWBgnrXI/7N04HxySg5BnbkVRs=", + "dev": true, + "requires": { + "babel-helper-function-name": "6.24.1", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helper-replace-supers": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz", + "integrity": "sha1-v22/5Dk40XNpohPKiov3S2qQqxo=", + "dev": true, + "requires": { + "babel-helper-optimise-call-expression": "6.24.1", + "babel-messages": "6.23.0", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helpers": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz", + "integrity": "sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-template": "6.26.0" + } + }, + "babel-jest": { + "version": "22.4.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-22.4.0.tgz", + "integrity": "sha512-A/safCd5jSf1D98XoHCN3YYuGurtUPntuPh8b7UxsLNfEp/QC8UwdL+VEGSLN5Fk3+tS/Jdbf5NK/T2it8RGYw==", + "dev": true, + "requires": { + "babel-plugin-istanbul": "4.1.5", + "babel-preset-jest": "22.2.0" + } + }, + "babel-loader": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-7.1.4.tgz", + "integrity": "sha512-/hbyEvPzBJuGpk9o80R0ZyTej6heEOr59GoEUtn8qFKbnx4cJm9FWES6J/iv644sYgrtVw9JJQkjaLW/bqb5gw==", + "dev": true, + "requires": { + "find-cache-dir": "1.0.0", + "loader-utils": "1.1.0", + "mkdirp": "0.5.1" + } + }, + "babel-messages": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", + "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-check-es2015-constants": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz", + "integrity": "sha1-NRV7EBQm/S/9PaP3XH0ekYNbv4o=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-istanbul": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.5.tgz", + "integrity": "sha1-Z2DN2Xf0EdPhdbsGTyvDJ9mbK24=", + "dev": true, + "requires": { + "find-up": "2.1.0", + "istanbul-lib-instrument": "1.9.2", + "test-exclude": "4.2.0" + }, + "dependencies": { + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "2.0.0" + } + } + } + }, + "babel-plugin-jest-hoist": { + "version": "22.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-22.2.0.tgz", + "integrity": "sha512-NwicD5n1YQaj6sM3PVULdPBDk1XdlWvh8xBeUJg3nqZwp79Vofb8Q7GOVeWoZZ/RMlMuJMMrEAgSQl/p392nLA==", + "dev": true + }, + "babel-plugin-syntax-async-functions": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz", + "integrity": "sha1-ytnK0RkbWtY0vzCuCHI5HgZHvpU=", + "dev": true + }, + "babel-plugin-syntax-async-generators": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-async-generators/-/babel-plugin-syntax-async-generators-6.13.0.tgz", + "integrity": "sha1-a8lj67FuzLrmuStZbrfzXDQqi5o=", + "dev": true + }, + "babel-plugin-syntax-class-constructor-call": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-class-constructor-call/-/babel-plugin-syntax-class-constructor-call-6.18.0.tgz", + "integrity": "sha1-nLnTn+Q8hgC+yBRkVt3L1OGnZBY=", + "dev": true + }, + "babel-plugin-syntax-class-properties": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz", + "integrity": "sha1-1+sjt5oxf4VDlixQW4J8fWysJ94=", + "dev": true + }, + "babel-plugin-syntax-decorators": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-decorators/-/babel-plugin-syntax-decorators-6.13.0.tgz", + "integrity": "sha1-MSVjtNvePMgGzuPkFszurd0RrAs=", + "dev": true + }, + "babel-plugin-syntax-dynamic-import": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-dynamic-import/-/babel-plugin-syntax-dynamic-import-6.18.0.tgz", + "integrity": "sha1-jWomIpyDdFqZgqRBBRVyyqF5sdo=", + "dev": true + }, + "babel-plugin-syntax-exponentiation-operator": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz", + "integrity": "sha1-nufoM3KQ2pUoggGmpX9BcDF4MN4=", + "dev": true + }, + "babel-plugin-syntax-export-extensions": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-export-extensions/-/babel-plugin-syntax-export-extensions-6.13.0.tgz", + "integrity": "sha1-cKFITw+QiaToStRLrDU8lbmxJyE=", + "dev": true + }, + "babel-plugin-syntax-flow": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-flow/-/babel-plugin-syntax-flow-6.18.0.tgz", + "integrity": "sha1-TDqyCiryaqIM0lmVw5jE63AxDI0=", + "dev": true + }, + "babel-plugin-syntax-jsx": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", + "integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY=", + "dev": true + }, + "babel-plugin-syntax-object-rest-spread": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz", + "integrity": "sha1-/WU28rzhODb/o6VFjEkDpZe7O/U=", + "dev": true + }, + "babel-plugin-syntax-trailing-function-commas": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz", + "integrity": "sha1-ugNgk3+NBuQBgKQ/4NVhb/9TLPM=", + "dev": true + }, + "babel-plugin-transform-async-generator-functions": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-async-generator-functions/-/babel-plugin-transform-async-generator-functions-6.24.1.tgz", + "integrity": "sha1-8FiQAUX9PpkHpt3yjaWfIVJYpds=", + "dev": true, + "requires": { + "babel-helper-remap-async-to-generator": "6.24.1", + "babel-plugin-syntax-async-generators": "6.13.0", + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-async-to-generator": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz", + "integrity": "sha1-ZTbjeK/2yx1VF6wOQOs+n8jQh2E=", + "dev": true, + "requires": { + "babel-helper-remap-async-to-generator": "6.24.1", + "babel-plugin-syntax-async-functions": "6.13.0", + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-class-constructor-call": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-class-constructor-call/-/babel-plugin-transform-class-constructor-call-6.24.1.tgz", + "integrity": "sha1-gNwoVQWsBn3LjWxl4vbxGrd2Xvk=", + "dev": true, + "requires": { + "babel-plugin-syntax-class-constructor-call": "6.18.0", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0" + } + }, + "babel-plugin-transform-class-properties": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.24.1.tgz", + "integrity": "sha1-anl2PqYdM9NvN7YRqp3vgagbRqw=", + "dev": true, + "requires": { + "babel-helper-function-name": "6.24.1", + "babel-plugin-syntax-class-properties": "6.13.0", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0" + } + }, + "babel-plugin-transform-decorators": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-decorators/-/babel-plugin-transform-decorators-6.24.1.tgz", + "integrity": "sha1-eIAT2PjGtSIr33s0Q5Df13Vp4k0=", + "dev": true, + "requires": { + "babel-helper-explode-class": "6.24.1", + "babel-plugin-syntax-decorators": "6.13.0", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-plugin-transform-es2015-arrow-functions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz", + "integrity": "sha1-RSaSy3EdX3ncf4XkQM5BufJE0iE=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-block-scoped-functions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz", + "integrity": "sha1-u8UbSflk1wy42OC5ToICRs46YUE=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-block-scoping": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz", + "integrity": "sha1-1w9SmcEwjQXBL0Y4E7CgnnOxiV8=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0", + "lodash": "4.17.5" + } + }, + "babel-plugin-transform-es2015-classes": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz", + "integrity": "sha1-WkxYpQyclGHlZLSyo7+ryXolhNs=", + "dev": true, + "requires": { + "babel-helper-define-map": "6.26.0", + "babel-helper-function-name": "6.24.1", + "babel-helper-optimise-call-expression": "6.24.1", + "babel-helper-replace-supers": "6.24.1", + "babel-messages": "6.23.0", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-plugin-transform-es2015-computed-properties": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz", + "integrity": "sha1-b+Ko0WiV1WNPTNmZttNICjCBWbM=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-template": "6.26.0" + } + }, + "babel-plugin-transform-es2015-destructuring": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz", + "integrity": "sha1-mXux8auWf2gtKwh2/jWNYOdlxW0=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-duplicate-keys": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz", + "integrity": "sha1-c+s9MQypaePvnskcU3QabxV2Qj4=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-plugin-transform-es2015-for-of": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz", + "integrity": "sha1-9HyVsrYT3x0+zC/bdXNiPHUkhpE=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-function-name": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz", + "integrity": "sha1-g0yJhTvDaxrw86TF26qU/Y6sqos=", + "dev": true, + "requires": { + "babel-helper-function-name": "6.24.1", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-plugin-transform-es2015-literals": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz", + "integrity": "sha1-T1SgLWzWbPkVKAAZox0xklN3yi4=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-modules-amd": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz", + "integrity": "sha1-Oz5UAXI5hC1tGcMBHEvS8AoA0VQ=", + "dev": true, + "requires": { + "babel-plugin-transform-es2015-modules-commonjs": "6.26.0", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0" + } + }, + "babel-plugin-transform-es2015-modules-commonjs": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.0.tgz", + "integrity": "sha1-DYOUApt9xqvhqX7xgeAHWN0uXYo=", + "dev": true, + "requires": { + "babel-plugin-transform-strict-mode": "6.24.1", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-plugin-transform-es2015-modules-systemjs": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz", + "integrity": "sha1-/4mhQrkRmpBhlfXxBuzzBdlAfSM=", + "dev": true, + "requires": { + "babel-helper-hoist-variables": "6.24.1", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0" + } + }, + "babel-plugin-transform-es2015-modules-umd": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz", + "integrity": "sha1-rJl+YoXNGO1hdq22B9YCNErThGg=", + "dev": true, + "requires": { + "babel-plugin-transform-es2015-modules-amd": "6.24.1", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0" + } + }, + "babel-plugin-transform-es2015-object-super": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz", + "integrity": "sha1-JM72muIcuDp/hgPa0CH1cusnj40=", + "dev": true, + "requires": { + "babel-helper-replace-supers": "6.24.1", + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-parameters": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz", + "integrity": "sha1-V6w1GrScrxSpfNE7CfZv3wpiXys=", + "dev": true, + "requires": { + "babel-helper-call-delegate": "6.24.1", + "babel-helper-get-function-arity": "6.24.1", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-plugin-transform-es2015-shorthand-properties": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz", + "integrity": "sha1-JPh11nIch2YbvZmkYi5R8U3jiqA=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-plugin-transform-es2015-spread": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz", + "integrity": "sha1-1taKmfia7cRTbIGlQujdnxdG+NE=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-sticky-regex": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz", + "integrity": "sha1-AMHNsaynERLN8M9hJsLta0V8zbw=", + "dev": true, + "requires": { + "babel-helper-regex": "6.26.0", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-plugin-transform-es2015-template-literals": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz", + "integrity": "sha1-qEs0UPfp+PH2g51taH2oS7EjbY0=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-typeof-symbol": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz", + "integrity": "sha1-3sCfHN3/lLUqxz1QXITfWdzOs3I=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-unicode-regex": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz", + "integrity": "sha1-04sS9C6nMj9yk4fxinxa4frrNek=", + "dev": true, + "requires": { + "babel-helper-regex": "6.26.0", + "babel-runtime": "6.26.0", + "regexpu-core": "2.0.0" + } + }, + "babel-plugin-transform-exponentiation-operator": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz", + "integrity": "sha1-KrDJx/MJj6SJB3cruBP+QejeOg4=", + "dev": true, + "requires": { + "babel-helper-builder-binary-assignment-operator-visitor": "6.24.1", + "babel-plugin-syntax-exponentiation-operator": "6.13.0", + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-export-extensions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-export-extensions/-/babel-plugin-transform-export-extensions-6.22.0.tgz", + "integrity": "sha1-U3OLR+deghhYnuqUbLvTkQm75lM=", + "dev": true, + "requires": { + "babel-plugin-syntax-export-extensions": "6.13.0", + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-flow-strip-types": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-flow-strip-types/-/babel-plugin-transform-flow-strip-types-6.22.0.tgz", + "integrity": "sha1-hMtnKTXUNxT9wyvOhFaNh0Qc988=", + "dev": true, + "requires": { + "babel-plugin-syntax-flow": "6.18.0", + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-object-rest-spread": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz", + "integrity": "sha1-DzZpLVD+9rfi1LOsFHgTepY7ewY=", + "dev": true, + "requires": { + "babel-plugin-syntax-object-rest-spread": "6.13.0", + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-react-jsx": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx/-/babel-plugin-transform-react-jsx-6.24.1.tgz", + "integrity": "sha1-hAoCjn30YN/DotKfDA2R9jduZqM=", + "dev": true, + "requires": { + "babel-helper-builder-react-jsx": "6.26.0", + "babel-plugin-syntax-jsx": "6.18.0", + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-regenerator": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz", + "integrity": "sha1-4HA2lvveJ/Cj78rPi03KL3s6jy8=", + "dev": true, + "requires": { + "regenerator-transform": "0.10.1" + } + }, + "babel-plugin-transform-runtime": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-runtime/-/babel-plugin-transform-runtime-6.23.0.tgz", + "integrity": "sha1-iEkNRGUC6puOfvsP4J7E2ZR5se4=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-strict-mode": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz", + "integrity": "sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-preset-env": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/babel-preset-env/-/babel-preset-env-1.6.1.tgz", + "integrity": "sha512-W6VIyA6Ch9ePMI7VptNn2wBM6dbG0eSz25HEiL40nQXCsXGTGZSTZu1Iap+cj3Q0S5a7T9+529l/5Bkvd+afNA==", + "dev": true, + "requires": { + "babel-plugin-check-es2015-constants": "6.22.0", + "babel-plugin-syntax-trailing-function-commas": "6.22.0", + "babel-plugin-transform-async-to-generator": "6.24.1", + "babel-plugin-transform-es2015-arrow-functions": "6.22.0", + "babel-plugin-transform-es2015-block-scoped-functions": "6.22.0", + "babel-plugin-transform-es2015-block-scoping": "6.26.0", + "babel-plugin-transform-es2015-classes": "6.24.1", + "babel-plugin-transform-es2015-computed-properties": "6.24.1", + "babel-plugin-transform-es2015-destructuring": "6.23.0", + "babel-plugin-transform-es2015-duplicate-keys": "6.24.1", + "babel-plugin-transform-es2015-for-of": "6.23.0", + "babel-plugin-transform-es2015-function-name": "6.24.1", + "babel-plugin-transform-es2015-literals": "6.22.0", + "babel-plugin-transform-es2015-modules-amd": "6.24.1", + "babel-plugin-transform-es2015-modules-commonjs": "6.26.0", + "babel-plugin-transform-es2015-modules-systemjs": "6.24.1", + "babel-plugin-transform-es2015-modules-umd": "6.24.1", + "babel-plugin-transform-es2015-object-super": "6.24.1", + "babel-plugin-transform-es2015-parameters": "6.24.1", + "babel-plugin-transform-es2015-shorthand-properties": "6.24.1", + "babel-plugin-transform-es2015-spread": "6.22.0", + "babel-plugin-transform-es2015-sticky-regex": "6.24.1", + "babel-plugin-transform-es2015-template-literals": "6.22.0", + "babel-plugin-transform-es2015-typeof-symbol": "6.23.0", + "babel-plugin-transform-es2015-unicode-regex": "6.24.1", + "babel-plugin-transform-exponentiation-operator": "6.24.1", + "babel-plugin-transform-regenerator": "6.26.0", + "browserslist": "2.11.3", + "invariant": "2.2.2", + "semver": "5.3.0" + } + }, + "babel-preset-es2015": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-preset-es2015/-/babel-preset-es2015-6.24.1.tgz", + "integrity": "sha1-1EBQ1rwsn+6nAqrzjXJ6AhBTiTk=", + "dev": true, + "requires": { + "babel-plugin-check-es2015-constants": "6.22.0", + "babel-plugin-transform-es2015-arrow-functions": "6.22.0", + "babel-plugin-transform-es2015-block-scoped-functions": "6.22.0", + "babel-plugin-transform-es2015-block-scoping": "6.26.0", + "babel-plugin-transform-es2015-classes": "6.24.1", + "babel-plugin-transform-es2015-computed-properties": "6.24.1", + "babel-plugin-transform-es2015-destructuring": "6.23.0", + "babel-plugin-transform-es2015-duplicate-keys": "6.24.1", + "babel-plugin-transform-es2015-for-of": "6.23.0", + "babel-plugin-transform-es2015-function-name": "6.24.1", + "babel-plugin-transform-es2015-literals": "6.22.0", + "babel-plugin-transform-es2015-modules-amd": "6.24.1", + "babel-plugin-transform-es2015-modules-commonjs": "6.26.0", + "babel-plugin-transform-es2015-modules-systemjs": "6.24.1", + "babel-plugin-transform-es2015-modules-umd": "6.24.1", + "babel-plugin-transform-es2015-object-super": "6.24.1", + "babel-plugin-transform-es2015-parameters": "6.24.1", + "babel-plugin-transform-es2015-shorthand-properties": "6.24.1", + "babel-plugin-transform-es2015-spread": "6.22.0", + "babel-plugin-transform-es2015-sticky-regex": "6.24.1", + "babel-plugin-transform-es2015-template-literals": "6.22.0", + "babel-plugin-transform-es2015-typeof-symbol": "6.23.0", + "babel-plugin-transform-es2015-unicode-regex": "6.24.1", + "babel-plugin-transform-regenerator": "6.26.0" + } + }, + "babel-preset-jest": { + "version": "22.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-22.2.0.tgz", + "integrity": "sha512-p61cPMGYlSgfNScn1yQuVnLguWE4bjhB/br4KQDMbYZG+v6ryE5Ch7TKukjA6mRuIQj1zhyou7Sbpqrh4/N6Pg==", + "dev": true, + "requires": { + "babel-plugin-jest-hoist": "22.2.0", + "babel-plugin-syntax-object-rest-spread": "6.13.0" + } + }, + "babel-preset-stage-1": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-preset-stage-1/-/babel-preset-stage-1-6.24.1.tgz", + "integrity": "sha1-dpLNfc1oSZB+auSgqFWJz7niv7A=", + "dev": true, + "requires": { + "babel-plugin-transform-class-constructor-call": "6.24.1", + "babel-plugin-transform-export-extensions": "6.22.0", + "babel-preset-stage-2": "6.24.1" + } + }, + "babel-preset-stage-2": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-preset-stage-2/-/babel-preset-stage-2-6.24.1.tgz", + "integrity": "sha1-2eKWD7PXEYfw5k7sYrwHdnIZvcE=", + "dev": true, + "requires": { + "babel-plugin-syntax-dynamic-import": "6.18.0", + "babel-plugin-transform-class-properties": "6.24.1", + "babel-plugin-transform-decorators": "6.24.1", + "babel-preset-stage-3": "6.24.1" + } + }, + "babel-preset-stage-3": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-preset-stage-3/-/babel-preset-stage-3-6.24.1.tgz", + "integrity": "sha1-g2raCp56f6N8sTj7kyb4eTSkg5U=", + "dev": true, + "requires": { + "babel-plugin-syntax-trailing-function-commas": "6.22.0", + "babel-plugin-transform-async-generator-functions": "6.24.1", + "babel-plugin-transform-async-to-generator": "6.24.1", + "babel-plugin-transform-exponentiation-operator": "6.24.1", + "babel-plugin-transform-object-rest-spread": "6.26.0" + } + }, + "babel-register": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.26.0.tgz", + "integrity": "sha1-btAhFz4vy0htestFxgCahW9kcHE=", + "dev": true, + "requires": { + "babel-core": "6.26.0", + "babel-runtime": "6.26.0", + "core-js": "2.5.3", + "home-or-tmp": "2.0.0", + "lodash": "4.17.5", + "mkdirp": "0.5.1", + "source-map-support": "0.4.18" + }, + "dependencies": { + "core-js": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.3.tgz", + "integrity": "sha1-isw4NFgk8W2DZbfJtCWRaOjtYD4=", + "dev": true + }, + "source-map-support": { + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", + "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", + "dev": true, + "requires": { + "source-map": "0.5.7" + } + } + } + }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "dev": true, + "requires": { + "core-js": "2.5.3", + "regenerator-runtime": "0.11.1" + }, + "dependencies": { + "core-js": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.3.tgz", + "integrity": "sha1-isw4NFgk8W2DZbfJtCWRaOjtYD4=", + "dev": true + } + } + }, + "babel-template": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", + "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "lodash": "4.17.5" + } + }, + "babel-traverse": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", + "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", + "dev": true, + "requires": { + "babel-code-frame": "6.26.0", + "babel-messages": "6.23.0", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "debug": "2.6.9", + "globals": "9.18.0", + "invariant": "2.2.2", + "lodash": "4.17.5" + } + }, + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "esutils": "2.0.2", + "lodash": "4.17.5", + "to-fast-properties": "1.0.3" + }, + "dependencies": { + "to-fast-properties": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", + "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=", + "dev": true + } + } + }, + "babylon": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", + "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "requires": { + "cache-base": "1.0.1", + "class-utils": "0.3.6", + "component-emitter": "1.2.1", + "define-property": "1.0.0", + "isobject": "3.0.1", + "mixin-deep": "1.3.1", + "pascalcase": "0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "1.0.2" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "base64-js": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.3.tgz", + "integrity": "sha512-MsAhsUW1GxCdgYSO6tAfZrNapmUKk7mWx/k5mFY/A1gBtkaCaNapTg+FExCw1r9yeaZhqx/xPg43xgTFH6KL5w==", + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", + "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "dev": true, + "optional": true, + "requires": { + "tweetnacl": "0.14.5" + } + }, + "big.js": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz", + "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==", + "dev": true + }, + "binary-extensions": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.11.0.tgz", + "integrity": "sha1-RqoXUftqL5PuXmibsQh9SxTGwgU=", + "dev": true + }, + "binaryextensions": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/binaryextensions/-/binaryextensions-2.1.1.tgz", + "integrity": "sha512-XBaoWE9RW8pPdPQNibZsW2zh8TW6gcarXp1FZPwT8Uop8ScSNldJEWf2k9l3HeTqdrEwsOsFcq74RiJECW34yA==", + "dev": true + }, + "block-stream": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", + "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", + "dev": true, + "requires": { + "inherits": "2.0.3" + } + }, + "bluebird": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", + "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==" + }, + "bn.js": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", + "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", + "dev": true + }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", + "dev": true + }, + "boom": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", + "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", + "dev": true, + "requires": { + "hoek": "4.2.0" + } + }, + "brace-expansion": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", + "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "dev": true, + "requires": { + "expand-range": "1.8.2", + "preserve": "0.2.0", + "repeat-element": "1.1.2" + } + }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", + "dev": true + }, + "browser-process-hrtime": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-0.1.2.tgz", + "integrity": "sha1-Ql1opY00R/AqBKqJQYf86K+Le44=", + "dev": true + }, + "browser-resolve": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.2.tgz", + "integrity": "sha1-j/CbCixCFxihBRwmCzLkj0QpOM4=", + "dev": true, + "requires": { + "resolve": "1.1.7" + }, + "dependencies": { + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "dev": true + } + } + }, + "browserify-aes": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.1.1.tgz", + "integrity": "sha512-UGnTYAnB2a3YuYKIRy1/4FB2HdM866E0qC46JXvVTYKlBlZlnvfpSfY6OKfXZAkv70eJ2a1SqzpAo5CRhZGDFg==", + "dev": true, + "requires": { + "buffer-xor": "1.0.3", + "cipher-base": "1.0.4", + "create-hash": "1.1.3", + "evp_bytestokey": "1.0.3", + "inherits": "2.0.3", + "safe-buffer": "5.1.1" + } + }, + "browserify-cipher": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.0.tgz", + "integrity": "sha1-mYgkSHS/XtTijalWZtzWasj8Njo=", + "dev": true, + "requires": { + "browserify-aes": "1.1.1", + "browserify-des": "1.0.0", + "evp_bytestokey": "1.0.3" + } + }, + "browserify-des": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.0.tgz", + "integrity": "sha1-2qJ3cXRwki7S/hhZQRihdUOXId0=", + "dev": true, + "requires": { + "cipher-base": "1.0.4", + "des.js": "1.0.0", + "inherits": "2.0.3" + } + }, + "browserify-rsa": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", + "dev": true, + "requires": { + "bn.js": "4.11.8", + "randombytes": "2.0.6" + } + }, + "browserify-sign": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", + "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", + "dev": true, + "requires": { + "bn.js": "4.11.8", + "browserify-rsa": "4.0.1", + "create-hash": "1.1.3", + "create-hmac": "1.1.6", + "elliptic": "6.4.0", + "inherits": "2.0.3", + "parse-asn1": "5.1.0" + } + }, + "browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "dev": true, + "requires": { + "pako": "1.0.6" + } + }, + "browserslist": { + "version": "2.11.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-2.11.3.tgz", + "integrity": "sha512-yWu5cXT7Av6mVwzWc8lMsJMHWn4xyjSuGYi4IozbVTLUOEYPSagUB8kiMDUHA1fS3zjr8nkxkn9jdvug4BBRmA==", + "dev": true, + "requires": { + "caniuse-lite": "1.0.30000828", + "electron-to-chromium": "1.3.33" + } + }, + "bser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.0.0.tgz", + "integrity": "sha1-mseNPtXZFYBP2HrLFYvHlxR6Fxk=", + "dev": true, + "requires": { + "node-int64": "0.4.0" + } + }, + "buffer": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", + "dev": true, + "requires": { + "base64-js": "1.2.3", + "ieee754": "1.1.11", + "isarray": "1.0.0" + } + }, + "buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", + "dev": true + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=" + }, + "builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", + "dev": true + }, + "cacache": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-10.0.4.tgz", + "integrity": "sha512-Dph0MzuH+rTQzGPNT9fAnrPmMmjKfST6trxJeK7NQuHRaVw24VzPRWTmg9MpcwOVQZO0E1FBICUlFeNaKPIfHA==", + "dev": true, + "requires": { + "bluebird": "3.5.1", + "chownr": "1.0.1", + "glob": "7.1.2", + "graceful-fs": "4.1.11", + "lru-cache": "4.1.2", + "mississippi": "2.0.0", + "mkdirp": "0.5.1", + "move-concurrently": "1.0.1", + "promise-inflight": "1.0.1", + "rimraf": "2.6.2", + "ssri": "5.3.0", + "unique-filename": "1.1.0", + "y18n": "4.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.2.tgz", + "integrity": "sha512-wgeVXhrDwAWnIF/yZARsFnMBtdFXOg1b8RIrhilp+0iDYN4mdQcNZElDZ0e4B64BhaxeQ5zN7PMyvu7we1kPeQ==", + "dev": true, + "requires": { + "pseudomap": "1.0.2", + "yallist": "2.1.2" + } + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + } + } + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "requires": { + "collection-visit": "1.0.0", + "component-emitter": "1.2.1", + "get-value": "2.0.6", + "has-value": "1.0.0", + "isobject": "3.0.1", + "set-value": "2.0.0", + "to-object-path": "0.3.0", + "union-value": "1.0.0", + "unset-value": "1.0.0" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "cacheable-request": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-2.1.4.tgz", + "integrity": "sha1-DYCIAbY0KtM8kd+dC0TcCbkeXD0=", + "dev": true, + "requires": { + "clone-response": "1.0.2", + "get-stream": "3.0.0", + "http-cache-semantics": "3.8.1", + "keyv": "3.0.0", + "lowercase-keys": "1.0.0", + "normalize-url": "2.0.1", + "responselike": "1.0.2" + }, + "dependencies": { + "lowercase-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.0.tgz", + "integrity": "sha1-TjNms55/VFfjXxMkvfb4jQv8cwY=", + "dev": true + }, + "normalize-url": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-2.0.1.tgz", + "integrity": "sha512-D6MUW4K/VzoJ4rJ01JFKxDrtY1v9wrgzCX5f2qj/lzH1m/lW6MhUZFKerVsnyjOhOsYzI9Kqqak+10l4LvLpMw==", + "dev": true, + "requires": { + "prepend-http": "2.0.0", + "query-string": "5.1.1", + "sort-keys": "2.0.0" + } + }, + "prepend-http": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", + "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=", + "dev": true + }, + "query-string": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz", + "integrity": "sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==", + "dev": true, + "requires": { + "decode-uri-component": "0.2.0", + "object-assign": "4.1.1", + "strict-uri-encode": "1.1.0" + } + }, + "sort-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz", + "integrity": "sha1-ZYU1WEhh7JfXMNbPQYIuH1ZoQSg=", + "dev": true, + "requires": { + "is-plain-obj": "1.1.0" + } + } + } + }, + "caller-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", + "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", + "dev": true, + "requires": { + "callsites": "0.2.0" + }, + "dependencies": { + "callsites": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", + "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", + "dev": true + } + } + }, + "callsites": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", + "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=", + "dev": true + }, + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" + }, + "camelcase-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", + "dev": true, + "requires": { + "camelcase": "2.1.1", + "map-obj": "1.0.1" + }, + "dependencies": { + "camelcase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", + "dev": true + } + } + }, + "caniuse-api": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-1.6.1.tgz", + "integrity": "sha1-tTTnxzTE+B7F++isoq0kNUuWLGw=", + "dev": true, + "requires": { + "browserslist": "1.7.7", + "caniuse-db": "1.0.30000804", + "lodash.memoize": "4.1.2", + "lodash.uniq": "4.5.0" + }, + "dependencies": { + "browserslist": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-1.7.7.tgz", + "integrity": "sha1-C9dnBCWL6CmyOYu1Dkti0aFmsLk=", + "dev": true, + "requires": { + "caniuse-db": "1.0.30000804", + "electron-to-chromium": "1.3.33" + } + } + } + }, + "caniuse-db": { + "version": "1.0.30000804", + "resolved": "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30000804.tgz", + "integrity": "sha1-hP60IBj8ZM9q/2Nx5DEV8pLAAXk=", + "dev": true + }, + "caniuse-lite": { + "version": "1.0.30000828", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000828.tgz", + "integrity": "sha512-v+ySC6Ih8N8CyGZYd4svPipuFIqskKsTOi18chFM0qtu1G8mGuSYajb+h49XDWgmzX8MRDOp1Agw6KQaPUdIhg==", + "dev": true + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "dev": true + }, + "center-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", + "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", + "dev": true, + "optional": true, + "requires": { + "align-text": "0.1.4", + "lazy-cache": "1.0.4" + } + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + }, + "chardet": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz", + "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=", + "dev": true + }, + "check-node-version": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/check-node-version/-/check-node-version-3.1.1.tgz", + "integrity": "sha512-52fHDe/0pbidY3InI33Beyb/oarySfLANlXxLGBl9lLVrLIW88XWIwu4jGJrQ1imuWzX5ukNGWXUyCgmgVUD8A==", + "dev": true, + "requires": { + "chalk": "2.3.0", + "map-values": "1.0.1", + "minimist": "1.2.0", + "object-filter": "1.0.2", + "object.assign": "4.1.0", + "run-parallel": "1.1.6", + "semver": "5.3.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + }, + "chalk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", + "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "4.5.0" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "supports-color": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", + "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", + "dev": true, + "requires": { + "has-flag": "2.0.0" + } + } + } + }, + "cheerio": { + "version": "1.0.0-rc.2", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.2.tgz", + "integrity": "sha1-S59TqBsn5NXawxwP/Qz6A8xoMNs=", + "dev": true, + "requires": { + "css-select": "1.2.0", + "dom-serializer": "0.1.0", + "entities": "1.1.1", + "htmlparser2": "3.9.2", + "lodash": "4.17.5", + "parse5": "3.0.3" + } + }, + "chokidar": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.3.tgz", + "integrity": "sha512-zW8iXYZtXMx4kux/nuZVXjkLP+CyIK5Al5FHnj1OgTKGZfp4Oy6/ymtMSKFv3GD8DviEmUPmJg9eFdJ/JzudMg==", + "dev": true, + "requires": { + "anymatch": "2.0.0", + "async-each": "1.0.1", + "braces": "2.3.1", + "fsevents": "1.1.3", + "glob-parent": "3.1.0", + "inherits": "2.0.3", + "is-binary-path": "1.0.1", + "is-glob": "4.0.0", + "normalize-path": "2.1.1", + "path-is-absolute": "1.0.1", + "readdirp": "2.1.0", + "upath": "1.0.4" + }, + "dependencies": { + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "requires": { + "micromatch": "3.1.10", + "normalize-path": "2.1.1" + } + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "braces": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.1.tgz", + "integrity": "sha512-SO5lYHA3vO6gz66erVvedSCkp7AKWdv6VcQ2N4ysXfPxdAlxAMMAdwegGGcv1Bqwm7naF1hNdk5d6AAIEHV2nQ==", + "dev": true, + "requires": { + "arr-flatten": "1.1.0", + "array-unique": "0.3.2", + "define-property": "1.0.0", + "extend-shallow": "2.0.1", + "fill-range": "4.0.0", + "isobject": "3.0.1", + "kind-of": "6.0.2", + "repeat-element": "1.1.2", + "snapdragon": "0.8.2", + "snapdragon-node": "2.1.1", + "split-string": "3.1.0", + "to-regex": "3.0.2" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "1.0.2" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "2.6.9", + "define-property": "0.2.5", + "extend-shallow": "2.0.1", + "posix-character-classes": "0.1.1", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "0.1.6" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "0.1.6", + "is-data-descriptor": "0.1.4", + "kind-of": "5.1.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "0.3.2", + "define-property": "1.0.0", + "expand-brackets": "2.1.4", + "extend-shallow": "2.0.1", + "fragment-cache": "0.2.1", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "1.0.2" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "2.0.1", + "is-number": "3.0.0", + "repeat-string": "1.6.1", + "to-regex-range": "2.1.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "requires": { + "is-glob": "3.1.0", + "path-dirname": "1.0.2" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "2.1.1" + } + } + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "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.6" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "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.6" + } + } + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-glob": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz", + "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=", + "dev": true, + "requires": { + "is-extglob": "2.1.1" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "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.6" + } + } + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "4.0.0", + "array-unique": "0.3.2", + "braces": "2.3.1", + "define-property": "2.0.2", + "extend-shallow": "3.0.2", + "extglob": "2.0.4", + "fragment-cache": "0.2.1", + "kind-of": "6.0.2", + "nanomatch": "1.2.9", + "object.pick": "1.3.0", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + } + } + } + }, + "chownr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.0.1.tgz", + "integrity": "sha1-4qdQQqlVGQi+vSW4Uj1fl2nXkYE=", + "dev": true + }, + "chrome-trace-event": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-0.1.2.tgz", + "integrity": "sha1-kPNohdU0WlBiEzLwcXtZWIPV2YI=", + "dev": true + }, + "ci-info": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.1.2.tgz", + "integrity": "sha512-uTGIPNx/nSpBdsF6xnseRXLLtfr9VLqkz8ZqHXr3Y7b6SftyRxBGjwMtJj1OhNbmlc1wZzLNAlAcvyIiE8a6ZA==", + "dev": true + }, + "cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "dev": true, + "requires": { + "inherits": "2.0.3", + "safe-buffer": "5.1.1" + } + }, + "circular-json": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", + "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", + "dev": true + }, + "circular-json-es6": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/circular-json-es6/-/circular-json-es6-2.0.2.tgz", + "integrity": "sha512-ODYONMMNb3p658Zv+Pp+/XPa5s6q7afhz3Tzyvo+VRh9WIrJ64J76ZC4GQxnlye/NesTn09jvOiuE8+xxfpwhQ==", + "dev": true + }, + "clap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/clap/-/clap-1.2.3.tgz", + "integrity": "sha512-4CoL/A3hf90V3VIEjeuhSvlGFEHKzOz+Wfc2IVZc+FaUgU0ZQafJTP49fvnULipOPcAfqhyI2duwQyns6xqjYA==", + "dev": true, + "requires": { + "chalk": "1.1.3" + } + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, + "requires": { + "arr-union": "3.1.0", + "define-property": "0.2.5", + "isobject": "3.0.1", + "static-extend": "0.1.2" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "0.1.6" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "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.6" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "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.6" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "0.1.6", + "is-data-descriptor": "0.1.4", + "kind-of": "5.1.0" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "classnames": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.5.tgz", + "integrity": "sha1-+zgB1FNGdknvNgPH1hoCvRKb3m0=" + }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "dev": true, + "requires": { + "restore-cursor": "2.0.0" + } + }, + "cli-spinners": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-0.1.2.tgz", + "integrity": "sha1-u3ZNiOGF+54eaiofGXcjGPYF4xw=", + "dev": true + }, + "cli-table": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.1.tgz", + "integrity": "sha1-9TsFJmqLGguTSz0IIebi3FkUriM=", + "dev": true, + "requires": { + "colors": "1.0.3" + }, + "dependencies": { + "colors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=", + "dev": true + } + } + }, + "cli-truncate": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-0.2.1.tgz", + "integrity": "sha1-nxXPuwcFAFNpIWxiasfQWrkN1XQ=", + "dev": true, + "requires": { + "slice-ansi": "0.0.4", + "string-width": "1.0.2" + }, + "dependencies": { + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + } + } + }, + "cli-width": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", + "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", + "dev": true + }, + "clipboard": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-1.7.1.tgz", + "integrity": "sha1-Ng1taUbpmnof7zleQrqStem1oWs=", + "requires": { + "good-listener": "1.2.2", + "select": "1.1.2", + "tiny-emitter": "2.0.2" + } + }, + "cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "requires": { + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wrap-ansi": "2.1.0" + }, + "dependencies": { + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + } + } + }, + "clone": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.3.tgz", + "integrity": "sha1-KY1+IjFmD0DAA8LtMUDezz9TCF8=", + "dev": true + }, + "clone-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-buffer/-/clone-buffer-1.0.0.tgz", + "integrity": "sha1-4+JbIHrE5wGvch4staFnksrD3Fg=", + "dev": true + }, + "clone-deep": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-2.0.2.tgz", + "integrity": "sha512-SZegPTKjCgpQH63E+eN6mVEEPdQBOUzjyJm5Pora4lrwWRFS8I0QAxV/KD6vV/i0WuijHZWQC1fMsPEdxfdVCQ==", + "dev": true, + "requires": { + "for-own": "1.0.0", + "is-plain-object": "2.0.4", + "kind-of": "6.0.2", + "shallow-clone": "1.0.0" + }, + "dependencies": { + "for-own": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", + "dev": true, + "requires": { + "for-in": "1.0.2" + } + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, + "clone-response": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", + "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "dev": true, + "requires": { + "mimic-response": "1.0.0" + } + }, + "clone-stats": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-0.0.1.tgz", + "integrity": "sha1-uI+UqCzzi4eR1YBG6kAprYjKmdE=", + "dev": true + }, + "cloneable-readable": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cloneable-readable/-/cloneable-readable-1.1.2.tgz", + "integrity": "sha512-Bq6+4t+lbM8vhTs/Bef5c5AdEMtapp/iFb6+s4/Hh9MVTt8OLKH7ZOOZSCT+Ys7hsHvqv0GuMPJ1lnQJVHvxpg==", + "dev": true, + "requires": { + "inherits": "2.0.3", + "process-nextick-args": "2.0.0", + "readable-stream": "2.3.5" + }, + "dependencies": { + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", + "dev": true + }, + "readable-stream": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.5.tgz", + "integrity": "sha512-tK0yDhrkygt/knjowCUiWP9YdV7c5R+8cR0r/kt9ZhBU906Fs6RpQJCEilamRJj1Nx2rWI6LkW9gKqjTkshhEw==", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "2.0.0", + "safe-buffer": "5.1.1", + "string_decoder": "1.0.3", + "util-deprecate": "1.0.2" + } + } + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true + }, + "coa": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/coa/-/coa-1.0.4.tgz", + "integrity": "sha1-qe8VNmDWqGqL3sAomlxoTSF0Mv0=", + "dev": true, + "requires": { + "q": "1.5.1" + } + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + }, + "codecov": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/codecov/-/codecov-3.0.0.tgz", + "integrity": "sha1-wnO4xPEpRXI+jcnSWAPYk0Pl8o4=", + "dev": true, + "requires": { + "argv": "0.0.2", + "request": "2.81.0", + "urlgrey": "0.4.4" + }, + "dependencies": { + "ajv": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", + "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", + "dev": true, + "requires": { + "co": "4.6.0", + "json-stable-stringify": "1.0.1" + } + }, + "assert-plus": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", + "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=", + "dev": true + }, + "aws-sign2": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", + "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=", + "dev": true + }, + "boom": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", + "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", + "dev": true, + "requires": { + "hoek": "2.16.3" + } + }, + "cryptiles": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", + "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", + "dev": true, + "requires": { + "boom": "2.10.1" + } + }, + "form-data": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", + "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", + "dev": true, + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.5", + "mime-types": "2.1.17" + } + }, + "har-schema": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz", + "integrity": "sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4=", + "dev": true + }, + "har-validator": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz", + "integrity": "sha1-M0gdDxu/9gDdID11gSpqX7oALio=", + "dev": true, + "requires": { + "ajv": "4.11.8", + "har-schema": "1.0.5" + } + }, + "hawk": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", + "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", + "dev": true, + "requires": { + "boom": "2.10.1", + "cryptiles": "2.0.5", + "hoek": "2.16.3", + "sntp": "1.0.9" + } + }, + "hoek": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", + "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=", + "dev": true + }, + "http-signature": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", + "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", + "dev": true, + "requires": { + "assert-plus": "0.2.0", + "jsprim": "1.4.1", + "sshpk": "1.13.1" + } + }, + "performance-now": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", + "integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=", + "dev": true + }, + "qs": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", + "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=", + "dev": true + }, + "request": { + "version": "2.81.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.81.0.tgz", + "integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=", + "dev": true, + "requires": { + "aws-sign2": "0.6.0", + "aws4": "1.6.0", + "caseless": "0.12.0", + "combined-stream": "1.0.5", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.1.4", + "har-validator": "4.2.1", + "hawk": "3.1.3", + "http-signature": "1.1.1", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.17", + "oauth-sign": "0.8.2", + "performance-now": "0.2.0", + "qs": "6.4.0", + "safe-buffer": "5.1.1", + "stringstream": "0.0.5", + "tough-cookie": "2.3.3", + "tunnel-agent": "0.6.0", + "uuid": "3.1.0" + } + }, + "sntp": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", + "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", + "dev": true, + "requires": { + "hoek": "2.16.3" + } + } + } + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "dev": true, + "requires": { + "map-visit": "1.0.0", + "object-visit": "1.0.1" + } + }, + "color": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/color/-/color-0.11.4.tgz", + "integrity": "sha1-bXtcdPtl6EHNSHkq0e1eB7kE12Q=", + "dev": true, + "requires": { + "clone": "1.0.3", + "color-convert": "1.9.1", + "color-string": "0.3.0" + } + }, + "color-convert": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", + "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "color-string": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-0.3.0.tgz", + "integrity": "sha1-J9RvtnAlxcL6JZk7+/V55HhBuZE=", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "colormin": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colormin/-/colormin-1.1.2.tgz", + "integrity": "sha1-6i90IKcrlogaOKrlnsEkpvcpgTM=", + "dev": true, + "requires": { + "color": "0.11.4", + "css-color-names": "0.0.4", + "has": "1.0.1" + } + }, + "colors": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/colors/-/colors-0.5.1.tgz", + "integrity": "sha1-fQAj6usVTo7p/Oddy5I9DtFmd3Q=", + "dev": true + }, + "combined-stream": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", + "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", + "dev": true, + "requires": { + "delayed-stream": "1.0.0" + } + }, + "commander": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.14.1.tgz", + "integrity": "sha512-+YR16o3rK53SmWHU3rEM3tPAh2rwb1yPcQX5irVn7mb0gXbwuCCrnkbV5+PBfETdfg1vui07nM6PCG1zndcjQw==" + }, + "comment-parser": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-0.4.2.tgz", + "integrity": "sha1-+lo/eAEwcBFIZtx7jpzzF6ljX3Q=", + "requires": { + "readable-stream": "2.3.3" + } + }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", + "dev": true + }, + "computed-style": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/computed-style/-/computed-style-0.1.4.tgz", + "integrity": "sha1-fzRP2FhLLkJb7cpKGvwOMAuwXXQ=" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "concat-stream": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.0.tgz", + "integrity": "sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "readable-stream": "2.3.3", + "typedarray": "0.0.6" + } + }, + "concurrently": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-3.5.0.tgz", + "integrity": "sha1-jPG3cHppFqeKT/W3e7BN7FSzebI=", + "dev": true, + "requires": { + "chalk": "0.5.1", + "commander": "2.6.0", + "date-fns": "1.29.0", + "lodash": "4.17.5", + "rx": "2.3.24", + "spawn-command": "0.0.2-1", + "supports-color": "3.2.3", + "tree-kill": "1.2.0" + }, + "dependencies": { + "ansi-regex": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz", + "integrity": "sha1-DY6UaWej2BQ/k+JOKYUl/BsiNfk=", + "dev": true + }, + "ansi-styles": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.1.0.tgz", + "integrity": "sha1-6uy/Zs1waIJ2Cy9GkVgrj1XXp94=", + "dev": true + }, + "chalk": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.5.1.tgz", + "integrity": "sha1-Zjs6ZItotV0EaQ1JFnqoN4WPIXQ=", + "dev": true, + "requires": { + "ansi-styles": "1.1.0", + "escape-string-regexp": "1.0.5", + "has-ansi": "0.1.0", + "strip-ansi": "0.3.0", + "supports-color": "0.2.0" + }, + "dependencies": { + "supports-color": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-0.2.0.tgz", + "integrity": "sha1-2S3iaU6z9nMjlz1649i1W0wiGQo=", + "dev": true + } + } + }, + "commander": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.6.0.tgz", + "integrity": "sha1-nfflL7Kgyw+4kFjugMMQQiXzfh0=", + "dev": true + }, + "has-ansi": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-0.1.0.tgz", + "integrity": "sha1-hPJlqujA5qiKEtcCKJS3VoiUxi4=", + "dev": true, + "requires": { + "ansi-regex": "0.2.1" + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", + "dev": true + }, + "strip-ansi": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.3.0.tgz", + "integrity": "sha1-JfSOoiynkYfzF0pNuHWTR7sSYiA=", + "dev": true, + "requires": { + "ansi-regex": "0.2.1" + } + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "dev": true, + "requires": { + "has-flag": "1.0.0" + } + } + } + }, + "config-chain": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.11.tgz", + "integrity": "sha1-q6CXR9++TD5w52am5BWG4YWfxvI=", + "requires": { + "ini": "1.3.5", + "proto-list": "1.2.4" + } + }, + "console-browserify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", + "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", + "dev": true, + "requires": { + "date-now": "0.1.4" + } + }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "dev": true + }, + "constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", + "dev": true + }, + "contains-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", + "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=" + }, + "content-type-parser": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/content-type-parser/-/content-type-parser-1.0.2.tgz", + "integrity": "sha512-lM4l4CnMEwOLHAHr/P6MEZwZFPJFtAAKgL6pogbXmVZggIqXhdB6RbBtPOTsw2FcXwYhehRGERJmRrjOiIB8pQ==", + "dev": true + }, + "convert-source-map": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.1.tgz", + "integrity": "sha1-uCeAl7m8IpNl3lxiz1/K7YtVmeU=", + "dev": true + }, + "copy-concurrently": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", + "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", + "dev": true, + "requires": { + "aproba": "1.2.0", + "fs-write-stream-atomic": "1.0.10", + "iferr": "0.1.5", + "mkdirp": "0.5.1", + "rimraf": "2.6.2", + "run-queue": "1.0.3" + } + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", + "dev": true + }, + "core-js": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.3.tgz", + "integrity": "sha1-isw4NFgk8W2DZbfJtCWRaOjtYD4=", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cosmiconfig": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-2.2.2.tgz", + "integrity": "sha512-GiNXLwAFPYHy25XmTPpafYvn3CLAkJ8FLsscq78MQd1Kh0OU6Yzhn4eV2MVF4G9WEQZoWEGltatdR+ntGPMl5A==", + "dev": true, + "requires": { + "is-directory": "0.3.1", + "js-yaml": "3.10.0", + "minimist": "1.2.0", + "object-assign": "4.1.1", + "os-homedir": "1.0.2", + "parse-json": "2.2.0", + "require-from-string": "1.2.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "create-ecdh": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.0.tgz", + "integrity": "sha1-iIxyNZbN92EvZJgjPuvXo1MBc30=", + "dev": true, + "requires": { + "bn.js": "4.11.8", + "elliptic": "6.4.0" + } + }, + "create-hash": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.1.3.tgz", + "integrity": "sha1-YGBCrIuSYnUPSDyt2rD1gZFy2P0=", + "dev": true, + "requires": { + "cipher-base": "1.0.4", + "inherits": "2.0.3", + "ripemd160": "2.0.1", + "sha.js": "2.4.11" + } + }, + "create-hmac": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.6.tgz", + "integrity": "sha1-rLniIaThe9sHbpBlfEK5PjcmzwY=", + "dev": true, + "requires": { + "cipher-base": "1.0.4", + "create-hash": "1.1.3", + "inherits": "2.0.3", + "ripemd160": "2.0.1", + "safe-buffer": "5.1.1", + "sha.js": "2.4.11" + } + }, + "cross-env": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-3.2.4.tgz", + "integrity": "sha1-ngWF8neGTtQhznVvgamA/w1piro=", + "dev": true, + "requires": { + "cross-spawn": "5.1.0", + "is-windows": "1.0.1" + } + }, + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "requires": { + "lru-cache": "4.1.1", + "shebang-command": "1.2.0", + "which": "1.3.0" + }, + "dependencies": { + "lru-cache": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.1.tgz", + "integrity": "sha512-q4spe4KTfsAS1SUHLO0wz8Qiyf1+vMIAgpRYioFYDMNqKfHQbg+AVDH3i4fvpl71/P1L0dBl+fQi+P37UYf0ew==", + "requires": { + "pseudomap": "1.0.2", + "yallist": "2.1.2" + } + } + } + }, + "cryptiles": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", + "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", + "dev": true, + "requires": { + "boom": "5.2.0" + }, + "dependencies": { + "boom": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", + "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", + "dev": true, + "requires": { + "hoek": "4.2.0" + } + } + } + }, + "crypto-browserify": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", + "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "dev": true, + "requires": { + "browserify-cipher": "1.0.0", + "browserify-sign": "4.0.4", + "create-ecdh": "4.0.0", + "create-hash": "1.1.3", + "create-hmac": "1.1.6", + "diffie-hellman": "5.0.2", + "inherits": "2.0.3", + "pbkdf2": "3.0.14", + "public-encrypt": "4.0.0", + "randombytes": "2.0.6", + "randomfill": "1.0.4" + } + }, + "css-color-names": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", + "integrity": "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=", + "dev": true + }, + "css-select": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", + "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", + "dev": true, + "requires": { + "boolbase": "1.0.0", + "css-what": "2.1.0", + "domutils": "1.5.1", + "nth-check": "1.0.1" + } + }, + "css-what": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.0.tgz", + "integrity": "sha1-lGfQMsOM+u+58teVASUwYvh/ob0=", + "dev": true + }, + "cssnano": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-3.10.0.tgz", + "integrity": "sha1-Tzj2zqK5sX+gFJDyPx3GjqZcHDg=", + "dev": true, + "requires": { + "autoprefixer": "6.7.7", + "decamelize": "1.2.0", + "defined": "1.0.0", + "has": "1.0.1", + "object-assign": "4.1.1", + "postcss": "5.2.18", + "postcss-calc": "5.3.1", + "postcss-colormin": "2.2.2", + "postcss-convert-values": "2.6.1", + "postcss-discard-comments": "2.0.4", + "postcss-discard-duplicates": "2.1.0", + "postcss-discard-empty": "2.1.0", + "postcss-discard-overridden": "0.1.1", + "postcss-discard-unused": "2.2.3", + "postcss-filter-plugins": "2.0.2", + "postcss-merge-idents": "2.1.7", + "postcss-merge-longhand": "2.0.2", + "postcss-merge-rules": "2.1.2", + "postcss-minify-font-values": "1.0.5", + "postcss-minify-gradients": "1.0.5", + "postcss-minify-params": "1.2.2", + "postcss-minify-selectors": "2.1.1", + "postcss-normalize-charset": "1.1.1", + "postcss-normalize-url": "3.0.8", + "postcss-ordered-values": "2.2.3", + "postcss-reduce-idents": "2.4.0", + "postcss-reduce-initial": "1.0.1", + "postcss-reduce-transforms": "1.0.4", + "postcss-svgo": "2.1.6", + "postcss-unique-selectors": "2.0.2", + "postcss-value-parser": "3.3.0", + "postcss-zindex": "2.2.0" + }, + "dependencies": { + "autoprefixer": { + "version": "6.7.7", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-6.7.7.tgz", + "integrity": "sha1-Hb0cg1ZY41zj+ZhAmdsAWFx4IBQ=", + "dev": true, + "requires": { + "browserslist": "1.7.7", + "caniuse-db": "1.0.30000804", + "normalize-range": "0.1.2", + "num2fraction": "1.2.2", + "postcss": "5.2.18", + "postcss-value-parser": "3.3.0" + } + }, + "browserslist": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-1.7.7.tgz", + "integrity": "sha1-C9dnBCWL6CmyOYu1Dkti0aFmsLk=", + "dev": true, + "requires": { + "caniuse-db": "1.0.30000804", + "electron-to-chromium": "1.3.33" + } + } + } + }, + "csso": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/csso/-/csso-2.3.2.tgz", + "integrity": "sha1-3dUsWHAz9J6Utx/FVWnyUuj/X4U=", + "dev": true, + "requires": { + "clap": "1.2.3", + "source-map": "0.5.7" + } + }, + "cssom": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.2.tgz", + "integrity": "sha1-uANhcMefB6kP8vFuIihAJ6JDhIs=", + "dev": true + }, + "cssstyle": { + "version": "0.2.37", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-0.2.37.tgz", + "integrity": "sha1-VBCXI0yyUTyDzu06zdwn/yeYfVQ=", + "dev": true, + "requires": { + "cssom": "0.3.2" + } + }, + "currently-unhandled": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", + "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", + "dev": true, + "requires": { + "array-find-index": "1.0.2" + } + }, + "cyclist": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-0.2.2.tgz", + "integrity": "sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=", + "dev": true + }, + "damerau-levenshtein": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.4.tgz", + "integrity": "sha1-AxkcQyy27qFou3fzpV/9zLiXhRQ=", + "dev": true + }, + "dargs": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/dargs/-/dargs-5.1.0.tgz", + "integrity": "sha1-7H6lDHhWTNNsnV7Bj2Yyn63ieCk=", + "dev": true + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, + "requires": { + "assert-plus": "1.0.0" + } + }, + "date-fns": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.29.0.tgz", + "integrity": "sha512-lbTXWZ6M20cWH8N9S6afb0SBm6tMk+uUg6z3MqHPKE9atmsY3kJkTm8vKe93izJ2B2+q5MV990sM2CHgtAZaOw==", + "dev": true + }, + "date-now": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", + "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=", + "dev": true + }, + "dateformat": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", + "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==", + "dev": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true + }, + "decompress-response": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", + "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", + "dev": true, + "requires": { + "mimic-response": "1.0.0" + } + }, + "deep-equal-ident": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/deep-equal-ident/-/deep-equal-ident-1.1.1.tgz", + "integrity": "sha1-BvS4nlNxDNbOpKd4HHqVZkLejck=", + "dev": true, + "requires": { + "lodash.isequal": "3.0.4" + } + }, + "deep-extend": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.2.tgz", + "integrity": "sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8=", + "dev": true + }, + "deep-freeze": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/deep-freeze/-/deep-freeze-0.0.1.tgz", + "integrity": "sha1-OgsABd4YZygZ39OM0x+RF5yJPoQ=", + "dev": true + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "default-require-extensions": { + "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": "2.0.0" + }, + "dependencies": { + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "requires": { + "is-utf8": "0.2.1" + } + } + } + }, + "define-properties": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.2.tgz", + "integrity": "sha1-g6c/L+pWmJj7c3GTyPhzyvbUXJQ=", + "dev": true, + "requires": { + "foreach": "2.0.5", + "object-keys": "1.0.11" + } + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "requires": { + "is-descriptor": "1.0.2", + "isobject": "3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "defined": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", + "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=", + "dev": true + }, + "del": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", + "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=", + "dev": true, + "requires": { + "globby": "5.0.0", + "is-path-cwd": "1.0.0", + "is-path-in-cwd": "1.0.0", + "object-assign": "4.1.1", + "pify": "2.3.0", + "pinkie-promise": "2.0.1", + "rimraf": "2.6.2" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "delegate": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz", + "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==" + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "dev": true + }, + "des.js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", + "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "minimalistic-assert": "1.0.0" + } + }, + "detect-conflict": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/detect-conflict/-/detect-conflict-1.0.1.tgz", + "integrity": "sha1-CIZXpmqWHAUBnbfEIwiDsca0F24=", + "dev": true + }, + "detect-indent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", + "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", + "dev": true, + "requires": { + "repeating": "2.0.1" + } + }, + "detect-newline": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", + "integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=", + "dev": true + }, + "diff": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.4.0.tgz", + "integrity": "sha512-QpVuMTEoJMF7cKzi6bvWhRulU1fZqZnvyVQgNhPaxxuTYwyjn/j1v9falseQ/uXWwPnO56RBfwtg4h/EQXmucA==", + "dev": true + }, + "diffie-hellman": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.2.tgz", + "integrity": "sha1-tYNXOScM/ias9jIJn97SoH8gnl4=", + "dev": true, + "requires": { + "bn.js": "4.11.8", + "miller-rabin": "4.0.1", + "randombytes": "2.0.6" + } + }, + "discontinuous-range": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz", + "integrity": "sha1-44Mx8IRLukm5qctxx3FYWqsbxlo=", + "dev": true + }, + "doctrine": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", + "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", + "requires": { + "esutils": "2.0.2", + "isarray": "1.0.0" + } + }, + "dom-react": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/dom-react/-/dom-react-2.2.0.tgz", + "integrity": "sha1-3GJwYI7VbL35DJo+w1U/m1oL17M=" + }, + "dom-scroll-into-view": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/dom-scroll-into-view/-/dom-scroll-into-view-1.2.1.tgz", + "integrity": "sha1-6PNnMt0ImwIBqI14Fdw/iObWbH4=" + }, + "dom-serializer": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz", + "integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=", + "dev": true, + "requires": { + "domelementtype": "1.1.3", + "entities": "1.1.1" + }, + "dependencies": { + "domelementtype": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz", + "integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs=", + "dev": true + } + } + }, + "domain-browser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", + "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", + "dev": true + }, + "domelementtype": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz", + "integrity": "sha1-sXrtguirWeUt2cGbF1bg/BhyBMI=", + "dev": true + }, + "domexception": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", + "integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==", + "dev": true, + "requires": { + "webidl-conversions": "4.0.2" + } + }, + "domhandler": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.1.tgz", + "integrity": "sha1-iS5HAAqZvlW783dP/qBWHYh5wlk=", + "dev": true, + "requires": { + "domelementtype": "1.3.0" + } + }, + "domutils": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", + "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", + "dev": true, + "requires": { + "dom-serializer": "0.1.0", + "domelementtype": "1.3.0" + } + }, + "duplexer3": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", + "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", + "dev": true + }, + "duplexify": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.5.4.tgz", + "integrity": "sha512-JzYSLYMhoVVBe8+mbHQ4KgpvHpm0DZpJuL8PY93Vyv1fW7jYJ90LoXa1di/CVbJM+TgMs91rbDapE/RNIfnJsA==", + "dev": true, + "requires": { + "end-of-stream": "1.4.1", + "inherits": "2.0.3", + "readable-stream": "2.3.3", + "stream-shift": "1.0.0" + } + }, + "ecc-jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "dev": true, + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, + "editions": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/editions/-/editions-1.3.4.tgz", + "integrity": "sha512-gzao+mxnYDzIysXKMQi/+M1mjy/rjestjg6OPoYTtI+3Izp23oiGZitsl9lPDPiTGXbcSIk1iJWhliSaglxnUg==", + "dev": true + }, + "editorconfig": { + "version": "0.13.3", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.13.3.tgz", + "integrity": "sha512-WkjsUNVCu+ITKDj73QDvi0trvpdDWdkDyHybDGSXPfekLCqwmpD7CP7iPbvBgosNuLcI96XTDwNa75JyFl7tEQ==", + "requires": { + "bluebird": "3.5.1", + "commander": "2.14.1", + "lru-cache": "3.2.0", + "semver": "5.3.0", + "sigmund": "1.0.1" + } + }, + "ejs": { + "version": "2.5.8", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.5.8.tgz", + "integrity": "sha512-QIDZL54fyV8MDcAsO91BMH1ft2qGGaHIJsJIA/+t+7uvXol1dm413fPcUgUb4k8F/9457rx4/KFE4XfDifrQxQ==", + "dev": true + }, + "electron-to-chromium": { + "version": "1.3.33", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.33.tgz", + "integrity": "sha1-vwBwPWKnxlI4E2V4w1LWxcBCpUU=", + "dev": true + }, + "elegant-spinner": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/elegant-spinner/-/elegant-spinner-1.0.1.tgz", + "integrity": "sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4=", + "dev": true + }, + "element-closest": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/element-closest/-/element-closest-2.0.2.tgz", + "integrity": "sha1-cqdAoQdFM4LijfnOXbtajfD5Zuw=" + }, + "elliptic": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.0.tgz", + "integrity": "sha1-ysmvh2LIWDYYcAPI3+GT5eLq5d8=", + "dev": true, + "requires": { + "bn.js": "4.11.8", + "brorand": "1.1.0", + "hash.js": "1.1.3", + "hmac-drbg": "1.0.1", + "inherits": "2.0.3", + "minimalistic-assert": "1.0.0", + "minimalistic-crypto-utils": "1.0.1" + } + }, + "emoji-regex": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-6.5.1.tgz", + "integrity": "sha512-PAHp6TxrCy7MGMFidro8uikr+zlJJKJ/Q6mm2ExZ7HwkyR9lSVFfE3kt36qcwa24BQL7y0G9axycGjK1A/0uNQ==", + "dev": true + }, + "emojis-list": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", + "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=", + "dev": true + }, + "encoding": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", + "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", + "requires": { + "iconv-lite": "0.4.19" + } + }, + "end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "dev": true, + "requires": { + "once": "1.4.0" + } + }, + "enhanced-resolve": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.0.0.tgz", + "integrity": "sha512-jox/62b2GofV1qTUQTMPEJSDIGycS43evqYzD/KVtEb9OCoki9cnacUPxCrZa7JfPzZSYOCZhu9O9luaMxAX8g==", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "memory-fs": "0.4.1", + "tapable": "1.0.0" + } + }, + "entities": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz", + "integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA=", + "dev": true + }, + "enzyme": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/enzyme/-/enzyme-3.3.0.tgz", + "integrity": "sha512-l8csyPyLmtxskTz6pX9W8eDOyH1ckEtDttXk/vlFWCjv00SkjTjtoUrogqp4yEvMyneU9dUJoOLnqFoiHb8IHA==", + "dev": true, + "requires": { + "cheerio": "1.0.0-rc.2", + "function.prototype.name": "1.1.0", + "has": "1.0.1", + "is-boolean-object": "1.0.0", + "is-callable": "1.1.3", + "is-number-object": "1.0.3", + "is-string": "1.0.4", + "is-subset": "0.1.1", + "lodash": "4.17.5", + "object-inspect": "1.5.0", + "object-is": "1.0.1", + "object.assign": "4.1.0", + "object.entries": "1.0.4", + "object.values": "1.0.4", + "raf": "3.4.0", + "rst-selector-parser": "2.2.3" + } + }, + "enzyme-adapter-react-16": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.1.1.tgz", + "integrity": "sha512-kC8pAtU2Jk3OJ0EG8Y2813dg9Ol0TXi7UNxHzHiWs30Jo/hj7alc//G1YpKUsPP1oKl9X+Lkx+WlGJpPYA+nvw==", + "dev": true, + "requires": { + "enzyme-adapter-utils": "1.3.0", + "lodash": "4.17.5", + "object.assign": "4.1.0", + "object.values": "1.0.4", + "prop-types": "15.6.0", + "react-reconciler": "0.7.0", + "react-test-renderer": "16.2.0" + }, + "dependencies": { + "prop-types": { + "version": "15.6.0", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.0.tgz", + "integrity": "sha1-zq8IMCL8RrSjX2nhPvda7Q1jmFY=", + "dev": true, + "requires": { + "fbjs": "0.8.16", + "loose-envify": "1.3.1", + "object-assign": "4.1.1" + } + }, + "react-test-renderer": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.2.0.tgz", + "integrity": "sha512-Kd4gJFtpNziR9ElOE/C23LeflKLZPRpNQYWP3nQBY43SJ5a+xyEGSeMrm2zxNKXcnCbBS/q1UpD9gqd5Dv+rew==", + "dev": true, + "requires": { + "fbjs": "0.8.16", + "object-assign": "4.1.1", + "prop-types": "15.6.0" + } + } + } + }, + "enzyme-adapter-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/enzyme-adapter-utils/-/enzyme-adapter-utils-1.3.0.tgz", + "integrity": "sha512-vVXSt6uDv230DIv+ebCG66T1Pm36Kv+m74L1TrF4kaE7e1V7Q/LcxO0QRkajk5cA6R3uu9wJf5h13wOTezTbjA==", + "dev": true, + "requires": { + "lodash": "4.17.5", + "object.assign": "4.1.0", + "prop-types": "15.6.0" + }, + "dependencies": { + "prop-types": { + "version": "15.6.0", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.0.tgz", + "integrity": "sha1-zq8IMCL8RrSjX2nhPvda7Q1jmFY=", + "dev": true, + "requires": { + "fbjs": "0.8.16", + "loose-envify": "1.3.1", + "object-assign": "4.1.1" + } + } + } + }, + "enzyme-matchers": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/enzyme-matchers/-/enzyme-matchers-4.2.0.tgz", + "integrity": "sha512-5Gf/mAVYx6KPAUuxuDhAGt/gu9ndPd6duFcVnH2rbEad2clgTpHZL4Df49FHFukrjEEubX9rhfeAKx0/sbfVkQ==", + "dev": true, + "requires": { + "circular-json-es6": "2.0.2", + "deep-equal-ident": "1.1.1" + } + }, + "enzyme-to-json": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/enzyme-to-json/-/enzyme-to-json-3.3.1.tgz", + "integrity": "sha512-PrgRyZAgEwOrh5/8BtBWrwGcv1mC7yNohytIciAX6SUqDaXg1BlU8CepYQ9BgnDP1i1jTB65qJJITMMCph+T6A==", + "dev": true, + "requires": { + "lodash": "4.17.5" + } + }, + "errno": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", + "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", + "dev": true, + "requires": { + "prr": "1.0.1" + } + }, + "error": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/error/-/error-7.0.2.tgz", + "integrity": "sha1-pfdf/02ZJhJt2sDqXcOOaJFTywI=", + "dev": true, + "requires": { + "string-template": "0.2.1", + "xtend": "4.0.1" + } + }, + "error-ex": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", + "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", + "requires": { + "is-arrayish": "0.2.1" + } + }, + "es-abstract": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.10.0.tgz", + "integrity": "sha512-/uh/DhdqIOSkAWifU+8nG78vlQxdLckUdI/sPgy0VhuXi2qJ7T8czBmqIYtLQVpCIFYafChnsRsB5pyb1JdmCQ==", + "dev": true, + "requires": { + "es-to-primitive": "1.1.1", + "function-bind": "1.1.1", + "has": "1.0.1", + "is-callable": "1.1.3", + "is-regex": "1.0.4" + } + }, + "es-to-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.1.1.tgz", + "integrity": "sha1-RTVSSKiJeQNLZ5Lhm7gfK3l13Q0=", + "dev": true, + "requires": { + "is-callable": "1.1.3", + "is-date-object": "1.0.1", + "is-symbol": "1.0.1" + } + }, + "es6-promise": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.4.tgz", + "integrity": "sha512-/NdNZVJg+uZgtm9eS3O6lrOLYmQag2DjdEXuPaHlZ6RuVqgqaVZfgYCepEIKsLqwdQArOPtC3XzRLqGGfT8KQQ==", + "dev": true + }, + "es6-promisify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", + "dev": true, + "requires": { + "es6-promise": "4.2.4" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "escodegen": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.9.0.tgz", + "integrity": "sha512-v0MYvNQ32bzwoG2OSFzWAkuahDQHK92JBN0pTAALJ4RIxEZe766QJPDR8Hqy7XNUy5K3fnVL76OqYAdc4TZEIw==", + "dev": true, + "requires": { + "esprima": "3.1.3", + "estraverse": "4.2.0", + "esutils": "2.0.2", + "optionator": "0.8.2", + "source-map": "0.5.7" + }, + "dependencies": { + "esprima": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", + "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", + "dev": true + } + } + }, + "eslint": { + "version": "4.16.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-4.16.0.tgz", + "integrity": "sha512-YVXV4bDhNoHHcv0qzU4Meof7/P26B4EuaktMi5L1Tnt52Aov85KmYA8c5D+xyZr/BkhvwUqr011jDSD/QTULxg==", + "dev": true, + "requires": { + "ajv": "5.5.2", + "babel-code-frame": "6.26.0", + "chalk": "2.3.0", + "concat-stream": "1.6.0", + "cross-spawn": "5.1.0", + "debug": "3.1.0", + "doctrine": "2.1.0", + "eslint-scope": "3.7.1", + "eslint-visitor-keys": "1.0.0", + "espree": "3.5.3", + "esquery": "1.0.0", + "esutils": "2.0.2", + "file-entry-cache": "2.0.0", + "functional-red-black-tree": "1.0.1", + "glob": "7.1.2", + "globals": "11.3.0", + "ignore": "3.3.7", + "imurmurhash": "0.1.4", + "inquirer": "3.3.0", + "is-resolvable": "1.1.0", + "js-yaml": "3.10.0", + "json-stable-stringify-without-jsonify": "1.0.1", + "levn": "0.3.0", + "lodash": "4.17.5", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "natural-compare": "1.4.0", + "optionator": "0.8.2", + "path-is-inside": "1.0.2", + "pluralize": "7.0.0", + "progress": "2.0.0", + "require-uncached": "1.0.3", + "semver": "5.3.0", + "strip-ansi": "4.0.0", + "strip-json-comments": "2.0.1", + "table": "4.0.2", + "text-table": "0.2.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 + }, + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + }, + "chalk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", + "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "4.5.0" + } + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "requires": { + "esutils": "2.0.2" + } + }, + "globals": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.3.0.tgz", + "integrity": "sha512-kkpcKNlmQan9Z5ZmgqKH/SMbSmjxQ7QjyNqfXVc8VJcoBV2UEg+sxQD15GQofGRh2hfpwUb70VC31DR7Rq5Hdw==", + "dev": true + }, + "progress": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.0.tgz", + "integrity": "sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8=", + "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" + } + }, + "supports-color": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", + "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", + "dev": true, + "requires": { + "has-flag": "2.0.0" + } + } + } + }, + "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-import-resolver-node": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz", + "integrity": "sha512-sfmTqJfPSizWu4aymbPr4Iidp5yKm8yDkHp+Ir3YiTHiiDfxh69mOUsmiqW6RZ9zRXFaF64GtYmN7e+8GHBv6Q==", + "requires": { + "debug": "2.6.9", + "resolve": "1.5.0" + } + }, + "eslint-module-utils": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.1.1.tgz", + "integrity": "sha512-jDI/X5l/6D1rRD/3T43q8Qgbls2nq5km5KSqiwlyUbGo5+04fXhMKdCPhjwbqAa6HXWaMxj8Q4hQDIh7IadJQw==", + "requires": { + "debug": "2.6.9", + "pkg-dir": "1.0.0" + } + }, + "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=" + }, + "eslint-plugin-import": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.8.0.tgz", + "integrity": "sha512-Rf7dfKJxZ16QuTgVv1OYNxkZcsu/hULFnC+e+w0Gzi6jMC3guQoWQgxYxc54IDRinlb6/0v5z/PxxIKmVctN+g==", + "requires": { + "builtin-modules": "1.1.1", + "contains-path": "0.1.0", + "debug": "2.6.9", + "doctrine": "1.5.0", + "eslint-import-resolver-node": "0.3.2", + "eslint-module-utils": "2.1.1", + "has": "1.0.1", + "lodash.cond": "4.5.2", + "minimatch": "3.0.4", + "read-pkg-up": "2.0.0" + } + }, + "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==", + "requires": { + "comment-parser": "0.4.2", + "lodash": "4.17.5" + } + }, + "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", + "integrity": "sha1-ZZJ3p1iwNsMFp+ShMFfDAc075z8=", + "dev": true, + "requires": { + "aria-query": "0.7.1", + "array-includes": "3.0.3", + "ast-types-flow": "0.0.7", + "axobject-query": "0.1.0", + "damerau-levenshtein": "1.0.4", + "emoji-regex": "6.5.1", + "jsx-ast-utils": "1.4.1" + } + }, + "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==", + "requires": { + "ignore": "3.3.7", + "minimatch": "3.0.4", + "resolve": "1.5.0", + "semver": "5.5.0" + }, + "dependencies": { + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==" + } + } + }, + "eslint-plugin-react": { + "version": "7.7.0", + "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": { + "doctrine": "2.1.0", + "has": "1.0.1", + "jsx-ast-utils": "2.0.1", + "prop-types": "15.6.1" + }, + "dependencies": { + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "requires": { + "esutils": "2.0.2" + } + }, + "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.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.1.tgz", + "integrity": "sha512-4ec7bY1Y66LymSUOH/zARVYObB23AT2h8cf6e/O6ZALB/N0sqZFEx7rq6EYPX2MkOdKORuooI/H5k9TlR4q7kQ==", + "dev": true, + "requires": { + "fbjs": "0.8.16", + "loose-envify": "1.3.1", + "object-assign": "4.1.1" + } + } + } + }, + "eslint-plugin-wordpress": { + "version": "git://github.com/WordPress-Coding-Standards/eslint-plugin-wordpress.git#1774343f6226052a46b081e01db3fca8793cc9f1", + "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==", + "requires": { + "requireindex": "1.2.0" + } + }, + "eslint-scope": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.1.tgz", + "integrity": "sha1-PWPD7f2gLgbgGkUq2IyqzHzctug=", + "dev": true, + "requires": { + "esrecurse": "4.2.0", + "estraverse": "4.2.0" + } + }, + "eslint-visitor-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", + "integrity": "sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==", + "dev": true + }, + "espree": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.3.tgz", + "integrity": "sha512-Zy3tAJDORxQZLl2baguiRU1syPERAIg0L+JB2MWorORgTu/CplzvxS9WWA7Xh4+Q+eOQihNs/1o1Xep8cvCxWQ==", + "dev": true, + "requires": { + "acorn": "5.4.1", + "acorn-jsx": "3.0.1" + } + }, + "esprima": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", + "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==", + "dev": true + }, + "esquery": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.0.tgz", + "integrity": "sha1-z7qLV9f7qT8XKYqKAGoEzaE9gPo=", + "dev": true, + "requires": { + "estraverse": "4.2.0" + } + }, + "esrecurse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.0.tgz", + "integrity": "sha1-+pVo2Y04I/mkHZHpAtyrnqblsWM=", + "dev": true, + "requires": { + "estraverse": "4.2.0", + "object-assign": "4.1.1" + } + }, + "estraverse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", + "dev": true + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=" + }, + "events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=", + "dev": true + }, + "evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "dev": true, + "requires": { + "md5.js": "1.3.4", + "safe-buffer": "5.1.1" + } + }, + "exec-sh": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.2.1.tgz", + "integrity": "sha512-aLt95pexaugVtQerpmE51+4QfWrNc304uez7jvj6fWnN8GeEHpttB8F36n8N7uVhUMbH/1enbxQ9HImZ4w/9qg==", + "dev": true, + "requires": { + "merge": "1.2.0" + } + }, + "execa": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", + "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", + "requires": { + "cross-spawn": "5.1.0", + "get-stream": "3.0.0", + "is-stream": "1.1.0", + "npm-run-path": "2.0.2", + "p-finally": "1.0.0", + "signal-exit": "3.0.2", + "strip-eof": "1.0.0" + } + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", + "dev": true + }, + "exit-hook": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz", + "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=", + "dev": true + }, + "expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "dev": true, + "requires": { + "is-posix-bracket": "0.1.1" + } + }, + "expand-range": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", + "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", + "dev": true, + "requires": { + "fill-range": "2.2.3" + } + }, + "expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", + "dev": true, + "requires": { + "homedir-polyfill": "1.0.1" + } + }, + "expect": { + "version": "22.4.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-22.4.0.tgz", + "integrity": "sha512-Fiy862jT3qc70hwIHwwCBNISmaqBrfWKKrtqyMJ6iwZr+6KXtcnHojZFtd63TPRvRl8EQTJ+YXYy2lK6/6u+Hw==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "jest-diff": "22.4.0", + "jest-get-type": "22.1.0", + "jest-matcher-utils": "22.4.0", + "jest-message-util": "22.4.0", + "jest-regex-util": "22.1.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + } + } + }, + "extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=", + "dev": true + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "requires": { + "assign-symbols": "1.0.0", + "is-extendable": "1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "2.0.4" + } + } + } + }, + "external-editor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.1.0.tgz", + "integrity": "sha512-E44iT5QVOUJBKij4IIV3uvxuNlbKS38Tw1HiupxEIHPv9qtC2PrDYohbXV5U+1jnfIXttny8gUhj+oZvflFlzA==", + "dev": true, + "requires": { + "chardet": "0.4.2", + "iconv-lite": "0.4.19", + "tmp": "0.0.33" + }, + "dependencies": { + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "1.0.2" + } + } + } + }, + "extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + }, + "extract-text-webpack-plugin": { + "version": "4.0.0-beta.0", + "resolved": "https://registry.npmjs.org/extract-text-webpack-plugin/-/extract-text-webpack-plugin-4.0.0-beta.0.tgz", + "integrity": "sha512-Hypkn9jUTnFr0DpekNam53X47tXn3ucY08BQumv7kdGgeVUBLq3DJHJTi6HNxv4jl9W+Skxjz9+RnK0sJyqqjA==", + "dev": true, + "requires": { + "async": "2.6.0", + "loader-utils": "1.1.0", + "schema-utils": "0.4.5", + "webpack-sources": "1.1.0" + } + }, + "extract-zip": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.6.6.tgz", + "integrity": "sha1-EpDt6NINCHK0Kf0/NRyhKOxe+Fw=", + "dev": true, + "requires": { + "concat-stream": "1.6.0", + "debug": "2.6.9", + "mkdirp": "0.5.0", + "yauzl": "2.4.1" + }, + "dependencies": { + "mkdirp": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.0.tgz", + "integrity": "sha1-HXMHam35hs2TROFecfzAWkyavxI=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + } + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "dev": true + }, + "fast-deep-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz", + "integrity": "sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8=", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "fb-watchman": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.0.tgz", + "integrity": "sha1-VOmr99+i8mzZsWNsWIwa/AXeXVg=", + "dev": true, + "requires": { + "bser": "2.0.0" + } + }, + "fbjs": { + "version": "0.8.16", + "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.16.tgz", + "integrity": "sha1-XmdDL1UNxBtXK/VYR7ispk5TN9s=", + "requires": { + "core-js": "1.2.7", + "isomorphic-fetch": "2.2.1", + "loose-envify": "1.3.1", + "object-assign": "4.1.1", + "promise": "7.3.1", + "setimmediate": "1.0.5", + "ua-parser-js": "0.7.17" + }, + "dependencies": { + "core-js": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", + "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=" + } + } + }, + "fd-slicer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz", + "integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=", + "dev": true, + "requires": { + "pend": "1.2.0" + } + }, + "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" + } + }, + "file-entry-cache": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz", + "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=", + "dev": true, + "requires": { + "flat-cache": "1.3.0", + "object-assign": "4.1.1" + } + }, + "filename-regex": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", + "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=", + "dev": true + }, + "fileset": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/fileset/-/fileset-2.0.3.tgz", + "integrity": "sha1-jnVIqW08wjJ+5eZ0FocjozO7oqA=", + "dev": true, + "requires": { + "glob": "7.1.2", + "minimatch": "3.0.4" + } + }, + "fill-range": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.3.tgz", + "integrity": "sha1-ULd9/X5Gm8dJJHCWNpn+eoSFpyM=", + "dev": true, + "requires": { + "is-number": "2.1.0", + "isobject": "2.1.0", + "randomatic": "1.1.7", + "repeat-element": "1.1.2", + "repeat-string": "1.6.1" + } + }, + "find-cache-dir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-1.0.0.tgz", + "integrity": "sha1-kojj6ePMN0hxfTnq3hfPcfww7m8=", + "dev": true, + "requires": { + "commondir": "1.0.1", + "make-dir": "1.2.0", + "pkg-dir": "2.0.0" + }, + "dependencies": { + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "2.0.0" + } + }, + "pkg-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", + "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", + "dev": true, + "requires": { + "find-up": "2.1.0" + } + } + } + }, + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "requires": { + "path-exists": "2.1.0", + "pinkie-promise": "2.0.1" + } + }, + "findup": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/findup/-/findup-0.1.5.tgz", + "integrity": "sha1-itkpozk7rGJ5V6fl3kYjsGsOLOs=", + "dev": true, + "requires": { + "colors": "0.6.2", + "commander": "2.1.0" + }, + "dependencies": { + "colors": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz", + "integrity": "sha1-JCP+ZnisDF2uiFLl0OW+CMmXq8w=", + "dev": true + }, + "commander": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.1.0.tgz", + "integrity": "sha1-0SG7roYNmZKj1Re6lvVliOR8Z4E=", + "dev": true + } + } + }, + "first-chunk-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/first-chunk-stream/-/first-chunk-stream-2.0.0.tgz", + "integrity": "sha1-G97NuOCDwGZLkZRVgVd6Q6nzHXA=", + "dev": true, + "requires": { + "readable-stream": "2.3.3" + } + }, + "flat-cache": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.0.tgz", + "integrity": "sha1-0wMLMrOBVPTjt+nHCfSQ9++XxIE=", + "dev": true, + "requires": { + "circular-json": "0.3.3", + "del": "2.2.2", + "graceful-fs": "4.1.11", + "write": "0.2.1" + } + }, + "flatten": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/flatten/-/flatten-1.0.2.tgz", + "integrity": "sha1-2uRqnXj74lKSJYzB54CkHZXAN4I=", + "dev": true + }, + "flow-parser": { + "version": "0.69.0", + "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.69.0.tgz", + "integrity": "sha1-N4tRKNbQtVSosvFqTKPhq5ZJ8A4=", + "dev": true + }, + "flush-write-stream": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.0.3.tgz", + "integrity": "sha512-calZMC10u0FMUqoiunI2AiGIIUtUIvifNwkHhNupZH4cbNnW1Itkoh/Nf5HFYmDrwWPjrUxpkZT0KhuCq0jmGw==", + "dev": true, + "requires": { + "inherits": "2.0.3", + "readable-stream": "2.3.3" + } + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true + }, + "for-own": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", + "dev": true, + "requires": { + "for-in": "1.0.2" + } + }, + "foreach": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", + "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=", + "dev": true + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true + }, + "form-data": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.1.tgz", + "integrity": "sha1-b7lPvXGIUwbXPRXMSX/kzE7NRL8=", + "dev": true, + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.5", + "mime-types": "2.1.17" + } + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "dev": true, + "requires": { + "map-cache": "0.2.2" + } + }, + "from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "readable-stream": "2.3.3" + } + }, + "fs-write-stream-atomic": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", + "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "iferr": "0.1.5", + "imurmurhash": "0.1.4", + "readable-stream": "2.3.3" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fsevents": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.1.3.tgz", + "integrity": "sha512-WIr7iDkdmdbxu/Gh6eKEZJL6KPE74/5MEsf2whTOFNxbIoIixogroLdKYqB6FDav4Wavh/lZdzzd3b2KxIXC5Q==", + "dev": true, + "optional": true, + "requires": { + "nan": "2.8.0", + "node-pre-gyp": "0.6.39" + }, + "dependencies": { + "abbrev": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.0.tgz", + "integrity": "sha1-0FVMIlZjbi9W58LlrRg/hZQo2B8=", + "dev": true, + "optional": true + }, + "ajv": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", + "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", + "dev": true, + "optional": true, + "requires": { + "co": "4.6.0", + "json-stable-stringify": "1.0.1" + } + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "aproba": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.1.1.tgz", + "integrity": "sha1-ldNgDwdxCqDpKYxyatXs8urLq6s=", + "dev": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz", + "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=", + "dev": true, + "optional": true, + "requires": { + "delegates": "1.0.0", + "readable-stream": "2.2.9" + } + }, + "asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=", + "dev": true, + "optional": true + }, + "assert-plus": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", + "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=", + "dev": true, + "optional": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true, + "optional": true + }, + "aws-sign2": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", + "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=", + "dev": true, + "optional": true + }, + "aws4": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", + "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=", + "dev": true, + "optional": true + }, + "balanced-match": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", + "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=", + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", + "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "dev": true, + "optional": true, + "requires": { + "tweetnacl": "0.14.5" + } + }, + "block-stream": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", + "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", + "dev": true, + "requires": { + "inherits": "2.0.3" + } + }, + "boom": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", + "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", + "dev": true, + "requires": { + "hoek": "2.16.3" + } + }, + "brace-expansion": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.7.tgz", + "integrity": "sha1-Pv/DxQ4ABTH7cg6v+A8K6O8jz1k=", + "dev": true, + "requires": { + "balanced-match": "0.4.2", + "concat-map": "0.0.1" + } + }, + "buffer-shims": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz", + "integrity": "sha1-mXjOMXOIxkmth5MCjDR37wRKi1E=", + "dev": true + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "dev": true, + "optional": true + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, + "combined-stream": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", + "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", + "dev": true, + "requires": { + "delayed-stream": "1.0.0" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "cryptiles": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", + "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", + "dev": true, + "requires": { + "boom": "2.10.1" + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, + "optional": true, + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true, + "optional": true + } + } + }, + "debug": { + "version": "2.6.8", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", + "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=", + "dev": true, + "optional": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-extend": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.2.tgz", + "integrity": "sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8=", + "dev": true, + "optional": true + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "dev": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.2.tgz", + "integrity": "sha1-ca1dIEvxempsqPRQxhRUBm70YeE=", + "dev": true, + "optional": true + }, + "ecc-jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "dev": true, + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, + "extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=", + "dev": true, + "optional": true + }, + "extsprintf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.2.tgz", + "integrity": "sha1-4QgOBljjALBilJkMxw4VAiNf1VA=", + "dev": true + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true, + "optional": true + }, + "form-data": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", + "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", + "dev": true, + "optional": true, + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.5", + "mime-types": "2.1.15" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fstream": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", + "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "inherits": "2.0.3", + "mkdirp": "0.5.1", + "rimraf": "2.6.1" + } + }, + "fstream-ignore": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/fstream-ignore/-/fstream-ignore-1.0.5.tgz", + "integrity": "sha1-nDHa40dnAY/h0kmyTa2mfQktoQU=", + "dev": true, + "optional": true, + "requires": { + "fstream": "1.0.11", + "inherits": "2.0.3", + "minimatch": "3.0.4" + } + }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "dev": true, + "optional": true, + "requires": { + "aproba": "1.1.1", + "console-control-strings": "1.1.0", + "has-unicode": "2.0.1", + "object-assign": "4.1.1", + "signal-exit": "3.0.2", + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wide-align": "1.1.2" + } + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "optional": true, + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true, + "optional": true + } + } + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true + }, + "har-schema": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz", + "integrity": "sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4=", + "dev": true, + "optional": true + }, + "har-validator": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz", + "integrity": "sha1-M0gdDxu/9gDdID11gSpqX7oALio=", + "dev": true, + "optional": true, + "requires": { + "ajv": "4.11.8", + "har-schema": "1.0.5" + } + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "dev": true, + "optional": true + }, + "hawk": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", + "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", + "dev": true, + "requires": { + "boom": "2.10.1", + "cryptiles": "2.0.5", + "hoek": "2.16.3", + "sntp": "1.0.9" + } + }, + "hoek": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", + "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=", + "dev": true + }, + "http-signature": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", + "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", + "dev": true, + "optional": true, + "requires": { + "assert-plus": "0.2.0", + "jsprim": "1.4.0", + "sshpk": "1.13.0" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "ini": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.4.tgz", + "integrity": "sha1-BTfLedr1m1mhpRff9wbIbsA5Fi4=", + "dev": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true, + "optional": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true, + "optional": true + }, + "jodid25519": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/jodid25519/-/jodid25519-1.0.2.tgz", + "integrity": "sha1-BtSRIlUJNBlHfUJWM2BuDpB4KWc=", + "dev": true, + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true, + "optional": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "dev": true, + "optional": true + }, + "json-stable-stringify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "dev": true, + "optional": true, + "requires": { + "jsonify": "0.0.0" + } + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true, + "optional": true + }, + "jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", + "dev": true, + "optional": true + }, + "jsprim": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.0.tgz", + "integrity": "sha1-o7h+QCmNjDgFUtjMdiigu5WiKRg=", + "dev": true, + "optional": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.0.2", + "json-schema": "0.2.3", + "verror": "1.3.6" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true, + "optional": true + } + } + }, + "mime-db": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.27.0.tgz", + "integrity": "sha1-gg9XIpa70g7CXtVeW13oaeVDbrE=", + "dev": true + }, + "mime-types": { + "version": "2.1.15", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.15.tgz", + "integrity": "sha1-pOv1BkCUVpI3uM9wBGd20J/JKu0=", + "dev": true, + "requires": { + "mime-db": "1.27.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true, + "optional": true + }, + "node-pre-gyp": { + "version": "0.6.39", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.6.39.tgz", + "integrity": "sha512-OsJV74qxnvz/AMGgcfZoDaeDXKD3oY3QVIbBmwszTFkRisTSXbMQyn4UWzUMOtA5SVhrBZOTp0wcoSBgfMfMmQ==", + "dev": true, + "optional": true, + "requires": { + "detect-libc": "1.0.2", + "hawk": "3.1.3", + "mkdirp": "0.5.1", + "nopt": "4.0.1", + "npmlog": "4.1.0", + "rc": "1.2.1", + "request": "2.81.0", + "rimraf": "2.6.1", + "semver": "5.3.0", + "tar": "2.2.1", + "tar-pack": "3.4.0" + } + }, + "nopt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", + "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", + "dev": true, + "optional": true, + "requires": { + "abbrev": "1.1.0", + "osenv": "0.1.4" + } + }, + "npmlog": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.0.tgz", + "integrity": "sha512-ocolIkZYZt8UveuiDS0yAkkIjid1o7lPG8cYm05yNYzBn8ykQtaiPMEGp8fY9tKdDgm8okpdKzkvu1y9hUYugA==", + "dev": true, + "optional": true, + "requires": { + "are-we-there-yet": "1.1.4", + "console-control-strings": "1.1.0", + "gauge": "2.7.4", + "set-blocking": "2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=", + "dev": true, + "optional": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true, + "optional": true + }, + "osenv": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.4.tgz", + "integrity": "sha1-Qv5tWVPfBsgGS+bxdsPQWqqjRkQ=", + "dev": true, + "optional": true, + "requires": { + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "performance-now": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", + "integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=", + "dev": true, + "optional": true + }, + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", + "dev": true + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true, + "optional": true + }, + "qs": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", + "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=", + "dev": true, + "optional": true + }, + "rc": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.1.tgz", + "integrity": "sha1-LgPo5C7kULjLPc5lvhv4l04d/ZU=", + "dev": true, + "optional": true, + "requires": { + "deep-extend": "0.4.2", + "ini": "1.3.4", + "minimist": "1.2.0", + "strip-json-comments": "2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.2.9", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.9.tgz", + "integrity": "sha1-z3jsb0ptHrQ9JkiMrJfwQudLf8g=", + "dev": true, + "requires": { + "buffer-shims": "1.0.0", + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "string_decoder": "1.0.1", + "util-deprecate": "1.0.2" + } + }, + "request": { + "version": "2.81.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.81.0.tgz", + "integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=", + "dev": true, + "optional": true, + "requires": { + "aws-sign2": "0.6.0", + "aws4": "1.6.0", + "caseless": "0.12.0", + "combined-stream": "1.0.5", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.1.4", + "har-validator": "4.2.1", + "hawk": "3.1.3", + "http-signature": "1.1.1", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.15", + "oauth-sign": "0.8.2", + "performance-now": "0.2.0", + "qs": "6.4.0", + "safe-buffer": "5.0.1", + "stringstream": "0.0.5", + "tough-cookie": "2.3.2", + "tunnel-agent": "0.6.0", + "uuid": "3.0.1" + } + }, + "rimraf": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.1.tgz", + "integrity": "sha1-wjOOxkPfeht/5cVPqG9XQopV8z0=", + "dev": true, + "requires": { + "glob": "7.1.2" + } + }, + "safe-buffer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.0.1.tgz", + "integrity": "sha1-0mPKVGls2KMGtcplUekt5XkY++c=", + "dev": true + }, + "semver": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", + "dev": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true, + "optional": true + }, + "sntp": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", + "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", + "dev": true, + "requires": { + "hoek": "2.16.3" + } + }, + "sshpk": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.0.tgz", + "integrity": "sha1-/yo+T9BEl1Vf7Zezmg/YL6+zozw=", + "dev": true, + "optional": true, + "requires": { + "asn1": "0.2.3", + "assert-plus": "1.0.0", + "bcrypt-pbkdf": "1.0.1", + "dashdash": "1.14.1", + "ecc-jsbn": "0.1.1", + "getpass": "0.1.7", + "jodid25519": "1.0.2", + "jsbn": "0.1.1", + "tweetnacl": "0.14.5" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true, + "optional": true + } + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "string_decoder": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.1.tgz", + "integrity": "sha1-YuIA8DmVWmgQ2N8KM//A8BNmLZg=", + "dev": true, + "requires": { + "safe-buffer": "5.0.1" + } + }, + "stringstream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", + "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=", + "dev": true, + "optional": true + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true, + "optional": true + }, + "tar": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", + "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", + "dev": true, + "requires": { + "block-stream": "0.0.9", + "fstream": "1.0.11", + "inherits": "2.0.3" + } + }, + "tar-pack": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/tar-pack/-/tar-pack-3.4.0.tgz", + "integrity": "sha1-I74tf2cagzk3bL2wuP4/3r8xeYQ=", + "dev": true, + "optional": true, + "requires": { + "debug": "2.6.8", + "fstream": "1.0.11", + "fstream-ignore": "1.0.5", + "once": "1.4.0", + "readable-stream": "2.2.9", + "rimraf": "2.6.1", + "tar": "2.2.1", + "uid-number": "0.0.6" + } + }, + "tough-cookie": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.2.tgz", + "integrity": "sha1-8IH3bkyFcg5sN6X6ztc3FQ2EByo=", + "dev": true, + "optional": true, + "requires": { + "punycode": "1.4.1" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "dev": true, + "optional": true + }, + "uid-number": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz", + "integrity": "sha1-DqEOgDXo61uOREnwbaHHMGY7qoE=", + "dev": true, + "optional": true + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "uuid": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.0.1.tgz", + "integrity": "sha1-ZUS7ot/ajBzxfmKaOjBeK7H+5sE=", + "dev": true, + "optional": true + }, + "verror": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz", + "integrity": "sha1-z/XfEpRtKX0rqu+qJoniW+AcAFw=", + "dev": true, + "optional": true, + "requires": { + "extsprintf": "1.0.2" + } + }, + "wide-align": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz", + "integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==", + "dev": true, + "optional": true, + "requires": { + "string-width": "1.0.2" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + } + } + }, + "fstream": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", + "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "inherits": "2.0.3", + "mkdirp": "0.5.1", + "rimraf": "2.6.2" + } + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "function.prototype.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.0.tgz", + "integrity": "sha512-Bs0VRrTz4ghD8pTmbJQD1mZ8A/mN0ur/jGz+A6FBxPDUPkm1tNfF6bhTYPA7i7aF4lZJVr+OXTNNrnnIl58Wfg==", + "dev": true, + "requires": { + "define-properties": "1.1.2", + "function-bind": "1.1.1", + "is-callable": "1.1.3" + } + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "dev": true, + "requires": { + "aproba": "1.2.0", + "console-control-strings": "1.1.0", + "has-unicode": "2.0.1", + "object-assign": "4.1.1", + "signal-exit": "3.0.2", + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wide-align": "1.1.2" + }, + "dependencies": { + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + } + } + }, + "gaze": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.2.tgz", + "integrity": "sha1-hHIkZ3rbiHDWeSV+0ziP22HkAQU=", + "dev": true, + "requires": { + "globule": "1.2.0" + } + }, + "generate-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", + "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=", + "dev": true + }, + "generate-object-property": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", + "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", + "dev": true, + "requires": { + "is-property": "1.0.2" + } + }, + "get-caller-file": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz", + "integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U=" + }, + "get-stdin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", + "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", + "dev": true + }, + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", + "dev": true + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "requires": { + "assert-plus": "1.0.0" + } + }, + "gettext-parser": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/gettext-parser/-/gettext-parser-1.3.1.tgz", + "integrity": "sha512-W4t55eB/c7WrH0gbCHFiHuaEnJ1WiPJVnbFFiNEoh2QkOmuSLxs0PmJDGAmCQuTJCU740Fmb6D+2D/2xECWZGQ==", + "requires": { + "encoding": "0.1.12", + "safe-buffer": "5.1.1" + } + }, + "gh-got": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gh-got/-/gh-got-6.0.0.tgz", + "integrity": "sha512-F/mS+fsWQMo1zfgG9MD8KWvTWPPzzhuVwY++fhQ5Ggd+0P+CAMHtzMZhNxG+TqGfHDChJKsbh6otfMGqO2AKBw==", + "dev": true, + "requires": { + "got": "7.1.0", + "is-plain-obj": "1.1.0" + }, + "dependencies": { + "got": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/got/-/got-7.1.0.tgz", + "integrity": "sha512-Y5WMo7xKKq1muPsxD+KmrR8DH5auG7fBdDVueZwETwV6VytKyU9OX/ddpq2/1hp1vIPvVb4T81dKQz3BivkNLw==", + "dev": true, + "requires": { + "decompress-response": "3.3.0", + "duplexer3": "0.1.4", + "get-stream": "3.0.0", + "is-plain-obj": "1.1.0", + "is-retry-allowed": "1.1.0", + "is-stream": "1.1.0", + "isurl": "1.0.0", + "lowercase-keys": "1.0.1", + "p-cancelable": "0.3.0", + "p-timeout": "1.2.1", + "safe-buffer": "5.1.1", + "timed-out": "4.0.1", + "url-parse-lax": "1.0.0", + "url-to-options": "1.0.1" + } + }, + "p-cancelable": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.3.0.tgz", + "integrity": "sha512-RVbZPLso8+jFeq1MfNvgXtCRED2raz/dKpacfTNxsx6pLEpEomM7gah6VeHSYV3+vo0OAi4MkArtQcWWXuQoyw==", + "dev": true + }, + "p-timeout": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-1.2.1.tgz", + "integrity": "sha1-XrOzU7f86Z8QGhA4iAuwVOu+o4Y=", + "dev": true, + "requires": { + "p-finally": "1.0.0" + } + }, + "url-parse-lax": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", + "integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=", + "dev": true, + "requires": { + "prepend-http": "1.0.4" + } + } + } + }, + "github-username": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/github-username/-/github-username-4.1.0.tgz", + "integrity": "sha1-y+KABBiDIG2kISrp5LXxacML9Bc=", + "dev": true, + "requires": { + "gh-got": "6.0.0" + } + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "glob-all": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-all/-/glob-all-3.1.0.tgz", + "integrity": "sha1-iRPd+17hrHgSZWJBsD1SF8ZLAqs=", + "dev": true, + "requires": { + "glob": "7.1.2", + "yargs": "1.2.6" + }, + "dependencies": { + "minimist": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.1.0.tgz", + "integrity": "sha1-md9lelJXTCHJBXSX33QnkLK0wN4=", + "dev": true + }, + "yargs": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-1.2.6.tgz", + "integrity": "sha1-nHtKgv1dWVsr8Xq23MQxNUMv40s=", + "dev": true, + "requires": { + "minimist": "0.1.0" + } + } + } + }, + "glob-base": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", + "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", + "dev": true, + "requires": { + "glob-parent": "2.0.0", + "is-glob": "2.0.1" + } + }, + "glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "dev": true, + "requires": { + "is-glob": "2.0.1" + } + }, + "global-modules": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "dev": true, + "requires": { + "global-prefix": "1.0.2", + "is-windows": "1.0.1", + "resolve-dir": "1.0.1" + } + }, + "global-prefix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=", + "dev": true, + "requires": { + "expand-tilde": "2.0.2", + "homedir-polyfill": "1.0.1", + "ini": "1.3.5", + "is-windows": "1.0.1", + "which": "1.3.0" + } + }, + "globals": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", + "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", + "dev": true + }, + "globby": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", + "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", + "dev": true, + "requires": { + "array-union": "1.0.2", + "arrify": "1.0.1", + "glob": "7.1.2", + "object-assign": "4.1.1", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" + } + }, + "globule": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.0.tgz", + "integrity": "sha1-HcScaCLdnoovoAuiopUAboZkvQk=", + "dev": true, + "requires": { + "glob": "7.1.2", + "lodash": "4.17.5", + "minimatch": "3.0.4" + } + }, + "good-listener": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz", + "integrity": "sha1-1TswzfkxPf+33JoNR3CWqm0UXFA=", + "requires": { + "delegate": "3.2.0" + } + }, + "got": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/got/-/got-8.3.0.tgz", + "integrity": "sha512-kBNy/S2CGwrYgDSec5KTWGKUvupwkkTVAjIsVFF2shXO13xpZdFP4d4kxa//CLX2tN/rV0aYwK8vY6UKWGn2vQ==", + "dev": true, + "requires": { + "@sindresorhus/is": "0.7.0", + "cacheable-request": "2.1.4", + "decompress-response": "3.3.0", + "duplexer3": "0.1.4", + "get-stream": "3.0.0", + "into-stream": "3.1.0", + "is-retry-allowed": "1.1.0", + "isurl": "1.0.0", + "lowercase-keys": "1.0.1", + "mimic-response": "1.0.0", + "p-cancelable": "0.4.0", + "p-timeout": "2.0.1", + "pify": "3.0.0", + "safe-buffer": "5.1.1", + "timed-out": "4.0.1", + "url-parse-lax": "3.0.0", + "url-to-options": "1.0.1" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } + } + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" + }, + "grouped-queue": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/grouped-queue/-/grouped-queue-0.3.3.tgz", + "integrity": "sha1-wWfSpTGcWg4JZO9qJbfC34mWyFw=", + "dev": true, + "requires": { + "lodash": "4.17.5" + } + }, + "growly": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", + "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", + "dev": true + }, + "handlebars": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.11.tgz", + "integrity": "sha1-Ywo13+ApS8KB7a5v/F0yn8eYLcw=", + "dev": true, + "requires": { + "async": "1.5.2", + "optimist": "0.6.1", + "source-map": "0.4.4", + "uglify-js": "2.8.29" + }, + "dependencies": { + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "dev": true, + "requires": { + "amdefine": "1.0.1" + } + } + } + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "dev": true + }, + "har-validator": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", + "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", + "dev": true, + "requires": { + "ajv": "5.5.2", + "har-schema": "2.0.0" + } + }, + "has": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.1.tgz", + "integrity": "sha1-hGFzP1OLCDfJNh45qauelwTcLyg=", + "requires": { + "function-bind": "1.1.1" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "has-color": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/has-color/-/has-color-0.1.7.tgz", + "integrity": "sha1-ZxRKUmDDT8PMpnfQQdr1L+e3iy8=", + "dev": true + }, + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "dev": true + }, + "has-symbol-support-x": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz", + "integrity": "sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw==", + "dev": true + }, + "has-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", + "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", + "dev": true + }, + "has-to-string-tag-x": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz", + "integrity": "sha512-vdbKfmw+3LoOYVr+mtxHaX5a96+0f3DljYd8JOqvOLsf5mw2Otda2qCDT9qRqLAhrjyQ0h7ual5nOiASpsGNFw==", + "dev": true, + "requires": { + "has-symbol-support-x": "1.4.2" + } + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "dev": true + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "dev": true, + "requires": { + "get-value": "2.0.6", + "has-values": "1.0.0", + "isobject": "3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "dev": true, + "requires": { + "is-number": "3.0.0", + "kind-of": "4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "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.6" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "hash-base": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-2.0.2.tgz", + "integrity": "sha1-ZuodhW206KVHDK32/OI65SRO8uE=", + "dev": true, + "requires": { + "inherits": "2.0.3" + } + }, + "hash.js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.3.tgz", + "integrity": "sha512-/UETyP0W22QILqS+6HowevwhEFJ3MBJnwTf75Qob9Wz9t0DPuisL8kW8YZMK62dHAKE1c1p+gY1TtOLY+USEHA==", + "dev": true, + "requires": { + "inherits": "2.0.3", + "minimalistic-assert": "1.0.0" + } + }, + "hawk": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", + "integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==", + "dev": true, + "requires": { + "boom": "4.3.1", + "cryptiles": "3.1.2", + "hoek": "4.2.0", + "sntp": "2.1.0" + } + }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "dev": true, + "requires": { + "hash.js": "1.1.3", + "minimalistic-assert": "1.0.0", + "minimalistic-crypto-utils": "1.0.1" + } + }, + "hoek": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz", + "integrity": "sha512-v0XCLxICi9nPfYrS9RL8HbYnXi9obYAeLbSP00BmnZwCK9+Ih9WOjoZ8YoHCoav2csqn4FOz4Orldsy2dmDwmQ==", + "dev": true + }, + "hoist-non-react-statics": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz", + "integrity": "sha1-qkSM8JhtVcxAdzsXF0t90GbLfPs=" + }, + "home-or-tmp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", + "integrity": "sha1-42w/LSyufXRqhX440Y1fMqeILbg=", + "dev": true, + "requires": { + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" + } + }, + "homedir-polyfill": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz", + "integrity": "sha1-TCu8inWJmP7r9e1oWA921GdotLw=", + "dev": true, + "requires": { + "parse-passwd": "1.0.0" + } + }, + "hosted-git-info": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.5.0.tgz", + "integrity": "sha512-pNgbURSuab90KbTqvRPsseaTxOJCZBD0a7t+haSN33piP9cCM4l0CqdzAif2hUqm716UovKB2ROmiabGAKVXyg==" + }, + "hpq": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hpq/-/hpq-1.2.0.tgz", + "integrity": "sha1-nGGLI5YqLXPW6Cugh0l4vLNov6I=" + }, + "html-comment-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/html-comment-regex/-/html-comment-regex-1.1.1.tgz", + "integrity": "sha1-ZouTd26q5V696POtRkswekljYl4=", + "dev": true + }, + "html-encoding-sniffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz", + "integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==", + "dev": true, + "requires": { + "whatwg-encoding": "1.0.3" + } + }, + "htmlparser2": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.9.2.tgz", + "integrity": "sha1-G9+HrMoPP55T+k/M6w9LTLsAszg=", + "dev": true, + "requires": { + "domelementtype": "1.3.0", + "domhandler": "2.4.1", + "domutils": "1.5.1", + "entities": "1.1.1", + "inherits": "2.0.3", + "readable-stream": "2.3.3" + } + }, + "http-cache-semantics": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz", + "integrity": "sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w==", + "dev": true + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "jsprim": "1.4.1", + "sshpk": "1.13.1" + } + }, + "https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", + "dev": true + }, + "https-proxy-agent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.0.tgz", + "integrity": "sha512-uUWcfXHvy/dwfM9bqa6AozvAjS32dZSTUYd/4SEpYKRg6LEcPLshksnQYRudM9AyNvUARMfAg5TLjUDyX/K4vA==", + "dev": true, + "requires": { + "agent-base": "4.2.0", + "debug": "3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, + "iconv-lite": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", + "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" + }, + "ieee754": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.11.tgz", + "integrity": "sha512-VhDzCKN7K8ufStx/CLj5/PDTMgph+qwN5Pkd5i0sGnVwk56zJ0lkT8Qzi1xqWLS0Wp29DgDtNeS7v8/wMoZeHg==", + "dev": true + }, + "iferr": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", + "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=", + "dev": true + }, + "ignore": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.7.tgz", + "integrity": "sha512-YGG3ejvBNHRqu0559EOxxNFihD0AjpvHlC/pdGKd3X3ofe+CoJkYazwNJYTNebqpPKN+VVQbh4ZFn1DivMNuHA==" + }, + "import-local": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-1.0.0.tgz", + "integrity": "sha512-vAaZHieK9qjGo58agRBg+bhHX3hoTZU/Oa3GESWLz7t1U62fk63aHuDJJEteXoDeTCcPmUT+z38gkHPZkkmpmQ==", + "dev": true, + "requires": { + "pkg-dir": "2.0.0", + "resolve-cwd": "2.0.0" + }, + "dependencies": { + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "2.0.0" + } + }, + "pkg-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", + "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", + "dev": true, + "requires": { + "find-up": "2.1.0" + } + } + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "in-publish": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.0.tgz", + "integrity": "sha1-4g/146KvwmkDILbcVSaCqcf631E=", + "dev": true + }, + "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.1" + } + }, + "indexes-of": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", + "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=", + "dev": true + }, + "indexof": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" + }, + "inquirer": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.3.0.tgz", + "integrity": "sha512-h+xtnyk4EwKvFWHrUYsWErEVR+igKtLdchu+o0Z1RL7VU/jVMFbYir2bp6bAj8efFNxWqHX0dIss6fJQ+/+qeQ==", + "dev": true, + "requires": { + "ansi-escapes": "3.0.0", + "chalk": "2.3.0", + "cli-cursor": "2.1.0", + "cli-width": "2.2.0", + "external-editor": "2.1.0", + "figures": "2.0.0", + "lodash": "4.17.5", + "mute-stream": "0.0.7", + "run-async": "2.3.0", + "rx-lite": "4.0.8", + "rx-lite-aggregates": "4.0.8", + "string-width": "2.1.1", + "strip-ansi": "4.0.0", + "through": "2.3.8" + }, + "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 + }, + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + }, + "chalk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", + "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "4.5.0" + } + }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "dev": true, + "requires": { + "restore-cursor": "2.0.0" + } + }, + "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" + } + }, + "onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "dev": true, + "requires": { + "mimic-fn": "1.2.0" + } + }, + "restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "dev": true, + "requires": { + "onetime": "2.0.1", + "signal-exit": "3.0.2" + } + }, + "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" + } + }, + "supports-color": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", + "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", + "dev": true, + "requires": { + "has-flag": "2.0.0" + } + } + } + }, + "interpret": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz", + "integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=", + "dev": true + }, + "into-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-3.1.0.tgz", + "integrity": "sha1-lvsKk2wSur1v8XUqF9BWFqvQlMY=", + "dev": true, + "requires": { + "from2": "2.3.0", + "p-is-promise": "1.1.0" + } + }, + "invariant": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.2.tgz", + "integrity": "sha1-nh9WrArNtr8wMwbzOL47IErmA2A=", + "requires": { + "loose-envify": "1.3.1" + } + }, + "invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=" + }, + "is-absolute-url": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-2.1.0.tgz", + "integrity": "sha1-UFMN+4T8yap9vnhS6Do3uTufKqY=", + "dev": true + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + }, + "dependencies": { + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "dev": true, + "requires": { + "binary-extensions": "1.11.0" + } + }, + "is-boolean-object": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.0.0.tgz", + "integrity": "sha1-mPiygDBoQhmpXzdc+9iM40Bd/5M=", + "dev": true + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-builtin-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", + "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", + "requires": { + "builtin-modules": "1.1.1" + } + }, + "is-callable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.3.tgz", + "integrity": "sha1-hut1OSgF3cM69xySoO7fdO52BLI=", + "dev": true + }, + "is-ci": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.1.0.tgz", + "integrity": "sha512-c7TnwxLePuqIlxHgr7xtxzycJPegNHFuIrBkwbf8hc58//+Op1CqFkyS+xnIMkwn9UsJIwc174BIjkyBmSpjKg==", + "dev": true, + "requires": { + "ci-info": "1.1.2" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + }, + "dependencies": { + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, + "is-date-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", + "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", + "dev": true + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "1.0.0", + "is-data-descriptor": "1.0.0", + "kind-of": "6.0.2" + }, + "dependencies": { + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, + "is-directory": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", + "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=", + "dev": true + }, + "is-dotfile": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", + "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=", + "dev": true + }, + "is-equal-shallow": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", + "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", + "dev": true, + "requires": { + "is-primitive": "2.0.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true + }, + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-finite": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", + "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-generator-fn": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-1.0.0.tgz", + "integrity": "sha1-lp1J4bszKfa7fwkIm+JleLLd1Go=", + "dev": true + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + }, + "is-my-json-valid": { + "version": "2.17.1", + "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.17.1.tgz", + "integrity": "sha512-Q2khNw+oBlWuaYvEEHtKSw/pCxD2L5Rc1C+UQme9X6JdRDh7m5D7HkozA0qa3DUkQ6VzCnEm8mVIQPyIRkI5sQ==", + "dev": true, + "requires": { + "generate-function": "2.0.0", + "generate-object-property": "1.2.0", + "jsonpointer": "4.0.1", + "xtend": "4.0.1" + } + }, + "is-number": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", + "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + } + }, + "is-number-object": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.3.tgz", + "integrity": "sha1-8mWrian0RQNO9q/xWo8AsA9VF5k=", + "dev": true + }, + "is-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.1.tgz", + "integrity": "sha1-iVJojF7C/9awPsyF52ngKQMINHA=", + "dev": true + }, + "is-observable": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/is-observable/-/is-observable-0.2.0.tgz", + "integrity": "sha1-s2ExHYPG5dcmyr9eJQsCNxBvWuI=", + "dev": true, + "requires": { + "symbol-observable": "0.2.4" + }, + "dependencies": { + "symbol-observable": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-0.2.4.tgz", + "integrity": "sha1-lag9smGG1q9+ehjb2XYKL4bQj0A=", + "dev": true + } + } + }, + "is-odd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-odd/-/is-odd-2.0.0.tgz", + "integrity": "sha512-OTiixgpZAT1M4NHgS5IguFp/Vz2VI3U7Goh4/HA1adtwyLtSBrxYlcSYkhpAE07s4fKEcjrFxyvtQBND4vFQyQ==", + "dev": true, + "requires": { + "is-number": "4.0.0" + }, + "dependencies": { + "is-number": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", + "dev": true + } + } + }, + "is-path-cwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", + "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", + "dev": true + }, + "is-path-in-cwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz", + "integrity": "sha1-ZHdYK4IU1gI0YJRWcAO+ip6sBNw=", + "dev": true, + "requires": { + "is-path-inside": "1.0.1" + } + }, + "is-path-inside": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", + "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", + "dev": true, + "requires": { + "path-is-inside": "1.0.2" + } + }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", + "dev": true + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "is-posix-bracket": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", + "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=", + "dev": true + }, + "is-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", + "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=", + "dev": true + }, + "is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", + "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", + "dev": true + }, + "is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", + "dev": true + }, + "is-regex": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", + "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", + "dev": true, + "requires": { + "has": "1.0.1" + } + }, + "is-resolvable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", + "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==", + "dev": true + }, + "is-retry-allowed": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz", + "integrity": "sha1-EaBgVotnM5REAz0BJaYaINVk+zQ=", + "dev": true + }, + "is-scoped": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-scoped/-/is-scoped-1.0.0.tgz", + "integrity": "sha1-RJypgpnnEwOCViieyytUDcQ3yzA=", + "dev": true, + "requires": { + "scoped-regex": "1.0.0" + } + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" + }, + "is-string": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.4.tgz", + "integrity": "sha1-zDqbaYV9Yh6WNyWiTK7shzuCbmQ=", + "dev": true + }, + "is-subset": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz", + "integrity": "sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY=", + "dev": true + }, + "is-svg": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-svg/-/is-svg-2.1.0.tgz", + "integrity": "sha1-z2EJDaDZ77yrhyLeum8DIgjbsOk=", + "dev": true, + "requires": { + "html-comment-regex": "1.1.1" + } + }, + "is-symbol": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.1.tgz", + "integrity": "sha1-PMWfAAJRlLarLjjbrmaJJWtmBXI=", + "dev": true + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", + "dev": true + }, + "is-windows": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.1.tgz", + "integrity": "sha1-MQ23D3QtJZoWo2kgK1GvhCMzENk=", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + }, + "isomorphic-fetch": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", + "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=", + "requires": { + "node-fetch": "1.7.3", + "whatwg-fetch": "2.0.3" + } + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true + }, + "istanbul-api": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/istanbul-api/-/istanbul-api-1.2.2.tgz", + "integrity": "sha512-kH5YRdqdbs5hiH4/Rr1Q0cSAGgjh3jTtg8vu9NLebBAoK3adVO4jk81J+TYOkTr2+Q4NLeb1ACvmEt65iG/Vbw==", + "dev": true, + "requires": { + "async": "2.6.0", + "fileset": "2.0.3", + "istanbul-lib-coverage": "1.1.2", + "istanbul-lib-hook": "1.1.0", + "istanbul-lib-instrument": "1.9.2", + "istanbul-lib-report": "1.1.3", + "istanbul-lib-source-maps": "1.2.3", + "istanbul-reports": "1.1.4", + "js-yaml": "3.10.0", + "mkdirp": "0.5.1", + "once": "1.4.0" + } + }, + "istanbul-lib-coverage": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-1.1.2.tgz", + "integrity": "sha512-tZYA0v5A7qBSsOzcebJJ/z3lk3oSzH62puG78DbBA1+zupipX2CakDyiPV3pOb8He+jBwVimuwB0dTnh38hX0w==", + "dev": true + }, + "istanbul-lib-hook": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-1.1.0.tgz", + "integrity": "sha512-U3qEgwVDUerZ0bt8cfl3dSP3S6opBoOtk3ROO5f2EfBr/SRiD9FQqzwaZBqFORu8W7O0EXpai+k7kxHK13beRg==", + "dev": true, + "requires": { + "append-transform": "0.4.0" + } + }, + "istanbul-lib-instrument": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-1.9.2.tgz", + "integrity": "sha512-nz8t4HQ2206a/3AXi+NHFWEa844DMpPsgbcUteJbt1j8LX1xg56H9rOMnhvcvVvPbW60qAIyrSk44H8ZDqaSSA==", + "dev": true, + "requires": { + "babel-generator": "6.26.1", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "istanbul-lib-coverage": "1.1.2", + "semver": "5.3.0" + } + }, + "istanbul-lib-report": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-1.1.3.tgz", + "integrity": "sha512-D4jVbMDtT2dPmloPJS/rmeP626N5Pr3Rp+SovrPn1+zPChGHcggd/0sL29jnbm4oK9W0wHjCRsdch9oLd7cm6g==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "1.1.2", + "mkdirp": "0.5.1", + "path-parse": "1.0.5", + "supports-color": "3.2.3" + }, + "dependencies": { + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", + "dev": true + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "dev": true, + "requires": { + "has-flag": "1.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.3.tgz", + "integrity": "sha512-fDa0hwU/5sDXwAklXgAoCJCOsFsBplVQ6WBldz5UwaqOzmDhUK4nfuR7/G//G2lERlblUNJB8P6e8cXq3a7MlA==", + "dev": true, + "requires": { + "debug": "3.1.0", + "istanbul-lib-coverage": "1.1.2", + "mkdirp": "0.5.1", + "rimraf": "2.6.2", + "source-map": "0.5.7" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, + "istanbul-reports": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-1.1.4.tgz", + "integrity": "sha512-DfSTVOTkuO+kRmbO8Gk650Wqm1WRGr6lrdi2EwDK1vxpS71vdlLd613EpzOKdIFioB5f/scJTjeWBnvd1FWejg==", + "dev": true, + "requires": { + "handlebars": "4.0.11" + } + }, + "istextorbinary": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-2.2.1.tgz", + "integrity": "sha512-TS+hoFl8Z5FAFMK38nhBkdLt44CclNRgDHWeMgsV8ko3nDlr/9UI2Sf839sW7enijf8oKsZYXRvM8g0it9Zmcw==", + "dev": true, + "requires": { + "binaryextensions": "2.1.1", + "editions": "1.3.4", + "textextensions": "2.2.0" + } + }, + "isurl": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isurl/-/isurl-1.0.0.tgz", + "integrity": "sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w==", + "dev": true, + "requires": { + "has-to-string-tag-x": "1.4.1", + "is-object": "1.0.1" + } + }, + "jed": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/jed/-/jed-1.1.1.tgz", + "integrity": "sha1-elSbvZ/+FYWwzQoZHiAwVb7ldLQ=" + }, + "jest": { + "version": "22.4.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-22.4.0.tgz", + "integrity": "sha512-eze1JLbBDkrbZMnE6xIlBxHkqPAmuHbz4GQbED8qRVtnpea3o6Tt/Dc3SBs3qnlTo7svema8Ho5bqLfdHyabyQ==", + "dev": true, + "requires": { + "import-local": "1.0.0", + "jest-cli": "22.4.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 + }, + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + }, + "chalk": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.1.tgz", + "integrity": "sha512-QUU4ofkDoMIVO7hcx1iPTISs88wsO8jA92RQIm4JAwZvFGGAV2hSAA1NX7oVj2Ej2Q6NDTcRDjPTFrMCRZoJ6g==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "5.2.0" + } + }, + "cliui": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.0.0.tgz", + "integrity": "sha512-nY3W5Gu2racvdDk//ELReY+dHjb9PlIcVDFXP72nVIhq2Gy3LuVXYwJoPVudwQnv1shtohpgkdCKT2YaKY0CKw==", + "dev": true, + "requires": { + "string-width": "2.1.1", + "strip-ansi": "4.0.0", + "wrap-ansi": "2.1.0" + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "2.0.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "jest-cli": { + "version": "22.4.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-22.4.0.tgz", + "integrity": "sha512-0JlBb/PvHGQZR2I9GZwsycHgWHhriBmvBWPaaPYUT186oiIIDY4ezDxFOFt2Ts0yNTRg3iY9mTyHsfWbT5VRWA==", + "dev": true, + "requires": { + "ansi-escapes": "3.0.0", + "chalk": "2.3.1", + "exit": "0.1.2", + "glob": "7.1.2", + "graceful-fs": "4.1.11", + "import-local": "1.0.0", + "is-ci": "1.1.0", + "istanbul-api": "1.2.2", + "istanbul-lib-coverage": "1.1.2", + "istanbul-lib-instrument": "1.9.2", + "istanbul-lib-source-maps": "1.2.3", + "jest-changed-files": "22.2.0", + "jest-config": "22.4.0", + "jest-environment-jsdom": "22.4.0", + "jest-get-type": "22.1.0", + "jest-haste-map": "22.4.0", + "jest-message-util": "22.4.0", + "jest-regex-util": "22.1.0", + "jest-resolve-dependencies": "22.1.0", + "jest-runner": "22.4.0", + "jest-runtime": "22.4.0", + "jest-snapshot": "22.4.0", + "jest-util": "22.4.0", + "jest-validate": "22.4.0", + "jest-worker": "22.2.2", + "micromatch": "2.3.11", + "node-notifier": "5.2.1", + "realpath-native": "1.0.0", + "rimraf": "2.6.2", + "slash": "1.0.0", + "string-length": "2.0.0", + "strip-ansi": "4.0.0", + "which": "1.3.0", + "yargs": "10.1.2" + } + }, + "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" + } + }, + "supports-color": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.2.0.tgz", + "integrity": "sha512-F39vS48la4YvTZUPVeTqsjsFNrvcMwrV3RLZINsmHo+7djCvuUzSIeXOnZ5hmjef4bajL1dNccN+tg5XAliO5Q==", + "dev": true, + "requires": { + "has-flag": "3.0.0" + } + }, + "yargs": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-10.1.2.tgz", + "integrity": "sha512-ivSoxqBGYOqQVruxD35+EyCFDYNEFL/Uo6FcOnz+9xZdZzK0Zzw4r4KhbrME1Oo2gOggwJod2MnsdamSG7H9ig==", + "dev": true, + "requires": { + "cliui": "4.0.0", + "decamelize": "1.2.0", + "find-up": "2.1.0", + "get-caller-file": "1.0.2", + "os-locale": "2.1.0", + "require-directory": "2.1.1", + "require-main-filename": "1.0.1", + "set-blocking": "2.0.0", + "string-width": "2.1.1", + "which-module": "2.0.0", + "y18n": "3.2.1", + "yargs-parser": "8.1.0" + } + }, + "yargs-parser": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-8.1.0.tgz", + "integrity": "sha512-yP+6QqN8BmrgW2ggLtTbdrOyBNSI7zBa4IykmiV5R1wl1JWNxQvWhMfMdmzIYtKU7oP3OOInY/tl2ov3BDjnJQ==", + "dev": true, + "requires": { + "camelcase": "4.1.0" + } + } + } + }, + "jest-changed-files": { + "version": "22.2.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-22.2.0.tgz", + "integrity": "sha512-SzqOvoPMrXB0NPvDrSPeKETpoUNCtNDOsFbCzAGWxqWVvNyrIMLpUjVExT3u3LfdVrENlrNGCfh5YoFd8+ZeXg==", + "dev": true, + "requires": { + "throat": "4.1.0" + } + }, + "jest-config": { + "version": "22.4.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-22.4.0.tgz", + "integrity": "sha512-hZs8qHjCybOpqni0Kwt40eAavYN/3KnJJwYxSJsBRedJ98IgGSiI18SjybCSccKayA7eHgw1A+dLkHcfI4LItQ==", + "dev": true, + "requires": { + "chalk": "2.3.1", + "glob": "7.1.2", + "jest-environment-jsdom": "22.4.0", + "jest-environment-node": "22.4.0", + "jest-get-type": "22.1.0", + "jest-jasmine2": "22.4.0", + "jest-regex-util": "22.1.0", + "jest-resolve": "22.4.0", + "jest-util": "22.4.0", + "jest-validate": "22.4.0", + "pretty-format": "22.4.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + }, + "chalk": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.1.tgz", + "integrity": "sha512-QUU4ofkDoMIVO7hcx1iPTISs88wsO8jA92RQIm4JAwZvFGGAV2hSAA1NX7oVj2Ej2Q6NDTcRDjPTFrMCRZoJ6g==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "5.2.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.2.0.tgz", + "integrity": "sha512-F39vS48la4YvTZUPVeTqsjsFNrvcMwrV3RLZINsmHo+7djCvuUzSIeXOnZ5hmjef4bajL1dNccN+tg5XAliO5Q==", + "dev": true, + "requires": { + "has-flag": "3.0.0" + } + } + } + }, + "jest-diff": { + "version": "22.4.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-22.4.0.tgz", + "integrity": "sha512-+/t20WmnkOkB8MOaGaPziI8zWKxquMvYw4Ub+wOzi7AUhmpFXz43buWSxVoZo4J5RnCozpGbX3/FssjJ5KV9Nw==", + "dev": true, + "requires": { + "chalk": "2.3.1", + "diff": "3.4.0", + "jest-get-type": "22.1.0", + "pretty-format": "22.4.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + }, + "chalk": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.1.tgz", + "integrity": "sha512-QUU4ofkDoMIVO7hcx1iPTISs88wsO8jA92RQIm4JAwZvFGGAV2hSAA1NX7oVj2Ej2Q6NDTcRDjPTFrMCRZoJ6g==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "5.2.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.2.0.tgz", + "integrity": "sha512-F39vS48la4YvTZUPVeTqsjsFNrvcMwrV3RLZINsmHo+7djCvuUzSIeXOnZ5hmjef4bajL1dNccN+tg5XAliO5Q==", + "dev": true, + "requires": { + "has-flag": "3.0.0" + } + } + } + }, + "jest-docblock": { + "version": "22.4.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-22.4.0.tgz", + "integrity": "sha512-lDY7GZ+/CJb02oULYLBDj7Hs5shBhVpDYpIm8LUyqw9X2J22QRsM19gmGQwIFqGSJmpc/LRrSYudeSrG510xlQ==", + "dev": true, + "requires": { + "detect-newline": "2.1.0" + } + }, + "jest-environment-jsdom": { + "version": "22.4.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-22.4.0.tgz", + "integrity": "sha512-SAUCte4KFLaD2YhYwHFVEI2GkR4BHqHJsnbFgmQMGgHnZ2CfjSZE8Bnb+jlarbxIG4GXl31+2e9rjBpzbY9gKQ==", + "dev": true, + "requires": { + "jest-mock": "22.2.0", + "jest-util": "22.4.0", + "jsdom": "11.6.2" + } + }, + "jest-environment-node": { + "version": "22.4.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-22.4.0.tgz", + "integrity": "sha512-ihSKa2MU5jkAhmRJ17FU4nisbbfW6spvl6Jtwmm5W9kmTVa2sa9UoHWbOWAb7HXuLi3PGGjzTfEt5o3uIzisnQ==", + "dev": true, + "requires": { + "jest-mock": "22.2.0", + "jest-util": "22.4.0" + } + }, + "jest-enzyme": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/jest-enzyme/-/jest-enzyme-4.2.0.tgz", + "integrity": "sha512-nna99NnU6sDbWqVX0153c81RUuxI/spTgw4Xobh049NcKihu0OAtAawbuSzZUnlCqdZOoXlKMudfjUPm0sCTsg==", + "dev": true, + "requires": { + "enzyme-matchers": "4.2.0", + "enzyme-to-json": "3.3.1" + } + }, + "jest-get-type": { + "version": "22.1.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-22.1.0.tgz", + "integrity": "sha512-nD97IVOlNP6fjIN5i7j5XRH+hFsHL7VlauBbzRvueaaUe70uohrkz7pL/N8lx/IAwZRTJ//wOdVgh85OgM7g3w==", + "dev": true + }, + "jest-haste-map": { + "version": "22.4.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-22.4.0.tgz", + "integrity": "sha512-znYomZ+GaRcuFLQz7hmwQOfLkHY2Y2Aoyd29ZcXLrwBEWts5U/c7lFsqo54XUJUlMhrM5M2IOaAUWjZ1CRqAOQ==", + "dev": true, + "requires": { + "fb-watchman": "2.0.0", + "graceful-fs": "4.1.11", + "jest-docblock": "22.4.0", + "jest-serializer": "22.4.0", + "jest-worker": "22.2.2", + "micromatch": "2.3.11", + "sane": "2.4.1" + } + }, + "jest-jasmine2": { + "version": "22.4.0", + "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-22.4.0.tgz", + "integrity": "sha512-oL7bNLfEL9jPVjmiwqQuwrAJ/5ddmKHSpns0kCpAmv1uQ47Q5aC9zBTXZbDWP5GVbVHj2hbYtNbkwTiXJr0e8w==", + "dev": true, + "requires": { + "callsites": "2.0.0", + "chalk": "2.3.1", + "co": "4.6.0", + "expect": "22.4.0", + "graceful-fs": "4.1.11", + "is-generator-fn": "1.0.0", + "jest-diff": "22.4.0", + "jest-matcher-utils": "22.4.0", + "jest-message-util": "22.4.0", + "jest-snapshot": "22.4.0", + "source-map-support": "0.5.3" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + }, + "chalk": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.1.tgz", + "integrity": "sha512-QUU4ofkDoMIVO7hcx1iPTISs88wsO8jA92RQIm4JAwZvFGGAV2hSAA1NX7oVj2Ej2Q6NDTcRDjPTFrMCRZoJ6g==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "5.2.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.2.0.tgz", + "integrity": "sha512-F39vS48la4YvTZUPVeTqsjsFNrvcMwrV3RLZINsmHo+7djCvuUzSIeXOnZ5hmjef4bajL1dNccN+tg5XAliO5Q==", + "dev": true, + "requires": { + "has-flag": "3.0.0" + } + } + } + }, + "jest-leak-detector": { + "version": "22.4.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-22.4.0.tgz", + "integrity": "sha512-r3NEIVNh4X3fEeJtUIrKXWKhNokwUM2ILp5LD8w1KrEanPsFtZmYjmyZYjDTX2dXYr33TW65OvbRE3hWFAyq6g==", + "dev": true, + "requires": { + "pretty-format": "22.4.0" + } + }, + "jest-matcher-utils": { + "version": "22.4.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-22.4.0.tgz", + "integrity": "sha512-03m3issxUXpWMwDYTfmL8hRNewUB0yCRTeXPm+eq058rZxLHD9f5NtSSO98CWHqe4UyISIxd9Ao9iDVjHWd2qg==", + "dev": true, + "requires": { + "chalk": "2.3.1", + "jest-get-type": "22.1.0", + "pretty-format": "22.4.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + }, + "chalk": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.1.tgz", + "integrity": "sha512-QUU4ofkDoMIVO7hcx1iPTISs88wsO8jA92RQIm4JAwZvFGGAV2hSAA1NX7oVj2Ej2Q6NDTcRDjPTFrMCRZoJ6g==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "5.2.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.2.0.tgz", + "integrity": "sha512-F39vS48la4YvTZUPVeTqsjsFNrvcMwrV3RLZINsmHo+7djCvuUzSIeXOnZ5hmjef4bajL1dNccN+tg5XAliO5Q==", + "dev": true, + "requires": { + "has-flag": "3.0.0" + } + } + } + }, + "jest-message-util": { + "version": "22.4.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-22.4.0.tgz", + "integrity": "sha512-eyCJB0T3hrlpFF2FqQoIB093OulP+1qvATQmD3IOgJgMGqPL6eYw8TbC5P/VCWPqKhGL51xvjIIhow5eZ2wHFw==", + "dev": true, + "requires": { + "@babel/code-frame": "7.0.0-beta.40", + "chalk": "2.3.1", + "micromatch": "2.3.11", + "slash": "1.0.0", + "stack-utils": "1.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + }, + "chalk": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.1.tgz", + "integrity": "sha512-QUU4ofkDoMIVO7hcx1iPTISs88wsO8jA92RQIm4JAwZvFGGAV2hSAA1NX7oVj2Ej2Q6NDTcRDjPTFrMCRZoJ6g==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "5.2.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.2.0.tgz", + "integrity": "sha512-F39vS48la4YvTZUPVeTqsjsFNrvcMwrV3RLZINsmHo+7djCvuUzSIeXOnZ5hmjef4bajL1dNccN+tg5XAliO5Q==", + "dev": true, + "requires": { + "has-flag": "3.0.0" + } + } + } + }, + "jest-mock": { + "version": "22.2.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-22.2.0.tgz", + "integrity": "sha512-eOfoUYLOB/JlxChOFkh/bzpWGqUXb9I+oOpkprHHs9L7nUNfL8Rk28h1ycWrqzWCEQ/jZBg/xIv7VdQkfAkOhw==", + "dev": true + }, + "jest-regex-util": { + "version": "22.1.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-22.1.0.tgz", + "integrity": "sha512-on0LqVS6Xeh69sw3d1RukVnur+lVOl3zkmb0Q54FHj9wHoq6dbtWqb3TSlnVUyx36hqjJhjgs/QLqs07Bzu72Q==", + "dev": true + }, + "jest-resolve": { + "version": "22.4.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-22.4.0.tgz", + "integrity": "sha512-Vs/5VeJEHLpB0ubpYuU9QpBjcCUZRHoHnoV58ZC+N3EXyMJr/MgoqUNpo4OHGQERWlUpvl4YLAAO5uxSMF2VIg==", + "dev": true, + "requires": { + "browser-resolve": "1.11.2", + "chalk": "2.3.1" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + }, + "chalk": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.1.tgz", + "integrity": "sha512-QUU4ofkDoMIVO7hcx1iPTISs88wsO8jA92RQIm4JAwZvFGGAV2hSAA1NX7oVj2Ej2Q6NDTcRDjPTFrMCRZoJ6g==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "5.2.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.2.0.tgz", + "integrity": "sha512-F39vS48la4YvTZUPVeTqsjsFNrvcMwrV3RLZINsmHo+7djCvuUzSIeXOnZ5hmjef4bajL1dNccN+tg5XAliO5Q==", + "dev": true, + "requires": { + "has-flag": "3.0.0" + } + } + } + }, + "jest-resolve-dependencies": { + "version": "22.1.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-22.1.0.tgz", + "integrity": "sha512-76Ll61bD/Sus8wK8d+lw891EtiBJGJkWG8OuVDTEX0z3z2+jPujvQqSB2eQ+kCHyCsRwJ2PSjhn3UHqae/oEtA==", + "dev": true, + "requires": { + "jest-regex-util": "22.1.0" + } + }, + "jest-runner": { + "version": "22.4.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-22.4.0.tgz", + "integrity": "sha512-x5QJQrSQs/oaZq2UxtKJxCjGq3fNF7guKRLxAIS39QIaRSAynS4agniMyvHMnLaYsBh6yzUea2SDeNHayQh+TQ==", + "dev": true, + "requires": { + "exit": "0.1.2", + "jest-config": "22.4.0", + "jest-docblock": "22.4.0", + "jest-haste-map": "22.4.0", + "jest-jasmine2": "22.4.0", + "jest-leak-detector": "22.4.0", + "jest-message-util": "22.4.0", + "jest-runtime": "22.4.0", + "jest-util": "22.4.0", + "jest-worker": "22.2.2", + "throat": "4.1.0" + } + }, + "jest-runtime": { + "version": "22.4.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-22.4.0.tgz", + "integrity": "sha512-aixL2DIXoFQ2ubnurzK4kbNXLl3+m0m7wIBb5VWaJdl1/3nV1UCSjZ9/dJZzpWGGfXsoGw2RZd8sS0nS5s+tdw==", + "dev": true, + "requires": { + "babel-core": "6.26.0", + "babel-jest": "22.4.0", + "babel-plugin-istanbul": "4.1.5", + "chalk": "2.3.1", + "convert-source-map": "1.5.1", + "exit": "0.1.2", + "graceful-fs": "4.1.11", + "jest-config": "22.4.0", + "jest-haste-map": "22.4.0", + "jest-regex-util": "22.1.0", + "jest-resolve": "22.4.0", + "jest-util": "22.4.0", + "jest-validate": "22.4.0", + "json-stable-stringify": "1.0.1", + "micromatch": "2.3.11", + "realpath-native": "1.0.0", + "slash": "1.0.0", + "strip-bom": "3.0.0", + "write-file-atomic": "2.3.0", + "yargs": "10.1.2" + }, + "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 + }, + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + }, + "chalk": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.1.tgz", + "integrity": "sha512-QUU4ofkDoMIVO7hcx1iPTISs88wsO8jA92RQIm4JAwZvFGGAV2hSAA1NX7oVj2Ej2Q6NDTcRDjPTFrMCRZoJ6g==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "5.2.0" + } + }, + "cliui": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.0.0.tgz", + "integrity": "sha512-nY3W5Gu2racvdDk//ELReY+dHjb9PlIcVDFXP72nVIhq2Gy3LuVXYwJoPVudwQnv1shtohpgkdCKT2YaKY0CKw==", + "dev": true, + "requires": { + "string-width": "2.1.1", + "strip-ansi": "4.0.0", + "wrap-ansi": "2.1.0" + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "2.0.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "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" + } + }, + "supports-color": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.2.0.tgz", + "integrity": "sha512-F39vS48la4YvTZUPVeTqsjsFNrvcMwrV3RLZINsmHo+7djCvuUzSIeXOnZ5hmjef4bajL1dNccN+tg5XAliO5Q==", + "dev": true, + "requires": { + "has-flag": "3.0.0" + } + }, + "yargs": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-10.1.2.tgz", + "integrity": "sha512-ivSoxqBGYOqQVruxD35+EyCFDYNEFL/Uo6FcOnz+9xZdZzK0Zzw4r4KhbrME1Oo2gOggwJod2MnsdamSG7H9ig==", + "dev": true, + "requires": { + "cliui": "4.0.0", + "decamelize": "1.2.0", + "find-up": "2.1.0", + "get-caller-file": "1.0.2", + "os-locale": "2.1.0", + "require-directory": "2.1.1", + "require-main-filename": "1.0.1", + "set-blocking": "2.0.0", + "string-width": "2.1.1", + "which-module": "2.0.0", + "y18n": "3.2.1", + "yargs-parser": "8.1.0" + } + }, + "yargs-parser": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-8.1.0.tgz", + "integrity": "sha512-yP+6QqN8BmrgW2ggLtTbdrOyBNSI7zBa4IykmiV5R1wl1JWNxQvWhMfMdmzIYtKU7oP3OOInY/tl2ov3BDjnJQ==", + "dev": true, + "requires": { + "camelcase": "4.1.0" + } + } + } + }, + "jest-serializer": { + "version": "22.4.0", + "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-22.4.0.tgz", + "integrity": "sha512-dnqde95MiYfdc1ZJpjEiHCRvRGGJHPsZQARJFucEGIaOzxqqS9/tt2WzD/OUSGT6kxaEGLQE92faVJGdoCu+Rw==", + "dev": true + }, + "jest-snapshot": { + "version": "22.4.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-22.4.0.tgz", + "integrity": "sha512-6Zz4F9G1Nbr93kfm5h3A2+OkE+WGpgJlskYE4iSNN2uYfoTL5b9W6aB9Orpx+ueReHyqmy7HET7Z3EmYlL3hKw==", + "dev": true, + "requires": { + "chalk": "2.3.1", + "jest-diff": "22.4.0", + "jest-matcher-utils": "22.4.0", + "mkdirp": "0.5.1", + "natural-compare": "1.4.0", + "pretty-format": "22.4.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + }, + "chalk": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.1.tgz", + "integrity": "sha512-QUU4ofkDoMIVO7hcx1iPTISs88wsO8jA92RQIm4JAwZvFGGAV2hSAA1NX7oVj2Ej2Q6NDTcRDjPTFrMCRZoJ6g==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "5.2.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.2.0.tgz", + "integrity": "sha512-F39vS48la4YvTZUPVeTqsjsFNrvcMwrV3RLZINsmHo+7djCvuUzSIeXOnZ5hmjef4bajL1dNccN+tg5XAliO5Q==", + "dev": true, + "requires": { + "has-flag": "3.0.0" + } + } + } + }, + "jest-util": { + "version": "22.4.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-22.4.0.tgz", + "integrity": "sha512-652EArz3XScAGAUMhbny7FrFGlmJkp+56CO+9RTrKPtGfbtVDF2WB2D8G+6D6zorDmDW5hNtKNIGNdGfG2kj1g==", + "dev": true, + "requires": { + "callsites": "2.0.0", + "chalk": "2.3.1", + "graceful-fs": "4.1.11", + "is-ci": "1.1.0", + "jest-message-util": "22.4.0", + "mkdirp": "0.5.1" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + }, + "chalk": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.1.tgz", + "integrity": "sha512-QUU4ofkDoMIVO7hcx1iPTISs88wsO8jA92RQIm4JAwZvFGGAV2hSAA1NX7oVj2Ej2Q6NDTcRDjPTFrMCRZoJ6g==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "5.2.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.2.0.tgz", + "integrity": "sha512-F39vS48la4YvTZUPVeTqsjsFNrvcMwrV3RLZINsmHo+7djCvuUzSIeXOnZ5hmjef4bajL1dNccN+tg5XAliO5Q==", + "dev": true, + "requires": { + "has-flag": "3.0.0" + } + } + } + }, + "jest-validate": { + "version": "22.4.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-22.4.0.tgz", + "integrity": "sha512-l5JwbIAso8jGp/5/Dy86BCVjOra/Rb81wyXcFTGa4VxbtIh4AEOp2WixgprHLwp+YlUrHugZwaGyuagjB+iB+A==", + "dev": true, + "requires": { + "chalk": "2.3.1", + "jest-config": "22.4.0", + "jest-get-type": "22.1.0", + "leven": "2.1.0", + "pretty-format": "22.4.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + }, + "chalk": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.1.tgz", + "integrity": "sha512-QUU4ofkDoMIVO7hcx1iPTISs88wsO8jA92RQIm4JAwZvFGGAV2hSAA1NX7oVj2Ej2Q6NDTcRDjPTFrMCRZoJ6g==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "5.2.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.2.0.tgz", + "integrity": "sha512-F39vS48la4YvTZUPVeTqsjsFNrvcMwrV3RLZINsmHo+7djCvuUzSIeXOnZ5hmjef4bajL1dNccN+tg5XAliO5Q==", + "dev": true, + "requires": { + "has-flag": "3.0.0" + } + } + } + }, + "jest-worker": { + "version": "22.2.2", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-22.2.2.tgz", + "integrity": "sha512-ZylDXjrFNt/OP6cUxwJFWwDgazP7hRjtCQbocFHyiwov+04Wm1x5PYzMGNJT53s4nwr0oo9ocYTImS09xOlUnw==", + "dev": true, + "requires": { + "merge-stream": "1.0.1" + } + }, + "jquery": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.2.1.tgz", + "integrity": "sha1-XE2d5lKvbNCncBVKYxu6ErAVx4c=" + }, + "js-base64": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.4.3.tgz", + "integrity": "sha512-H7ErYLM34CvDMto3GbD6xD0JLUGYXR3QTcH6B/tr4Hi/QpSThnCsIp+Sy5FRTw3B0d6py4HcNkW7nO/wdtGWEw==", + "dev": true + }, + "js-beautify": { + "version": "1.6.14", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.6.14.tgz", + "integrity": "sha1-07j3Mi0CuSd9WL0jgmTDJ+WARM0=", + "requires": { + "config-chain": "1.1.11", + "editorconfig": "0.13.3", + "mkdirp": "0.5.1", + "nopt": "3.0.6" + } + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=" + }, + "js-yaml": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.10.0.tgz", + "integrity": "sha512-O2v52ffjLa9VeM43J4XocZE//WT9N0IiwDa3KSHH7Tu8CtH+1qM8SIZvnsTh6v+4yFy5KUY3BHUVwjpfAWsjIA==", + "dev": true, + "requires": { + "argparse": "1.0.9", + "esprima": "4.0.0" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true, + "optional": true + }, + "jscodeshift": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jscodeshift/-/jscodeshift-0.5.0.tgz", + "integrity": "sha512-JAcQINNMFpdzzpKJN8k5xXjF3XDuckB1/48uScSzcnNyK199iWEc9AxKL9OoX5144M2w5zEx9Qs4/E/eBZZUlw==", + "dev": true, + "requires": { + "babel-plugin-transform-flow-strip-types": "6.22.0", + "babel-preset-es2015": "6.24.1", + "babel-preset-stage-1": "6.24.1", + "babel-register": "6.26.0", + "babylon": "7.0.0-beta.42", + "colors": "1.2.1", + "flow-parser": "0.69.0", + "lodash": "4.17.5", + "micromatch": "2.3.11", + "neo-async": "2.5.0", + "node-dir": "0.1.8", + "nomnom": "1.8.1", + "recast": "0.14.7", + "temp": "0.8.3", + "write-file-atomic": "1.3.4" + }, + "dependencies": { + "ansi-styles": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.0.0.tgz", + "integrity": "sha1-yxAt8cVvUSPquLZ817mAJ6AnkXg=", + "dev": true + }, + "babylon": { + "version": "7.0.0-beta.42", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.42.tgz", + "integrity": "sha512-h6E/OkkvcBw/JimbL0p8dIaxrcuQn3QmIYGC/GtJlRYif5LTKBYPHXYwqluJpfS/kOXoz0go+9mkmOVC0M+zWw==", + "dev": true + }, + "chalk": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.4.0.tgz", + "integrity": "sha1-UZmj3c0MHv4jvAjBsCewYXbgxk8=", + "dev": true, + "requires": { + "ansi-styles": "1.0.0", + "has-color": "0.1.7", + "strip-ansi": "0.1.1" + } + }, + "colors": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.2.1.tgz", + "integrity": "sha512-s8+wktIuDSLffCywiwSxQOMqtPxML11a/dtHE17tMn4B1MSWw/C22EKf7M2KGUBcDaVFEGT+S8N02geDXeuNKg==", + "dev": true + }, + "nomnom": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/nomnom/-/nomnom-1.8.1.tgz", + "integrity": "sha1-IVH3Ikcrp55Qp2/BJbuMjy5Nwqc=", + "dev": true, + "requires": { + "chalk": "0.4.0", + "underscore": "1.6.0" + } + }, + "strip-ansi": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.1.1.tgz", + "integrity": "sha1-OeipjQRNFQZgq+SmgIrPcLt7yZE=", + "dev": true + }, + "underscore": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz", + "integrity": "sha1-izixDKze9jM3uLJOT/htRa6lKag=", + "dev": true + }, + "write-file-atomic": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-1.3.4.tgz", + "integrity": "sha1-+Aek8LHZ6ROuekgRLmzDrxmRtF8=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "imurmurhash": "0.1.4", + "slide": "1.1.6" + } + } + } + }, + "jsdom": { + "version": "11.6.2", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-11.6.2.tgz", + "integrity": "sha512-pAeZhpbSlUp5yQcS6cBQJwkbzmv4tWFaYxHbFVSxzXefqjvtRA851Z5N2P+TguVG9YeUDcgb8pdeVQRJh0XR3Q==", + "dev": true, + "requires": { + "abab": "1.0.4", + "acorn": "5.4.1", + "acorn-globals": "4.1.0", + "array-equal": "1.0.0", + "browser-process-hrtime": "0.1.2", + "content-type-parser": "1.0.2", + "cssom": "0.3.2", + "cssstyle": "0.2.37", + "domexception": "1.0.1", + "escodegen": "1.9.0", + "html-encoding-sniffer": "1.0.2", + "left-pad": "1.2.0", + "nwmatcher": "1.4.3", + "parse5": "4.0.0", + "pn": "1.1.0", + "request": "2.83.0", + "request-promise-native": "1.0.5", + "sax": "1.2.4", + "symbol-tree": "3.2.2", + "tough-cookie": "2.3.3", + "w3c-hr-time": "1.0.1", + "webidl-conversions": "4.0.2", + "whatwg-encoding": "1.0.3", + "whatwg-url": "6.4.0", + "ws": "4.0.0", + "xml-name-validator": "3.0.0" + }, + "dependencies": { + "parse5": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz", + "integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==", + "dev": true + } + } + }, + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", + "dev": true + }, + "json-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", + "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=", + "dev": true + }, + "json-parse-better-errors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.1.tgz", + "integrity": "sha512-xyQpxeWWMKyJps9CuGJYeng6ssI5bpqS9ltQpdVQ90t4ql6NdnxFKh95JcRt2cun/DjMVNrdjniLPuMA69xmCw==", + "dev": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", + "dev": true + }, + "json-stable-stringify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "dev": true, + "requires": { + "jsonify": "0.0.0" + } + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, + "json5": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", + "dev": true + }, + "jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", + "dev": true + }, + "jsonpointer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", + "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=", + "dev": true + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "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 + }, + "keyv": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.0.0.tgz", + "integrity": "sha512-eguHnq22OE3uVoSYG0LVWNP+4ppamWr9+zWBe1bsNcovIMy6huUJFPgy4mGwCd/rnl3vOLGW1MTlu4c57CT1xA==", + "dev": true, + "requires": { + "json-buffer": "3.0.0" + } + }, + "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.6" + } + }, + "lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", + "dev": true, + "optional": true + }, + "lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "requires": { + "invert-kv": "1.0.0" + } + }, + "left-pad": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.2.0.tgz", + "integrity": "sha1-0wpzxrggHY99jnlWupYWCHpo4O4=", + "dev": true + }, + "leven": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", + "integrity": "sha1-wuep93IJTe6dNCAq6KzORoeHVYA=", + "dev": true + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "1.1.2", + "type-check": "0.3.2" + } + }, + "line-height": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/line-height/-/line-height-0.3.1.tgz", + "integrity": "sha1-SxIF7d4YKHKl76PI9iCzGHqcVMk=", + "requires": { + "computed-style": "0.1.4" + } + }, + "listr": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/listr/-/listr-0.13.0.tgz", + "integrity": "sha1-ILsLowuuZg7oTMBQPfS+PVYjiH0=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "cli-truncate": "0.2.1", + "figures": "1.7.0", + "indent-string": "2.1.0", + "is-observable": "0.2.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.1", + "log-symbols": "1.0.2", + "log-update": "1.0.2", + "ora": "0.2.3", + "p-map": "1.2.0", + "rxjs": "5.5.8", + "stream-to-observable": "0.2.0", + "strip-ansi": "3.0.1" + }, + "dependencies": { + "figures": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", + "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", + "dev": true, + "requires": { + "escape-string-regexp": "1.0.5", + "object-assign": "4.1.1" + } + }, + "log-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-1.0.2.tgz", + "integrity": "sha1-N2/3tY6jCGoPCfrMdGF+ylAeGhg=", + "dev": true, + "requires": { + "chalk": "1.1.3" + } + } + } + }, + "listr-silent-renderer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz", + "integrity": "sha1-kktaN1cVN3C/Go4/v3S4u/P5JC4=", + "dev": true + }, + "listr-update-renderer": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/listr-update-renderer/-/listr-update-renderer-0.4.0.tgz", + "integrity": "sha1-NE2YDaLKLosUW6MFkI8yrj9MyKc=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "cli-truncate": "0.2.1", + "elegant-spinner": "1.0.1", + "figures": "1.7.0", + "indent-string": "3.2.0", + "log-symbols": "1.0.2", + "log-update": "1.0.2", + "strip-ansi": "3.0.1" + }, + "dependencies": { + "figures": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", + "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", + "dev": true, + "requires": { + "escape-string-regexp": "1.0.5", + "object-assign": "4.1.1" + } + }, + "indent-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", + "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=", + "dev": true + }, + "log-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-1.0.2.tgz", + "integrity": "sha1-N2/3tY6jCGoPCfrMdGF+ylAeGhg=", + "dev": true, + "requires": { + "chalk": "1.1.3" + } + } + } + }, + "listr-verbose-renderer": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/listr-verbose-renderer/-/listr-verbose-renderer-0.4.1.tgz", + "integrity": "sha1-ggb0z21S3cWCfl/RSYng6WWTOjU=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "cli-cursor": "1.0.2", + "date-fns": "1.29.0", + "figures": "1.7.0" + }, + "dependencies": { + "cli-cursor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", + "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", + "dev": true, + "requires": { + "restore-cursor": "1.0.1" + } + }, + "figures": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", + "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", + "dev": true, + "requires": { + "escape-string-regexp": "1.0.5", + "object-assign": "4.1.1" + } + }, + "onetime": { + "version": "1.1.0", + "resolved": "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", + "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", + "dev": true + }, + "restore-cursor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", + "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", + "dev": true, + "requires": { + "exit-hook": "1.1.1", + "onetime": "1.1.0" + } + } + } + }, + "load-json-file": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", + "requires": { + "graceful-fs": "4.1.11", + "parse-json": "2.2.0", + "pify": "2.3.0", + "strip-bom": "3.0.0" + } + }, + "loader-runner": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.3.0.tgz", + "integrity": "sha1-9IKuqC1UPgeSFwDVpG7yb9rGuKI=", + "dev": true + }, + "loader-utils": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.1.0.tgz", + "integrity": "sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=", + "dev": true, + "requires": { + "big.js": "3.2.0", + "emojis-list": "2.1.0", + "json5": "0.5.1" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "requires": { + "p-locate": "2.0.0", + "path-exists": "3.0.0" + }, + "dependencies": { + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + } + } + }, + "lodash": { + "version": "4.17.5", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.5.tgz", + "integrity": "sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw==" + }, + "lodash-es": { + "version": "4.17.5", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.5.tgz", + "integrity": "sha512-Ez3ONp3TK9gX1HYKp6IhetcVybD+2F+Yp6GS9dfH8ue6EOCEzQtQEh4K0FYWBP9qLv+lzeQAYXw+3ySfxyZqkw==" + }, + "lodash._baseisequal": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/lodash._baseisequal/-/lodash._baseisequal-3.0.7.tgz", + "integrity": "sha1-2AJfdjOdKTQnZ9zIh85cuVpbUfE=", + "dev": true, + "requires": { + "lodash.isarray": "3.0.4", + "lodash.istypedarray": "3.0.6", + "lodash.keys": "3.1.2" + } + }, + "lodash._bindcallback": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz", + "integrity": "sha1-5THCdkTPi1epnhftlbNcdIeJOS4=", + "dev": true + }, + "lodash._getnative": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", + "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=", + "dev": true + }, + "lodash.assign": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", + "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=", + "dev": true + }, + "lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", + "dev": true + }, + "lodash.cond": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/lodash.cond/-/lodash.cond-4.5.2.tgz", + "integrity": "sha1-9HGh2khr5g9quVXRcRVSPdHSVdU=" + }, + "lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", + "dev": true + }, + "lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=", + "dev": true + }, + "lodash.isarray": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", + "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", + "dev": true + }, + "lodash.isequal": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-3.0.4.tgz", + "integrity": "sha1-HDXrO27wzR/1F0Pj6jz3/f/ay2Q=", + "dev": true, + "requires": { + "lodash._baseisequal": "3.0.7", + "lodash._bindcallback": "3.0.1" + } + }, + "lodash.istypedarray": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/lodash.istypedarray/-/lodash.istypedarray-3.0.6.tgz", + "integrity": "sha1-yaR3SYYHUB2OhJTSg7h8OSgc72I=", + "dev": true + }, + "lodash.keys": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", + "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", + "dev": true, + "requires": { + "lodash._getnative": "3.9.1", + "lodash.isarguments": "3.1.0", + "lodash.isarray": "3.0.4" + } + }, + "lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", + "dev": true + }, + "lodash.merge": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.1.tgz", + "integrity": "sha512-AOYza4+Hf5z1/0Hztxpm2/xiPZgi/cjMqdnKTUWTBSKchJlxXXuUSxCCl8rJlf4g6yww/j6mA8nC8Hw/EZWxKQ==", + "dev": true + }, + "lodash.mergewith": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz", + "integrity": "sha512-eWw5r+PYICtEBgrBE5hhlT6aAa75f411bgDz/ZL2KZqYV03USvucsxcHUIlGTDTECs1eunpI7HOV7U+WLDvNdQ==", + "dev": true + }, + "lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", + "dev": true + }, + "lodash.tail": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.tail/-/lodash.tail-4.1.1.tgz", + "integrity": "sha1-0jM6NtnncXyK0vfKyv7HwytERmQ=", + "dev": true + }, + "lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", + "dev": true + }, + "log-symbols": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", + "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "dev": true, + "requires": { + "chalk": "2.3.2" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + }, + "chalk": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.2.tgz", + "integrity": "sha512-ZM4j2/ld/YZDc3Ma8PgN7gyAk+kHMMMyzLNryCPGhWrsfAuDVeuid5bpRFTDgMH9JBK2lA4dyyAkkZYF/WcqDQ==", + "dev": true, + "requires": { + "ansi-styles": "3.2.1", + "escape-string-regexp": "1.0.5", + "supports-color": "5.3.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.3.0.tgz", + "integrity": "sha512-0aP01LLIskjKs3lq52EC0aGBAJhLq7B2Rd8HC/DR/PtNNpcLilNmHC12O+hu0usQpo7wtHNRqtrhBwtDb0+dNg==", + "dev": true, + "requires": { + "has-flag": "3.0.0" + } + } + } + }, + "log-update": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-1.0.2.tgz", + "integrity": "sha1-GZKfZMQJPS0ucHWh2tivWcKWuNE=", + "dev": true, + "requires": { + "ansi-escapes": "1.4.0", + "cli-cursor": "1.0.2" + }, + "dependencies": { + "ansi-escapes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", + "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=", + "dev": true + }, + "cli-cursor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", + "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", + "dev": true, + "requires": { + "restore-cursor": "1.0.1" + } + }, + "onetime": { + "version": "1.1.0", + "resolved": "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", + "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", + "dev": true + }, + "restore-cursor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", + "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", + "dev": true, + "requires": { + "exit-hook": "1.1.1", + "onetime": "1.1.0" + } + } + } + }, + "longest": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", + "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", + "dev": true + }, + "loose-envify": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", + "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=", + "requires": { + "js-tokens": "3.0.2" + } + }, + "loud-rejection": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", + "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", + "dev": true, + "requires": { + "currently-unhandled": "0.4.1", + "signal-exit": "3.0.2" + } + }, + "lowercase-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", + "dev": true + }, + "lru-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-3.2.0.tgz", + "integrity": "sha1-cXibO39Tmb7IVl3aOKow0qCX7+4=", + "requires": { + "pseudomap": "1.0.2" + } + }, + "macaddress": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/macaddress/-/macaddress-0.2.8.tgz", + "integrity": "sha1-WQTcU3w57G2+/q6QIycTX6hRHxI=", + "dev": true + }, + "make-dir": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.2.0.tgz", + "integrity": "sha512-aNUAa4UMg/UougV25bbrU4ZaaKNjJ/3/xnvg/twpmKROPdKZPZ9wGgI0opdZzO8q/zUFawoUuixuOv33eZ61Iw==", + "dev": true, + "requires": { + "pify": "3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } + } + }, + "makeerror": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz", + "integrity": "sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw=", + "dev": true, + "requires": { + "tmpl": "1.0.4" + } + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "dev": true + }, + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", + "dev": true + }, + "map-values": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-values/-/map-values-1.0.1.tgz", + "integrity": "sha1-douOecAJvytk/ugG4ip7HEGQyZA=", + "dev": true + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "dev": true, + "requires": { + "object-visit": "1.0.1" + } + }, + "material-colors": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.5.tgz", + "integrity": "sha1-UpJZPmdUyxvMK5gDDk4Najr8nqE=" + }, + "math-expression-evaluator": { + "version": "1.2.17", + "resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz", + "integrity": "sha1-3oGf282E3M2PrlnGrreWFbnSZqw=", + "dev": true + }, + "md5.js": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz", + "integrity": "sha1-6b296UogpawYsENA/Fdk1bCdkB0=", + "dev": true, + "requires": { + "hash-base": "3.0.4", + "inherits": "2.0.3" + }, + "dependencies": { + "hash-base": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", + "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "safe-buffer": "5.1.1" + } + } + } + }, + "mem": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz", + "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=", + "requires": { + "mimic-fn": "1.2.0" + } + }, + "mem-fs": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/mem-fs/-/mem-fs-1.1.3.tgz", + "integrity": "sha1-uK6NLj/Lb10/kWXBLUVRoGXZicw=", + "dev": true, + "requires": { + "through2": "2.0.3", + "vinyl": "1.2.0", + "vinyl-file": "2.0.0" + } + }, + "mem-fs-editor": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mem-fs-editor/-/mem-fs-editor-3.0.2.tgz", + "integrity": "sha1-3Qpuryu4prN3QAZ6pUnrUwEFr58=", + "dev": true, + "requires": { + "commondir": "1.0.1", + "deep-extend": "0.4.2", + "ejs": "2.5.8", + "glob": "7.1.2", + "globby": "6.1.0", + "mkdirp": "0.5.1", + "multimatch": "2.1.0", + "rimraf": "2.6.2", + "through2": "2.0.3", + "vinyl": "2.1.0" + }, + "dependencies": { + "clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", + "dev": true + }, + "clone-stats": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", + "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=", + "dev": true + }, + "globby": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", + "dev": true, + "requires": { + "array-union": "1.0.2", + "glob": "7.1.2", + "object-assign": "4.1.1", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" + } + }, + "replace-ext": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", + "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", + "dev": true + }, + "vinyl": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.1.0.tgz", + "integrity": "sha1-Ah+cLPlR1rk5lDyJ617lrdT9kkw=", + "dev": true, + "requires": { + "clone": "2.1.2", + "clone-buffer": "1.0.0", + "clone-stats": "1.0.0", + "cloneable-readable": "1.1.2", + "remove-trailing-separator": "1.1.0", + "replace-ext": "1.0.0" + } + } + } + }, + "memize": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/memize/-/memize-1.0.5.tgz", + "integrity": "sha512-Dm8Jhb5kiC4+ynYsVR4QDXKt+o2dfqGuY4hE2x+XlXZkdndlT80bJxfcMv5QGp/FCy6MhG7f5ElpmKPFKOSEpg==" + }, + "memory-fs": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", + "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", + "dev": true, + "requires": { + "errno": "0.1.7", + "readable-stream": "2.3.3" + } + }, + "meow": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", + "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", + "dev": true, + "requires": { + "camelcase-keys": "2.1.0", + "decamelize": "1.2.0", + "loud-rejection": "1.6.0", + "map-obj": "1.0.1", + "minimist": "1.2.0", + "normalize-package-data": "2.4.0", + "object-assign": "4.1.1", + "read-pkg-up": "1.0.1", + "redent": "1.0.0", + "trim-newlines": "1.0.0" + }, + "dependencies": { + "load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "parse-json": "2.2.0", + "pify": "2.3.0", + "pinkie-promise": "2.0.1", + "strip-bom": "2.0.0" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" + } + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "dev": true, + "requires": { + "load-json-file": "1.1.0", + "normalize-package-data": "2.4.0", + "path-type": "1.1.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "dev": true, + "requires": { + "find-up": "1.1.2", + "read-pkg": "1.1.0" + } + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "requires": { + "is-utf8": "0.2.1" + } + } + } + }, + "merge": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/merge/-/merge-1.2.0.tgz", + "integrity": "sha1-dTHjnUlJwoGma4xabgJl6LBYlNo=" + }, + "merge-stream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-1.0.1.tgz", + "integrity": "sha1-QEEgLVCKNCugAXQAjfDCUbjBNeE=", + "dev": true, + "requires": { + "readable-stream": "2.3.3" + } + }, + "micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "dev": true, + "requires": { + "arr-diff": "2.0.0", + "array-unique": "0.2.1", + "braces": "1.8.5", + "expand-brackets": "0.1.5", + "extglob": "0.3.2", + "filename-regex": "2.0.1", + "is-extglob": "1.0.0", + "is-glob": "2.0.1", + "kind-of": "3.2.2", + "normalize-path": "2.1.1", + "object.omit": "2.0.1", + "parse-glob": "3.0.4", + "regex-cache": "0.4.4" + } + }, + "miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "dev": true, + "requires": { + "bn.js": "4.11.8", + "brorand": "1.1.0" + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true + }, + "mime-db": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz", + "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=", + "dev": true + }, + "mime-types": { + "version": "2.1.17", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", + "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", + "dev": true, + "requires": { + "mime-db": "1.30.0" + } + }, + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==" + }, + "mimic-response": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.0.tgz", + "integrity": "sha1-3z02Uqc/3ta5sLJBRub9BSNTRY4=", + "dev": true + }, + "minimalistic-assert": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz", + "integrity": "sha1-cCvi3aazf0g2vLP121ZkG2Sh09M=", + "dev": true + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "1.1.8" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + }, + "mississippi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-2.0.0.tgz", + "integrity": "sha512-zHo8v+otD1J10j/tC+VNoGK9keCuByhKovAvdn74dmxJl9+mWHnx6EMsDN4lgRoMI/eYo2nchAxniIbUPb5onw==", + "dev": true, + "requires": { + "concat-stream": "1.6.0", + "duplexify": "3.5.4", + "end-of-stream": "1.4.1", + "flush-write-stream": "1.0.3", + "from2": "2.3.0", + "parallel-transform": "1.1.0", + "pump": "2.0.1", + "pumpify": "1.4.0", + "stream-each": "1.2.2", + "through2": "2.0.3" + } + }, + "mixin-deep": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", + "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", + "dev": true, + "requires": { + "for-in": "1.0.2", + "is-extendable": "1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "2.0.4" + } + } + } + }, + "mixin-object": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mixin-object/-/mixin-object-2.0.1.tgz", + "integrity": "sha1-T7lJRB2rGCVA8f4DW6YOGUel5X4=", + "dev": true, + "requires": { + "for-in": "0.1.8", + "is-extendable": "0.1.1" + }, + "dependencies": { + "for-in": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-0.1.8.tgz", + "integrity": "sha1-2Hc5COMSVhCZUrH9ubP6hn0ndeE=", + "dev": true + } + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + } + }, + "moment": { + "version": "2.21.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.21.0.tgz", + "integrity": "sha512-TCZ36BjURTeFTM/CwRcViQlfkMvL1/vFISuNLO5GkcVm1+QHfbSiNqZuWeMFjj1/3+uAjXswgRk30j1kkLYJBQ==" + }, + "moment-timezone": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.13.tgz", + "integrity": "sha1-mc5cfYJyYusPH3AgRBd/YHRde5A=", + "requires": { + "moment": "2.21.0" + } + }, + "mousetrap": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/mousetrap/-/mousetrap-1.6.1.tgz", + "integrity": "sha1-KghfXHUSlMdefoH27CVFspy/Qtk=" + }, + "move-concurrently": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", + "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=", + "dev": true, + "requires": { + "aproba": "1.2.0", + "copy-concurrently": "1.0.5", + "fs-write-stream-atomic": "1.0.10", + "mkdirp": "0.5.1", + "rimraf": "2.6.2", + "run-queue": "1.0.3" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "multimatch": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-2.1.0.tgz", + "integrity": "sha1-nHkGoi+0wCkZ4vX3UWG0zb1LKis=", + "dev": true, + "requires": { + "array-differ": "1.0.0", + "array-union": "1.0.2", + "arrify": "1.0.1", + "minimatch": "3.0.4" + } + }, + "mute-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", + "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", + "dev": true + }, + "nan": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.8.0.tgz", + "integrity": "sha1-7XFfP+neArV6XmJS2QqWZ14fCFo=", + "dev": true + }, + "nanomatch": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.9.tgz", + "integrity": "sha512-n8R9bS8yQ6eSXaV6jHUpKzD8gLsin02w1HSFiegwrs9E098Ylhw5jdyKPaYqvHknHaSCKTPp7C8dGCQ0q9koXA==", + "dev": true, + "requires": { + "arr-diff": "4.0.0", + "array-unique": "0.3.2", + "define-property": "2.0.2", + "extend-shallow": "3.0.2", + "fragment-cache": "0.2.1", + "is-odd": "2.0.0", + "is-windows": "1.0.2", + "kind-of": "6.0.2", + "object.pick": "1.3.0", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + }, + "dependencies": { + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "nearley": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.11.1.tgz", + "integrity": "sha512-1azpqq1JvHKZNPEixS1jNEXf4kDilhFtr8AIZIGjP8N0TcAcUhKgi354niI5pM4JoOsMQ+H6vzCYWQa95LQjcw==", + "dev": true, + "requires": { + "nomnom": "1.6.2", + "railroad-diagrams": "1.0.0", + "randexp": "0.4.6", + "semver": "5.5.0" + }, + "dependencies": { + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", + "dev": true + } + } + }, + "neo-async": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.5.0.tgz", + "integrity": "sha512-nJmSswG4As/MkRq7QZFuH/sf/yuv8ODdMZrY4Bedjp77a5MK4A6s7YbBB64c9u79EBUOfXUXBvArmvzTD0X+6g==", + "dev": true + }, + "nice-try": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.4.tgz", + "integrity": "sha512-2NpiFHqC87y/zFke0fC0spBXL3bBsoh/p5H1EFhshxjCR5+0g2d6BiXbUFz9v1sAcxsk2htp2eQnNIci2dIYcA==", + "dev": true + }, + "node-dir": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/node-dir/-/node-dir-0.1.8.tgz", + "integrity": "sha1-VfuN62mQcHB/tn+RpGDwRIKUx30=", + "dev": true + }, + "node-fetch": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", + "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==", + "requires": { + "encoding": "0.1.12", + "is-stream": "1.1.0" + } + }, + "node-gyp": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.6.2.tgz", + "integrity": "sha1-m/vlRWIoYoSDjnUOrAUpWFP6HGA=", + "dev": true, + "requires": { + "fstream": "1.0.11", + "glob": "7.1.2", + "graceful-fs": "4.1.11", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "nopt": "3.0.6", + "npmlog": "4.1.2", + "osenv": "0.1.4", + "request": "2.83.0", + "rimraf": "2.6.2", + "semver": "5.3.0", + "tar": "2.2.1", + "which": "1.3.0" + } + }, + "node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=", + "dev": true + }, + "node-libs-browser": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.1.0.tgz", + "integrity": "sha512-5AzFzdoIMb89hBGMZglEegffzgRg+ZFoUmisQ8HI4j1KDdpx13J0taNp2y9xPbur6W61gepGDDotGBVQ7mfUCg==", + "dev": true, + "requires": { + "assert": "1.4.1", + "browserify-zlib": "0.2.0", + "buffer": "4.9.1", + "console-browserify": "1.1.0", + "constants-browserify": "1.0.0", + "crypto-browserify": "3.12.0", + "domain-browser": "1.2.0", + "events": "1.1.1", + "https-browserify": "1.0.0", + "os-browserify": "0.3.0", + "path-browserify": "0.0.0", + "process": "0.11.10", + "punycode": "1.4.1", + "querystring-es3": "0.2.1", + "readable-stream": "2.3.3", + "stream-browserify": "2.0.1", + "stream-http": "2.8.1", + "string_decoder": "1.0.3", + "timers-browserify": "2.0.6", + "tty-browserify": "0.0.0", + "url": "0.11.0", + "util": "0.10.3", + "vm-browserify": "0.0.4" + } + }, + "node-notifier": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.2.1.tgz", + "integrity": "sha512-MIBs+AAd6dJ2SklbbE8RUDRlIVhU8MaNLh1A9SUZDUHPiZkWLFde6UNwG41yQHZEToHgJMXqyVZ9UcS/ReOVTg==", + "dev": true, + "requires": { + "growly": "1.3.0", + "semver": "5.5.0", + "shellwords": "0.1.1", + "which": "1.3.0" + }, + "dependencies": { + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", + "dev": true + } + } + }, + "node-sass": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.7.2.tgz", + "integrity": "sha512-CaV+wLqZ7//Jdom5aUFCpGNoECd7BbNhjuwdsX/LkXBrHl8eb1Wjw4HvWqcFvhr5KuNgAk8i/myf/MQ1YYeroA==", + "dev": true, + "requires": { + "async-foreach": "0.1.3", + "chalk": "1.1.3", + "cross-spawn": "3.0.1", + "gaze": "1.1.2", + "get-stdin": "4.0.1", + "glob": "7.1.2", + "in-publish": "2.0.0", + "lodash.assign": "4.2.0", + "lodash.clonedeep": "4.5.0", + "lodash.mergewith": "4.6.1", + "meow": "3.7.0", + "mkdirp": "0.5.1", + "nan": "2.8.0", + "node-gyp": "3.6.2", + "npmlog": "4.1.2", + "request": "2.79.0", + "sass-graph": "2.2.4", + "stdout-stream": "1.4.0", + "true-case-path": "1.0.2" + }, + "dependencies": { + "assert-plus": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", + "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=", + "dev": true + }, + "aws-sign2": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", + "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=", + "dev": true + }, + "boom": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", + "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", + "dev": true, + "requires": { + "hoek": "2.16.3" + } + }, + "caseless": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz", + "integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=", + "dev": true + }, + "cross-spawn": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz", + "integrity": "sha1-ElYDfsufDF9549bvE14wdwGEuYI=", + "dev": true, + "requires": { + "lru-cache": "4.1.1", + "which": "1.3.0" + } + }, + "cryptiles": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", + "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", + "dev": true, + "requires": { + "boom": "2.10.1" + } + }, + "form-data": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", + "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", + "dev": true, + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.5", + "mime-types": "2.1.17" + } + }, + "har-validator": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz", + "integrity": "sha1-zcvAgYgmWtEZtqWnyKtw7s+10n0=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "commander": "2.14.1", + "is-my-json-valid": "2.17.1", + "pinkie-promise": "2.0.1" + } + }, + "hawk": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", + "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", + "dev": true, + "requires": { + "boom": "2.10.1", + "cryptiles": "2.0.5", + "hoek": "2.16.3", + "sntp": "1.0.9" + } + }, + "hoek": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", + "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=", + "dev": true + }, + "http-signature": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", + "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", + "dev": true, + "requires": { + "assert-plus": "0.2.0", + "jsprim": "1.4.1", + "sshpk": "1.13.1" + } + }, + "lru-cache": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.1.tgz", + "integrity": "sha512-q4spe4KTfsAS1SUHLO0wz8Qiyf1+vMIAgpRYioFYDMNqKfHQbg+AVDH3i4fvpl71/P1L0dBl+fQi+P37UYf0ew==", + "dev": true, + "requires": { + "pseudomap": "1.0.2", + "yallist": "2.1.2" + } + }, + "qs": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.3.2.tgz", + "integrity": "sha1-51vV9uJoEioqDgvaYwslUMFmUCw=", + "dev": true + }, + "request": { + "version": "2.79.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.79.0.tgz", + "integrity": "sha1-Tf5b9r6LjNw3/Pk+BLZVd3InEN4=", + "dev": true, + "requires": { + "aws-sign2": "0.6.0", + "aws4": "1.6.0", + "caseless": "0.11.0", + "combined-stream": "1.0.5", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.1.4", + "har-validator": "2.0.6", + "hawk": "3.1.3", + "http-signature": "1.1.1", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.17", + "oauth-sign": "0.8.2", + "qs": "6.3.2", + "stringstream": "0.0.5", + "tough-cookie": "2.3.3", + "tunnel-agent": "0.4.3", + "uuid": "3.1.0" + } + }, + "sntp": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", + "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", + "dev": true, + "requires": { + "hoek": "2.16.3" + } + }, + "tunnel-agent": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz", + "integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=", + "dev": true + } + } + }, + "nomnom": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/nomnom/-/nomnom-1.6.2.tgz", + "integrity": "sha1-hKZqJgF0QI/Ft3oY+IjszET7aXE=", + "dev": true, + "requires": { + "colors": "0.5.1", + "underscore": "1.4.4" + } + }, + "nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "requires": { + "abbrev": "1.1.1" + } + }, + "normalize-package-data": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", + "requires": { + "hosted-git-info": "2.5.0", + "is-builtin-module": "1.0.0", + "semver": "5.3.0", + "validate-npm-package-license": "3.0.1" + } + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "1.1.0" + } + }, + "normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", + "dev": true + }, + "normalize-url": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-1.9.1.tgz", + "integrity": "sha1-LMDWazHqIwNkWENuNiDYWVTGbDw=", + "dev": true, + "requires": { + "object-assign": "4.1.1", + "prepend-http": "1.0.4", + "query-string": "4.3.4", + "sort-keys": "1.1.2" + } + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "requires": { + "path-key": "2.0.1" + } + }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "dev": true, + "requires": { + "are-we-there-yet": "1.1.4", + "console-control-strings": "1.1.0", + "gauge": "2.7.4", + "set-blocking": "2.0.0" + } + }, + "nth-check": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.1.tgz", + "integrity": "sha1-mSms32KPwsQQmN6rgqxYDPFJquQ=", + "dev": true, + "requires": { + "boolbase": "1.0.0" + } + }, + "num2fraction": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", + "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=", + "dev": true + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + }, + "nwmatcher": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/nwmatcher/-/nwmatcher-1.4.3.tgz", + "integrity": "sha512-IKdSTiDWCarf2JTS5e9e2+5tPZGdkRJ79XjYV0pzK8Q9BpsFyBq1RGKxzs7Q8UBushGw7m6TzVKz6fcY99iSWw==", + "dev": true + }, + "oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "dev": true, + "requires": { + "copy-descriptor": "0.1.1", + "define-property": "0.2.5", + "kind-of": "3.2.2" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "0.1.6" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "0.1.6", + "is-data-descriptor": "0.1.4", + "kind-of": "5.1.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + } + } + }, + "object-filter": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/object-filter/-/object-filter-1.0.2.tgz", + "integrity": "sha1-rwt5f/6+r4pSxmN87b6IFs/sG8g=", + "dev": true + }, + "object-inspect": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.5.0.tgz", + "integrity": "sha512-UmOFbHbwvv+XHj7BerrhVq+knjceBdkvU5AriwLMvhv2qi+e7DJzxfBeFpILEjVzCp+xA+W/pIf06RGPWlZNfw==", + "dev": true + }, + "object-is": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.0.1.tgz", + "integrity": "sha1-CqYOyZiaCz7Xlc9NBvYs8a1lObY=", + "dev": true + }, + "object-keys": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.11.tgz", + "integrity": "sha1-xUYBd4rVYPEULODgG8yotW0TQm0=", + "dev": true + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "dev": true, + "requires": { + "isobject": "3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "object.assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "dev": true, + "requires": { + "define-properties": "1.1.2", + "function-bind": "1.1.1", + "has-symbols": "1.0.0", + "object-keys": "1.0.11" + } + }, + "object.entries": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.0.4.tgz", + "integrity": "sha1-G/mk3SKI9bM/Opk9JXZh8F0WGl8=", + "dev": true, + "requires": { + "define-properties": "1.1.2", + "es-abstract": "1.10.0", + "function-bind": "1.1.1", + "has": "1.0.1" + } + }, + "object.getownpropertydescriptors": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", + "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", + "dev": true, + "requires": { + "define-properties": "1.1.2", + "es-abstract": "1.10.0" + } + }, + "object.omit": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", + "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", + "dev": true, + "requires": { + "for-own": "0.1.5", + "is-extendable": "0.1.1" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "dev": true, + "requires": { + "isobject": "3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "object.values": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.0.4.tgz", + "integrity": "sha1-5STaCbT2b/Bd9FdUbscqyZ8TBpo=", + "dev": true, + "requires": { + "define-properties": "1.1.2", + "es-abstract": "1.10.0", + "function-bind": "1.1.1", + "has": "1.0.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "dev": true, + "requires": { + "mimic-fn": "1.2.0" + } + }, + "optimist": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "dev": true, + "requires": { + "minimist": "0.0.8", + "wordwrap": "0.0.3" + } + }, + "optionator": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", + "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "dev": true, + "requires": { + "deep-is": "0.1.3", + "fast-levenshtein": "2.0.6", + "levn": "0.3.0", + "prelude-ls": "1.1.2", + "type-check": "0.3.2", + "wordwrap": "1.0.0" + }, + "dependencies": { + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + } + } + }, + "ora": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/ora/-/ora-0.2.3.tgz", + "integrity": "sha1-N1J9Igrc1Tw5tzVx11QVbV22V6Q=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "cli-cursor": "1.0.2", + "cli-spinners": "0.1.2", + "object-assign": "4.1.1" + }, + "dependencies": { + "cli-cursor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", + "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", + "dev": true, + "requires": { + "restore-cursor": "1.0.1" + } + }, + "onetime": { + "version": "1.1.0", + "resolved": "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", + "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", + "dev": true + }, + "restore-cursor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", + "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", + "dev": true, + "requires": { + "exit-hook": "1.1.1", + "onetime": "1.1.0" + } + } + } + }, + "os-browserify": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", + "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", + "dev": true + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true + }, + "os-locale": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz", + "integrity": "sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==", + "requires": { + "execa": "0.7.0", + "lcid": "1.0.0", + "mem": "1.1.0" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "osenv": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.4.tgz", + "integrity": "sha1-Qv5tWVPfBsgGS+bxdsPQWqqjRkQ=", + "dev": true, + "requires": { + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" + } + }, + "p-cancelable": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.4.0.tgz", + "integrity": "sha512-/AodqPe1y/GYbhSlnMjxukLGQfQIgsmjSy2CXCNB96kg4ozKvmlovuHEKICToOO/yS3LLWgrWI1dFtFfrePS1g==", + "dev": true + }, + "p-each-series": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-1.0.0.tgz", + "integrity": "sha1-kw89Et0fUOdDRFeiLNbwSsatf3E=", + "dev": true, + "requires": { + "p-reduce": "1.0.0" + } + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" + }, + "p-is-promise": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-1.1.0.tgz", + "integrity": "sha1-nJRWmJ6fZYgBewQ01WCXZ1w9oF4=", + "dev": true + }, + "p-lazy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-lazy/-/p-lazy-1.0.0.tgz", + "integrity": "sha1-7FPIAvLuOsKPFmzILQsrAt4nqDU=", + "dev": true + }, + "p-limit": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.2.0.tgz", + "integrity": "sha512-Y/OtIaXtUPr4/YpMv1pCL5L5ed0rumAaAeBSj12F+bSlMdys7i8oQF/GUJmfpTS/QoaRrS/k6pma29haJpsMng==", + "requires": { + "p-try": "1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "requires": { + "p-limit": "1.2.0" + } + }, + "p-map": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-1.2.0.tgz", + "integrity": "sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA==", + "dev": true + }, + "p-reduce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-reduce/-/p-reduce-1.0.0.tgz", + "integrity": "sha1-GMKw3ZNqRpClKfgjH1ig/bakffo=", + "dev": true + }, + "p-timeout": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-2.0.1.tgz", + "integrity": "sha512-88em58dDVB/KzPEx1X0N3LwFfYZPyDc4B6eF38M1rk9VTZMbxXXgjugz8mmwpS9Ox4BDZ+t6t3QP5+/gazweIA==", + "dev": true, + "requires": { + "p-finally": "1.0.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=" + }, + "pako": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.6.tgz", + "integrity": "sha512-lQe48YPsMJAig+yngZ87Lus+NF+3mtu7DVOBu6b/gHO1YpKwIj5AWjZ/TOS7i46HD/UixzWb1zeWDZfGZ3iYcg==", + "dev": true + }, + "parallel-transform": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.1.0.tgz", + "integrity": "sha1-1BDwZbBdojCB/NEPKIVMKb2jOwY=", + "dev": true, + "requires": { + "cyclist": "0.2.2", + "inherits": "2.0.3", + "readable-stream": "2.3.3" + } + }, + "parse-asn1": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.0.tgz", + "integrity": "sha1-N8T5t+06tlx0gXtfJICTf7+XxxI=", + "dev": true, + "requires": { + "asn1.js": "4.10.1", + "browserify-aes": "1.1.1", + "create-hash": "1.1.3", + "evp_bytestokey": "1.0.3", + "pbkdf2": "3.0.14" + } + }, + "parse-glob": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", + "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", + "dev": true, + "requires": { + "glob-base": "0.3.0", + "is-dotfile": "1.0.3", + "is-extglob": "1.0.0", + "is-glob": "2.0.1" + } + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "requires": { + "error-ex": "1.3.1" + } + }, + "parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=", + "dev": true + }, + "parse5": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-3.0.3.tgz", + "integrity": "sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA==", + "dev": true, + "requires": { + "@types/node": "9.4.6" + } + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", + "dev": true + }, + "path-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz", + "integrity": "sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo=", + "dev": true + }, + "path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", + "dev": true + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "requires": { + "pinkie-promise": "2.0.1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" + }, + "path-parse": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", + "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=" + }, + "path-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", + "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", + "requires": { + "pify": "2.3.0" + } + }, + "pbkdf2": { + "version": "3.0.14", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.14.tgz", + "integrity": "sha512-gjsZW9O34fm0R7PaLHRJmLLVfSoesxztjPjE9o6R+qtVJij90ltg1joIovN9GKrRW3t1PzhDDG3UMEMFfZ+1wA==", + "dev": true, + "requires": { + "create-hash": "1.1.3", + "create-hmac": "1.1.6", + "ripemd160": "2.0.1", + "safe-buffer": "5.1.1", + "sha.js": "2.4.11" + } + }, + "pegjs": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/pegjs/-/pegjs-0.10.0.tgz", + "integrity": "sha1-z4uvrm7d/0tafvsYUmnqr0YQ3b0=", + "dev": true + }, + "pegjs-loader": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/pegjs-loader/-/pegjs-loader-0.5.4.tgz", + "integrity": "sha512-ViH8WwUkc/N8H59zuarORrgCi7uxn+gDIq+Ydriw1GFJi/oUg2xvhsgDDujO6dAxRsxXMgqWESx6TKYIqHorqA==", + "dev": true, + "requires": { + "loader-utils": "0.2.17" + }, + "dependencies": { + "loader-utils": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz", + "integrity": "sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=", + "dev": true, + "requires": { + "big.js": "3.2.0", + "emojis-list": "2.1.0", + "json5": "0.5.1", + "object-assign": "4.1.1" + } + } + } + }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", + "dev": true + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "dev": true + }, + "phpegjs": { + "version": "1.0.0-beta7", + "resolved": "https://registry.npmjs.org/phpegjs/-/phpegjs-1.0.0-beta7.tgz", + "integrity": "sha1-uLbthQGYB//Q7+ID4AKj5e2LTZQ=", + "dev": true + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "requires": { + "pinkie": "2.0.4" + } + }, + "pkg-dir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-1.0.0.tgz", + "integrity": "sha1-ektQio1bstYp1EcFb/TpyTFM89Q=", + "requires": { + "find-up": "1.1.2" + } + }, + "pluralize": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-7.0.0.tgz", + "integrity": "sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow==", + "dev": true + }, + "pn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", + "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==", + "dev": true + }, + "popper.js": { + "version": "1.12.9", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.12.9.tgz", + "integrity": "sha1-DfvC3/lsRRuzMu3Pz6r1ZtMx1bM=" + }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", + "dev": true + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "dev": true, + "requires": { + "chalk": "1.1.3", + "js-base64": "2.4.3", + "source-map": "0.5.7", + "supports-color": "3.2.3" + }, + "dependencies": { + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", + "dev": true + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "dev": true, + "requires": { + "has-flag": "1.0.0" + } + } + } + }, + "postcss-calc": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-5.3.1.tgz", + "integrity": "sha1-d7rnypKK2FcW4v2kLyYb98HWW14=", + "dev": true, + "requires": { + "postcss": "5.2.18", + "postcss-message-helpers": "2.0.0", + "reduce-css-calc": "1.3.0" + } + }, + "postcss-colormin": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-2.2.2.tgz", + "integrity": "sha1-ZjFBfV8OkJo9fsJrJMio0eT5bks=", + "dev": true, + "requires": { + "colormin": "1.1.2", + "postcss": "5.2.18", + "postcss-value-parser": "3.3.0" + } + }, + "postcss-convert-values": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-2.6.1.tgz", + "integrity": "sha1-u9hZPFwf0uPRwyK7kl3K6Nrk1i0=", + "dev": true, + "requires": { + "postcss": "5.2.18", + "postcss-value-parser": "3.3.0" + } + }, + "postcss-discard-comments": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-2.0.4.tgz", + "integrity": "sha1-vv6J+v1bPazlzM5Rt2uBUUvgDj0=", + "dev": true, + "requires": { + "postcss": "5.2.18" + } + }, + "postcss-discard-duplicates": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-2.1.0.tgz", + "integrity": "sha1-uavye4isGIFYpesSq8riAmO5GTI=", + "dev": true, + "requires": { + "postcss": "5.2.18" + } + }, + "postcss-discard-empty": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-2.1.0.tgz", + "integrity": "sha1-0rS9nVztXr2Nyt52QMfXzX9PkrU=", + "dev": true, + "requires": { + "postcss": "5.2.18" + } + }, + "postcss-discard-overridden": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-0.1.1.tgz", + "integrity": "sha1-ix6vVU9ob7KIzYdMVWZ7CqNmjVg=", + "dev": true, + "requires": { + "postcss": "5.2.18" + } + }, + "postcss-discard-unused": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/postcss-discard-unused/-/postcss-discard-unused-2.2.3.tgz", + "integrity": "sha1-vOMLLMWR/8Y0Mitfs0ZLbZNPRDM=", + "dev": true, + "requires": { + "postcss": "5.2.18", + "uniqs": "2.0.0" + } + }, + "postcss-filter-plugins": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/postcss-filter-plugins/-/postcss-filter-plugins-2.0.2.tgz", + "integrity": "sha1-bYWGJTTXNaxCDkqFgG4fXUKG2Ew=", + "dev": true, + "requires": { + "postcss": "5.2.18", + "uniqid": "4.1.1" + } + }, + "postcss-load-config": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-1.2.0.tgz", + "integrity": "sha1-U56a/J3chiASHr+djDZz4M5Q0oo=", + "dev": true, + "requires": { + "cosmiconfig": "2.2.2", + "object-assign": "4.1.1", + "postcss-load-options": "1.2.0", + "postcss-load-plugins": "2.3.0" + } + }, + "postcss-load-options": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postcss-load-options/-/postcss-load-options-1.2.0.tgz", + "integrity": "sha1-sJixVZ3awt8EvAuzdfmaXP4rbYw=", + "dev": true, + "requires": { + "cosmiconfig": "2.2.2", + "object-assign": "4.1.1" + } + }, + "postcss-load-plugins": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/postcss-load-plugins/-/postcss-load-plugins-2.3.0.tgz", + "integrity": "sha1-dFdoEWWZrKLwCfrUJrABdQSdjZI=", + "dev": true, + "requires": { + "cosmiconfig": "2.2.2", + "object-assign": "4.1.1" + } + }, + "postcss-loader": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-2.1.3.tgz", + "integrity": "sha512-RuBcNE8rjCkIB0IsbmkGFRmQJTeQJfCI88E0VTarPNTvaNSv9OFv1DvTwgtAN/qlzyiELsmmmtX/tEzKp/cdug==", + "dev": true, + "requires": { + "loader-utils": "1.1.0", + "postcss": "6.0.21", + "postcss-load-config": "1.2.0", + "schema-utils": "0.4.5" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + }, + "chalk": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.2.tgz", + "integrity": "sha512-ZM4j2/ld/YZDc3Ma8PgN7gyAk+kHMMMyzLNryCPGhWrsfAuDVeuid5bpRFTDgMH9JBK2lA4dyyAkkZYF/WcqDQ==", + "dev": true, + "requires": { + "ansi-styles": "3.2.1", + "escape-string-regexp": "1.0.5", + "supports-color": "5.3.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "postcss": { + "version": "6.0.21", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.21.tgz", + "integrity": "sha512-y/bKfbQz2Nn/QBC08bwvYUxEFOVGfPIUOTsJ2CK5inzlXW9SdYR1x4pEsG9blRAF/PX+wRNdOah+gx/hv4q7dw==", + "dev": true, + "requires": { + "chalk": "2.3.2", + "source-map": "0.6.1", + "supports-color": "5.3.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.3.0.tgz", + "integrity": "sha512-0aP01LLIskjKs3lq52EC0aGBAJhLq7B2Rd8HC/DR/PtNNpcLilNmHC12O+hu0usQpo7wtHNRqtrhBwtDb0+dNg==", + "dev": true, + "requires": { + "has-flag": "3.0.0" + } + } + } + }, + "postcss-merge-idents": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/postcss-merge-idents/-/postcss-merge-idents-2.1.7.tgz", + "integrity": "sha1-TFUwMTwI4dWzu/PSu8dH4njuonA=", + "dev": true, + "requires": { + "has": "1.0.1", + "postcss": "5.2.18", + "postcss-value-parser": "3.3.0" + } + }, + "postcss-merge-longhand": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-2.0.2.tgz", + "integrity": "sha1-I9kM0Sewp3mUkVMyc5A0oaTz1lg=", + "dev": true, + "requires": { + "postcss": "5.2.18" + } + }, + "postcss-merge-rules": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-2.1.2.tgz", + "integrity": "sha1-0d9d+qexrMO+VT8OnhDofGG19yE=", + "dev": true, + "requires": { + "browserslist": "1.7.7", + "caniuse-api": "1.6.1", + "postcss": "5.2.18", + "postcss-selector-parser": "2.2.3", + "vendors": "1.0.1" + }, + "dependencies": { + "browserslist": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-1.7.7.tgz", + "integrity": "sha1-C9dnBCWL6CmyOYu1Dkti0aFmsLk=", + "dev": true, + "requires": { + "caniuse-db": "1.0.30000804", + "electron-to-chromium": "1.3.33" + } + } + } + }, + "postcss-message-helpers": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-message-helpers/-/postcss-message-helpers-2.0.0.tgz", + "integrity": "sha1-pPL0+rbk/gAvCu0ABHjN9S+bpg4=", + "dev": true + }, + "postcss-minify-font-values": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-1.0.5.tgz", + "integrity": "sha1-S1jttWZB66fIR0qzUmyv17vey2k=", + "dev": true, + "requires": { + "object-assign": "4.1.1", + "postcss": "5.2.18", + "postcss-value-parser": "3.3.0" + } + }, + "postcss-minify-gradients": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-1.0.5.tgz", + "integrity": "sha1-Xb2hE3NwP4PPtKPqOIHY11/15uE=", + "dev": true, + "requires": { + "postcss": "5.2.18", + "postcss-value-parser": "3.3.0" + } + }, + "postcss-minify-params": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-1.2.2.tgz", + "integrity": "sha1-rSzgcTc7lDs9kwo/pZo1jCjW8fM=", + "dev": true, + "requires": { + "alphanum-sort": "1.0.2", + "postcss": "5.2.18", + "postcss-value-parser": "3.3.0", + "uniqs": "2.0.0" + } + }, + "postcss-minify-selectors": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-2.1.1.tgz", + "integrity": "sha1-ssapjAByz5G5MtGkllCBFDEXNb8=", + "dev": true, + "requires": { + "alphanum-sort": "1.0.2", + "has": "1.0.1", + "postcss": "5.2.18", + "postcss-selector-parser": "2.2.3" + } + }, + "postcss-normalize-charset": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-1.1.1.tgz", + "integrity": "sha1-757nEhLX/nWceO0WL2HtYrXLk/E=", + "dev": true, + "requires": { + "postcss": "5.2.18" + } + }, + "postcss-normalize-url": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-3.0.8.tgz", + "integrity": "sha1-EI90s/L82viRov+j6kWSJ5/HgiI=", + "dev": true, + "requires": { + "is-absolute-url": "2.1.0", + "normalize-url": "1.9.1", + "postcss": "5.2.18", + "postcss-value-parser": "3.3.0" + } + }, + "postcss-ordered-values": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-2.2.3.tgz", + "integrity": "sha1-7sbCpntsQSqNsgQud/6NpD+VwR0=", + "dev": true, + "requires": { + "postcss": "5.2.18", + "postcss-value-parser": "3.3.0" + } + }, + "postcss-reduce-idents": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/postcss-reduce-idents/-/postcss-reduce-idents-2.4.0.tgz", + "integrity": "sha1-wsbSDMlYKE9qv75j92Cb9AkFmtM=", + "dev": true, + "requires": { + "postcss": "5.2.18", + "postcss-value-parser": "3.3.0" + } + }, + "postcss-reduce-initial": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-1.0.1.tgz", + "integrity": "sha1-aPgGlfBF0IJjqHmtJA343WT2ROo=", + "dev": true, + "requires": { + "postcss": "5.2.18" + } + }, + "postcss-reduce-transforms": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-1.0.4.tgz", + "integrity": "sha1-/3b02CEkN7McKYpC0uFEQCV3GuE=", + "dev": true, + "requires": { + "has": "1.0.1", + "postcss": "5.2.18", + "postcss-value-parser": "3.3.0" + } + }, + "postcss-selector-parser": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-2.2.3.tgz", + "integrity": "sha1-+UN3iGBsPJrO4W/+jYsWKX8nu5A=", + "dev": true, + "requires": { + "flatten": "1.0.2", + "indexes-of": "1.0.1", + "uniq": "1.0.1" + } + }, + "postcss-svgo": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-2.1.6.tgz", + "integrity": "sha1-tt8YqmE7Zm4TPwittSGcJoSsEI0=", + "dev": true, + "requires": { + "is-svg": "2.1.0", + "postcss": "5.2.18", + "postcss-value-parser": "3.3.0", + "svgo": "0.7.2" + } + }, + "postcss-unique-selectors": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-2.0.2.tgz", + "integrity": "sha1-mB1X0p3csz57Hf4f1DuGSfkzyh0=", + "dev": true, + "requires": { + "alphanum-sort": "1.0.2", + "postcss": "5.2.18", + "uniqs": "2.0.0" + } + }, + "postcss-value-parser": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz", + "integrity": "sha1-h/OPnxj3dKSrTIojL1xc6IcqnRU=", + "dev": true + }, + "postcss-zindex": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/postcss-zindex/-/postcss-zindex-2.2.0.tgz", + "integrity": "sha1-0hCd3AVbka9n/EyzsCWUZjnSryI=", + "dev": true, + "requires": { + "has": "1.0.1", + "postcss": "5.2.18", + "uniqs": "2.0.0" + } + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "prepend-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", + "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", + "dev": true + }, + "preserve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", + "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=", + "dev": true + }, + "prettier": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.11.1.tgz", + "integrity": "sha512-T/KD65Ot0PB97xTrG8afQ46x3oiVhnfGjGESSI9NWYcG92+OUPZKkwHqGWXH2t9jK1crnQjubECW0FuOth+hxw==", + "dev": true + }, + "pretty-bytes": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-4.0.2.tgz", + "integrity": "sha1-sr+C5zUNZcbDOqlaqlpPYyf2HNk=", + "dev": true + }, + "pretty-format": { + "version": "22.4.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-22.4.0.tgz", + "integrity": "sha512-pvCxP2iODIIk9adXlo4S3GRj0BrJiil68kByAa1PrgG97c1tClh9dLMgp3Z6cHFZrclaABt0UH8PIhwHuFLqYA==", + "dev": true, + "requires": { + "ansi-regex": "3.0.0", + "ansi-styles": "3.2.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 + }, + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + } + } + }, + "private": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", + "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==", + "dev": true + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", + "dev": true + }, + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" + }, + "progress": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.0.tgz", + "integrity": "sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8=", + "dev": true + }, + "promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "requires": { + "asap": "2.0.6" + } + }, + "promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", + "dev": true + }, + "prop-types": { + "version": "15.5.10", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.5.10.tgz", + "integrity": "sha1-J5ffwxJhguOpXj37suiT3ddFYVQ=", + "requires": { + "fbjs": "0.8.16", + "loose-envify": "1.3.1" + } + }, + "proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=" + }, + "proxy-from-env": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz", + "integrity": "sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4=", + "dev": true + }, + "prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", + "dev": true + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" + }, + "public-encrypt": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.0.tgz", + "integrity": "sha1-OfaZ86RlYN1eusvKaTyvfGXBjMY=", + "dev": true, + "requires": { + "bn.js": "4.11.8", + "browserify-rsa": "4.0.1", + "create-hash": "1.1.3", + "parse-asn1": "5.1.0", + "randombytes": "2.0.6" + } + }, + "pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "dev": true, + "requires": { + "end-of-stream": "1.4.1", + "once": "1.4.0" + } + }, + "pumpify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.4.0.tgz", + "integrity": "sha512-2kmNR9ry+Pf45opRVirpNuIFotsxUGLaYqxIwuR77AYrYRMuFCz9eryHBS52L360O+NcR383CL4QYlMKPq4zYA==", + "dev": true, + "requires": { + "duplexify": "3.5.4", + "inherits": "2.0.3", + "pump": "2.0.1" + } + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + }, + "puppeteer": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-1.2.0.tgz", + "integrity": "sha512-4sY/6mB7+kNPGAzPGKq65tH0VG3ohUEkXHuOReB9K/tw3m1TqifYmxnMR/uDeci/UPwyk5K1gWYh8rw0U0Zscw==", + "dev": true, + "requires": { + "debug": "2.6.9", + "extract-zip": "1.6.6", + "https-proxy-agent": "2.2.0", + "mime": "1.6.0", + "progress": "2.0.0", + "proxy-from-env": "1.0.0", + "rimraf": "2.6.2", + "ws": "3.3.3" + }, + "dependencies": { + "ws": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz", + "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==", + "dev": true, + "requires": { + "async-limiter": "1.0.0", + "safe-buffer": "5.1.1", + "ultron": "1.1.1" + } + } + } + }, + "q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", + "dev": true + }, + "qs": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", + "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==", + "dev": true + }, + "query-string": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", + "integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=", + "dev": true, + "requires": { + "object-assign": "4.1.1", + "strict-uri-encode": "1.1.0" + } + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "dev": true + }, + "querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "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=" + }, + "raf": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.0.tgz", + "integrity": "sha512-pDP/NMRAXoTfrhCfyfSEwJAKLaxBU9eApMeBPB1TkDouZmvPerIClV8lTAd+uF8ZiTaVl69e1FCxQrAd/VTjGw==", + "dev": true, + "requires": { + "performance-now": "2.1.0" + } + }, + "railroad-diagrams": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz", + "integrity": "sha1-635iZ1SN3t+4mcG5Dlc3RVnN234=", + "dev": true + }, + "randexp": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz", + "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==", + "dev": true, + "requires": { + "discontinuous-range": "1.0.0", + "ret": "0.1.15" + } + }, + "randomatic": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz", + "integrity": "sha512-D5JUjPyJbaJDkuAazpVnSfVkLlpeO3wDlPROTMLGKG1zMFNFRgrciKo1ltz/AzNTkqE0HzDx655QOL51N06how==", + "dev": true, + "requires": { + "is-number": "3.0.0", + "kind-of": "4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "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.6" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "randombytes": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.6.tgz", + "integrity": "sha512-CIQ5OFxf4Jou6uOKe9t1AOgqpeU5fd70A8NPdHSGeYXqXsPe6peOwI0cUl88RWZ6sP1vPMV3avd/R6cZ5/sP1A==", + "dev": true, + "requires": { + "safe-buffer": "5.1.1" + } + }, + "randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "dev": true, + "requires": { + "randombytes": "2.0.6", + "safe-buffer": "5.1.1" + } + }, + "raw-loader": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-0.5.1.tgz", + "integrity": "sha1-DD0L6u2KAclm2Xh793goElKpeao=", + "dev": true + }, + "re-resizable": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/re-resizable/-/re-resizable-4.0.3.tgz", + "integrity": "sha512-6YpsC4JFT7zVG8/8gIXxdnrlHdz64/H7dpjHgXNCy5kwy2DIG1dVbdgASNUTwy4AaHgI8rvjJJzr2BxZAVu/3Q==" + }, + "react": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/react/-/react-16.3.0.tgz", + "integrity": "sha512-Qh35tNbwY8SLFELkN3PCLO16EARV+lgcmNkQnoZXfzAF1ASRpeucZYUwBlBzsRAzTb7KyfBaLQ4/K/DLC6MYeA==", + "requires": { + "fbjs": "0.8.16", + "loose-envify": "1.3.1", + "object-assign": "4.1.1", + "prop-types": "15.6.1" + }, + "dependencies": { + "prop-types": { + "version": "15.6.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.1.tgz", + "integrity": "sha512-4ec7bY1Y66LymSUOH/zARVYObB23AT2h8cf6e/O6ZALB/N0sqZFEx7rq6EYPX2MkOdKORuooI/H5k9TlR4q7kQ==", + "requires": { + "fbjs": "0.8.16", + "loose-envify": "1.3.1", + "object-assign": "4.1.1" + } + } + } + }, + "react-autosize-textarea": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/react-autosize-textarea/-/react-autosize-textarea-3.0.2.tgz", + "integrity": "sha1-K2hApp9xOHGavOpaQp7PcwF2jAc=", + "requires": { + "autosize": "4.0.1", + "line-height": "0.3.1", + "prop-types": "15.5.10" + } + }, + "react-click-outside": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/react-click-outside/-/react-click-outside-2.3.1.tgz", + "integrity": "sha1-MYc3698IGko7zUaCVmNnTL6YNus=", + "requires": { + "hoist-non-react-statics": "1.2.0" + } + }, + "react-color": { + "version": "2.13.4", + "resolved": "https://registry.npmjs.org/react-color/-/react-color-2.13.4.tgz", + "integrity": "sha512-rNJTTxMPTImI1NpFaKLggDIvHgKOYRXj0krVh8c+Mo1YNsrLko8O94yiFqqdnSQgtIPteiAcGEJgBo9V5+uqaw==", + "requires": { + "lodash": "4.17.5", + "material-colors": "1.2.5", + "prop-types": "15.5.10", + "reactcss": "1.2.3", + "tinycolor2": "1.4.1" + } + }, + "react-datepicker": { + "version": "0.61.0", + "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-0.61.0.tgz", + "integrity": "sha512-FJnqJCbYaU1ca7Jn9LaJ7iKTYHKAskVkVHCsOBDQd8vJZrESLAu3rtPbj3T6IB++dQO7qb0IlcCXtmC0geIAGA==", + "requires": { + "classnames": "2.2.5", + "eslint-plugin-import": "2.8.0", + "eslint-plugin-node": "5.2.1", + "moment": "2.20.1", + "prop-types": "15.6.0", + "react-onclickoutside": "6.7.1", + "react-popper": "0.7.5" + }, + "dependencies": { + "eslint-plugin-node": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-5.2.1.tgz", + "integrity": "sha512-xhPXrh0Vl/b7870uEbaumb2Q+LxaEcOQ3kS1jtIXanBAwpMre1l5q/l2l/hESYJGEFKuI78bp6Uw50hlpr7B+g==", + "requires": { + "ignore": "3.3.7", + "minimatch": "3.0.4", + "resolve": "1.5.0", + "semver": "5.3.0" + } + }, + "moment": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.20.1.tgz", + "integrity": "sha512-Yh9y73JRljxW5QxN08Fner68eFLxM5ynNOAw2LbIB1YAGeQzZT8QFSUvkAz609Zf+IHhhaUxqZK8dG3W/+HEvg==" + }, + "prop-types": { + "version": "15.6.0", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.0.tgz", + "integrity": "sha1-zq8IMCL8RrSjX2nhPvda7Q1jmFY=", + "requires": { + "fbjs": "0.8.16", + "loose-envify": "1.3.1", + "object-assign": "4.1.1" + } + } + } + }, + "react-dom": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.3.0.tgz", + "integrity": "sha512-xT/FxawAurL6AV8YtAP7LkdDJFFX2vvv17AqFLQRF81ZtWLXkV/0dcAaiFIy0lmoQEFT931TU9aaH+5dBUxTcw==", + "requires": { + "fbjs": "0.8.16", + "loose-envify": "1.3.1", + "object-assign": "4.1.1", + "prop-types": "15.6.1" + }, + "dependencies": { + "prop-types": { + "version": "15.6.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.1.tgz", + "integrity": "sha512-4ec7bY1Y66LymSUOH/zARVYObB23AT2h8cf6e/O6ZALB/N0sqZFEx7rq6EYPX2MkOdKORuooI/H5k9TlR4q7kQ==", + "requires": { + "fbjs": "0.8.16", + "loose-envify": "1.3.1", + "object-assign": "4.1.1" + } + } + } + }, + "react-is": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.3.0.tgz", + "integrity": "sha512-YOo+BNK2z8LiDxh2viaOklPqhwrMMsNPWnXTseOqJa8/ob8mv9aD9Z5FqqQnKNbIerBm+DoIwjAAAKcpdDo1/w==", + "dev": true + }, + "react-onclickoutside": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.7.1.tgz", + "integrity": "sha512-p84kBqGaMoa7VYT0vZ/aOYRfJB+gw34yjpda1Z5KeLflg70HipZOT+MXQenEhdkPAABuE2Astq4zEPdMqUQxcg==" + }, + "react-popper": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-0.7.5.tgz", + "integrity": "sha512-ya9dhhGCf74JTOB2uyksEHhIGw7w9tNZRUJF73lEq2h4H5JT6MBa4PdT4G+sx6fZwq+xKZAL/sVNAIuojPn7Dg==", + "requires": { + "popper.js": "1.12.9", + "prop-types": "15.5.10" + } + }, + "react-reconciler": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.7.0.tgz", + "integrity": "sha512-50JwZ3yNyMS8fchN+jjWEJOH3Oze7UmhxeoJLn2j6f3NjpfCRbcmih83XTWmzqtar/ivd5f7tvQhvvhism2fgg==", + "dev": true, + "requires": { + "fbjs": "0.8.16", + "loose-envify": "1.3.1", + "object-assign": "4.1.1", + "prop-types": "15.6.0" + }, + "dependencies": { + "prop-types": { + "version": "15.6.0", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.0.tgz", + "integrity": "sha1-zq8IMCL8RrSjX2nhPvda7Q1jmFY=", + "dev": true, + "requires": { + "fbjs": "0.8.16", + "loose-envify": "1.3.1", + "object-assign": "4.1.1" + } + } + } + }, + "react-redux": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-5.0.6.tgz", + "integrity": "sha512-8taaaGu+J7PMJQDJrk/xiWEYQmdo3mkXw6wPr3K3LxvXis3Fymiq7c13S+Tpls/AyNUAsoONkU81AP0RA6y6Vw==", + "requires": { + "hoist-non-react-statics": "2.3.1", + "invariant": "2.2.2", + "lodash": "4.17.5", + "lodash-es": "4.17.5", + "loose-envify": "1.3.1", + "prop-types": "15.5.10" + }, + "dependencies": { + "hoist-non-react-statics": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.3.1.tgz", + "integrity": "sha1-ND24TGAYxlB3iJgkATWhQg7iLOA=" + } + } + }, + "react-test-renderer": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.3.0.tgz", + "integrity": "sha512-7FAfgIT8Kvew36b2VFXnhMxjKkwAnicD1dyopq7Ot19WAhfJkyhYNwPzM1AnCfYccvEhO0x4FbqJETyMGQwgIg==", + "dev": true, + "requires": { + "fbjs": "0.8.16", + "object-assign": "4.1.1", + "prop-types": "15.6.1", + "react-is": "16.3.0" + }, + "dependencies": { + "prop-types": { + "version": "15.6.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.1.tgz", + "integrity": "sha512-4ec7bY1Y66LymSUOH/zARVYObB23AT2h8cf6e/O6ZALB/N0sqZFEx7rq6EYPX2MkOdKORuooI/H5k9TlR4q7kQ==", + "dev": true, + "requires": { + "fbjs": "0.8.16", + "loose-envify": "1.3.1", + "object-assign": "4.1.1" + } + } + } + }, + "reactcss": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz", + "integrity": "sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A==", + "requires": { + "lodash": "4.17.5" + } + }, + "read-chunk": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/read-chunk/-/read-chunk-2.1.0.tgz", + "integrity": "sha1-agTAkoAF7Z1C4aasVgDhnLx/9lU=", + "dev": true, + "requires": { + "pify": "3.0.0", + "safe-buffer": "5.1.1" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } + } + }, + "read-pkg": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", + "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", + "requires": { + "load-json-file": "2.0.0", + "normalize-package-data": "2.4.0", + "path-type": "2.0.0" + } + }, + "read-pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", + "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", + "requires": { + "find-up": "2.1.0", + "read-pkg": "2.0.0" + }, + "dependencies": { + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "requires": { + "locate-path": "2.0.0" + } + } + } + }, + "readable-stream": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", + "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "safe-buffer": "5.1.1", + "string_decoder": "1.0.3", + "util-deprecate": "1.0.2" + } + }, + "readdirp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz", + "integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "minimatch": "3.0.4", + "readable-stream": "2.3.3", + "set-immediate-shim": "1.0.1" + } + }, + "realpath-native": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/realpath-native/-/realpath-native-1.0.0.tgz", + "integrity": "sha512-XJtlRJ9jf0E1H1SLeJyQ9PGzQD7S65h1pRXEcAeK48doKOnKxcgPeNohJvD5u/2sI9J1oke6E8bZHS/fmW1UiQ==", + "dev": true, + "requires": { + "util.promisify": "1.0.0" + } + }, + "recast": { + "version": "0.14.7", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.14.7.tgz", + "integrity": "sha512-/nwm9pkrcWagN40JeJhkPaRxiHXBRkXyRh/hgU088Z/v+qCy+zIHHY6bC6o7NaKAxPqtE6nD8zBH1LfU0/Wx6A==", + "dev": true, + "requires": { + "ast-types": "0.11.3", + "esprima": "4.0.0", + "private": "0.1.8", + "source-map": "0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "dev": true, + "requires": { + "resolve": "1.5.0" + } + }, + "redent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", + "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", + "dev": true, + "requires": { + "indent-string": "2.1.0", + "strip-indent": "1.0.1" + } + }, + "reduce-css-calc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz", + "integrity": "sha1-dHyRTgSWFKTJz7umKYca0dKSdxY=", + "dev": true, + "requires": { + "balanced-match": "0.4.2", + "math-expression-evaluator": "1.2.17", + "reduce-function-call": "1.0.2" + }, + "dependencies": { + "balanced-match": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", + "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=", + "dev": true + } + } + }, + "reduce-function-call": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/reduce-function-call/-/reduce-function-call-1.0.2.tgz", + "integrity": "sha1-WiAL+S4ON3UXUv5FsKszD9S2vpk=", + "dev": true, + "requires": { + "balanced-match": "0.4.2" + }, + "dependencies": { + "balanced-match": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", + "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=", + "dev": true + } + } + }, + "redux": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/redux/-/redux-3.7.2.tgz", + "integrity": "sha512-pNqnf9q1hI5HHZRBkj3bAngGZW/JMCmexDlOxw4XagXY2o1327nHH54LoTjiPJ0gizoqPDRqWyX/00g0hD6w+A==", + "requires": { + "lodash": "4.17.5", + "lodash-es": "4.17.5", + "loose-envify": "1.3.1", + "symbol-observable": "1.2.0" + } + }, + "redux-multi": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/redux-multi/-/redux-multi-0.1.12.tgz", + "integrity": "sha1-KOH+XklnLLxb2KB/Cyrq8O+DVcI=" + }, + "redux-optimist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/redux-optimist/-/redux-optimist-1.0.0.tgz", + "integrity": "sha512-AG1v8o6UZcGXTEH2jVcWG6KD+gEix+Cj9JXAAzln9MPkauSVd98H7N7EOOyT/v4c9N1mJB4sm1zfspGlLDkUEw==" + }, + "refx": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/refx/-/refx-3.0.0.tgz", + "integrity": "sha512-qmd73YvYiVWfKPECtE90ujmPwwtAnmtEOkBKgfNEuqJ4trTeKbqFV2UY878yFvHBvU7BBu4/w/Q8pk/t0zDpYA==" + }, + "regenerate": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.3.3.tgz", + "integrity": "sha512-jVpo1GadrDAK59t/0jRx5VxYWQEDkkEKi6+HjE3joFVLfDOh9Xrdh0dF1eSq+BI/SwvTQ44gSscJ8N5zYL61sg==", + "dev": true + }, + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", + "dev": true + }, + "regenerator-transform": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.10.1.tgz", + "integrity": "sha512-PJepbvDbuK1xgIgnau7Y90cwaAmO/LCLMI2mPvaXq2heGMR3aWW5/BQvYrhJ8jgmQjXewXvBjzfqKcVOmhjZ6Q==", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "private": "0.1.8" + } + }, + "regex-cache": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", + "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", + "dev": true, + "requires": { + "is-equal-shallow": "0.1.3" + } + }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, + "requires": { + "extend-shallow": "3.0.2", + "safe-regex": "1.1.0" + } + }, + "regexpu-core": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-2.0.0.tgz", + "integrity": "sha1-SdA4g3uNz4v6W5pCE5k45uoq4kA=", + "dev": true, + "requires": { + "regenerate": "1.3.3", + "regjsgen": "0.2.0", + "regjsparser": "0.1.5" + } + }, + "regjsgen": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", + "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=", + "dev": true + }, + "regjsparser": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", + "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=", + "dev": true, + "requires": { + "jsesc": "0.5.0" + } + }, + "rememo": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/rememo/-/rememo-2.4.0.tgz", + "integrity": "sha512-4rqlLATPcha9lfdvylUWqSbceiTlYiBJvEJAyUiT/68cYPlNG1zXf7ABeve7s4YPrT6o3Q6zfN6n38ecAL71lw==", + "requires": { + "shallow-equal": "1.0.0" + } + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", + "dev": true + }, + "repeat-element": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", + "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true + }, + "repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "dev": true, + "requires": { + "is-finite": "1.0.2" + } + }, + "replace-ext": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-0.0.1.tgz", + "integrity": "sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ=", + "dev": true + }, + "request": { + "version": "2.83.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.83.0.tgz", + "integrity": "sha512-lR3gD69osqm6EYLk9wB/G1W/laGWjzH90t1vEa2xuxHD5KUrSzp9pUSfTm+YC5Nxt2T8nMPEvKlhbQayU7bgFw==", + "dev": true, + "requires": { + "aws-sign2": "0.7.0", + "aws4": "1.6.0", + "caseless": "0.12.0", + "combined-stream": "1.0.5", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.3.1", + "har-validator": "5.0.3", + "hawk": "6.0.2", + "http-signature": "1.2.0", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.17", + "oauth-sign": "0.8.2", + "performance-now": "2.1.0", + "qs": "6.5.1", + "safe-buffer": "5.1.1", + "stringstream": "0.0.5", + "tough-cookie": "2.3.3", + "tunnel-agent": "0.6.0", + "uuid": "3.1.0" + } + }, + "request-promise-core": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.1.tgz", + "integrity": "sha1-Pu4AssWqgyOc+wTFcA2jb4HNCLY=", + "dev": true, + "requires": { + "lodash": "4.17.5" + } + }, + "request-promise-native": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.5.tgz", + "integrity": "sha1-UoF3D2jgyXGeUWP9P6tIIhX0/aU=", + "dev": true, + "requires": { + "request-promise-core": "1.1.1", + "stealthy-require": "1.1.1", + "tough-cookie": "2.3.3" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" + }, + "require-from-string": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-1.2.1.tgz", + "integrity": "sha1-UpyczvJzgK3+yaL5ZbZJu+5jZBg=", + "dev": true + }, + "require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=" + }, + "require-uncached": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", + "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", + "dev": true, + "requires": { + "caller-path": "0.1.0", + "resolve-from": "1.0.1" + }, + "dependencies": { + "resolve-from": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", + "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", + "dev": true + } + } + }, + "requireindex": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz", + "integrity": "sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==" + }, + "resolve": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.5.0.tgz", + "integrity": "sha512-hgoSGrc3pjzAPHNBg+KnFcK2HwlHTs/YrAGUr6qgTVUZmXv1UEXXl0bZNBKMA9fud6lRYFdPGz0xXxycPzmmiw==", + "requires": { + "path-parse": "1.0.5" + } + }, + "resolve-cwd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", + "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", + "dev": true, + "requires": { + "resolve-from": "3.0.0" + } + }, + "resolve-dir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", + "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=", + "dev": true, + "requires": { + "expand-tilde": "2.0.2", + "global-modules": "1.0.0" + } + }, + "resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", + "dev": true + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", + "dev": true + }, + "responselike": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", + "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", + "dev": true, + "requires": { + "lowercase-keys": "1.0.1" + } + }, + "restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "dev": true, + "requires": { + "onetime": "2.0.1", + "signal-exit": "3.0.2" + } + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true + }, + "right-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", + "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", + "dev": true, + "optional": true, + "requires": { + "align-text": "0.1.4" + } + }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "dev": true, + "requires": { + "glob": "7.1.2" + } + }, + "ripemd160": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.1.tgz", + "integrity": "sha1-D0WEKVxTo2KK9+bXmsohzlfRxuc=", + "dev": true, + "requires": { + "hash-base": "2.0.2", + "inherits": "2.0.3" + } + }, + "rst-selector-parser": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz", + "integrity": "sha1-gbIw6i/MYGbInjRy3nlChdmwPZE=", + "dev": true, + "requires": { + "lodash.flattendeep": "4.4.0", + "nearley": "2.11.1" + } + }, + "rtlcss": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-2.2.1.tgz", + "integrity": "sha512-JjQ5DlrmwiItAjlmhoxrJq5ihgZcE0wMFxt7S17bIrt4Lw0WwKKFk+viRhvodB/0falyG/5fiO043ZDh6/aqTw==", + "dev": true, + "requires": { + "chalk": "2.3.0", + "findup": "0.1.5", + "mkdirp": "0.5.1", + "postcss": "6.0.17", + "strip-json-comments": "2.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + }, + "chalk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", + "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "4.5.0" + } + }, + "postcss": { + "version": "6.0.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.17.tgz", + "integrity": "sha512-Bl1nybsSzWYbP8O4gAVD8JIjZIul9hLNOPTGBIlVmZNUnNAGL+W0cpYWzVwfImZOwumct4c1SDvSbncVWKtXUw==", + "dev": true, + "requires": { + "chalk": "2.3.0", + "source-map": "0.6.1", + "supports-color": "5.1.0" + }, + "dependencies": { + "supports-color": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.1.0.tgz", + "integrity": "sha512-Ry0AwkoKjDpVKK4sV4h6o3UJmNRbjYm2uXhwfj3J56lMVdvnUNqzQVRztOOMGQ++w1K/TjNDFvpJk0F/LoeBCQ==", + "dev": true, + "requires": { + "has-flag": "2.0.0" + } + } + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", + "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", + "dev": true, + "requires": { + "has-flag": "2.0.0" + } + } + } + }, + "run-async": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", + "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", + "dev": true, + "requires": { + "is-promise": "2.1.0" + } + }, + "run-parallel": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.6.tgz", + "integrity": "sha1-KQA8miFj4B4tLfyQV18sbB1hoDk=", + "dev": true + }, + "run-queue": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", + "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", + "dev": true, + "requires": { + "aproba": "1.2.0" + } + }, + "rx": { + "version": "2.3.24", + "resolved": "https://registry.npmjs.org/rx/-/rx-2.3.24.tgz", + "integrity": "sha1-FPlQpCF9fjXapxu8vljv9o6ksrc=", + "dev": true + }, + "rx-lite": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-4.0.8.tgz", + "integrity": "sha1-Cx4Rr4vESDbwSmQH6S2kJGe3lEQ=", + "dev": true + }, + "rx-lite-aggregates": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz", + "integrity": "sha1-dTuHqJoRyVRnxKwWJsTvxOBcZ74=", + "dev": true, + "requires": { + "rx-lite": "4.0.8" + } + }, + "rxjs": { + "version": "5.5.8", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.8.tgz", + "integrity": "sha512-Bz7qou7VAIoGiglJZbzbXa4vpX5BmTTN2Dj/se6+SwADtw4SihqBIiEa7VmTXJ8pynvq0iFr5Gx9VLyye1rIxQ==", + "dev": true, + "requires": { + "symbol-observable": "1.0.1" + }, + "dependencies": { + "symbol-observable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.1.tgz", + "integrity": "sha1-g0D8RwLDEi310iKI+IKD9RPT/dQ=", + "dev": true + } + } + }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "dev": true, + "requires": { + "ret": "0.1.15" + } + }, + "sane": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/sane/-/sane-2.4.1.tgz", + "integrity": "sha512-fW9svvNd81XzHDZyis9/tEY1bZikDGryy8Hi1BErPyNPYv47CdLseUN+tI5FBHWXEENRtj1SWtX/jBnggLaP0w==", + "dev": true, + "requires": { + "anymatch": "1.3.2", + "exec-sh": "0.2.1", + "fb-watchman": "2.0.0", + "fsevents": "1.1.3", + "minimatch": "3.0.4", + "minimist": "1.2.0", + "walker": "1.0.7", + "watch": "0.18.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "sass-graph": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.4.tgz", + "integrity": "sha1-E/vWPNHK8JCLn9k0dq1DpR0eC0k=", + "dev": true, + "requires": { + "glob": "7.1.2", + "lodash": "4.17.5", + "scss-tokenizer": "0.2.3", + "yargs": "7.1.0" + }, + "dependencies": { + "camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", + "dev": true + }, + "load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "parse-json": "2.2.0", + "pify": "2.3.0", + "pinkie-promise": "2.0.1", + "strip-bom": "2.0.0" + } + }, + "os-locale": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", + "dev": true, + "requires": { + "lcid": "1.0.0" + } + }, + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" + } + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "dev": true, + "requires": { + "load-json-file": "1.1.0", + "normalize-package-data": "2.4.0", + "path-type": "1.1.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "dev": true, + "requires": { + "find-up": "1.1.2", + "read-pkg": "1.1.0" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "requires": { + "is-utf8": "0.2.1" + } + }, + "which-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", + "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=", + "dev": true + }, + "yargs": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz", + "integrity": "sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=", + "dev": true, + "requires": { + "camelcase": "3.0.0", + "cliui": "3.2.0", + "decamelize": "1.2.0", + "get-caller-file": "1.0.2", + "os-locale": "1.4.0", + "read-pkg-up": "1.0.1", + "require-directory": "2.1.1", + "require-main-filename": "1.0.1", + "set-blocking": "2.0.0", + "string-width": "1.0.2", + "which-module": "1.0.0", + "y18n": "3.2.1", + "yargs-parser": "5.0.0" + } + }, + "yargs-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0.tgz", + "integrity": "sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo=", + "dev": true, + "requires": { + "camelcase": "3.0.0" + } + } + } + }, + "sass-loader": { + "version": "6.0.7", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-6.0.7.tgz", + "integrity": "sha512-JoiyD00Yo1o61OJsoP2s2kb19L1/Y2p3QFcCdWdF6oomBGKVYuZyqHWemRBfQ2uGYsk+CH3eCguXNfpjzlcpaA==", + "dev": true, + "requires": { + "clone-deep": "2.0.2", + "loader-utils": "1.1.0", + "lodash.tail": "4.1.1", + "neo-async": "2.5.0", + "pify": "3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } + } + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true + }, + "schema-utils": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.5.tgz", + "integrity": "sha512-yYrjb9TX2k/J1Y5UNy3KYdZq10xhYcF8nMpAW6o3hy6Q8WSIEf9lJHG/ePnOBfziPM3fvQwfOwa13U/Fh8qTfA==", + "dev": true, + "requires": { + "ajv": "6.4.0", + "ajv-keywords": "3.1.0" + }, + "dependencies": { + "ajv": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.4.0.tgz", + "integrity": "sha1-06/3jpJ3VJdx2vAWTP9ISCt1T8Y=", + "dev": true, + "requires": { + "fast-deep-equal": "1.0.0", + "fast-json-stable-stringify": "2.0.0", + "json-schema-traverse": "0.3.1", + "uri-js": "3.0.2" + } + }, + "ajv-keywords": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.1.0.tgz", + "integrity": "sha1-rCsnk5xUPpXSwG5/f1wnvkqlQ74=", + "dev": true + } + } + }, + "scoped-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/scoped-regex/-/scoped-regex-1.0.0.tgz", + "integrity": "sha1-o0a7Gs1CB65wvXwMfKnlZra63bg=", + "dev": true + }, + "scss-tokenizer": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz", + "integrity": "sha1-jrBtualyMzOCTT9VMGQRSYR85dE=", + "dev": true, + "requires": { + "js-base64": "2.4.3", + "source-map": "0.4.4" + }, + "dependencies": { + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "dev": true, + "requires": { + "amdefine": "1.0.1" + } + } + } + }, + "select": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", + "integrity": "sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0=" + }, + "semver": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=" + }, + "serialize-javascript": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.4.0.tgz", + "integrity": "sha1-fJWFFNtqwkQ6irwGLcn3iGp/YAU=", + "dev": true + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, + "set-immediate-shim": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", + "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=", + "dev": true + }, + "set-value": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", + "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", + "dev": true, + "requires": { + "extend-shallow": "2.0.1", + "is-extendable": "0.1.1", + "is-plain-object": "2.0.4", + "split-string": "3.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" + }, + "sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "dev": true, + "requires": { + "inherits": "2.0.3", + "safe-buffer": "5.1.1" + } + }, + "shallow-clone": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-1.0.0.tgz", + "integrity": "sha512-oeXreoKR/SyNJtRJMAKPDSvd28OqEwG4eR/xc856cRGBII7gX9lvAqDxusPm0846z/w/hWYjI1NpKwJ00NHzRA==", + "dev": true, + "requires": { + "is-extendable": "0.1.1", + "kind-of": "5.1.0", + "mixin-object": "2.0.1" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "shallow-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.0.0.tgz", + "integrity": "sha1-UI0YOLPeWQq4dXsBGyXkMJAJRfc=" + }, + "shallowequal": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.0.2.tgz", + "integrity": "sha512-zlVXeVUKvo+HEv1e2KQF/csyeMKx2oHvatQ9l6XjCUj3agvC8XGf6R9HvIPDSmp8FNPvx7b5kaEJTRi7CqxtEw==" + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "requires": { + "shebang-regex": "1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" + }, + "shelljs": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.1.tgz", + "integrity": "sha512-YA/iYtZpzFe5HyWVGrb02FjPxc4EMCfpoU/Phg9fQoyMC72u9598OUBrsU8IrtwAKG0tO8IYaqbaLIw+k3IRGA==", + "dev": true, + "requires": { + "glob": "7.1.2", + "interpret": "1.1.0", + "rechoir": "0.6.2" + } + }, + "shellwords": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", + "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", + "dev": true + }, + "showdown": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/showdown/-/showdown-1.7.4.tgz", + "integrity": "sha1-a7yd0s2x5f3XSeza3GpHuFZZSuA=", + "requires": { + "yargs": "8.0.2" + } + }, + "sigmund": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", + "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=" + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + }, + "simple-html-tokenizer": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/simple-html-tokenizer/-/simple-html-tokenizer-0.4.1.tgz", + "integrity": "sha1-AomIu3/osuZkVnbYIFJYfUQLAtM=" + }, + "slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", + "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=", + "dev": true + }, + "slice-ansi": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", + "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=", + "dev": true + }, + "slide": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", + "integrity": "sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=", + "dev": true + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, + "requires": { + "base": "0.11.2", + "debug": "2.6.9", + "define-property": "0.2.5", + "extend-shallow": "2.0.1", + "map-cache": "0.2.2", + "source-map": "0.5.7", + "source-map-resolve": "0.5.1", + "use": "3.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "0.1.6" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "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.6" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "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.6" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "0.1.6", + "is-data-descriptor": "0.1.4", + "kind-of": "5.1.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, + "requires": { + "define-property": "1.0.0", + "isobject": "3.0.1", + "snapdragon-util": "3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "1.0.2" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, + "requires": { + "kind-of": "3.2.2" + } + }, + "sntp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz", + "integrity": "sha512-FL1b58BDrqS3A11lJ0zEdnJ3UOKqVxawAkF3k7F0CVN7VQ34aZrV+G8BZ1WC9ZL7NyrwsW0oviwsWDgRuVYtJg==", + "dev": true, + "requires": { + "hoek": "4.2.0" + } + }, + "sort-keys": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", + "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", + "dev": true, + "requires": { + "is-plain-obj": "1.1.0" + } + }, + "source-list-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.0.tgz", + "integrity": "sha512-I2UmuJSRr/T8jisiROLU3A3ltr+swpniSmNPI4Ml3ZCX6tVnDsuZzK7F2hl5jTqbZBWCEKlj5HRQiPExXLgE8A==", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "source-map-resolve": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.1.tgz", + "integrity": "sha512-0KW2wvzfxm8NCTb30z0LMNyPqWCdDGE2viwzUaucqJdkTRXtZiSY3I+2A6nVAjmdOy0I4gU8DwnVVGsk9jvP2A==", + "dev": true, + "requires": { + "atob": "2.1.0", + "decode-uri-component": "0.2.0", + "resolve-url": "0.2.1", + "source-map-url": "0.4.0", + "urix": "0.1.0" + } + }, + "source-map-support": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.3.tgz", + "integrity": "sha512-eKkTgWYeBOQqFGXRfKabMFdnWepo51vWqEdoeikaEPFiJC7MCU5j2h4+6Q8npkZTeLGbSyecZvRxiSoWl3rh+w==", + "dev": true, + "requires": { + "source-map": "0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", + "dev": true + }, + "spawn-command": { + "version": "0.0.2-1", + "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2-1.tgz", + "integrity": "sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A=", + "dev": true + }, + "spdx-correct": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-1.0.2.tgz", + "integrity": "sha1-SzBz2TP/UfORLwOsVRlJikFQ20A=", + "requires": { + "spdx-license-ids": "1.2.2" + } + }, + "spdx-expression-parse": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz", + "integrity": "sha1-m98vIOH0DtRH++JzJmGR/O1RYmw=" + }, + "spdx-license-ids": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz", + "integrity": "sha1-yd96NCRZSt5r0RkA1ZZpbcBrrFc=" + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, + "requires": { + "extend-shallow": "3.0.2" + } + }, + "sprintf-js": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.1.tgz", + "integrity": "sha1-Nr54Mgr+WAH2zqPueLblqrlA6gw=", + "dev": true + }, + "sshpk": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", + "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", + "dev": true, + "requires": { + "asn1": "0.2.3", + "assert-plus": "1.0.0", + "bcrypt-pbkdf": "1.0.1", + "dashdash": "1.14.1", + "ecc-jsbn": "0.1.1", + "getpass": "0.1.7", + "jsbn": "0.1.1", + "tweetnacl": "0.14.5" + } + }, + "ssri": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-5.3.0.tgz", + "integrity": "sha512-XRSIPqLij52MtgoQavH/x/dU1qVKtWUAAZeOHsR9c2Ddi4XerFy3mc1alf+dLJKl9EUIm/Ht+EowFkTUOA6GAQ==", + "dev": true, + "requires": { + "safe-buffer": "5.1.1" + } + }, + "stack-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.1.tgz", + "integrity": "sha1-1PM6tU6OOHeLDKXP07OvsS22hiA=", + "dev": true + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "dev": true, + "requires": { + "define-property": "0.2.5", + "object-copy": "0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "0.1.6" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "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.6" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "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.6" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "0.1.6", + "is-data-descriptor": "0.1.4", + "kind-of": "5.1.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "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=", + "dev": true, + "requires": { + "readable-stream": "2.3.3" + } + }, + "stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", + "dev": true + }, + "stream-browserify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz", + "integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "readable-stream": "2.3.3" + } + }, + "stream-each": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.2.tgz", + "integrity": "sha512-mc1dbFhGBxvTM3bIWmAAINbqiuAk9TATcfIQC8P+/+HJefgaiTlMn2dHvkX8qlI12KeYKSQ1Ua9RrIqrn1VPoA==", + "dev": true, + "requires": { + "end-of-stream": "1.4.1", + "stream-shift": "1.0.0" + } + }, + "stream-http": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.1.tgz", + "integrity": "sha512-cQ0jo17BLca2r0GfRdZKYAGLU6JRoIWxqSOakUMuKOT6MOK7AAlE856L33QuDmAy/eeOrhLee3dZKX0Uadu93A==", + "dev": true, + "requires": { + "builtin-status-codes": "3.0.0", + "inherits": "2.0.3", + "readable-stream": "2.3.3", + "to-arraybuffer": "1.0.1", + "xtend": "4.0.1" + } + }, + "stream-shift": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", + "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=", + "dev": true + }, + "stream-to-observable": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/stream-to-observable/-/stream-to-observable-0.2.0.tgz", + "integrity": "sha1-WdbqOT2HwsDdrBCqDVYbxrpvDhA=", + "dev": true, + "requires": { + "any-observable": "0.2.0" + } + }, + "strict-uri-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", + "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=", + "dev": true + }, + "string-length": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-2.0.0.tgz", + "integrity": "sha1-1A27aGo6zpYMHP/KVivyxF+DY+0=", + "dev": true, + "requires": { + "astral-regex": "1.0.0", + "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" + } + } + } + }, + "string-template": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string-template/-/string-template-0.2.1.tgz", + "integrity": "sha1-QpMuWYo1LQH8IuwzZ9nYTuxsmt0=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "requires": { + "is-fullwidth-code-point": "2.0.0", + "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=" + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + }, + "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" + } + } + } + }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "requires": { + "safe-buffer": "5.1.1" + } + }, + "stringstream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", + "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=", + "dev": true + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=" + }, + "strip-bom-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-stream/-/strip-bom-stream-2.0.0.tgz", + "integrity": "sha1-+H217yYT9paKpUWr/h7HKLaoKco=", + "dev": true, + "requires": { + "first-chunk-stream": "2.0.0", + "strip-bom": "2.0.0" + }, + "dependencies": { + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "requires": { + "is-utf8": "0.2.1" + } + } + } + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" + }, + "strip-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", + "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", + "dev": true, + "requires": { + "get-stdin": "4.0.1" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true + }, + "style-loader": { + "version": "0.20.3", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-0.20.3.tgz", + "integrity": "sha512-2I7AVP73MvK33U7B9TKlYZAqdROyMXDYSMvHLX43qy3GCOaJNiV6i0v/sv9idWIaQ42Yn2dNv79Q5mKXbKhAZg==", + "dev": true, + "requires": { + "loader-utils": "1.1.0", + "schema-utils": "0.4.5" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + }, + "svgo": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-0.7.2.tgz", + "integrity": "sha1-n1dyQTlSE1xv779Ar+ak+qiLS7U=", + "dev": true, + "requires": { + "coa": "1.0.4", + "colors": "1.1.2", + "csso": "2.3.2", + "js-yaml": "3.7.0", + "mkdirp": "0.5.1", + "sax": "1.2.4", + "whet.extend": "0.9.9" + }, + "dependencies": { + "colors": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", + "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", + "dev": true + }, + "esprima": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", + "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", + "dev": true + }, + "js-yaml": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.7.0.tgz", + "integrity": "sha1-XJZ93YN6m/3KXy3oQlOr6KHAO4A=", + "dev": true, + "requires": { + "argparse": "1.0.9", + "esprima": "2.7.3" + } + } + } + }, + "symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" + }, + "symbol-tree": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.2.tgz", + "integrity": "sha1-rifbOPZgp64uHDt9G8KQgZuFGeY=", + "dev": true + }, + "table": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/table/-/table-4.0.2.tgz", + "integrity": "sha512-UUkEAPdSGxtRpiV9ozJ5cMTtYiqz7Ni1OGqLXRCynrvzdtR1p+cfOWe2RJLwvUG8hNanaSRjecIqwOjqeatDsA==", + "dev": true, + "requires": { + "ajv": "5.5.2", + "ajv-keywords": "2.1.1", + "chalk": "2.3.0", + "lodash": "4.17.5", + "slice-ansi": "1.0.0", + "string-width": "2.1.1" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + }, + "chalk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", + "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "4.5.0" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "slice-ansi": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-1.0.0.tgz", + "integrity": "sha512-POqxBK6Lb3q6s047D/XsDVNPnF9Dl8JSaqe9h9lURl0OdNqy/ujDrOiIHtsqXMGbWWTIomRzAMaTyawAU//Reg==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "2.0.0" + } + }, + "supports-color": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", + "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", + "dev": true, + "requires": { + "has-flag": "2.0.0" + } + } + } + }, + "tapable": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.0.0.tgz", + "integrity": "sha512-dQRhbNQkRnaqauC7WqSJ21EEksgT0fYZX2lqXzGkpo8JNig9zGZTYoMGvyI2nWmXlE2VSVXVDu7wLVGu/mQEsg==", + "dev": true + }, + "tar": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", + "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", + "dev": true, + "requires": { + "block-stream": "0.0.9", + "fstream": "1.0.11", + "inherits": "2.0.3" + } + }, + "temp": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.8.3.tgz", + "integrity": "sha1-4Ma8TSa5AxJEEOT+2BEDAU38H1k=", + "dev": true, + "requires": { + "os-tmpdir": "1.0.2", + "rimraf": "2.2.8" + }, + "dependencies": { + "rimraf": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", + "integrity": "sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI=", + "dev": true + } + } + }, + "test-exclude": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-4.2.0.tgz", + "integrity": "sha512-8hMFzjxbPv6xSlwGhXSvOMJ/vTy3bkng+2pxmf6E1z6VF7I9nIyNfvHtaw+NBPgvz647gADBbMSbwLfZYppT/w==", + "dev": true, + "requires": { + "arrify": "1.0.1", + "micromatch": "2.3.11", + "object-assign": "4.1.1", + "read-pkg-up": "1.0.1", + "require-main-filename": "1.0.1" + }, + "dependencies": { + "load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "parse-json": "2.2.0", + "pify": "2.3.0", + "pinkie-promise": "2.0.1", + "strip-bom": "2.0.0" + } + }, + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" + } + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "dev": true, + "requires": { + "load-json-file": "1.1.0", + "normalize-package-data": "2.4.0", + "path-type": "1.1.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "dev": true, + "requires": { + "find-up": "1.1.2", + "read-pkg": "1.1.0" + } + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "requires": { + "is-utf8": "0.2.1" + } + } + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "textextensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/textextensions/-/textextensions-2.2.0.tgz", + "integrity": "sha512-j5EMxnryTvKxwH2Cq+Pb43tsf6sdEgw6Pdwxk83mPaq0ToeFJt6WE4J3s5BqY7vmjlLgkgXvhtXUxo80FyBhCA==", + "dev": true + }, + "throat": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/throat/-/throat-4.1.0.tgz", + "integrity": "sha1-iQN8vJLFarGJJua6TLsgDhVnKmo=", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "through2": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", + "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=", + "dev": true, + "requires": { + "readable-stream": "2.3.3", + "xtend": "4.0.1" + } + }, + "timed-out": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", + "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=", + "dev": true + }, + "timers-browserify": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.6.tgz", + "integrity": "sha512-HQ3nbYRAowdVd0ckGFvmJPPCOH/CHleFN/Y0YQCX1DVaB7t+KFvisuyN09fuP8Jtp1CpfSh8O8bMkHbdbPe6Pw==", + "dev": true, + "requires": { + "setimmediate": "1.0.5" + } + }, + "tiny-emitter": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.0.2.tgz", + "integrity": "sha512-2NM0auVBGft5tee/OxP4PI3d8WItkDM+fPnaRAVo6xTDI2knbz9eC5ArWGqtGlYqiH3RU5yMpdyTTO7MguC4ow==" + }, + "tinycolor2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.1.tgz", + "integrity": "sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g=" + }, + "tinymce": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/tinymce/-/tinymce-4.7.2.tgz", + "integrity": "sha1-JL9k/x0eaBkOFUYaZY3CV6Qmxe4=", + "dev": true + }, + "tmpl": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", + "integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=", + "dev": true + }, + "to-arraybuffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", + "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", + "dev": true + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, + "requires": { + "define-property": "2.0.2", + "extend-shallow": "3.0.2", + "regex-not": "1.0.2", + "safe-regex": "1.1.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "3.0.0", + "repeat-string": "1.6.1" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + } + } + } + }, + "tough-cookie": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz", + "integrity": "sha1-C2GKVWW23qkL80JdBNVe3EdadWE=", + "dev": true, + "requires": { + "punycode": "1.4.1" + } + }, + "tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", + "dev": true, + "requires": { + "punycode": "2.1.0" + }, + "dependencies": { + "punycode": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.0.tgz", + "integrity": "sha1-X4Y+3Im5bbCQdLrXlHvwkFbKTn0=", + "dev": true + } + } + }, + "tree-kill": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.0.tgz", + "integrity": "sha512-DlX6dR0lOIRDFxI0mjL9IYg6OTncLm/Zt+JiBhE5OlFcAR8yc9S7FFXU9so0oda47frdM/JFsk7UjNt9vscKcg==", + "dev": true + }, + "trim-newlines": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", + "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", + "dev": true + }, + "trim-right": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", + "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", + "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=", + "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.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + } + } + }, + "tty-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", + "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", + "dev": true + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true, + "requires": { + "safe-buffer": "5.1.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "dev": true, + "optional": true + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "1.1.2" + } + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, + "ua-parser-js": { + "version": "0.7.17", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.17.tgz", + "integrity": "sha512-uRdSdu1oA1rncCQL7sCj8vSyZkgtL7faaw9Tc9rZ3mGgraQ7+Pdx7w5mnOSF3gw9ZNG6oc+KXfkon3bKuROm0g==" + }, + "uglify-js": { + "version": "2.8.29", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", + "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", + "dev": true, + "optional": true, + "requires": { + "source-map": "0.5.7", + "uglify-to-browserify": "1.0.2", + "yargs": "3.10.0" + }, + "dependencies": { + "camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", + "dev": true, + "optional": true + }, + "cliui": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", + "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", + "dev": true, + "optional": true, + "requires": { + "center-align": "0.1.3", + "right-align": "0.1.3", + "wordwrap": "0.0.2" + } + }, + "wordwrap": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", + "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", + "dev": true, + "optional": true + }, + "yargs": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", + "dev": true, + "optional": true, + "requires": { + "camelcase": "1.2.1", + "cliui": "2.1.0", + "decamelize": "1.2.0", + "window-size": "0.1.0" + } + } + } + }, + "uglify-to-browserify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", + "dev": true, + "optional": true + }, + "uglifyjs-webpack-plugin": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.2.4.tgz", + "integrity": "sha512-z0IbjpW8b3O/OVn+TTZN4pI29RN1zktFBXLIzzfZ+++cUtZ1ERSlLWgpE/5OERuEUs1ijVQnpYAkSlpoVmQmSQ==", + "dev": true, + "requires": { + "cacache": "10.0.4", + "find-cache-dir": "1.0.0", + "schema-utils": "0.4.5", + "serialize-javascript": "1.4.0", + "source-map": "0.6.1", + "uglify-es": "3.3.9", + "webpack-sources": "1.1.0", + "worker-farm": "1.6.0" + }, + "dependencies": { + "commander": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.13.0.tgz", + "integrity": "sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "uglify-es": { + "version": "3.3.9", + "resolved": "https://registry.npmjs.org/uglify-es/-/uglify-es-3.3.9.tgz", + "integrity": "sha512-r+MU0rfv4L/0eeW3xZrd16t4NZfK8Ld4SWVglYBb7ez5uXFWHuVRs6xCTrf1yirs9a4j4Y27nn7SRfO6v67XsQ==", + "dev": true, + "requires": { + "commander": "2.13.0", + "source-map": "0.6.1" + } + } + } + }, + "ultron": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", + "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==", + "dev": true + }, + "underscore": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.4.4.tgz", + "integrity": "sha1-YaajIBBiKvoHljvzJSA88SI51gQ=", + "dev": true + }, + "union-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", + "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", + "dev": true, + "requires": { + "arr-union": "3.1.0", + "get-value": "2.0.6", + "is-extendable": "0.1.1", + "set-value": "0.4.3" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + }, + "set-value": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", + "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", + "dev": true, + "requires": { + "extend-shallow": "2.0.1", + "is-extendable": "0.1.1", + "is-plain-object": "2.0.4", + "to-object-path": "0.3.0" + } + } + } + }, + "uniq": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", + "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=", + "dev": true + }, + "uniqid": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/uniqid/-/uniqid-4.1.1.tgz", + "integrity": "sha1-iSIN32t1GuUrX3JISGNShZa7hME=", + "dev": true, + "requires": { + "macaddress": "0.2.8" + } + }, + "uniqs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/uniqs/-/uniqs-2.0.0.tgz", + "integrity": "sha1-/+3ks2slKQaW5uFl1KWe25mOawI=", + "dev": true + }, + "unique-filename": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.0.tgz", + "integrity": "sha1-0F8v5AMlYIcfMOk8vnNe6iAVFPM=", + "dev": true, + "requires": { + "unique-slug": "2.0.0" + } + }, + "unique-slug": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.0.tgz", + "integrity": "sha1-22Z258fMBimHj/GWCXx4hVrp9Ks=", + "dev": true, + "requires": { + "imurmurhash": "0.1.4" + } + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "dev": true, + "requires": { + "has-value": "0.3.1", + "isobject": "3.0.1" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "dev": true, + "requires": { + "get-value": "2.0.6", + "has-values": "0.1.4", + "isobject": "2.1.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "untildify": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-3.0.2.tgz", + "integrity": "sha1-fx8wIFWz/qDz6B3HjrNnZstl4/E=", + "dev": true + }, + "upath": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.0.4.tgz", + "integrity": "sha512-d4SJySNBXDaQp+DPrziv3xGS6w3d2Xt69FijJr86zMPBy23JEloMCEOUBBzuN7xCtjLCnmB9tI/z7SBCahHBOw==", + "dev": true + }, + "uri-js": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-3.0.2.tgz", + "integrity": "sha1-+QuFhQf4HepNz7s8TD2/orVX+qo=", + "dev": true, + "requires": { + "punycode": "2.1.0" + }, + "dependencies": { + "punycode": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.0.tgz", + "integrity": "sha1-X4Y+3Im5bbCQdLrXlHvwkFbKTn0=", + "dev": true + } + } + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "dev": true + }, + "url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "dev": true, + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", + "dev": true + } + } + }, + "url-parse-lax": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", + "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=", + "dev": true, + "requires": { + "prepend-http": "2.0.0" + }, + "dependencies": { + "prepend-http": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", + "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=", + "dev": true + } + } + }, + "url-to-options": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/url-to-options/-/url-to-options-1.0.1.tgz", + "integrity": "sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k=", + "dev": true + }, + "urlgrey": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/urlgrey/-/urlgrey-0.4.4.tgz", + "integrity": "sha1-iS/pWWCAXoVRnxzUOJ8stMu3ZS8=", + "dev": true + }, + "use": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.0.tgz", + "integrity": "sha512-6UJEQM/L+mzC3ZJNM56Q4DFGLX/evKGRg15UJHGB9X5j5Z3AFbgZvjUh2yq/UJUY4U5dh7Fal++XbNg1uzpRAw==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + }, + "dependencies": { + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, + "util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "dev": true, + "requires": { + "inherits": "2.0.1" + }, + "dependencies": { + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", + "dev": true + } + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "util.promisify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", + "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", + "dev": true, + "requires": { + "define-properties": "1.1.2", + "object.getownpropertydescriptors": "2.0.3" + } + }, + "uuid": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", + "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==" + }, + "v8-compile-cache": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-1.1.2.tgz", + "integrity": "sha512-ejdrifsIydN1XDH7EuR2hn8ZrkRKUYF7tUcBjBy/lhrCvs2K+zRlbW9UHc0IQ9RsYFZJFqJrieoIHfkCa0DBRA==", + "dev": true + }, + "validate-npm-package-license": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz", + "integrity": "sha1-KAS6vnEq0zeUWaz74kdGqywwP7w=", + "requires": { + "spdx-correct": "1.0.2", + "spdx-expression-parse": "1.0.4" + } + }, + "vendors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.1.tgz", + "integrity": "sha1-N61zyO5Bf7PVgOeFMSMH0nSEfyI=", + "dev": true + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "1.3.0" + } + }, + "vinyl": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-1.2.0.tgz", + "integrity": "sha1-XIgDbPVl5d8FVYv8kR+GVt8hiIQ=", + "dev": true, + "requires": { + "clone": "1.0.3", + "clone-stats": "0.0.1", + "replace-ext": "0.0.1" + } + }, + "vinyl-file": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vinyl-file/-/vinyl-file-2.0.0.tgz", + "integrity": "sha1-p+v1/779obfRjRQPyweyI++2dRo=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "pify": "2.3.0", + "pinkie-promise": "2.0.1", + "strip-bom": "2.0.0", + "strip-bom-stream": "2.0.0", + "vinyl": "1.2.0" + }, + "dependencies": { + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "requires": { + "is-utf8": "0.2.1" + } + } + } + }, + "vm-browserify": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz", + "integrity": "sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=", + "dev": true, + "requires": { + "indexof": "0.0.1" + } + }, + "w3c-hr-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz", + "integrity": "sha1-gqwr/2PZUOqeMYmlimViX+3xkEU=", + "dev": true, + "requires": { + "browser-process-hrtime": "0.1.2" + } + }, + "walker": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz", + "integrity": "sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=", + "dev": true, + "requires": { + "makeerror": "1.0.11" + } + }, + "watch": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/watch/-/watch-0.18.0.tgz", + "integrity": "sha1-KAlUdsbffJDJYxOJkMClQj60uYY=", + "dev": true, + "requires": { + "exec-sh": "0.2.1", + "minimist": "1.2.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "watchpack": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.5.0.tgz", + "integrity": "sha512-RSlipNQB1u48cq0wH/BNfCu1tD/cJ8ydFIkNYhp9o+3d+8unClkIovpW5qpFPgmL9OE48wfAnlZydXByWP82AA==", + "dev": true, + "requires": { + "chokidar": "2.0.3", + "graceful-fs": "4.1.11", + "neo-async": "2.5.0" + } + }, + "webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true + }, + "webpack": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.4.1.tgz", + "integrity": "sha512-iLUJcsEAjaPKWbB32ADr29Pg9fPUVfFEMPK4VXyZGftzhSEFg2BLjHLoBYZ14wdTEA8xqG/hjpuX8qOmabRYvw==", + "dev": true, + "requires": { + "acorn": "5.4.1", + "acorn-dynamic-import": "3.0.0", + "ajv": "6.4.0", + "ajv-keywords": "3.1.0", + "chrome-trace-event": "0.1.2", + "enhanced-resolve": "4.0.0", + "eslint-scope": "3.7.1", + "loader-runner": "2.3.0", + "loader-utils": "1.1.0", + "memory-fs": "0.4.1", + "micromatch": "3.1.10", + "mkdirp": "0.5.1", + "neo-async": "2.5.0", + "node-libs-browser": "2.1.0", + "schema-utils": "0.4.5", + "tapable": "1.0.0", + "uglifyjs-webpack-plugin": "1.2.4", + "watchpack": "1.5.0", + "webpack-sources": "1.1.0" + }, + "dependencies": { + "ajv": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.4.0.tgz", + "integrity": "sha1-06/3jpJ3VJdx2vAWTP9ISCt1T8Y=", + "dev": true, + "requires": { + "fast-deep-equal": "1.0.0", + "fast-json-stable-stringify": "2.0.0", + "json-schema-traverse": "0.3.1", + "uri-js": "3.0.2" + } + }, + "ajv-keywords": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.1.0.tgz", + "integrity": "sha1-rCsnk5xUPpXSwG5/f1wnvkqlQ74=", + "dev": true + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "braces": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.1.tgz", + "integrity": "sha512-SO5lYHA3vO6gz66erVvedSCkp7AKWdv6VcQ2N4ysXfPxdAlxAMMAdwegGGcv1Bqwm7naF1hNdk5d6AAIEHV2nQ==", + "dev": true, + "requires": { + "arr-flatten": "1.1.0", + "array-unique": "0.3.2", + "define-property": "1.0.0", + "extend-shallow": "2.0.1", + "fill-range": "4.0.0", + "isobject": "3.0.1", + "kind-of": "6.0.2", + "repeat-element": "1.1.2", + "snapdragon": "0.8.2", + "snapdragon-node": "2.1.1", + "split-string": "3.1.0", + "to-regex": "3.0.2" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "1.0.2" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "2.6.9", + "define-property": "0.2.5", + "extend-shallow": "2.0.1", + "posix-character-classes": "0.1.1", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "0.1.6" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "0.1.6", + "is-data-descriptor": "0.1.4", + "kind-of": "5.1.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "0.3.2", + "define-property": "1.0.0", + "expand-brackets": "2.1.4", + "extend-shallow": "2.0.1", + "fragment-cache": "0.2.1", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "1.0.2" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "2.0.1", + "is-number": "3.0.0", + "repeat-string": "1.6.1", + "to-regex-range": "2.1.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "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.6" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "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.6" + } + } + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "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.6" + } + } + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "4.0.0", + "array-unique": "0.3.2", + "braces": "2.3.1", + "define-property": "2.0.2", + "extend-shallow": "3.0.2", + "extglob": "2.0.4", + "fragment-cache": "0.2.1", + "kind-of": "6.0.2", + "nanomatch": "1.2.9", + "object.pick": "1.3.0", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + } + } + } + }, + "webpack-addons": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/webpack-addons/-/webpack-addons-1.1.5.tgz", + "integrity": "sha512-MGO0nVniCLFAQz1qv22zM02QPjcpAoJdy7ED0i3Zy7SY1IecgXCm460ib7H/Wq7e9oL5VL6S2BxaObxwIcag0g==", + "dev": true, + "requires": { + "jscodeshift": "0.4.1" + }, + "dependencies": { + "ansi-styles": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.0.0.tgz", + "integrity": "sha1-yxAt8cVvUSPquLZ817mAJ6AnkXg=", + "dev": true + }, + "ast-types": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.10.1.tgz", + "integrity": "sha512-UY7+9DPzlJ9VM8eY0b2TUZcZvF+1pO0hzMtAyjBYKhOmnvRlqYNYnWdtsMj0V16CGaMlpL0G1jnLbLo4AyotuQ==", + "dev": true + }, + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "chalk": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.4.0.tgz", + "integrity": "sha1-UZmj3c0MHv4jvAjBsCewYXbgxk8=", + "dev": true, + "requires": { + "ansi-styles": "1.0.0", + "has-color": "0.1.7", + "strip-ansi": "0.1.1" + } + }, + "colors": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.2.1.tgz", + "integrity": "sha512-s8+wktIuDSLffCywiwSxQOMqtPxML11a/dtHE17tMn4B1MSWw/C22EKf7M2KGUBcDaVFEGT+S8N02geDXeuNKg==", + "dev": true + }, + "jscodeshift": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/jscodeshift/-/jscodeshift-0.4.1.tgz", + "integrity": "sha512-iOX6If+hsw0q99V3n31t4f5VlD1TQZddH08xbT65ZqA7T4Vkx68emrDZMUOLVvCEAJ6NpAk7DECe3fjC/t52AQ==", + "dev": true, + "requires": { + "async": "1.5.2", + "babel-plugin-transform-flow-strip-types": "6.22.0", + "babel-preset-es2015": "6.24.1", + "babel-preset-stage-1": "6.24.1", + "babel-register": "6.26.0", + "babylon": "6.18.0", + "colors": "1.2.1", + "flow-parser": "0.69.0", + "lodash": "4.17.5", + "micromatch": "2.3.11", + "node-dir": "0.1.8", + "nomnom": "1.8.1", + "recast": "0.12.9", + "temp": "0.8.3", + "write-file-atomic": "1.3.4" + } + }, + "nomnom": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/nomnom/-/nomnom-1.8.1.tgz", + "integrity": "sha1-IVH3Ikcrp55Qp2/BJbuMjy5Nwqc=", + "dev": true, + "requires": { + "chalk": "0.4.0", + "underscore": "1.6.0" + } + }, + "recast": { + "version": "0.12.9", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.12.9.tgz", + "integrity": "sha512-y7ANxCWmMW8xLOaiopiRDlyjQ9ajKRENBH+2wjntIbk3A6ZR1+BLQttkmSHMY7Arl+AAZFwJ10grg2T6f1WI8A==", + "dev": true, + "requires": { + "ast-types": "0.10.1", + "core-js": "2.5.3", + "esprima": "4.0.0", + "private": "0.1.8", + "source-map": "0.6.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "strip-ansi": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.1.1.tgz", + "integrity": "sha1-OeipjQRNFQZgq+SmgIrPcLt7yZE=", + "dev": true + }, + "underscore": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz", + "integrity": "sha1-izixDKze9jM3uLJOT/htRa6lKag=", + "dev": true + }, + "write-file-atomic": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-1.3.4.tgz", + "integrity": "sha1-+Aek8LHZ6ROuekgRLmzDrxmRtF8=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "imurmurhash": "0.1.4", + "slide": "1.1.6" + } + } + } + }, + "webpack-cli": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-2.0.13.tgz", + "integrity": "sha512-0lnOi3yla8FsZVuMsbfnNRB/8DlfuDugKdekC+4ykydZG0+UOidMi5J5LLWN4c0VJ8PqC19yMXXkYyCq78OuqA==", + "dev": true, + "requires": { + "chalk": "2.3.2", + "cross-spawn": "6.0.5", + "diff": "3.5.0", + "enhanced-resolve": "4.0.0", + "glob-all": "3.1.0", + "global-modules": "1.0.0", + "got": "8.3.0", + "inquirer": "5.2.0", + "interpret": "1.1.0", + "jscodeshift": "0.5.0", + "listr": "0.13.0", + "loader-utils": "1.1.0", + "lodash": "4.17.5", + "log-symbols": "2.2.0", + "mkdirp": "0.5.1", + "p-each-series": "1.0.0", + "p-lazy": "1.0.0", + "prettier": "1.11.1", + "resolve-cwd": "2.0.0", + "supports-color": "5.3.0", + "v8-compile-cache": "1.1.2", + "webpack-addons": "1.1.5", + "yargs": "11.0.0", + "yeoman-environment": "2.0.5", + "yeoman-generator": "2.0.3" + }, + "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 + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + }, + "chalk": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.2.tgz", + "integrity": "sha512-ZM4j2/ld/YZDc3Ma8PgN7gyAk+kHMMMyzLNryCPGhWrsfAuDVeuid5bpRFTDgMH9JBK2lA4dyyAkkZYF/WcqDQ==", + "dev": true, + "requires": { + "ansi-styles": "3.2.1", + "escape-string-regexp": "1.0.5", + "supports-color": "5.3.0" + } + }, + "cliui": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.0.0.tgz", + "integrity": "sha512-nY3W5Gu2racvdDk//ELReY+dHjb9PlIcVDFXP72nVIhq2Gy3LuVXYwJoPVudwQnv1shtohpgkdCKT2YaKY0CKw==", + "dev": true, + "requires": { + "string-width": "2.1.1", + "strip-ansi": "4.0.0", + "wrap-ansi": "2.1.0" + } + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "1.0.4", + "path-key": "2.0.1", + "semver": "5.5.0", + "shebang-command": "1.2.0", + "which": "1.3.0" + } + }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "2.0.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "inquirer": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-5.2.0.tgz", + "integrity": "sha512-E9BmnJbAKLPGonz0HeWHtbKf+EeSP93paWO3ZYoUpq/aowXvYGjjCSuashhXPpzbArIjBbji39THkxTz9ZeEUQ==", + "dev": true, + "requires": { + "ansi-escapes": "3.0.0", + "chalk": "2.3.2", + "cli-cursor": "2.1.0", + "cli-width": "2.2.0", + "external-editor": "2.1.0", + "figures": "2.0.0", + "lodash": "4.17.5", + "mute-stream": "0.0.7", + "run-async": "2.3.0", + "rxjs": "5.5.8", + "string-width": "2.1.1", + "strip-ansi": "4.0.0", + "through": "2.3.8" + } + }, + "lodash": { + "version": "4.17.5", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.5.tgz", + "integrity": "sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw==", + "dev": true + }, + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", + "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" + } + }, + "supports-color": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.3.0.tgz", + "integrity": "sha512-0aP01LLIskjKs3lq52EC0aGBAJhLq7B2Rd8HC/DR/PtNNpcLilNmHC12O+hu0usQpo7wtHNRqtrhBwtDb0+dNg==", + "dev": true, + "requires": { + "has-flag": "3.0.0" + } + }, + "yargs": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-11.0.0.tgz", + "integrity": "sha512-Rjp+lMYQOWtgqojx1dEWorjCofi1YN7AoFvYV7b1gx/7dAAeuI4kN5SZiEvr0ZmsZTOpDRcCqrpI10L31tFkBw==", + "dev": true, + "requires": { + "cliui": "4.0.0", + "decamelize": "1.2.0", + "find-up": "2.1.0", + "get-caller-file": "1.0.2", + "os-locale": "2.1.0", + "require-directory": "2.1.1", + "require-main-filename": "1.0.1", + "set-blocking": "2.0.0", + "string-width": "2.1.1", + "which-module": "2.0.0", + "y18n": "3.2.1", + "yargs-parser": "9.0.2" + } + }, + "yargs-parser": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-9.0.2.tgz", + "integrity": "sha1-nM9qQ0YP5O1Aqbto9I1DuKaMwHc=", + "dev": true, + "requires": { + "camelcase": "4.1.0" + } + } + } + }, + "webpack-rtl-plugin": { + "version": "github:yoavf/webpack-rtl-plugin#fc5a2f20dd99fde8f86f297844aefde601780fa3", + "dev": true, + "requires": { + "@romainberger/css-diff": "1.0.3", + "async": "2.6.0", + "cssnano": "3.10.0", + "postcss": "5.2.18", + "rtlcss": "2.2.1", + "webpack-sources": "0.1.5" + }, + "dependencies": { + "source-list-map": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-0.1.8.tgz", + "integrity": "sha1-xVCyq1Qn9rPyH1r+rYjE9Vh7IQY=", + "dev": true + }, + "webpack-sources": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-0.1.5.tgz", + "integrity": "sha1-qh86vw8NdNtxEcQOUAuE+WZkB1A=", + "dev": true, + "requires": { + "source-list-map": "0.1.8", + "source-map": "0.5.7" + } + } + } + }, + "webpack-sources": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.1.0.tgz", + "integrity": "sha512-aqYp18kPphgoO5c/+NaUvEeACtZjMESmDChuD3NBciVpah3XpMEU9VAAtIaB1BsfJWWTSdv8Vv1m3T0aRk2dUw==", + "dev": true, + "requires": { + "source-list-map": "2.0.0", + "source-map": "0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "whatwg-encoding": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.3.tgz", + "integrity": "sha512-jLBwwKUhi8WtBfsMQlL4bUUcT8sMkAtQinscJAe/M4KHCkHuUJAF6vuB0tueNIw4c8ziO6AkRmgY+jL3a0iiPw==", + "dev": true, + "requires": { + "iconv-lite": "0.4.19" + } + }, + "whatwg-fetch": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz", + "integrity": "sha1-nITsLc9oGH/wC8ZOEnS0QhduHIQ=" + }, + "whatwg-url": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.4.0.tgz", + "integrity": "sha512-Z0CVh/YE217Foyb488eo+iBv+r7eAQ0wSTyApi9n06jhcA3z6Nidg/EGvl0UFkg7kMdKxfBzzr+o9JF+cevgMg==", + "dev": true, + "requires": { + "lodash.sortby": "4.7.0", + "tr46": "1.0.1", + "webidl-conversions": "4.0.2" + } + }, + "whet.extend": { + "version": "0.9.9", + "resolved": "https://registry.npmjs.org/whet.extend/-/whet.extend-0.9.9.tgz", + "integrity": "sha1-+HfVv2SMl+WqVC+twW1qJZucEaE=", + "dev": true + }, + "which": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", + "integrity": "sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==", + "requires": { + "isexe": "2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" + }, + "wide-align": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz", + "integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==", + "dev": true, + "requires": { + "string-width": "1.0.2" + }, + "dependencies": { + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + } + } + }, + "window-size": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", + "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", + "dev": true, + "optional": true + }, + "wordwrap": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", + "dev": true + }, + "worker-farm": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.6.0.tgz", + "integrity": "sha512-6w+3tHbM87WnSWnENBUvA2pxJPLhQUg5LKwUQHq3r+XPhIM+Gh2R5ycbwPCyuGbNg+lPgdcnQUhuC02kJCvffQ==", + "dev": true, + "requires": { + "errno": "0.1.7" + } + }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "requires": { + "string-width": "1.0.2", + "strip-ansi": "3.0.1" + }, + "dependencies": { + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "write": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", + "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", + "dev": true, + "requires": { + "mkdirp": "0.5.1" + } + }, + "write-file-atomic": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.3.0.tgz", + "integrity": "sha512-xuPeK4OdjWqtfi59ylvVL0Yn35SF3zgcAcv7rBPFHVaEapaDr4GdGgm3j7ckTwH9wHL7fGmgfAnb0+THrHb8tA==", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "imurmurhash": "0.1.4", + "signal-exit": "3.0.2" + } + }, + "ws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-4.0.0.tgz", + "integrity": "sha512-QYslsH44bH8O7/W2815u5DpnCpXWpEK44FmaHffNwgJI4JMaSZONgPBTOfrxJ29mXKbXak+LsJ2uAkDTYq2ptQ==", + "dev": true, + "requires": { + "async-limiter": "1.0.0", + "safe-buffer": "5.1.1", + "ultron": "1.1.1" + } + }, + "xml-name-validator": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", + "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", + "dev": true + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", + "dev": true + }, + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=" + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" + }, + "yargs": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-8.0.2.tgz", + "integrity": "sha1-YpmpBVsc78lp/355wdkY3Osiw2A=", + "requires": { + "camelcase": "4.1.0", + "cliui": "3.2.0", + "decamelize": "1.2.0", + "get-caller-file": "1.0.2", + "os-locale": "2.1.0", + "read-pkg-up": "2.0.0", + "require-directory": "2.1.1", + "require-main-filename": "1.0.1", + "set-blocking": "2.0.0", + "string-width": "2.1.1", + "which-module": "2.0.0", + "y18n": "3.2.1", + "yargs-parser": "7.0.0" + } + }, + "yargs-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-7.0.0.tgz", + "integrity": "sha1-jQrELxbqVd69MyyvTEA4s+P139k=", + "requires": { + "camelcase": "4.1.0" + } + }, + "yauzl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz", + "integrity": "sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=", + "dev": true, + "requires": { + "fd-slicer": "1.0.1" + } + }, + "yeoman-environment": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/yeoman-environment/-/yeoman-environment-2.0.5.tgz", + "integrity": "sha512-6/W7/B54OPHJXob0n0+pmkwFsirC8cokuQkPSmT/D0lCcSxkKtg/BA6ZnjUBIwjuGqmw3DTrT4en++htaUju5g==", + "dev": true, + "requires": { + "chalk": "2.3.2", + "debug": "3.1.0", + "diff": "3.4.0", + "escape-string-regexp": "1.0.5", + "globby": "6.1.0", + "grouped-queue": "0.3.3", + "inquirer": "3.3.0", + "is-scoped": "1.0.0", + "lodash": "4.17.5", + "log-symbols": "2.2.0", + "mem-fs": "1.1.3", + "text-table": "0.2.0", + "untildify": "3.0.2" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + }, + "chalk": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.2.tgz", + "integrity": "sha512-ZM4j2/ld/YZDc3Ma8PgN7gyAk+kHMMMyzLNryCPGhWrsfAuDVeuid5bpRFTDgMH9JBK2lA4dyyAkkZYF/WcqDQ==", + "dev": true, + "requires": { + "ansi-styles": "3.2.1", + "escape-string-regexp": "1.0.5", + "supports-color": "5.3.0" + } + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "globby": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", + "dev": true, + "requires": { + "array-union": "1.0.2", + "glob": "7.1.2", + "object-assign": "4.1.1", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.3.0.tgz", + "integrity": "sha512-0aP01LLIskjKs3lq52EC0aGBAJhLq7B2Rd8HC/DR/PtNNpcLilNmHC12O+hu0usQpo7wtHNRqtrhBwtDb0+dNg==", + "dev": true, + "requires": { + "has-flag": "3.0.0" + } + } + } + }, + "yeoman-generator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/yeoman-generator/-/yeoman-generator-2.0.3.tgz", + "integrity": "sha512-mODmrZ26a94djmGZZuIiomSGlN4wULdou29ZwcySupb2e9FdvoCl7Ps2FqHFjEHio3kOl/iBeaNqrnx3C3NwWg==", + "dev": true, + "requires": { + "async": "2.6.0", + "chalk": "2.3.2", + "cli-table": "0.3.1", + "cross-spawn": "5.1.0", + "dargs": "5.1.0", + "dateformat": "3.0.3", + "debug": "3.1.0", + "detect-conflict": "1.0.1", + "error": "7.0.2", + "find-up": "2.1.0", + "github-username": "4.1.0", + "istextorbinary": "2.2.1", + "lodash": "4.17.5", + "make-dir": "1.2.0", + "mem-fs-editor": "3.0.2", + "minimist": "1.2.0", + "pretty-bytes": "4.0.2", + "read-chunk": "2.1.0", + "read-pkg-up": "3.0.0", + "rimraf": "2.6.2", + "run-async": "2.3.0", + "shelljs": "0.8.1", + "text-table": "0.2.0", + "through2": "2.0.3", + "yeoman-environment": "2.0.5" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + }, + "chalk": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.2.tgz", + "integrity": "sha512-ZM4j2/ld/YZDc3Ma8PgN7gyAk+kHMMMyzLNryCPGhWrsfAuDVeuid5bpRFTDgMH9JBK2lA4dyyAkkZYF/WcqDQ==", + "dev": true, + "requires": { + "ansi-styles": "3.2.1", + "escape-string-regexp": "1.0.5", + "supports-color": "5.3.0" + } + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "2.0.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "parse-json": "4.0.0", + "pify": "3.0.0", + "strip-bom": "3.0.0" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "1.3.1", + "json-parse-better-errors": "1.0.1" + } + }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "requires": { + "pify": "3.0.0" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "dev": true, + "requires": { + "load-json-file": "4.0.0", + "normalize-package-data": "2.4.0", + "path-type": "3.0.0" + } + }, + "read-pkg-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", + "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", + "dev": true, + "requires": { + "find-up": "2.1.0", + "read-pkg": "3.0.0" + } + }, + "supports-color": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.3.0.tgz", + "integrity": "sha512-0aP01LLIskjKs3lq52EC0aGBAJhLq7B2Rd8HC/DR/PtNNpcLilNmHC12O+hu0usQpo7wtHNRqtrhBwtDb0+dNg==", + "dev": true, + "requires": { + "has-flag": "3.0.0" + } + } + } + } + } } diff --git a/package.json b/package.json index 4aee9b19470d7d..96f331aeaf77c2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "2.2.0", + "version": "2.6.0", "description": "A new WordPress editor experience", "main": "build/app.js", "repository": "git+https://github.com/WordPress/gutenberg.git", @@ -17,7 +17,8 @@ "dependencies": { "@wordpress/a11y": "1.0.6", "@wordpress/autop": "1.0.4", - "@wordpress/hooks": "1.1.4", + "@wordpress/hooks": "1.1.6", + "@wordpress/i18n": "1.1.0", "@wordpress/url": "1.0.3", "classnames": "2.2.5", "clipboard": "1.7.1", @@ -25,126 +26,104 @@ "dom-scroll-into-view": "1.2.1", "element-closest": "2.0.2", "escape-string-regexp": "1.0.5", - "eslint-plugin-wordpress": "git://github.com/WordPress-Coding-Standards/eslint-plugin-wordpress.git#327b6bdec434177a6e841bd3210e87627ccfcecb", + "eslint-plugin-wordpress": "git://github.com/WordPress-Coding-Standards/eslint-plugin-wordpress.git#1774343f6226052a46b081e01db3fca8793cc9f1", "hpq": "1.2.0", - "is-equal-shallow": "0.1.3", - "jed": "1.1.1", "jquery": "3.2.1", "js-beautify": "1.6.14", - "lodash": "4.17.4", + "lodash": "4.17.5", "memize": "1.0.5", - "moment": "2.18.1", + "moment": "2.21.0", "moment-timezone": "0.5.13", "mousetrap": "1.6.1", "prop-types": "15.5.10", "querystringify": "1.0.0", "re-resizable": "4.0.3", - "react": "16.2.0", - "react-autosize-textarea": "2.0.0", + "react": "16.3.0", + "react-autosize-textarea": "3.0.2", "react-click-outside": "2.3.1", "react-color": "2.13.4", "react-datepicker": "0.61.0", - "react-dom": "16.2.0", + "react-dom": "16.3.0", "react-redux": "5.0.6", "redux": "3.7.2", "redux-multi": "0.1.12", - "redux-optimist": "0.0.2", + "redux-optimist": "1.0.0", "refx": "3.0.0", "rememo": "2.4.0", + "shallowequal": "1.0.2", "showdown": "1.7.4", "simple-html-tokenizer": "0.4.1", "tinycolor2": "1.4.1", "uuid": "3.1.0" }, "devDependencies": { - "@wordpress/babel-preset-default": "1.1.1", + "@wordpress/babel-plugin-makepot": "1.0.0", + "@wordpress/babel-preset-default": "1.2.0", + "@wordpress/custom-templated-path-webpack-plugin": "1.0.0", "@wordpress/jest-preset-default": "1.0.3", "@wordpress/scripts": "1.1.0", - "autoprefixer": "6.7.7", + "autoprefixer": "8.2.0", "babel-core": "6.26.0", "babel-eslint": "8.0.3", - "babel-loader": "7.1.2", - "babel-traverse": "6.26.0", + "babel-loader": "7.1.4", + "babel-plugin-transform-async-generator-functions": "6.24.1", "check-node-version": "3.1.1", "codecov": "3.0.0", "concurrently": "3.5.0", + "core-js": "2.5.3", "cross-env": "3.2.4", - "cypress": "1.4.1", "deep-freeze": "0.0.1", "eslint": "4.16.0", "eslint-config-wordpress": "2.0.0", "eslint-plugin-jest": "21.5.0", "eslint-plugin-jsx-a11y": "6.0.2", - "eslint-plugin-react": "7.5.1", - "expose-loader": "0.7.3", - "extract-text-webpack-plugin": "3.0.0", - "gettext-parser": "1.3.0", - "husky": "0.14.3", - "lint-staged": "6.1.0", + "eslint-plugin-react": "7.7.0", + "extract-text-webpack-plugin": "4.0.0-beta.0", "node-sass": "4.7.2", "pegjs": "0.10.0", "pegjs-loader": "0.5.4", "phpegjs": "1.0.0-beta7", - "postcss-loader": "2.0.6", - "prismjs": "1.6.0", + "postcss-loader": "2.1.3", + "puppeteer": "1.2.0", "raw-loader": "0.5.1", - "react-test-renderer": "16.0.0", - "sass-loader": "6.0.6", - "sass-variables-loader": "0.1.3", + "react-test-renderer": "16.3.0", + "sass-loader": "6.0.7", "sprintf-js": "1.1.1", - "style-loader": "0.18.2", + "style-loader": "0.20.3", "tinymce": "4.7.2", - "webpack": "3.10.0", + "webpack": "4.4.1", + "webpack-cli": "2.0.13", "webpack-rtl-plugin": "github:yoavf/webpack-rtl-plugin#develop" }, "babel": { "presets": [ "@wordpress/default" ], + "plugins": [ + "transform-async-generator-functions" + ], "env": { - "gettext": { + "production": { "plugins": [ [ - "./i18n/babel-plugin", + "@wordpress/babel-plugin-makepot", { "output": "languages/gutenberg.pot" } - ] + ], + "transform-async-generator-functions" ] } } }, - "jest": { - "collectCoverageFrom": [ - "(blocks|components|date|editor|element|i18n|data|utils|edit-post)/**/*.js" - ], - "moduleNameMapper": { - "@wordpress\\/(blocks|components|date|editor|element|i18n|data|utils|edit-post)": "$1" - }, - "preset": "@wordpress/jest-preset-default", - "setupFiles": [ - "<rootDir>/test/unit/setup-blocks.js", - "<rootDir>/test/unit/setup-wp-aliases.js" - ], - "transform": { - "\\.pegjs$": "<rootDir>/test/unit/pegjs-transform.js" - } - }, - "lint-staged": { - "*.js": [ - "eslint --fix", - "git add" - ] - }, "scripts": { "prebuild": "check-node-version --package", - "build": "cross-env BABEL_ENV=default NODE_ENV=production webpack", - "gettext-strings": "cross-env BABEL_ENV=gettext webpack", + "build": "cross-env NODE_ENV=production webpack", "lint": "eslint .", "lint:fix": "eslint . --fix", "lint-php": "docker-compose run --rm composer run-script lint", "predev": "check-node-version --package", - "dev": "cross-env BABEL_ENV=default webpack --watch", + "dev": "cross-env webpack --watch", "test": "npm run lint && npm run test-unit", "test-php": "npm run lint-php && npm run test-unit-php", "ci": "concurrently \"npm run lint && npm run build\" \"npm run test-unit:coverage-ci\"", @@ -153,14 +132,14 @@ "fixtures:generate": "npm run fixtures:server-registered && cross-env GENERATE_MISSING_FIXTURES=y npm run test-unit", "fixtures:regenerate": "npm run fixtures:clean && npm run fixtures:generate", "package-plugin": "./bin/build-plugin-zip.sh", - "precommit": "lint-staged", - "test-unit": "wp-scripts test-unit-js", + "pot-to-php": "./bin/pot-to-php.js", + "test-unit": "wp-scripts test-unit-js --config test/unit/jest.config.json", "test-unit-php": "docker-compose run --rm wordpress_phpunit phpunit", "test-unit-php-multisite": "docker-compose run -e WP_MULTISITE=1 --rm wordpress_phpunit phpunit", "test-unit:coverage": "npm run test-unit -- --coverage", "test-unit:coverage-ci": "npm run test-unit -- --coverage --maxWorkers 1 && codecov", "test-unit:watch": "npm run test-unit -- --watch", - "test-e2e": "cypress run --browser chrome", - "test-e2e:watch": "cypress open" + "test-e2e": "wp-scripts test-unit-js --config test/e2e/jest.config.json", + "test-e2e:watch": "npm run test-e2e -- --watch" } } diff --git a/phpunit/class-meta-box-test.php b/phpunit/class-meta-box-test.php index c62ef1ab1940e1..805737a4a54d77 100644 --- a/phpunit/class-meta-box-test.php +++ b/phpunit/class-meta-box-test.php @@ -111,55 +111,6 @@ public function setUp() { ); } - /** - * Tests for empty meta box. - */ - public function test_gutenberg_is_meta_box_empty_with_empty_meta_box() { - $context = 'side'; - $post_type = 'post'; - $meta_boxes = $this->meta_boxes; - $meta_boxes[ $post_type ][ $context ] = array(); - - $is_empty = gutenberg_is_meta_box_empty( $meta_boxes, $context, $post_type ); - $this->assertTrue( $is_empty ); - } - - /** - * Tests for non empty meta box area. - */ - public function test_gutenberg_is_meta_box_empty_with_non_empty_meta_box() { - $context = 'normal'; - $post_type = 'post'; - $meta_boxes = $this->meta_boxes; - - $is_empty = gutenberg_is_meta_box_empty( $meta_boxes, $context, $post_type ); - $this->assertFalse( $is_empty ); - } - - /** - * Tests for non existant location. - */ - public function test_gutenberg_is_meta_box_empty_with_non_existant_location() { - $context = 'test'; - $post_type = 'post'; - $meta_boxes = $this->meta_boxes; - - $is_empty = gutenberg_is_meta_box_empty( $meta_boxes, $context, $post_type ); - $this->assertTrue( $is_empty ); - } - - /** - * Tests for non existant page. - */ - public function test_gutenberg_is_meta_box_empty_with_non_existant_page() { - $context = 'normal'; - $post_type = 'test'; - $meta_boxes = $this->meta_boxes; - - $is_empty = gutenberg_is_meta_box_empty( $meta_boxes, $context, $post_type ); - $this->assertTrue( $is_empty ); - } - /** * Test filtering of meta box data. */ diff --git a/phpunit/class-rest-blocks-controller-test.php b/phpunit/class-rest-blocks-controller-test.php index d45071adb39f67..f29ff5c2f767cb 100644 --- a/phpunit/class-rest-blocks-controller-test.php +++ b/phpunit/class-rest-blocks-controller-test.php @@ -197,6 +197,128 @@ public function test_get_item_schema() { $this->assertArrayHasKey( 'content', $properties ); } + /** + * 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 shared blocks. + * + * @dataProvider data_capabilities + */ + public function test_capabilities( $action, $role, $expected_status ) { + if ( $role ) { + $user_id = $this->factory->user->create( array( 'role' => $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:core/paragraph --><p>Test</p><!-- /wp:core/paragraph -->', + ) + ); + + $response = $this->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 = $this->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:core/paragraph --><p>Hello!</p><!-- /wp:core/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:core/paragraph --><p>Test</p><!-- /wp:core/paragraph -->', + ) + ); + + $response = $this->server->dispatch( $request ); + $this->assertEquals( $expected_status, $response->get_status() ); + + $request = new WP_REST_Request( 'DELETE', '/wp/v2/blocks/' . $post_id ); + + $response = $this->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:core/paragraph --><p>Test</p><!-- /wp:core/paragraph -->', + ) + ); + + $response = $this->server->dispatch( $request ); + $this->assertEquals( $expected_status, $response->get_status() ); + + $request = new WP_REST_Request( 'DELETE', '/wp/v2/blocks/' . self::$post_id ); + + $response = $this->server->dispatch( $request ); + $this->assertEquals( $expected_status, $response->get_status() ); + + break; + + default: + $this->fail( "'$action' is not a valid action." ); + } + + if ( isset( $user_id ) ) { + self::delete_user( $user_id ); + } + } + public function test_context_param() { $this->markTestSkipped( 'Controller doesn\'t implement get_context_param().' ); } diff --git a/phpunit/class-reusable-blocks-render-test.php b/phpunit/class-reusable-blocks-render-test.php index 9d4a2119b29150..6c20bebc5998f0 100644 --- a/phpunit/class-reusable-blocks-render-test.php +++ b/phpunit/class-reusable-blocks-render-test.php @@ -1,14 +1,14 @@ <?php /** - * Reusable block rendering tests. + * Shared block rendering tests. * * @package Gutenberg */ /** - * Tests reusable block rendering. + * Tests shared block rendering. */ -class Reusable_Blocks_Render_Test extends WP_UnitTestCase { +class Shared_Blocks_Render_Test extends WP_UnitTestCase { /** * Fake user ID. * @@ -67,7 +67,7 @@ public static function wpTearDownAfterClass() { } /** - * Test rendering of a reusable block. + * Test rendering of a shared block. */ public function test_render() { $block_type = WP_Block_Type_Registry::get_instance()->get_registered( 'core/block' ); @@ -76,7 +76,7 @@ public function test_render() { } /** - * Test rendering of a reusable block when 'ref' is missing, which should fail by + * Test rendering of a shared block when 'ref' is missing, which should fail by * rendering an empty string. */ public function test_ref_empty() { @@ -86,7 +86,7 @@ public function test_ref_empty() { } /** - * Test rendering of a reusable block when 'ref' points to wrong post type, which + * Test rendering of a shared block when 'ref' points to wrong post type, which * should fail by rendering an empty string. */ public function test_ref_wrong_post_type() { diff --git a/phpunit/class-vendor-script-filename-test.php b/phpunit/class-vendor-script-filename-test.php index a16f2cc35b208f..7b4d116d0e351c 100644 --- a/phpunit/class-vendor-script-filename-test.php +++ b/phpunit/class-vendor-script-filename-test.php @@ -14,13 +14,9 @@ function vendor_script_filename_cases() { 'react.HASH.js', ), array( - 'https://unpkg.com/react-dom@next/umd/react-dom.development.js', + 'https://unpkg.com/react-dom@16.3.0/umd/react-dom.development.js', 'react-dom.HASH.js', ), - array( - 'https://unpkg.com/react-dom@next/umd/react-dom-server.development.js', - 'react-dom-server.HASH.js', - ), array( 'https://fiddle.azurewebsites.net/tinymce/nightly/tinymce.js', 'tinymce.HASH.js', @@ -35,13 +31,9 @@ function vendor_script_filename_cases() { 'react.min.HASH.js', ), array( - 'https://unpkg.com/react-dom@next/umd/react-dom.production.min.js', + 'https://unpkg.com/react-dom@16.3.0/umd/react-dom.production.min.js', 'react-dom.min.HASH.js', ), - array( - 'https://unpkg.com/react-dom@next/umd/react-dom-server.production.min.js', - 'react-dom-server.min.HASH.js', - ), array( 'https://fiddle.azurewebsites.net/tinymce/nightly/tinymce.min.js', 'tinymce.min.HASH.js', diff --git a/phpunit/fixtures/wpautop-expected.html b/phpunit/fixtures/wpautop-expected.html deleted file mode 100644 index 44fd881f570ba7..00000000000000 --- a/phpunit/fixtures/wpautop-expected.html +++ /dev/null @@ -1,25 +0,0 @@ -<p>First Auto Paragraph</p> - -<!--more--> - -<!-- wp:core/paragraph --> -<p>First Gutenberg Paragraph</p> -<!-- /wp:core/paragraph --> - -<p>Second Auto Paragraph</p> - -<!-- wp:core/test-self-closing /--> - -<!-- wp:core/test-ignore-autop --> -Can't - -Touch - -This -<!-- /wp:core/test-ignore-autop --> - -<!-- wp:core/paragraph --> -<p>Third Gutenberg Paragraph</p> -<!-- /wp:core/paragraph --> - -<p>Third Auto Paragraph</p> diff --git a/phpunit/fixtures/wpautop-original.html b/phpunit/fixtures/wpautop-original.html deleted file mode 100644 index 1e8cade0ee8196..00000000000000 --- a/phpunit/fixtures/wpautop-original.html +++ /dev/null @@ -1,25 +0,0 @@ -First Auto Paragraph - -<!--more--> - -<!-- wp:core/paragraph --> -<p>First Gutenberg Paragraph</p> -<!-- /wp:core/paragraph --> - -Second Auto Paragraph - -<!-- wp:core/test-self-closing /--> - -<!-- wp:core/test-ignore-autop --> -Can't - -Touch - -This -<!-- /wp:core/test-ignore-autop --> - -<!-- wp:core/paragraph --> -<p>Third Gutenberg Paragraph</p> -<!-- /wp:core/paragraph --> - -Third Auto Paragraph diff --git a/post-content.js b/post-content.js index 35e29f5935a631..d556c8c74df1f9 100644 --- a/post-content.js +++ b/post-content.js @@ -8,11 +8,11 @@ window._wpGutenbergPost.title = { window._wpGutenbergPost.content = { raw: [ '<!-- wp:cover-image {"url":"https://cldup.com/Fz-ASbo2s3.jpg","align":"wide"} -->', - '<section class="wp-block-cover-image has-background-dim alignwide" style="background-image:url(https://cldup.com/Fz-ASbo2s3.jpg)"><h2>Of Mountains &amp; Printing Presses</h2></section>', + '<div class="wp-block-cover-image has-background-dim alignwide" style="background-image:url(https://cldup.com/Fz-ASbo2s3.jpg)"><p class="wp-block-cover-image-text">Of Mountains &amp; Printing Presses</p></div>', '<!-- /wp:cover-image -->', '<!-- wp:paragraph -->', - '<p>The goal of this new editor is to make adding rich content to WordPress simple and enjoyable. This whole post is composed of <em>pieces of content</em>—somewhat similar to LEGO bricks—that you can move around and interact with. Move your cursor around and you&#x27;ll notice the different blocks light up with outlines and arrows. Press the arrows to reposition blocks quickly, without fearing about losing things in the process of copying and pasting.</p>', + '<p>The goal of this new editor is to make adding rich content to WordPress simple and enjoyable. This whole post is composed of <em>pieces of content</em>—somewhat similar to LEGO bricks—that you can move around and interact with. Move your cursor around and you\'ll notice the different blocks light up with outlines and arrows. Press the arrows to reposition blocks quickly, without fearing about losing things in the process of copying and pasting.</p>', '<!-- /wp:paragraph -->', '<!-- wp:paragraph -->', @@ -32,15 +32,15 @@ window._wpGutenbergPost.content = { '<!-- /wp:heading -->', '<!-- wp:paragraph -->', - '<p>Handling images and media with the utmost care is a primary focus of the new editor. Hopefully, you&#x27;ll find aspects of adding captions or going full-width with your pictures much easier and robust than before.</p>', + '<p>Handling images and media with the utmost care is a primary focus of the new editor. Hopefully, you\'ll find aspects of adding captions or going full-width with your pictures much easier and robust than before.</p>', '<!-- /wp:paragraph -->', '<!-- wp:image {"align":"center"} -->', - '<figure class="wp-block-image aligncenter"><img src="https://cldup.com/cXyG__fTLN.jpg" alt="Beautiful landscape" /><figcaption>Give it a try. Press the &quot;wide&quot; button on the image toolbar.</figcaption></figure>', + '<figure class="wp-block-image aligncenter"><img src="https://cldup.com/cXyG__fTLN.jpg" alt="Beautiful landscape" /><figcaption>If your theme supports it, you\'ll see the "wide" button on the image toolbar. Give it a try.</figcaption></figure>', '<!-- /wp:image -->', '<!-- wp:paragraph -->', - '<p>Try selecting and removing or editing the caption, now you don&#x27;t have to be careful about selecting the image or other text by mistake and ruining the presentation.</p>', + '<p>Try selecting and removing or editing the caption, now you don\'t have to be careful about selecting the image or other text by mistake and ruining the presentation.</p>', '<!-- /wp:paragraph -->', '<!-- wp:heading -->', @@ -48,11 +48,11 @@ window._wpGutenbergPost.content = { '<!-- /wp:heading -->', '<!-- wp:paragraph -->', - '<p>Imagine everything that WordPress can do is available to you quickly and in the same place on the interface. No need to figure out HTML tags, classes, or remember complicated shortcode syntax. That&#x27;s the spirit behind the inserter—the <code>(+)</code> button you&#x27;ll see around the editor—which allows you to browse all available content blocks and add them into your post. Plugins and themes are able to register their own, opening up all sort of possibilities for rich editing and publishing.</p>', + '<p>Imagine everything that WordPress can do is available to you quickly and in the same place on the interface. No need to figure out HTML tags, classes, or remember complicated shortcode syntax. That\'s the spirit behind the inserter—the <code>(+)</code> button you\'ll see around the editor—which allows you to browse all available content blocks and add them into your post. Plugins and themes are able to register their own, opening up all sort of possibilities for rich editing and publishing.</p>', '<!-- /wp:paragraph -->', '<!-- wp:paragraph -->', - '<p>Go give it a try, you may discover things WordPress can already add into your posts that you didn&#x27;t know about. Here&#x27;s a short list of what you can currently find there:</p>', + '<p>Go give it a try, you may discover things WordPress can already add into your posts that you didn\'t know about. Here\'s a short list of what you can currently find there:</p>', '<!-- /wp:paragraph -->', '<!-- wp:list -->', @@ -76,7 +76,7 @@ window._wpGutenbergPost.content = { '<!-- /wp:quote -->', '<!-- wp:paragraph -->', - '<p>The information corresponding to the source of the quote is a separate text field, similar to captions under images, so the structure of the quote is protected even if you select, modify, or remove the source. It&#x27;s always easy to add it back.</p>', + '<p>The information corresponding to the source of the quote is a separate text field, similar to captions under images, so the structure of the quote is protected even if you select, modify, or remove the source. It\'s always easy to add it back.</p>', '<!-- /wp:paragraph -->', '<!-- wp:paragraph -->', @@ -104,7 +104,7 @@ window._wpGutenbergPost.content = { '<!-- /wp:paragraph -->', '<!-- wp:image {"align":"full"} -->', - '<figure class="wp-block-image alignfull"><img src="https://cldup.com/8lhI-gKnI2.jpg" alt="Accessibility is important don&#x27;t forget image alt attribute" /></figure>', + '<figure class="wp-block-image alignfull"><img src="https://cldup.com/8lhI-gKnI2.jpg" alt="Accessibility is important don\'t forget image alt attribute" /></figure>', '<!-- /wp:image -->', '<!-- wp:paragraph -->', @@ -126,8 +126,8 @@ window._wpGutenbergPost.content = { '<p>Any block can opt into these alignments. The embed block has them also, and is responsive out of the box:</p>', '<!-- /wp:paragraph -->', - '<!-- wp:embed {"url":"https://vimeo.com/22439234","align":"wide"} -->', - '<figure class="wp-block-embed alignwide">https://vimeo.com/22439234</figure>', + '<!-- wp:embed {"url":"https://vimeo.com/22439234","align":"wide","type":"video","providerNameSlug":"vimeo"} -->', + '<figure class="wp-block-embed alignwide is-type-video is-provider-vimeo">https://vimeo.com/22439234</figure>', '<!-- /wp:embed -->', '<!-- wp:paragraph -->', diff --git a/test/e2e/.eslintrc.js b/test/e2e/.eslintrc.js deleted file mode 100644 index df03ebfead0ccd..00000000000000 --- a/test/e2e/.eslintrc.js +++ /dev/null @@ -1,181 +0,0 @@ -// For the most part, this file mirrors the same configuration from the root -// `.eslintrc.json`, the exceptions being that since Cypress is preconfigured -// with Mocha & Chai, we need to broadly disable the Jest rules, which is -// otherwise difficult to do. This file could be made more minimal once all of -// the Gutenberg-specific rules are migrated to a common WordPress config. - -module.exports = { - "root": true, - "parser": "babel-eslint", - "extends": [ - "wordpress" - ], - "env": { - "browser": false, - "es6": true, - "node": true, - "mocha": true - }, - "parserOptions": { - "sourceType": "module" - }, - "globals": { - "cy": true, - "Cypress": true, - "expect": true - }, - "plugins": [ - "wordpress" - ], - "rules": { - "array-bracket-spacing": [ "error", "always" ], - "brace-style": [ "error", "1tbs" ], - "camelcase": [ "error", { "properties": "never" } ], - "comma-dangle": [ "error", "always-multiline" ], - "comma-spacing": "error", - "comma-style": "error", - "computed-property-spacing": [ "error", "always" ], - "constructor-super": "error", - "dot-notation": "error", - "eol-last": "error", - "eqeqeq": "error", - "func-call-spacing": "error", - "indent": [ "error", "tab", { "SwitchCase": 1 } ], - "key-spacing": "error", - "keyword-spacing": "error", - "lines-around-comment": "off", - "no-alert": "error", - "no-bitwise": "error", - "no-caller": "error", - "no-console": "error", - "no-const-assign": "error", - "no-debugger": "error", - "no-dupe-args": "error", - "no-dupe-class-members": "error", - "no-dupe-keys": "error", - "no-duplicate-case": "error", - "no-duplicate-imports": "error", - "no-else-return": "error", - "no-eval": "error", - "no-extra-semi": "error", - "no-fallthrough": "error", - "no-lonely-if": "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": "ImportDeclaration[source.value=/^@wordpress\\u002F.+\\u002F/]", - "message": "Path access on WordPress dependencies is not allowed." - }, - { - "selector": "ImportDeclaration[source.value=/^blocks$/]", - "message": "Use @wordpress/blocks as import path instead." - }, - { - "selector": "ImportDeclaration[source.value=/^components$/]", - "message": "Use @wordpress/components as import path instead." - }, - { - "selector": "ImportDeclaration[source.value=/^date$/]", - "message": "Use @wordpress/date as import path instead." - }, - { - "selector": "ImportDeclaration[source.value=/^editor$/]", - "message": "Use @wordpress/editor as import path instead." - }, - { - "selector": "ImportDeclaration[source.value=/^element$/]", - "message": "Use @wordpress/element as import path instead." - }, - { - "selector": "ImportDeclaration[source.value=/^i18n$/]", - "message": "Use @wordpress/i18n as import path instead." - }, - { - "selector": "ImportDeclaration[source.value=/^data$/]", - "message": "Use @wordpress/data as import path instead." - }, - { - "selector": "ImportDeclaration[source.value=/^utils$/]", - "message": "Use @wordpress/utils as import path instead." - }, - { - "selector": "ImportDeclaration[source.value=/^edit-poost$/]", - "message": "Use @wordpress/edit-post as import path instead." - }, - { - "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-undef": "error", - "no-undef-init": "error", - "no-unreachable": "error", - "no-unsafe-negation": "error", - "no-unused-expressions": "error", - "no-unused-vars": "error", - "no-useless-computed-key": "error", - "no-useless-constructor": "error", - "no-useless-return": "error", - "no-var": "error", - "no-whitespace-before-property": "error", - "object-curly-spacing": [ "error", "always" ], - "padded-blocks": [ "error", "never" ], - "prefer-const": "error", - "quote-props": [ "error", "as-needed" ], - "semi": "error", - "semi-spacing": "error", - "space-before-blocks": [ "error", "always" ], - "space-before-function-paren": [ "error", "never" ], - "space-in-parens": [ "error", "always" ], - "space-infix-ops": [ "error", { "int32Hint": false } ], - "space-unary-ops": [ "error", { - "overrides": { - "!": true - } - } ], - "template-curly-spacing": [ "error", "always" ], - "valid-jsdoc": [ "error", { - "prefer": { - "arg": "param", - "argument": "param", - "extends": "augments", - "returns": "return" - }, - "preferType": { - "array": "Array", - "bool": "boolean", - "Boolean": "boolean", - "float": "number", - "Float": "number", - "int": "number", - "integer": "number", - "Integer": "number", - "Number": "number", - "object": "Object", - "String": "string", - "Void": "void" - }, - "requireParamDescription": false, - "requireReturn": false - } ], - "valid-typeof": "error", - "yoda": "off" - } -}; diff --git a/test/e2e/integration/001-hello-gutenberg.js b/test/e2e/integration/001-hello-gutenberg.js deleted file mode 100644 index 37e21e5ec84acd..00000000000000 --- a/test/e2e/integration/001-hello-gutenberg.js +++ /dev/null @@ -1,37 +0,0 @@ -describe( 'Hello Gutenberg', () => { - before( () => { - cy.newPost(); - } ); - - it( 'Should show the New Post Page in Gutenberg', () => { - // Assertions - cy.url().should( 'include', 'post-new.php' ); - cy.get( '[placeholder="Add title"]' ).should( 'exist' ); - } ); - - it( 'Should have no history', () => { - cy.get( '.editor-history__undo:not( :disabled )' ).should( 'not.exist' ); - cy.get( '.editor-history__redo:not( :disabled )' ).should( 'not.exist' ); - } ); - - it( 'Should not prompt to confirm unsaved changes', ( done ) => { - const timeout = setTimeout( () => { - done( new Error( 'Expected page reload' ) ); - }, 5000 ); - - cy.window().then( ( window ) => { - function verify( event ) { - expect( event.returnValue ).to.equal( '' ); - - window.removeEventListener( 'beforeunload', verify ); - clearTimeout( timeout ); - - done(); - } - - window.addEventListener( 'beforeunload', verify ); - - cy.reload(); - } ); - } ); -} ); diff --git a/test/e2e/integration/002-adding-blocks.js b/test/e2e/integration/002-adding-blocks.js deleted file mode 100644 index 1f899413a941b4..00000000000000 --- a/test/e2e/integration/002-adding-blocks.js +++ /dev/null @@ -1,38 +0,0 @@ -describe( 'Adding blocks', () => { - before( () => { - cy.newPost(); - } ); - - it( 'Should insert content using the placeholder and the regular inserter', () => { - const lastBlockSelector = '.editor-block-list__block-edit:last [contenteditable="true"]:first'; - - // Using the placeholder - cy.get( '.editor-default-block-appender' ).click(); - cy.get( lastBlockSelector ).type( 'Paragraph block' ); - - // Using the slash command - // Test commented because Cypress is not update the selection object properly, - // so the slash inserter is not showing up. - /* cy.get( '.edit-post-header [aria-label="Add block"]' ).click(); - cy.get( '[placeholder="Search for a block"]' ).type( 'Paragraph' ); - cy.get( '.editor-inserter__block' ).contains( 'Paragraph' ).click(); - cy.get( lastBlockSelector ).type( '/quote{enter}' ); - cy.get( lastBlockSelector ).type( 'Quote block' ); */ - - // Using the regular inserter - cy.get( '.edit-post-header [aria-label="Add block"]' ).click(); - cy.get( '[placeholder="Search for a block"]' ).type( 'code' ); - cy.get( '.editor-inserter__block' ).contains( 'Code' ).click(); - cy.get( '[placeholder="Write code…"]' ).type( 'Code block' ); - - // Switch to Text Mode to check HTML Output - cy.get( '.edit-post-more-menu [aria-label="More"]' ).click(); - cy.get( 'button' ).contains( 'Code Editor' ).click(); - - // Assertions - cy.get( '.edit-post-text-editor' ) - .should( 'contain', 'Paragraph block' ) - // .should( 'contain', 'Quote block' ) - .should( 'contain', 'Code block' ); - } ); -} ); diff --git a/test/e2e/integration/003-multi-block-selection.js b/test/e2e/integration/003-multi-block-selection.js deleted file mode 100644 index f79d24125027b7..00000000000000 --- a/test/e2e/integration/003-multi-block-selection.js +++ /dev/null @@ -1,60 +0,0 @@ -describe( 'Multi-block selection', () => { - before( () => { - cy.newPost(); - } ); - - it( 'Should select/unselect multiple blocks', () => { - const lastBlockSelector = '.editor-block-list__block-edit:last [contenteditable="true"]:first'; - const firstBlockContainerSelector = '.editor-block-list__block:first'; - const lastBlockContainerSelector = '.editor-block-list__block:last'; - const multiSelectedCssClass = 'is-multi-selected'; - - // Creating test blocks - cy.get( '.editor-default-block-appender' ).click(); - cy.get( lastBlockSelector ).type( 'First Paragraph' ); - cy.get( '.edit-post-header [aria-label="Add block"]' ).click(); - cy.get( '[placeholder="Search for a block"]' ).type( 'Paragraph' ); - cy.get( '.editor-inserter__block' ).contains( 'Paragraph' ).click(); - cy.get( lastBlockSelector ).type( 'Second Paragraph' ); - - // Default: No selection - cy.get( firstBlockContainerSelector ).should( 'not.have.class', multiSelectedCssClass ); - cy.get( lastBlockContainerSelector ).should( 'not.have.class', multiSelectedCssClass ); - - // Multiselect via Shift + click - cy.get( firstBlockContainerSelector ).click(); - cy.get( 'body' ).type( '{shift}', { release: false } ); - cy.get( lastBlockContainerSelector ).click(); - - // Verify selection - cy.get( firstBlockContainerSelector ).should( 'have.class', multiSelectedCssClass ); - cy.get( lastBlockContainerSelector ).should( 'have.class', multiSelectedCssClass ); - - // Unselect - cy.get( 'body' ).type( '{shift}' ); // releasing shift - cy.get( lastBlockContainerSelector ).click(); - - // No selection - cy.get( firstBlockContainerSelector ).should( 'not.have.class', multiSelectedCssClass ); - cy.get( lastBlockContainerSelector ).should( 'not.have.class', multiSelectedCssClass ); - - // Multiselect via keyboard - const isMacOs = Cypress.platform === 'darwin'; - if ( isMacOs ) { - cy.get( 'body' ).type( '{meta}a' ); - } else { - cy.get( 'body' ).type( '{ctrl}a' ); - } - - // Verify selection - cy.get( firstBlockContainerSelector ).should( 'have.class', multiSelectedCssClass ); - cy.get( lastBlockContainerSelector ).should( 'have.class', multiSelectedCssClass ); - - // Unselect - cy.get( 'body' ).type( '{esc}' ); - - // No selection - cy.get( firstBlockContainerSelector ).should( 'not.have.class', multiSelectedCssClass ); - cy.get( lastBlockContainerSelector ).should( 'not.have.class', multiSelectedCssClass ); - } ); -} ); diff --git a/test/e2e/integration/004-managing-links.js b/test/e2e/integration/004-managing-links.js deleted file mode 100644 index 7c482c5a533a2c..00000000000000 --- a/test/e2e/integration/004-managing-links.js +++ /dev/null @@ -1,64 +0,0 @@ -describe( 'Managing links', () => { - before( () => { - cy.newPost(); - } ); - - const fixedIsOn = 'button.is-selected:contains("Fix Toolbar to Top")'; - const fixedIsOff = 'button:contains("Fix Toolbar to Top"):not(".is-selected")'; - - const setFixedToolbar = ( b ) => { - cy.get( '.edit-post-more-menu button' ).click(); - - cy.get( 'body' ).then( ( $body ) => { - const candidate = b ? fixedIsOff : fixedIsOn; - const toggleNeeded = $body.find( candidate ); - if ( toggleNeeded.length ) { - return 'button:contains("Fix Toolbar to Top")'; - } - - return '.edit-post-more-menu button'; - } ).then( ( selector ) => { - cy.log( ' selector " + selector ', selector ); - cy.get( selector ).click(); - } ); - }; - - it( 'Pressing Left and Esc in Link Dialog in "Fixed to Toolbar" mode', () => { - setFixedToolbar( true ); - - cy.get( '.editor-default-block-appender' ).click(); - - cy.get( 'button[aria-label="Link"]' ).click(); - - // Typing "left" should not close the dialog - cy.focused().type( '{leftarrow}' ); - cy.get( '.blocks-format-toolbar__link-modal' ).should( 'be.visible' ); - - // Escape should close the dialog still. - cy.focused().type( '{esc}' ); - cy.get( '.blocks-format-toolbar__link-modal' ).should( 'not.exist' ); - } ); - - it( 'Pressing Left and Esc in Link Dialog in "Docked Toolbar" mode', () => { - setFixedToolbar( false ); - - const lastBlockSelector = '.editor-block-list__block-edit:last [contenteditable="true"]:first'; - - cy.get( lastBlockSelector ).click(); - cy.focused().type( 'test' ); - - // we need to trigger isTyping = false - cy.get( lastBlockSelector ).trigger( 'mousemove', { clientX: 200, clientY: 300 } ); - cy.get( lastBlockSelector ).trigger( 'mousemove', { clientX: 250, clientY: 350 } ); - - cy.get( 'button[aria-label="Link"]' ).click(); - - // Typing "left" should not close the dialog - cy.get( '.blocks-url-input input' ).type( '{leftarrow}' ); - cy.get( '.blocks-format-toolbar__link-modal' ).should( 'be.visible' ); - - // Escape should close the dialog still. - cy.focused().type( '{esc}' ); - cy.get( '.blocks-format-toolbar__link-modal' ).should( 'not.exist' ); - } ); -} ); diff --git a/test/e2e/plugins/index.js b/test/e2e/plugins/index.js deleted file mode 100644 index 4f3eb1ddebb823..00000000000000 --- a/test/e2e/plugins/index.js +++ /dev/null @@ -1,14 +0,0 @@ -const promisify = require( 'util.promisify' ); -const exec = promisify( require( 'child_process' ).exec ); - -module.exports = ( on, config ) => { - // Retrieve the port that the docker container is running on - return exec( 'docker-compose run -T --rm cli option get siteurl' ) - .then( ( stdout ) => { - config.baseUrl = stdout.trim(); - return config; - } ) - .catch( () => { - return config; - } ); -}; diff --git a/test/e2e/support/gutenberg-commands.js b/test/e2e/support/gutenberg-commands.js deleted file mode 100644 index 3ece8e5001f649..00000000000000 --- a/test/e2e/support/gutenberg-commands.js +++ /dev/null @@ -1,11 +0,0 @@ -Cypress.Commands.add( 'newPost', () => { - cy.visitAdmin( '/post-new.php' ); -} ); - -Cypress.Commands.add( 'visitAdmin', ( adminPath ) => { - cy.visit( '/wp-admin/' + adminPath ).location( 'pathname' ).then( ( path ) => { - if ( path.endsWith( '/wp-login.php' ) ) { - cy.login(); - } - } ); -} ); diff --git a/test/e2e/support/index.js b/test/e2e/support/index.js deleted file mode 100644 index 6ed6aa3c8e2aa6..00000000000000 --- a/test/e2e/support/index.js +++ /dev/null @@ -1,6 +0,0 @@ -import './user-commands'; -import './gutenberg-commands'; - -Cypress.Cookies.defaults( { - whitelist: /^wordpress_/, -} ); diff --git a/test/e2e/support/user-commands.js b/test/e2e/support/user-commands.js deleted file mode 100644 index 44b6a4453e8769..00000000000000 --- a/test/e2e/support/user-commands.js +++ /dev/null @@ -1,14 +0,0 @@ -Cypress.Commands.add( 'login', ( username = Cypress.env( 'username' ), password = Cypress.env( 'password' ) ) => { - // A best practice would be to avoid this in each test - // and fake it by calling an API and setting a cookie - // (not sure this is possible in WP) - - cy.location( 'pathname' ).then( ( path ) => { - if ( ! path.endsWith( '/wp-login.php' ) ) { - cy.visit( '/wp-login.php' ); - } - } ); - cy.get( '#user_login' ).type( username ); - cy.get( '#user_pass' ).type( password ); - cy.get( '#wp-submit' ).click(); -} ); diff --git a/test/unit/setup-wp-aliases.js b/test/unit/setup-wp-aliases.js index e532151560bd35..78949bac55aa22 100644 --- a/test/unit/setup-wp-aliases.js +++ b/test/unit/setup-wp-aliases.js @@ -1,20 +1,23 @@ // Set up `wp.*` aliases. Handled by Webpack outside of the test build. global.wp = { shortcode: { - next: () => {}, + next() {}, + regexp: jest.fn().mockReturnValue( new RegExp() ), }, }; [ 'element', - 'i18n', 'components', 'utils', 'blocks', 'date', 'editor', 'data', + 'core-data', 'edit-post', + 'viewport', + 'plugins', ].forEach( entryPointName => { Object.defineProperty( global.wp, entryPointName, { get: () => require( entryPointName ), diff --git a/utils/dom.js b/utils/dom.js index c90b4c7a01cf02..36b407bfe4106f 100644 --- a/utils/dom.js +++ b/utils/dom.js @@ -1,13 +1,14 @@ /** * External dependencies */ -import { includes } from 'lodash'; +import { includes, first } from 'lodash'; +import tinymce from 'tinymce'; /** * Browser dependencies */ -const { getComputedStyle } = window; -const { TEXT_NODE } = window.Node; +const { getComputedStyle, DOMRect } = window; +const { TEXT_NODE, ELEMENT_NODE } = window.Node; /** * Check whether the caret is horizontally at the edge of the container. @@ -35,6 +36,11 @@ export function isHorizontalEdge( container, isReverse, collapseRanges = false ) return true; } + // If the container is empty, the caret is always at the edge. + if ( tinymce.DOM.isEmpty( container ) ) { + return true; + } + const selection = window.getSelection(); let range = selection.rangeCount ? selection.getRangeAt( 0 ) : null; if ( collapseRanges ) { @@ -107,11 +113,7 @@ export function isVerticalEdge( container, isReverse, collapseRanges = false ) { return false; } - // Adjust for empty containers. - const rangeRect = - range.startContainer.nodeType === window.Node.ELEMENT_NODE ? - range.startContainer.getBoundingClientRect() : - range.getClientRects()[ 0 ]; + const rangeRect = getRectangleFromRange( range ); if ( ! rangeRect ) { return false; @@ -133,11 +135,47 @@ export function isVerticalEdge( container, isReverse, collapseRanges = false ) { return true; } +/** + * Get the rectangle of a given Range. + * + * @param {Range} range The range. + * + * @return {DOMRect} The rectangle. + */ +export function getRectangleFromRange( range ) { + // For uncollapsed ranges, get the rectangle that bounds the contents of the + // range; this a rectangle enclosing the union of the bounding rectangles + // for all the elements in the range. + if ( ! range.collapsed ) { + return range.getBoundingClientRect(); + } + + // If the collapsed range starts (and therefore ends) at an element node, + // `getClientRects` will return undefined. To fix this we can get the + // bounding rectangle of the element node to create a DOMRect based on that. + if ( range.startContainer.nodeType === ELEMENT_NODE ) { + const { x, y, height } = range.startContainer.getBoundingClientRect(); + + // Create a new DOMRect with zero width. + return new DOMRect( x, y, 0, height ); + } + + // For normal collapsed ranges (exception above), the bounding rectangle of + // the range may be inaccurate in some browsers. There will only be one + // rectangle since it is a collapsed range, so it is safe to pass this as + // the union of them. This works consistently in all browsers. + return first( range.getClientRects() ); +} + +/** + * Get the rectangle for the selection in a container. + * + * @param {Element} container Editable container. + * + * @return {?DOMRect} The rectangle. + */ export function computeCaretRect( container ) { - if ( - includes( [ 'INPUT', 'TEXTAREA' ], container.tagName ) || - ! container.isContentEditable - ) { + if ( ! container.isContentEditable ) { return; } @@ -148,10 +186,7 @@ export function computeCaretRect( container ) { return; } - // Adjust for empty containers. - return range.startContainer.nodeType === window.Node.ELEMENT_NODE ? - range.startContainer.getBoundingClientRect() : - range.getClientRects()[ 0 ]; + return getRectangleFromRange( range ); } /** @@ -264,6 +299,12 @@ export function placeCaretAtVerticalEdge( container, isReverse, rect, mayUseScro return; } + // Offset by a buffer half the height of the caret rect. This is needed + // because caretRangeFromPoint may default to the end of the selection if + // offset is too close to the edge. It's unclear how to precisely calculate + // this threshold; it may be the padded area of some combination of line + // height, caret height, and font size. The buffer offset is effectively + // equivalent to a point at half the height of a line of text. const buffer = rect.height / 2; const editableRect = container.getBoundingClientRect(); const x = rect.left + ( rect.width / 2 ); @@ -311,16 +352,21 @@ export function placeCaretAtVerticalEdge( container, isReverse, rect, mayUseScro } /** - * Check whether the given node in an input field. + * Check whether the given element is a text field, where text field is defined + * by the ability to select within the input, or that it is contenteditable. + * + * See: https://html.spec.whatwg.org/#textFieldSelection * * @param {HTMLElement} element The HTML element. * - * @return {boolean} True if the element is an input field, false if not. + * @return {boolean} True if the element is an text field, false if not. */ -export function isInputField( { nodeName, contentEditable } ) { +export function isTextField( element ) { + const { nodeName, selectionStart, contentEditable } = element; + return ( - nodeName === 'INPUT' || - nodeName === 'TEXTAREA' || + ( nodeName === 'INPUT' && selectionStart !== null ) || + ( nodeName === 'TEXTAREA' ) || contentEditable === 'true' ); } @@ -332,7 +378,7 @@ export function isInputField( { nodeName, contentEditable } ) { * @return {boolean} True if there is selection, false if not. */ export function documentHasSelection() { - if ( isInputField( document.activeElement ) ) { + if ( isTextField( document.activeElement ) ) { return true; } @@ -366,3 +412,37 @@ export function getScrollContainer( node ) { // Continue traversing return getScrollContainer( node.parentNode ); } + +/** + * Given two DOM nodes, replaces the former with the latter in the DOM. + * + * @param {Element} processedNode Node to be removed. + * @param {Element} newNode Node to be inserted in its place. + * @return {void} + */ +export function replace( processedNode, newNode ) { + insertAfter( newNode, processedNode.parentNode ); + remove( processedNode ); +} + +/** + * Given a DOM node, removes it from the DOM. + * + * @param {Element} node Node to be removed. + * @return {void} + */ +export function remove( node ) { + node.parentNode.removeChild( node ); +} + +/** + * Given two DOM nodes, inserts the former in the DOM as the next sibling of + * the latter. + * + * @param {Element} newNode Node to be inserted. + * @param {Element} referenceNode Node after which to perform the insertion. + * @return {void} + */ +export function insertAfter( newNode, referenceNode ) { + referenceNode.parentNode.insertBefore( newNode, referenceNode.nextSibling ); +} diff --git a/utils/focus/focusable.js b/utils/focus/focusable.js index bdee0928f7da9f..c94de3d6544e08 100644 --- a/utils/focus/focusable.js +++ b/utils/focus/focusable.js @@ -33,7 +33,7 @@ const SELECTOR = [ 'object', 'embed', 'area[href]', - '[contenteditable]', + '[contenteditable]:not([contenteditable=false])', ].join( ',' ); /** diff --git a/utils/focus/tabbable.js b/utils/focus/tabbable.js index 88280228f96a97..e70cba256401d1 100644 --- a/utils/focus/tabbable.js +++ b/utils/focus/tabbable.js @@ -27,7 +27,7 @@ function getTabIndex( element ) { * * @return {boolean} Whether element is tabbable. */ -function isTabbableIndex( element ) { +export function isTabbableIndex( element ) { return getTabIndex( element ) !== -1; } diff --git a/utils/focus/test/focusable.js b/utils/focus/test/focusable.js index f21991936a163b..867940169a5528 100644 --- a/utils/focus/test/focusable.js +++ b/utils/focus/test/focusable.js @@ -85,6 +85,27 @@ describe( 'focusable', () => { expect( find( map ) ).toEqual( [] ); } ); + it( 'finds contenteditable', () => { + const node = createElement( 'div' ); + const div = createElement( 'div' ); + node.appendChild( div ); + + div.setAttribute( 'contenteditable', '' ); + expect( find( node ) ).toEqual( [ div ] ); + + div.setAttribute( 'contenteditable', 'true' ); + expect( find( node ) ).toEqual( [ div ] ); + } ); + + it( 'ignores contenteditable=false', () => { + const node = createElement( 'div' ); + const div = createElement( 'div' ); + node.appendChild( div ); + + div.setAttribute( 'contenteditable', 'false' ); + expect( find( node ) ).toEqual( [] ); + } ); + it( 'ignores invisible inputs', () => { const node = createElement( 'div' ); const input = createElement( 'input' ); diff --git a/utils/mediaupload.js b/utils/mediaupload.js index b1c020b4cdea89..14316e04a87a07 100644 --- a/utils/mediaupload.js +++ b/utils/mediaupload.js @@ -1,45 +1,55 @@ /** * External Dependencies */ -import { compact } from 'lodash'; +import { compact, get, startsWith } from 'lodash'; /** - * Media Upload is used by image and gallery blocks to handle uploading an image. + * Media Upload is used by audio, image, gallery and video blocks to handle uploading a media file * when a file upload button is activated. * * TODO: future enhancement to add an upload indicator. * - * @param {Array} filesList List of files. - * @param {Function} onImagesChange Function to be called each time a file or a temporary representation of the file is available. + * @param {Array} filesList List of files. + * @param {Function} onFileChange Function to be called each time a file or a temporary representation of the file is available. + * @param {string} allowedType The type of media that can be uploaded. */ -export function mediaUpload( filesList, onImagesChange ) { +export function mediaUpload( filesList, onFileChange, allowedType ) { // Cast filesList to array const files = [ ...filesList ]; - const imagesSet = []; - const setAndUpdateImages = ( idx, value ) => { - imagesSet[ idx ] = value; - onImagesChange( compact( imagesSet ) ); + const filesSet = []; + const setAndUpdateFiles = ( idx, value ) => { + filesSet[ idx ] = value; + onFileChange( compact( filesSet ) ); }; + const isAllowedType = ( fileType ) => startsWith( fileType, `${ allowedType }/` ); files.forEach( ( mediaFile, idx ) => { - // Only allow image uploads, may need updating if used for video - if ( ! /^image\//.test( mediaFile.type ) ) { + if ( ! isAllowedType( mediaFile.type ) ) { return; } - // Set temporary URL to create placeholder image, this is replaced - // with final image from media gallery when upload is `done` below - imagesSet.push( { url: window.URL.createObjectURL( mediaFile ) } ); - onImagesChange( imagesSet ); + // Set temporary URL to create placeholder media file, this is replaced + // with final file from media gallery when upload is `done` below + filesSet.push( { url: window.URL.createObjectURL( mediaFile ) } ); + onFileChange( filesSet ); return createMediaFromFile( mediaFile ).then( ( savedMedia ) => { - setAndUpdateImages( idx, { id: savedMedia.id, url: savedMedia.source_url, link: savedMedia.link } ); + const mediaObject = { + id: savedMedia.id, + url: savedMedia.source_url, + link: savedMedia.link, + }; + const caption = get( savedMedia, [ 'caption', 'raw' ] ); + if ( caption ) { + mediaObject.caption = [ caption ]; + } + setAndUpdateFiles( idx, mediaObject ); }, () => { // Reset to empty on failure. // TODO: Better failure messaging - setAndUpdateImages( idx, null ); + setAndUpdateFiles( idx, null ); } ); } ); @@ -50,14 +60,16 @@ export function mediaUpload( filesList, onImagesChange ) { * * @return {Promise} Media Object Promise. */ -export function createMediaFromFile( file ) { +function createMediaFromFile( file ) { // Create upload payload const data = new window.FormData(); data.append( 'file', file, file.name || file.type.replace( '/', '.' ) ); - - return new wp.api.models.Media().save( null, { - data: data, + return wp.apiRequest( { + path: '/wp/v2/media', + data, contentType: false, + processData: false, + method: 'POST', } ); } diff --git a/utils/test/dom.js b/utils/test/dom.js index b908403ecb4df2..9f3bc16db01def 100644 --- a/utils/test/dom.js +++ b/utils/test/dom.js @@ -1,7 +1,7 @@ /** * Internal dependencies */ -import { isHorizontalEdge, placeCaretAtHorizontalEdge, isInputField } from '../dom'; +import { isHorizontalEdge, placeCaretAtHorizontalEdge, isTextField } from '../dom'; describe( 'DOM', () => { let parent; @@ -92,13 +92,53 @@ describe( 'DOM', () => { } ); } ); - describe( 'isInputfield', () => { - it( 'should return true for an input element', () => { - expect( isInputField( document.createElement( 'input' ) ) ).toBe( true ); + describe( 'isTextField', () => { + /** + * A sampling of input types expected not to be text eligible. + * + * @type {string[]} + */ + const NON_TEXT_INPUT_TYPES = [ + 'button', + 'checkbox', + 'image', + 'hidden', + 'radio', + 'submit', + ]; + + /** + * A sampling of input types expected to be text eligible. + * + * @type {string[]} + */ + const TEXT_INPUT_TYPES = [ + 'text', + 'password', + 'search', + 'url', + ]; + + it( 'should return false for non-text input elements', () => { + NON_TEXT_INPUT_TYPES.forEach( ( type ) => { + const input = document.createElement( 'input' ); + input.type = type; + + expect( isTextField( input ) ).toBe( false ); + } ); + } ); + + it( 'should return true for text input elements', () => { + TEXT_INPUT_TYPES.forEach( ( type ) => { + const input = document.createElement( 'input' ); + input.type = type; + + expect( isTextField( input ) ).toBe( true ); + } ); } ); it( 'should return true for an textarea element', () => { - expect( isInputField( document.createElement( 'textarea' ) ) ).toBe( true ); + expect( isTextField( document.createElement( 'textarea' ) ) ).toBe( true ); } ); it( 'should return true for a contenteditable element', () => { @@ -106,11 +146,11 @@ describe( 'DOM', () => { div.contentEditable = 'true'; - expect( isInputField( div ) ).toBe( true ); + expect( isTextField( div ) ).toBe( true ); } ); it( 'should return true for a normal div element', () => { - expect( isInputField( document.createElement( 'div' ) ) ).toBe( false ); + expect( isTextField( document.createElement( 'div' ) ) ).toBe( false ); } ); } ); } ); diff --git a/webpack.config.js b/webpack.config.js index 740f261fffa330..bf9b00b68afc2e 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,12 +1,16 @@ /** * External dependencies */ -const webpack = require( 'webpack' ); const ExtractTextPlugin = require( 'extract-text-webpack-plugin' ); const WebpackRTLPlugin = require( 'webpack-rtl-plugin' ); -const { reduce, escapeRegExp, castArray, get } = require( 'lodash' ); +const { get } = require( 'lodash' ); const { basename } = require( 'path' ); +/** + * WordPress dependencies + */ +const CustomTemplatedPathPlugin = require( '@wordpress/custom-templated-path-webpack-plugin' ); + // Main CSS loader for everything but blocks.. const mainCSSExtractTextPlugin = new ExtractTextPlugin( { filename: './[basename]/build/style.css', @@ -46,95 +50,73 @@ const extractConfig = { ], }; +/** + * Given a string, returns a new string with dash separators converedd to + * camel-case equivalent. This is not as aggressive as `_.camelCase` in + * converting to uppercase, where Lodash will convert letters following + * numbers. + * + * @param {string} string Input dash-delimited string. + * + * @return {string} Camel-cased string. + */ +function camelCaseDash( string ) { + return string.replace( + /-([a-z])/, + ( match, letter ) => letter.toUpperCase() + ); +} + const entryPointNames = [ 'blocks', 'components', 'date', 'editor', 'element', - 'i18n', 'utils', 'data', - [ 'editPost', 'edit-post' ], + 'viewport', + 'core-data', + 'plugins', + 'edit-post', ]; const packageNames = [ 'hooks', + 'i18n', +]; + +const coreGlobals = [ + 'api-request', ]; const externals = { react: 'React', 'react-dom': 'ReactDOM', - 'react-dom/server': 'ReactDOMServer', tinymce: 'tinymce', moment: 'moment', jquery: 'jQuery', + lodash: 'lodash', + 'lodash-es': 'lodash', }; -[ ...entryPointNames, ...packageNames ].forEach( name => { +[ + ...entryPointNames, + ...packageNames, + ...coreGlobals, +].forEach( ( name ) => { externals[ `@wordpress/${ name }` ] = { - this: [ 'wp', name ], + this: [ 'wp', camelCaseDash( name ) ], }; } ); -/** - * Webpack plugin for handling specific template tags in Webpack configuration - * values like those supported in the base Webpack functionality (e.g. `name`). - * - * @see webpack.TemplatedPathPlugin - */ -class CustomTemplatedPathPlugin { - /** - * CustomTemplatedPathPlugin constructor. Initializes handlers as a tuple - * set of RegExp, handler, where the regular expression is used in matching - * a Webpack asset path. - * - * @param {Object.<string,Function>} handlers Object keyed by tag to match, - * with function value returning - * replacement string. - * - * @return {void} - */ - constructor( handlers ) { - this.handlers = reduce( handlers, ( result, handler, key ) => { - const regexp = new RegExp( `\\[${ escapeRegExp( key ) }\\]`, 'gi' ); - return [ ...result, [ regexp, handler ] ]; - }, [] ); - } - - /** - * Webpack plugin application logic. - * - * @param {Object} compiler Webpack compiler - * - * @return {void} - */ - apply( compiler ) { - compiler.plugin( 'compilation', ( compilation ) => { - compilation.mainTemplate.plugin( 'asset-path', ( path, data ) => { - for ( let i = 0; i < this.handlers.length; i++ ) { - const [ regexp, handler ] = this.handlers[ i ]; - if ( regexp.test( path ) ) { - return path.replace( regexp, handler( path, data ) ); - } - } - - return path; - } ); - } ); - } -} - const config = { - entry: Object.assign( - entryPointNames.reduce( ( memo, entryPoint ) => { - // Normalized entry point as an array of [ name, path ]. If a path - // is not explicitly defined, use the name. - entryPoint = castArray( entryPoint ); - const [ name, path = name ] = entryPoint; + mode: process.env.NODE_ENV === 'production' ? 'production' : 'development', + entry: Object.assign( + entryPointNames.reduce( ( memo, path ) => { + const name = camelCaseDash( path ); memo[ name ] = `./${ path }`; - return memo; }, {} ), packageNames.reduce( ( memo, packageName ) => { @@ -154,6 +136,9 @@ const config = { __dirname, 'node_modules', ], + alias: { + 'lodash-es': 'lodash', + }, }, module: { rules: [ @@ -190,9 +175,6 @@ const config = { ], }, plugins: [ - new webpack.DefinePlugin( { - 'process.env.NODE_ENV': JSON.stringify( process.env.NODE_ENV || 'development' ), - } ), blocksCSSPlugin, editBlocksCSSPlugin, mainCSSExtractTextPlugin, @@ -201,13 +183,21 @@ const config = { suffix: '-rtl', minify: process.env.NODE_ENV === 'production' ? { safe: true } : false, } ), - new webpack.LoaderOptionsPlugin( { - minimize: process.env.NODE_ENV === 'production', - debug: process.env.NODE_ENV !== 'production', - } ), new CustomTemplatedPathPlugin( { basename( path, data ) { - const rawRequest = get( data, [ 'chunk', 'entryModule', 'rawRequest' ] ); + let rawRequest; + + const entryModule = get( data, [ 'chunk', 'entryModule' ], {} ); + switch ( entryModule.type ) { + case 'javascript/auto': + rawRequest = entryModule.rawRequest; + break; + + case 'javascript/esm': + rawRequest = entryModule.rootModule.rawRequest; + break; + } + if ( rawRequest ) { return basename( rawRequest ); } @@ -221,13 +211,8 @@ const config = { }, }; -switch ( process.env.NODE_ENV ) { - case 'production': - config.plugins.push( new webpack.optimize.UglifyJsPlugin() ); - break; - - default: - config.devtool = 'source-map'; +if ( config.mode !== 'production' ) { + config.devtool = process.env.SOURCEMAP || 'source-map'; } module.exports = config; From d81a30879cc84411abacac60281d027f482a0123 Mon Sep 17 00:00:00 2001 From: Niranjan Uma Shankar <niranjan.u@gmail.com> Date: Sun, 15 Apr 2018 17:01:53 +0530 Subject: [PATCH 43/43] Merge --- .eslintrc.js | 100 ++++ .../test/embedded-content-reducer.js | 22 + .../test/integration/classic-in.html | 8 + .../test/integration/classic-out.html | 31 + blocks/api/templates.js | 60 ++ blocks/api/test/templates.js | 177 ++++++ blocks/autocomplete/README.md | 6 + blocks/autocomplete/index.js | 109 ++++ blocks/autocomplete/test/index.js | 122 ++++ blocks/autocompleters/README.md | 4 + blocks/autocompleters/block.js | 51 ++ blocks/autocompleters/test/block.js | 91 +++ blocks/autocompleters/user.js | 34 ++ blocks/editor-settings/index.js | 60 ++ blocks/hooks/default-autocompleters.js | 31 + blocks/hooks/test/default-autocompleters.js | 43 ++ blocks/inspector-advanced-controls/index.js | 6 + blocks/library/block/indicator/index.js | 22 + blocks/library/block/indicator/style.scss | 12 + blocks/library/columns/editor.scss | 54 ++ blocks/library/nextpage/editor.scss | 35 ++ blocks/library/nextpage/index.js | 61 ++ .../nextpage/test/__snapshots__/index.js.snap | 11 + blocks/library/nextpage/test/index.js | 13 + blocks/test/fixtures/core__nextpage.html | 3 + blocks/test/fixtures/core__nextpage.json | 10 + .../test/fixtures/core__nextpage.parsed.json | 12 + .../fixtures/core__nextpage.serialized.html | 3 + components/autocomplete/README.md | 121 ++++ components/autocomplete/completer-compat.js | 61 ++ components/button-group/index.js | 19 + components/button-group/style.scss | 19 + components/code-editor/README.md | 56 ++ components/code-editor/editor.js | 105 ++++ components/code-editor/index.js | 95 +++ .../test/__snapshots__/editor.js.snap | 7 + components/code-editor/test/editor.js | 24 + components/dashicon/style.scss | 4 + components/disabled/README.md | 33 + components/disabled/index.js | 93 +++ components/disabled/style.scss | 13 + components/disabled/test/index.js | 97 +++ components/draggable/README.md | 37 ++ components/draggable/index.js | 169 ++++++ components/draggable/style.scss | 20 + components/drop-zone/README.md | 47 ++ components/focusable-iframe/README.md | 39 ++ components/focusable-iframe/index.js | 69 +++ .../higher-order/if-condition/README.md | 20 + components/higher-order/if-condition/index.js | 25 + .../higher-order/with-global-events/README.md | 31 + .../higher-order/with-global-events/index.js | 64 ++ .../with-global-events/listener.js | 45 ++ .../with-global-events/test/index.js | 93 +++ .../with-global-events/test/listener.js | 87 +++ components/menu-group/index.js | 50 ++ components/menu-group/style.scss | 9 + .../test/__snapshots__/index.js.snap | 24 + components/menu-group/test/index.js | 30 + components/menu-item/index.js | 64 ++ components/menu-item/shortcut.js | 15 + components/menu-item/style.scss | 35 ++ .../test/__snapshots__/index.js.snap | 25 + components/menu-item/test/index.js | 37 ++ components/menu-items-choice/index.js | 29 + components/query-controls/category-select.js | 20 + components/query-controls/index.js | 85 +++ components/scroll-lock/README.md | 21 + components/scroll-lock/index.js | 114 ++++ components/scroll-lock/index.scss | 4 + components/scroll-lock/test/index.js | 53 ++ components/slot-fill/test/index.js | 28 + core-data/README.md | 49 ++ core-data/actions.js | 66 ++ core-data/index.js | 21 + core-data/reducer.js | 89 +++ core-data/resolvers.js | 46 ++ core-data/selectors.js | 71 +++ .../test/__mocks__/@wordpress/api-request.js | 1 + core-data/test/reducer.js | 111 ++++ core-data/test/resolvers.js | 68 +++ core-data/test/selectors.js | 94 +++ docs/deprecated.md | 29 + docs/extensibility/autocomplete.md | 85 +++ docs/extensibility/extending-blocks.md | 129 ++++ docs/extensibility/meta-box.md | 84 +++ docs/extensibility/theme-support.md | 55 ++ edit-post/README.md | 111 ++++ .../components/keyboard-shortcuts/index.js | 50 ++ .../plugin-more-menu-group/index.js | 28 + .../components/plugin-more-menu-item/index.js | 56 ++ .../plugin-sidebar-more-menu-item.js | 41 ++ .../components/sidebar/block-sidebar/index.js | 31 + .../sidebar/block-sidebar/style.scss | 21 + .../sidebar/document-sidebar/index.js | 44 ++ .../sidebar/plugin-sidebar/index.js | 41 ++ .../sidebar/post-taxonomies/taxonomy-panel.js | 46 ++ .../sidebar/settings-header/index.js | 48 ++ .../sidebar/settings-header/style.scss | 29 + .../sidebar/sidebar-header/index.js | 51 ++ .../sidebar/sidebar-header/style.scss | 29 + edit-post/components/text-editor/index.js | 22 + edit-post/components/text-editor/style.scss | 40 ++ .../visual-editor/block-inspector-button.js | 55 ++ edit-post/components/visual-editor/index.js | 47 ++ edit-post/components/visual-editor/style.scss | 124 ++++ edit-post/editor.js | 24 + edit-post/hooks/blocks/index.js | 17 + edit-post/hooks/blocks/media-upload/index.js | 175 ++++++ edit-post/store/utils.js | 18 + editor/components/block-drop-zone/style.scss | 29 + .../components/block-list/block-draggable.js | 31 + editor/components/block-list/breadcrumb.js | 100 ++++ .../components/block-list/with-hover-areas.js | 62 ++ editor/components/block-mover/arrows.js | 11 + .../block-mover/mover-description.js | 111 ++++ .../block-mover/test/mover-description.js | 112 ++++ .../block-duplicate-button.js | 64 ++ .../shared-block-settings.js | 92 +++ .../test/shared-block-settings.js | 59 ++ .../test/__snapshots__/index.js.snap | 10 + .../multi-blocks-switcher.js.snap | 13 + .../components/block-switcher/test/index.js | 158 +++++ .../test/multi-blocks-switcher.js | 42 ++ editor/components/block-title/README.md | 10 + editor/components/block-title/index.js | 41 ++ editor/components/block-title/test/index.js | 43 ++ editor/components/observe-typing/README.md | 18 + editor/components/observe-typing/index.js | 194 ++++++ .../preserve-scroll-in-reorder/index.js | 80 +++ .../skip-to-selected-block/index.js | 31 + .../skip-to-selected-block/style.scss | 20 + .../template-validation-notice/index.js | 50 ++ .../template-validation-notice/style.scss | 9 + editor/components/writing-flow/style.scss | 10 + editor/store/array.js | 41 ++ editor/store/test/array.js | 45 ++ editor/utils/block-list.js | 69 +++ editor/utils/dom.js | 54 ++ element/serialize.js | 565 ++++++++++++++++++ element/test/serialize.js | 524 ++++++++++++++++ eslint/config.js | 161 +++++ phpunit/class-gutenberg-rest-api-test.php | 99 +++ plugins/README.md | 83 +++ plugins/api/index.js | 112 ++++ plugins/api/test/index.js | 57 ++ plugins/components/index.js | 2 + plugins/components/plugin-area/index.js | 75 +++ plugins/components/plugin-context/index.js | 8 + plugins/index.js | 2 + test/e2e/jest.config.json | 7 + .../__snapshots__/adding-blocks.test.js.snap | 21 + .../splitting-merging.test.js.snap | 17 + .../__snapshots__/templates.test.js.snap | 27 + test/e2e/specs/a11y.test.js | 26 + test/e2e/specs/adding-blocks.test.js | 98 +++ test/e2e/specs/hello.test.js | 31 + test/e2e/specs/managing-links.test.js | 66 ++ test/e2e/specs/meta-boxes.test.js | 41 ++ test/e2e/specs/multi-block-selection.test.js | 78 +++ test/e2e/specs/splitting-merging.test.js | 55 ++ test/e2e/specs/templates.test.js | 30 + test/e2e/support/bootstrap.js | 20 + test/e2e/support/plugins.js | 60 ++ test/e2e/support/utils.js | 60 ++ test/e2e/test-plugins/meta-box.php | 25 + test/e2e/test-plugins/templates.php | 46 ++ test/unit/jest.config.json | 22 + viewport/README.md | 65 ++ viewport/if-viewport-matches.js | 32 + viewport/index.js | 78 +++ viewport/store/actions.js | 15 + viewport/store/index.js | 17 + viewport/store/reducer.js | 19 + viewport/store/selectors.js | 28 + viewport/store/test/reducer.js | 31 + viewport/store/test/selectors.js | 26 + viewport/test/if-viewport-matches.js | 39 ++ viewport/test/with-viewport-match.js | 29 + viewport/with-viewport-match.js | 32 + 180 files changed, 9911 insertions(+) create mode 100644 .eslintrc.js create mode 100644 blocks/api/raw-handling/test/embedded-content-reducer.js create mode 100644 blocks/api/raw-handling/test/integration/classic-in.html create mode 100644 blocks/api/raw-handling/test/integration/classic-out.html create mode 100644 blocks/api/templates.js create mode 100644 blocks/api/test/templates.js create mode 100644 blocks/autocomplete/README.md create mode 100644 blocks/autocomplete/index.js create mode 100644 blocks/autocomplete/test/index.js create mode 100644 blocks/autocompleters/README.md create mode 100644 blocks/autocompleters/block.js create mode 100644 blocks/autocompleters/test/block.js create mode 100644 blocks/autocompleters/user.js create mode 100644 blocks/editor-settings/index.js create mode 100644 blocks/hooks/default-autocompleters.js create mode 100644 blocks/hooks/test/default-autocompleters.js create mode 100644 blocks/inspector-advanced-controls/index.js create mode 100644 blocks/library/block/indicator/index.js create mode 100644 blocks/library/block/indicator/style.scss create mode 100644 blocks/library/columns/editor.scss create mode 100644 blocks/library/nextpage/editor.scss create mode 100644 blocks/library/nextpage/index.js create mode 100644 blocks/library/nextpage/test/__snapshots__/index.js.snap create mode 100644 blocks/library/nextpage/test/index.js create mode 100644 blocks/test/fixtures/core__nextpage.html create mode 100644 blocks/test/fixtures/core__nextpage.json create mode 100644 blocks/test/fixtures/core__nextpage.parsed.json create mode 100644 blocks/test/fixtures/core__nextpage.serialized.html create mode 100644 components/autocomplete/README.md create mode 100644 components/autocomplete/completer-compat.js create mode 100644 components/button-group/index.js create mode 100644 components/button-group/style.scss create mode 100644 components/code-editor/README.md create mode 100644 components/code-editor/editor.js create mode 100644 components/code-editor/index.js create mode 100644 components/code-editor/test/__snapshots__/editor.js.snap create mode 100644 components/code-editor/test/editor.js create mode 100644 components/dashicon/style.scss create mode 100644 components/disabled/README.md create mode 100644 components/disabled/index.js create mode 100644 components/disabled/style.scss create mode 100644 components/disabled/test/index.js create mode 100644 components/draggable/README.md create mode 100644 components/draggable/index.js create mode 100644 components/draggable/style.scss create mode 100644 components/drop-zone/README.md create mode 100644 components/focusable-iframe/README.md create mode 100644 components/focusable-iframe/index.js create mode 100644 components/higher-order/if-condition/README.md create mode 100644 components/higher-order/if-condition/index.js create mode 100644 components/higher-order/with-global-events/README.md create mode 100644 components/higher-order/with-global-events/index.js create mode 100644 components/higher-order/with-global-events/listener.js create mode 100644 components/higher-order/with-global-events/test/index.js create mode 100644 components/higher-order/with-global-events/test/listener.js create mode 100644 components/menu-group/index.js create mode 100644 components/menu-group/style.scss create mode 100644 components/menu-group/test/__snapshots__/index.js.snap create mode 100644 components/menu-group/test/index.js create mode 100644 components/menu-item/index.js create mode 100644 components/menu-item/shortcut.js create mode 100644 components/menu-item/style.scss create mode 100644 components/menu-item/test/__snapshots__/index.js.snap create mode 100644 components/menu-item/test/index.js create mode 100644 components/menu-items-choice/index.js create mode 100644 components/query-controls/category-select.js create mode 100644 components/query-controls/index.js create mode 100644 components/scroll-lock/README.md create mode 100644 components/scroll-lock/index.js create mode 100644 components/scroll-lock/index.scss create mode 100644 components/scroll-lock/test/index.js create mode 100644 components/slot-fill/test/index.js create mode 100644 core-data/README.md create mode 100644 core-data/actions.js create mode 100644 core-data/index.js create mode 100644 core-data/reducer.js create mode 100644 core-data/resolvers.js create mode 100644 core-data/selectors.js create mode 100644 core-data/test/__mocks__/@wordpress/api-request.js create mode 100644 core-data/test/reducer.js create mode 100644 core-data/test/resolvers.js create mode 100644 core-data/test/selectors.js create mode 100644 docs/deprecated.md create mode 100644 docs/extensibility/autocomplete.md create mode 100644 docs/extensibility/extending-blocks.md create mode 100644 docs/extensibility/meta-box.md create mode 100644 docs/extensibility/theme-support.md create mode 100644 edit-post/README.md create mode 100644 edit-post/components/keyboard-shortcuts/index.js create mode 100644 edit-post/components/plugin-more-menu-group/index.js create mode 100644 edit-post/components/plugin-more-menu-item/index.js create mode 100644 edit-post/components/plugin-more-menu-item/plugin-sidebar-more-menu-item.js create mode 100644 edit-post/components/sidebar/block-sidebar/index.js create mode 100644 edit-post/components/sidebar/block-sidebar/style.scss create mode 100644 edit-post/components/sidebar/document-sidebar/index.js create mode 100644 edit-post/components/sidebar/plugin-sidebar/index.js create mode 100644 edit-post/components/sidebar/post-taxonomies/taxonomy-panel.js create mode 100644 edit-post/components/sidebar/settings-header/index.js create mode 100644 edit-post/components/sidebar/settings-header/style.scss create mode 100644 edit-post/components/sidebar/sidebar-header/index.js create mode 100644 edit-post/components/sidebar/sidebar-header/style.scss create mode 100644 edit-post/components/text-editor/index.js create mode 100644 edit-post/components/text-editor/style.scss create mode 100644 edit-post/components/visual-editor/block-inspector-button.js create mode 100644 edit-post/components/visual-editor/index.js create mode 100644 edit-post/components/visual-editor/style.scss create mode 100644 edit-post/editor.js create mode 100644 edit-post/hooks/blocks/index.js create mode 100644 edit-post/hooks/blocks/media-upload/index.js create mode 100644 edit-post/store/utils.js create mode 100644 editor/components/block-drop-zone/style.scss create mode 100644 editor/components/block-list/block-draggable.js create mode 100644 editor/components/block-list/breadcrumb.js create mode 100644 editor/components/block-list/with-hover-areas.js create mode 100644 editor/components/block-mover/arrows.js create mode 100644 editor/components/block-mover/mover-description.js create mode 100644 editor/components/block-mover/test/mover-description.js create mode 100644 editor/components/block-settings-menu/block-duplicate-button.js create mode 100644 editor/components/block-settings-menu/shared-block-settings.js create mode 100644 editor/components/block-settings-menu/test/shared-block-settings.js create mode 100644 editor/components/block-switcher/test/__snapshots__/index.js.snap create mode 100644 editor/components/block-switcher/test/__snapshots__/multi-blocks-switcher.js.snap create mode 100644 editor/components/block-switcher/test/index.js create mode 100644 editor/components/block-switcher/test/multi-blocks-switcher.js create mode 100644 editor/components/block-title/README.md create mode 100644 editor/components/block-title/index.js create mode 100644 editor/components/block-title/test/index.js create mode 100644 editor/components/observe-typing/README.md create mode 100644 editor/components/observe-typing/index.js create mode 100644 editor/components/preserve-scroll-in-reorder/index.js create mode 100644 editor/components/skip-to-selected-block/index.js create mode 100644 editor/components/skip-to-selected-block/style.scss create mode 100644 editor/components/template-validation-notice/index.js create mode 100644 editor/components/template-validation-notice/style.scss create mode 100644 editor/components/writing-flow/style.scss create mode 100644 editor/store/array.js create mode 100644 editor/store/test/array.js create mode 100644 editor/utils/block-list.js create mode 100644 editor/utils/dom.js create mode 100644 element/serialize.js create mode 100644 element/test/serialize.js create mode 100644 eslint/config.js create mode 100644 phpunit/class-gutenberg-rest-api-test.php create mode 100644 plugins/README.md create mode 100644 plugins/api/index.js create mode 100644 plugins/api/test/index.js create mode 100644 plugins/components/index.js create mode 100644 plugins/components/plugin-area/index.js create mode 100644 plugins/components/plugin-context/index.js create mode 100644 plugins/index.js create mode 100644 test/e2e/jest.config.json create mode 100644 test/e2e/specs/__snapshots__/adding-blocks.test.js.snap create mode 100644 test/e2e/specs/__snapshots__/splitting-merging.test.js.snap create mode 100644 test/e2e/specs/__snapshots__/templates.test.js.snap create mode 100644 test/e2e/specs/a11y.test.js create mode 100644 test/e2e/specs/adding-blocks.test.js create mode 100644 test/e2e/specs/hello.test.js create mode 100644 test/e2e/specs/managing-links.test.js create mode 100644 test/e2e/specs/meta-boxes.test.js create mode 100644 test/e2e/specs/multi-block-selection.test.js create mode 100644 test/e2e/specs/splitting-merging.test.js create mode 100644 test/e2e/specs/templates.test.js create mode 100644 test/e2e/support/bootstrap.js create mode 100644 test/e2e/support/plugins.js create mode 100644 test/e2e/support/utils.js create mode 100644 test/e2e/test-plugins/meta-box.php create mode 100644 test/e2e/test-plugins/templates.php create mode 100644 test/unit/jest.config.json create mode 100644 viewport/README.md create mode 100644 viewport/if-viewport-matches.js create mode 100644 viewport/index.js create mode 100644 viewport/store/actions.js create mode 100644 viewport/store/index.js create mode 100644 viewport/store/reducer.js create mode 100644 viewport/store/selectors.js create mode 100644 viewport/store/test/reducer.js create mode 100644 viewport/store/test/selectors.js create mode 100644 viewport/test/if-viewport-matches.js create mode 100644 viewport/test/with-viewport-match.js create mode 100644 viewport/with-viewport-match.js diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 00000000000000..60f644278265f0 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,100 @@ +/** + * External dependencies + */ +const { escapeRegExp } = require( 'lodash' ); + +/** + * Internal dependencies + */ +const { version } = require( './package' ); + +/** + * Regular expression string matching a SemVer string with equal major/minor to + * the current package version. Used in identifying deprecations. + * + * @type {string} + */ +const majorMinorRegExp = escapeRegExp( version.replace( /\.\d+$/, '' ) ) + '(\\.\\d+)?'; + +module.exports = { + root: true, + extends: [ + './eslint/config.js', + 'plugin:jest/recommended' + ], + env: { + 'jest/globals': true, + }, + globals: { + wpApiSettings: true, + }, + plugins: [ + 'jest', + ], + rules: { + 'no-restricted-syntax': [ + 'error', + { + selector: 'ImportDeclaration[source.value=/^@wordpress\\u002F.+\\u002F/]', + message: 'Path access on WordPress dependencies is not allowed.', + }, + { + selector: 'ImportDeclaration[source.value=/^blocks$/]', + message: 'Use @wordpress/blocks as import path instead.', + }, + { + selector: 'ImportDeclaration[source.value=/^components$/]', + message: 'Use @wordpress/components as import path instead.', + }, + { + selector: 'ImportDeclaration[source.value=/^date$/]', + message: 'Use @wordpress/date as import path instead.', + }, + { + selector: 'ImportDeclaration[source.value=/^editor$/]', + message: 'Use @wordpress/editor as import path instead.', + }, + { + selector: 'ImportDeclaration[source.value=/^element$/]', + message: 'Use @wordpress/element as import path instead.', + }, + { + selector: 'ImportDeclaration[source.value=/^data$/]', + message: 'Use @wordpress/data as import path instead.', + }, + { + selector: 'ImportDeclaration[source.value=/^utils$/]', + message: 'Use @wordpress/utils as import path instead.', + }, + { + selector: 'ImportDeclaration[source.value=/^edit-post$/]', + message: 'Use @wordpress/edit-post as import path instead.', + }, + { + selector: 'ImportDeclaration[source.value=/^viewport$/]', + message: 'Use @wordpress/viewport as import path instead.', + }, + { + selector: 'ImportDeclaration[source.value=/^plugins$/]', + message: 'Use @wordpress/plugins as import path instead.', + }, + { + "selector": "ImportDeclaration[source.value=/^core-data$/]", + "message": "Use @wordpress/core-data as import path instead." + }, + { + selector: 'CallExpression[callee.name="deprecated"] Property[key.name="version"][value.value=/' + majorMinorRegExp + '/]', + message: 'Deprecated functions must be removed before releasing this version.', + }, + ], + }, + overrides: [ + { + files: [ 'test/e2e/**/*.js' ], + globals: { + page: true, + browser: true, + }, + }, + ], +}; diff --git a/blocks/api/raw-handling/test/embedded-content-reducer.js b/blocks/api/raw-handling/test/embedded-content-reducer.js new file mode 100644 index 00000000000000..a05e2f5eb8ab99 --- /dev/null +++ b/blocks/api/raw-handling/test/embedded-content-reducer.js @@ -0,0 +1,22 @@ +/** + * Internal dependencies + */ +import embeddedContentReducer from '../embedded-content-reducer'; +import { deepFilterHTML } from '../utils'; + +describe( 'embeddedContentReducer', () => { + it( 'should move embedded content from paragraph', () => { + expect( deepFilterHTML( '<p><strong>test<img class="one"></strong><img class="two"></p>', [ embeddedContentReducer ] ) ) + .toEqual( '<img class="one"><img class="two"><p><strong>test</strong></p>' ); + } ); + + it( 'should move an anchor with just an image from paragraph', () => { + expect( deepFilterHTML( '<p><a href="#"><img class="one"></a><strong>test</strong></p>', [ embeddedContentReducer ] ) ) + .toEqual( '<a href="#"><img class="one"></a><p><strong>test</strong></p>' ); + } ); + + it( 'should move multiple images', () => { + expect( deepFilterHTML( '<p><a href="#"><img class="one"></a><img class="two"><strong>test</strong></p>', [ embeddedContentReducer ] ) ) + .toEqual( '<a href="#"><img class="one"></a><img class="two"><p><strong>test</strong></p>' ); + } ); +} ); diff --git a/blocks/api/raw-handling/test/integration/classic-in.html b/blocks/api/raw-handling/test/integration/classic-in.html new file mode 100644 index 00000000000000..82d9d73687ed86 --- /dev/null +++ b/blocks/api/raw-handling/test/integration/classic-in.html @@ -0,0 +1,8 @@ +<p>First paragraph</p> +<p><!--more--></p> +<p>Second paragraph</p> +<p>Third paragraph</p> + +<p>Fourth <!--more--> paragraph</p> +<p>Fifth paragraph</p> +<p>Sixth paragraph</p> diff --git a/blocks/api/raw-handling/test/integration/classic-out.html b/blocks/api/raw-handling/test/integration/classic-out.html new file mode 100644 index 00000000000000..1298053376dfa0 --- /dev/null +++ b/blocks/api/raw-handling/test/integration/classic-out.html @@ -0,0 +1,31 @@ +<!-- wp:paragraph --> +<p>First paragraph</p> +<!-- /wp:paragraph --> + +<!-- wp:more --> +<!--more--> +<!-- /wp:more --> + +<!-- wp:paragraph --> +<p>Second paragraph</p> +<!-- /wp:paragraph --> + +<!-- wp:paragraph --> +<p>Third paragraph</p> +<!-- /wp:paragraph --> + +<!-- wp:paragraph --> +<p>Fourth paragraph</p> +<!-- /wp:paragraph --> + +<!-- wp:more --> +<!--more--> +<!-- /wp:more --> + +<!-- wp:paragraph --> +<p>Fifth paragraph</p> +<!-- /wp:paragraph --> + +<!-- wp:paragraph --> +<p>Sixth paragraph</p> +<!-- /wp:paragraph --> diff --git a/blocks/api/templates.js b/blocks/api/templates.js new file mode 100644 index 00000000000000..b0cc2daf1aa96b --- /dev/null +++ b/blocks/api/templates.js @@ -0,0 +1,60 @@ +/** + * External dependencies + */ +import { every, map } from 'lodash'; + +/** + * Internal dependencies + */ +import { createBlock } from './factory'; + +/** + * Checks whether a list of blocks matches a template by comparing the block names. + * + * @param {Array} blocks Block list. + * @param {Array} template Block template. + * + * @return {boolean} Whether the list of blocks matches a templates + */ +export function doBlocksMatchTemplate( blocks = [], template = [] ) { + return ( + blocks.length === template.length && + every( template, ( [ name, , innerBlocksTemplate ], index ) => { + const block = blocks[ index ]; + return ( + name === block.name && + doBlocksMatchTemplate( block.innerBlocks, innerBlocksTemplate ) + ); + } ) + ); +} + +/** + * Synchronize a block list with a block template. + * + * Synchronnizing a block list with a block template means that we loop over the blocks + * keep the block as is if it matches the block at the same position in the template + * (If it has the same name) and if doesn't match, we create a new block based on the template. + * Extra blocks not present in the template are removed. + * + * @param {Array} blocks Block list. + * @param {Array} template Block template. + * + * @return {Array} Updated Block list. + */ +export function synchronizeBlocksWithTemplate( blocks = [], template = [] ) { + return map( template, ( [ name, attributes, innerBlocksTemplate ], index ) => { + const block = blocks[ index ]; + + if ( block && block.name === name ) { + const innerBlocks = synchronizeBlocksWithTemplate( block.innerBlocks, innerBlocksTemplate ); + return { ...block, innerBlocks }; + } + + return createBlock( + name, + attributes, + synchronizeBlocksWithTemplate( [], innerBlocksTemplate ) + ); + } ); +} diff --git a/blocks/api/test/templates.js b/blocks/api/test/templates.js new file mode 100644 index 00000000000000..1ee52fdadb7563 --- /dev/null +++ b/blocks/api/test/templates.js @@ -0,0 +1,177 @@ +/** + * External dependencies + */ +import { noop } from 'lodash'; + +/** + * Internal dependencies + */ +import { createBlock } from '../factory'; +import { getBlockTypes, unregisterBlockType, registerBlockType } from '../registration'; +import { doBlocksMatchTemplate, synchronizeBlocksWithTemplate } from '../templates'; + +describe( 'templates', () => { + afterEach( () => { + getBlockTypes().forEach( ( block ) => { + unregisterBlockType( block.name ); + } ); + } ); + + beforeEach( () => { + registerBlockType( 'core/test-block', { + attributes: {}, + save: noop, + category: 'common', + title: 'test block', + } ); + + registerBlockType( 'core/test-block-2', { + attributes: {}, + save: noop, + category: 'common', + title: 'test block', + } ); + } ); + + describe( 'doBlocksMatchTemplate', () => { + it( 'return true if for empty templates and blocks', () => { + expect( doBlocksMatchTemplate() ).toBe( true ); + } ); + + it( 'return true if the template matches the blocks', () => { + const template = [ + [ 'core/test-block' ], + [ 'core/test-block-2' ], + [ 'core/test-block-2' ], + ]; + const blockList = [ + createBlock( 'core/test-block' ), + createBlock( 'core/test-block-2' ), + createBlock( 'core/test-block-2' ), + ]; + expect( doBlocksMatchTemplate( blockList, template ) ).toBe( true ); + } ); + + it( 'return true if the template matches the blocks with nested blocks', () => { + const template = [ + [ 'core/test-block' ], + [ 'core/test-block-2', {}, [ + [ 'core/test-block' ], + ] ], + [ 'core/test-block-2' ], + ]; + const blockList = [ + createBlock( 'core/test-block' ), + createBlock( 'core/test-block-2', {}, [ createBlock( 'core/test-block' ) ] ), + createBlock( 'core/test-block-2' ), + ]; + expect( doBlocksMatchTemplate( blockList, template ) ).toBe( true ); + } ); + + it( 'return false if the template length doesn\'t match the blocks length', () => { + const template = [ + [ 'core/test-block' ], + [ 'core/test-block-2' ], + ]; + const blockList = [ + createBlock( 'core/test-block' ), + createBlock( 'core/test-block-2' ), + createBlock( 'core/test-block-2' ), + ]; + expect( doBlocksMatchTemplate( blockList, template ) ).toBe( false ); + } ); + + it( 'return false if the nested template doesn\'t match the blocks', () => { + const template = [ + [ 'core/test-block' ], + [ 'core/test-block-2', {}, [ + [ 'core/test-block' ], + ] ], + [ 'core/test-block-2' ], + ]; + const blockList = [ + createBlock( 'core/test-block' ), + createBlock( 'core/test-block-2', {}, [ createBlock( 'core/test-block-2' ) ] ), + createBlock( 'core/test-block-2' ), + ]; + expect( doBlocksMatchTemplate( blockList, template ) ).toBe( false ); + } ); + } ); + + describe( 'synchronizeBlocksWithTemplate', () => { + it( 'should create blocks for each template entry', () => { + const template = [ + [ 'core/test-block' ], + [ 'core/test-block-2' ], + [ 'core/test-block-2' ], + ]; + const blockList = []; + expect( synchronizeBlocksWithTemplate( blockList, template ) ).toMatchObject( [ + { name: 'core/test-block' }, + { name: 'core/test-block-2' }, + { name: 'core/test-block-2' }, + ] ); + } ); + + it( 'should create nested blocks', () => { + const template = [ + [ 'core/test-block', {}, [ + [ 'core/test-block-2' ], + ] ], + ]; + const blockList = []; + expect( synchronizeBlocksWithTemplate( blockList, template ) ).toMatchObject( [ + { name: 'core/test-block', innerBlocks: [ + { name: 'core/test-block-2' }, + ] }, + ] ); + } ); + + it( 'should append blocks if more blocks in the template', () => { + const template = [ + [ 'core/test-block' ], + [ 'core/test-block-2' ], + [ 'core/test-block-2' ], + ]; + + const block1 = createBlock( 'core/test-block' ); + const block2 = createBlock( 'core/test-block-2' ); + const blockList = [ block1, block2 ]; + expect( synchronizeBlocksWithTemplate( blockList, template ) ).toMatchObject( [ + block1, + block2, + { name: 'core/test-block-2' }, + ] ); + } ); + + it( 'should replace blocks if not matching blocks are found', () => { + const template = [ + [ 'core/test-block' ], + [ 'core/test-block-2' ], + [ 'core/test-block-2' ], + ]; + + const block1 = createBlock( 'core/test-block' ); + const block2 = createBlock( 'core/test-block' ); + const blockList = [ block1, block2 ]; + expect( synchronizeBlocksWithTemplate( blockList, template ) ).toMatchObject( [ + block1, + { name: 'core/test-block-2' }, + { name: 'core/test-block-2' }, + ] ); + } ); + + it( 'should remove blocks if extra blocks are found', () => { + const template = [ + [ 'core/test-block' ], + ]; + + const block1 = createBlock( 'core/test-block' ); + const block2 = createBlock( 'core/test-block' ); + const blockList = [ block1, block2 ]; + expect( synchronizeBlocksWithTemplate( blockList, template ) ).toEqual( [ + block1, + ] ); + } ); + } ); +} ); diff --git a/blocks/autocomplete/README.md b/blocks/autocomplete/README.md new file mode 100644 index 00000000000000..bf3b564d3cb8e8 --- /dev/null +++ b/blocks/autocomplete/README.md @@ -0,0 +1,6 @@ +Autocomplete +============ + +This is an Autocomplete component for use in block UI. It is based on `Autocomplete` from `@wordpress/components` and takes the same props. In addition, it passes its autocompleters through a `blocks.Autocomplete.completers` filter to give developers an opportunity to override or extend them. + +The autocompleter interface is documented with the original `Autocomplete` component in `@wordpress/components`. diff --git a/blocks/autocomplete/index.js b/blocks/autocomplete/index.js new file mode 100644 index 00000000000000..df366d82106c23 --- /dev/null +++ b/blocks/autocomplete/index.js @@ -0,0 +1,109 @@ +/** + * External dependencies + */ +import { clone } from 'lodash'; + +/** + * WordPress dependencies + */ +import { applyFilters, hasFilter } from '@wordpress/hooks'; +import { Component } from '@wordpress/element'; +import { Autocomplete as OriginalAutocomplete } from '@wordpress/components'; + +/* + * Use one array instance for fallback rather than inline array literals + * because the latter may cause rerender due to failed prop equality checks. + */ +const completersFallback = []; + +/** + * Wrap the default Autocomplete component with one that + * supports a filter hook for customizing its list of autocompleters. + * + * Since there may be many Autocomplete instances at one time, this component + * applies the filter on demand, when the component is first focused after + * receiving a new list of completers. + * + * This function is exported for unit test. + * + * @param {Function} Autocomplete Original component. + * @return {Function} Wrapped component + */ +export function withFilteredAutocompleters( Autocomplete ) { + return class FilteredAutocomplete extends Component { + constructor() { + super(); + + this.state = { completers: completersFallback }; + + this.saveParentRef = this.saveParentRef.bind( this ); + this.onFocus = this.onFocus.bind( this ); + } + + componentDidUpdate() { + const hasFocus = this.parentNode.contains( document.activeElement ); + + /* + * It's possible for props to be updated when the component has focus, + * so here, we ensure new completers are immediately applied while we + * have the focus. + * + * NOTE: This may trigger another render but only when the component has focus. + */ + if ( hasFocus && this.hasStaleCompleters() ) { + this.updateCompletersState(); + } + } + + onFocus() { + if ( this.hasStaleCompleters() ) { + this.updateCompletersState(); + } + } + + hasStaleCompleters() { + return ( + ! ( 'lastFilteredCompletersProp' in this.state ) || + this.state.lastFilteredCompletersProp !== this.props.completers + ); + } + + updateCompletersState() { + let { completers: nextCompleters } = this.props; + const lastFilteredCompletersProp = nextCompleters; + + if ( hasFilter( 'blocks.Autocomplete.completers' ) ) { + nextCompleters = applyFilters( + 'blocks.Autocomplete.completers', + // Provide copies so filters may directly modify them. + nextCompleters && nextCompleters.map( clone ) + ); + } + + this.setState( { + lastFilteredCompletersProp, + completers: nextCompleters || completersFallback, + } ); + } + + saveParentRef( parentNode ) { + this.parentNode = parentNode; + } + + render() { + const { completers } = this.state; + const autocompleteProps = { + ...this.props, + completers, + }; + + return ( + <div onFocus={ this.onFocus } ref={ this.saveParentRef }> + <Autocomplete onFocus={ this.onFocus } { ...autocompleteProps } /> + </div> + ); + } + }; +} + +export default withFilteredAutocompleters( OriginalAutocomplete ); diff --git a/blocks/autocomplete/test/index.js b/blocks/autocomplete/test/index.js new file mode 100644 index 00000000000000..493a99d5ebcc76 --- /dev/null +++ b/blocks/autocomplete/test/index.js @@ -0,0 +1,122 @@ +/** + * External dependencies + */ +import { mount, shallow } from 'enzyme'; + +/** + * WordPress dependencies + */ +import { addFilter, removeFilter } from '@wordpress/hooks'; + +/** + * Internal dependencies + */ +import { withFilteredAutocompleters } from '..'; + +function TestComponent() { + // Use a naturally focusable element because we will test with focus. + return <input />; +} +const FilteredComponent = withFilteredAutocompleters( TestComponent ); + +describe( 'Autocomplete', () => { + let wrapper = null; + + afterEach( () => { + removeFilter( 'blocks.Autocomplete.completers', 'test/autocompleters-hook' ); + + if ( wrapper ) { + wrapper.unmount(); + wrapper = null; + } + } ); + + it( 'filters supplied completers when next focused', () => { + const completersFilter = jest.fn(); + addFilter( + 'blocks.Autocomplete.completers', + 'test/autocompleters-hook', + completersFilter + ); + + const expectedCompleters = [ {}, {}, {} ]; + wrapper = mount( <FilteredComponent completers={ expectedCompleters } /> ); + + expect( completersFilter ).not.toHaveBeenCalled(); + wrapper.find( 'input' ).simulate( 'focus' ); + expect( completersFilter ).toHaveBeenCalledWith( expectedCompleters ); + } ); + + it( 'filters completers supplied when already focused', () => { + wrapper = mount( <FilteredComponent completers={ [] } /> ); + + wrapper.find( 'input' ).getDOMNode().focus(); + expect( wrapper.getDOMNode().contains( document.activeElement ) ).toBeTruthy(); + + const completersFilter = jest.fn(); + addFilter( + 'blocks.Autocomplete.completers', + 'test/autocompleters-hook', + completersFilter + ); + + const expectedCompleters = [ {}, {}, {} ]; + + expect( completersFilter ).not.toHaveBeenCalled(); + wrapper.setProps( { completers: expectedCompleters } ); + expect( completersFilter ).toHaveBeenCalledWith( expectedCompleters ); + } ); + + it( 'provides copies of completers to filter', () => { + const completersFilter = jest.fn(); + addFilter( + 'blocks.Autocomplete.completers', + 'test/autocompleters-hook', + completersFilter + ); + + const specifiedCompleters = [ {}, {}, {} ]; + wrapper = mount( <FilteredComponent completers={ specifiedCompleters } /> ); + + expect( completersFilter ).not.toHaveBeenCalled(); + wrapper.find( 'input' ).simulate( 'focus' ); + expect( completersFilter ).toHaveBeenCalledTimes( 1 ); + + const [ actualCompleters ] = completersFilter.mock.calls[ 0 ]; + expect( actualCompleters ).not.toBe( specifiedCompleters ); + expect( actualCompleters ).toEqual( specifiedCompleters ); + } ); + + it( 'supplies filtered completers to inner component', () => { + const expectedFilteredCompleters = [ {}, {} ]; + const completersFilter = jest.fn( () => expectedFilteredCompleters ); + addFilter( + 'blocks.Autocomplete.completers', + 'test/autocompleters-hook', + completersFilter + ); + + wrapper = mount( <FilteredComponent /> ); + + wrapper.find( 'input' ).simulate( 'focus' ); + + const filteredComponentWrapper = wrapper.childAt( 0 ); + const innerComponentWrapper = filteredComponentWrapper.childAt( 0 ); + expect( innerComponentWrapper.name() ).toBe( 'TestComponent' ); + expect( innerComponentWrapper.prop( 'completers' ) ).toEqual( expectedFilteredCompleters ); + } ); + + it( 'passes props to inner component', () => { + const expectedProps = { + expected1: 1, + expected2: 'two', + expected3: '🌳', + }; + + wrapper = shallow( <FilteredComponent { ...expectedProps } /> ); + + const innerComponentWrapper = wrapper.childAt( 0 ); + expect( innerComponentWrapper.name() ).toBe( 'TestComponent' ); + expect( innerComponentWrapper.props() ).toMatchObject( expectedProps ); + } ); +} ); diff --git a/blocks/autocompleters/README.md b/blocks/autocompleters/README.md new file mode 100644 index 00000000000000..a802c02f75dbac --- /dev/null +++ b/blocks/autocompleters/README.md @@ -0,0 +1,4 @@ +Autocompleters +============== + +The Autocompleter interface is documented [here](../../components/autocomplete/README.md) with the `Autocomplete` component in `@wordpress/components`. diff --git a/blocks/autocompleters/block.js b/blocks/autocompleters/block.js new file mode 100644 index 00000000000000..67bbd72f930571 --- /dev/null +++ b/blocks/autocompleters/block.js @@ -0,0 +1,51 @@ +/** + * External dependencies + */ +import { sortBy, once } from 'lodash'; + +/** + * Internal dependencies + */ +import './style.scss'; +import { createBlock, getBlockTypes } from '../api'; +import BlockIcon from '../block-icon'; + +/** + * A blocks repeater for replacing the current block with a selected block type. + * + * @type {Completer} + */ +export default { + name: 'blocks', + className: 'blocks-autocompleters__block', + triggerPrefix: '/', + options: once( function options() { + return Promise.resolve( + // Prioritize common category in block type options + sortBy( + getBlockTypes(), + ( { category } ) => 'common' !== category + ) + ); + } ), + getOptionKeywords( blockSettings ) { + const { title, keywords = [] } = blockSettings; + return [ ...keywords, title ]; + }, + getOptionLabel( blockSettings ) { + const { icon, title } = blockSettings; + return [ + <BlockIcon key="icon" icon={ icon } />, + title, + ]; + }, + allowContext( before, after ) { + return ! ( /\S/.test( before.toString() ) || /\S/.test( after.toString() ) ); + }, + getOptionCompletion( blockData ) { + return { + action: 'replace', + value: createBlock( blockData.name ), + }; + }, +}; diff --git a/blocks/autocompleters/test/block.js b/blocks/autocompleters/test/block.js new file mode 100644 index 00000000000000..de395591ffba11 --- /dev/null +++ b/blocks/autocompleters/test/block.js @@ -0,0 +1,91 @@ +/** + * External dependencies + */ +import { noop } from 'lodash'; + +/** + * Internal dependencies + */ +import { registerBlockType, unregisterBlockType, getBlockTypes } from '../../api'; +import { blockAutocompleter } from '../'; + +describe( 'block', () => { + const blockTypes = { + 'core/foo': { + save: noop, + category: 'common', + title: 'foo', + keywords: [ 'foo-keyword-1', 'foo-keyword-2' ], + }, + 'core/bar': { + save: noop, + category: 'layout', + title: 'bar', + // Intentionally empty keyword list + keywords: [], + }, + 'core/baz': { + save: noop, + category: 'common', + title: 'baz', + // Intentionally omitted keyword list + }, + }; + + beforeEach( () => { + Object.entries( blockTypes ).forEach( + ( [ name, settings ] ) => registerBlockType( name, settings ) + ); + } ); + + afterEach( () => { + getBlockTypes().forEach( ( block ) => { + unregisterBlockType( block.name ); + } ); + } ); + + it( 'should prioritize common blocks in options', () => { + return blockAutocompleter.options().then( ( options ) => { + expect( options ).toMatchObject( [ + blockTypes[ 'core/foo' ], + blockTypes[ 'core/baz' ], + blockTypes[ 'core/bar' ], + ] ); + } ); + } ); + + it( 'should render a block option label composed of @wordpress/element Elements and/or strings', () => { + expect.hasAssertions(); + + // Only verify that a populated label is returned. + // It is likely to be fragile to assert that the contents are renderable by @wordpress/element. + const isAllowedLabelType = label => Array.isArray( label ) || ( typeof label === 'string' ); + + getBlockTypes().forEach( blockType => { + const label = blockAutocompleter.getOptionLabel( blockType ); + expect( isAllowedLabelType( label ) ).toBeTruthy(); + } ); + } ); + + it( 'should derive option keywords from block keywords and block title', () => { + const optionKeywords = getBlockTypes().reduce( + ( map, blockType ) => map.set( + blockType.name, + blockAutocompleter.getOptionKeywords( blockType ) + ), + new Map() + ); + + expect( optionKeywords.get( 'core/foo' ) ).toEqual( [ + 'foo-keyword-1', + 'foo-keyword-2', + blockTypes[ 'core/foo' ].title, + ] ); + expect( optionKeywords.get( 'core/bar' ) ).toEqual( [ + blockTypes[ 'core/bar' ].title, + ] ); + expect( optionKeywords.get( 'core/baz' ) ).toEqual( [ + blockTypes[ 'core/baz' ].title, + ] ); + } ); +} ); diff --git a/blocks/autocompleters/user.js b/blocks/autocompleters/user.js new file mode 100644 index 00000000000000..0532472e82b513 --- /dev/null +++ b/blocks/autocompleters/user.js @@ -0,0 +1,34 @@ +/** +* A user mentions completer. +* +* @type {Completer} +*/ +export default { + name: 'users', + className: 'blocks-autocompleters__user', + triggerPrefix: '@', + options( search ) { + let payload = ''; + if ( search ) { + payload = '?search=' + encodeURIComponent( search ); + } + return wp.apiRequest( { path: '/wp/v2/users' + payload } ); + }, + isDebounced: true, + getOptionKeywords( user ) { + return [ user.slug, user.name ]; + }, + getOptionLabel( user ) { + return [ + <img key="avatar" className="blocks-autocompleters__user-avatar" alt="" src={ user.avatar_urls[ 24 ] } />, + <span key="name" className="blocks-autocompleters__user-name">{ user.name }</span>, + <span key="slug" className="blocks-autocompleters__user-slug">{ user.slug }</span>, + ]; + }, + allowNode() { + return true; + }, + getOptionCompletion( user ) { + return `@${ user.slug }`; + }, +}; diff --git a/blocks/editor-settings/index.js b/blocks/editor-settings/index.js new file mode 100644 index 00000000000000..4162711bb8c5e1 --- /dev/null +++ b/blocks/editor-settings/index.js @@ -0,0 +1,60 @@ +/** + * WordPress dependencies + */ +import { createContext, createHigherOrderComponent } from '@wordpress/element'; + +/** + * The default editor settings + * + * alignWide boolean Enable/Disable Wide/Full Alignments + * colors Array Palette colors + * maxWidth number Max width to constraint resizing + * blockTypes boolean|Array Allowed block types + * hasFixedToolbar boolean Whether or not the editor toolbar is fixed + */ +const DEFAULT_SETTINGS = { + alignWide: false, + colors: [ + '#f78da7', + '#cf2e2e', + '#ff6900', + '#fcb900', + '#7bdcb5', + '#00d084', + '#8ed1fc', + '#0693e3', + '#eee', + '#abb8c3', + '#313131', + ], + + // This is current max width of the block inner area + // It's used to constraint image resizing and this value could be overriden later by themes + maxWidth: 608, + + // Allowed block types for the editor, defaulting to true (all supported). + allowedBlockTypes: true, +}; + +const EditorSettings = createContext( DEFAULT_SETTINGS ); +EditorSettings.defaultSettings = DEFAULT_SETTINGS; + +export default EditorSettings; + +export const withEditorSettings = ( mapSettingsToProps ) => createHigherOrderComponent( + ( Component ) => { + return function WithSettingsComponent( props ) { + return ( + <EditorSettings.Consumer> + { settings => ( + <Component + { ...props } + { ...( mapSettingsToProps ? mapSettingsToProps( settings, props ) : { settings } ) } + /> + ) } + </EditorSettings.Consumer> + ); + }; + }, + 'withEditorSettings' +); diff --git a/blocks/hooks/default-autocompleters.js b/blocks/hooks/default-autocompleters.js new file mode 100644 index 00000000000000..00f4535903daea --- /dev/null +++ b/blocks/hooks/default-autocompleters.js @@ -0,0 +1,31 @@ +/** + * External dependencies + */ +import { clone } from 'lodash'; + +/** + * WordPress dependencies + */ +import { addFilter } from '@wordpress/hooks'; + +/** + * Internal dependencies + */ +import { userAutocompleter } from '../autocompleters'; + +// Exported for unit test. +export const defaultAutocompleters = [ userAutocompleter ]; + +function setDefaultCompleters( completers ) { + if ( ! completers ) { + // Provide copies so filters may directly modify them. + completers = defaultAutocompleters.map( clone ); + } + return completers; +} + +addFilter( + 'blocks.Autocomplete.completers', + 'blocks/autocompleters/set-default-completers', + setDefaultCompleters +); diff --git a/blocks/hooks/test/default-autocompleters.js b/blocks/hooks/test/default-autocompleters.js new file mode 100644 index 00000000000000..38f102f4e617a9 --- /dev/null +++ b/blocks/hooks/test/default-autocompleters.js @@ -0,0 +1,43 @@ +/** + * WordPress dependencies + */ +import { applyFilters } from '@wordpress/hooks'; + +/** + * Internal dependencies + */ +import { defaultAutocompleters } from '../default-autocompleters'; + +describe( 'default-autocompleters', () => { + it( 'provides default completers if none are provided', () => { + const result = applyFilters( 'blocks.Autocomplete.completers', null ); + /* + * Assert structural equality because defaults are provided as a + * list of cloned completers (and not referentially equal). + */ + expect( result ).toEqual( defaultAutocompleters ); + } ); + + it( 'does not provide default completers for empty completer list', () => { + const emptyList = []; + const result = applyFilters( 'blocks.Autocomplete.completers', emptyList ); + // Assert referential equality because the list should be unchanged. + expect( result ).toBe( emptyList ); + } ); + + it( 'does not provide default completers for a populated completer list', () => { + const populatedList = [ {}, {} ]; + const result = applyFilters( 'blocks.Autocomplete.completers', populatedList ); + // Assert referential equality because the list should be unchanged. + expect( result ).toBe( populatedList ); + } ); + + it( 'provides copies of defaults so they may be directly modified', () => { + const result = applyFilters( 'blocks.Autocomplete.completers', null ); + result.forEach( ( completer, i ) => { + const defaultCompleter = defaultAutocompleters[ i ]; + expect( completer ).not.toBe( defaultCompleter ); + expect( completer ).toEqual( defaultCompleter ); + } ); + } ); +} ); diff --git a/blocks/inspector-advanced-controls/index.js b/blocks/inspector-advanced-controls/index.js new file mode 100644 index 00000000000000..b725f38ee72bf4 --- /dev/null +++ b/blocks/inspector-advanced-controls/index.js @@ -0,0 +1,6 @@ +/** + * WordPress dependencies + */ +import { createSlotFill } from '@wordpress/components'; + +export default createSlotFill( 'InspectorAdvancedControls' ); diff --git a/blocks/library/block/indicator/index.js b/blocks/library/block/indicator/index.js new file mode 100644 index 00000000000000..ffeb4bffcfb324 --- /dev/null +++ b/blocks/library/block/indicator/index.js @@ -0,0 +1,22 @@ +/** + * WordPress dependencies + */ +import { Tooltip, Dashicon } from '@wordpress/components'; +import { __, sprintf } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import './style.scss'; + +function SharedBlockIndicator( { title } ) { + return ( + <Tooltip text={ sprintf( __( 'Shared Block: %s' ), title ) }> + <span className="shared-block-indicator"> + <Dashicon icon="controls-repeat" /> + </span> + </Tooltip> + ); +} + +export default SharedBlockIndicator; diff --git a/blocks/library/block/indicator/style.scss b/blocks/library/block/indicator/style.scss new file mode 100644 index 00000000000000..19f2f80557050f --- /dev/null +++ b/blocks/library/block/indicator/style.scss @@ -0,0 +1,12 @@ +.shared-block-indicator { + background: $white; + border-left: 1px dashed $light-gray-500; + color: $dark-gray-500; + border-top: 1px dashed $light-gray-500; + bottom: -$block-padding; + height: 30px; + padding: 5px; + position: absolute; + right: -$block-padding; + width: 30px; +} diff --git a/blocks/library/columns/editor.scss b/blocks/library/columns/editor.scss new file mode 100644 index 00000000000000..f23c87cb7dc138 --- /dev/null +++ b/blocks/library/columns/editor.scss @@ -0,0 +1,54 @@ +// These margins make sure that nested blocks stack/overlay with the parent block chrome +// This is sort of an experiment at making sure the editor looks as much like the end result as possible +// Potentially the rules here can apply to all nested blocks and enable stacking, in which case it should be moved elsewhere +.wp-block-columns .editor-block-list__layout { + &:first-child { + margin-left: -$block-padding; + } + &:last-child { + margin-right: -$block-padding; + } + + // This max-width is used to constrain the main editor column, it should not cascade into columns + .editor-block-list__block { + max-width: none; + } +} + +// Wide: show no left/right margin on wide, so they stack with the column side UI +.editor-block-list__block[data-align="wide"] .wp-block-columns .editor-block-list__layout { + &:first-child { + margin-left: 0; + } + &:last-child { + margin-right: 0; + } +} + +// Fullwide: show margin left/right to ensure there's room for the side UI +// This is not a 1:1 preview with the front-end where these margins would presumably be zero +// @todo this could be revisited, by for example showing this margin only when the parent block was selected first +// Then at least an unselected columns block would be an accurate preview +.editor-block-list__block[data-align="full"] .wp-block-columns .editor-block-list__layout { + &:first-child { + margin-left: $block-side-ui-padding; + } + &:last-child { + margin-right: $block-side-ui-padding; + } +} + +// Hide appender shortcuts in columns +// @todo This essentially duplicates the mobile styles for the appender component +// It would be nice to be able to use element queries in that component instead https://github.com/tomhodgins/element-queries-spec +.wp-block-columns { + .editor-inserter-with-shortcuts { + display: none; + } + + .editor-block-list__empty-block-inserter, + .editor-default-block-appender .editor-inserter { + left: auto; + right: $item-spacing; + } +} diff --git a/blocks/library/nextpage/editor.scss b/blocks/library/nextpage/editor.scss new file mode 100644 index 00000000000000..2811a2ae2f14be --- /dev/null +++ b/blocks/library/nextpage/editor.scss @@ -0,0 +1,35 @@ +.editor-visual-editor__block[data-type="core/nextpage"] { + max-width: 100%; +} + +.wp-block-nextpage { + display: block; + text-align: center; + white-space: nowrap; + + // Label + > span { + position: relative; + display: inline-block; + font-size: 12px; + text-transform: uppercase; + font-weight: 600; + font-family: $default-font; + color: $dark-gray-300; + border-radius: 4px; + background: $white; + padding: 6px 8px; + height: $icon-button-size-small; + } + + // Dashed line + &:before { + content: ''; + position: absolute; + top: calc( 50% ); + left: 0; + right: 0; + border-top: 3px dashed $light-gray-700; + z-index: z-index( '.editor-block-list__block .wp-block-more:before' ); + } +} diff --git a/blocks/library/nextpage/index.js b/blocks/library/nextpage/index.js new file mode 100644 index 00000000000000..e67e276609f8a6 --- /dev/null +++ b/blocks/library/nextpage/index.js @@ -0,0 +1,61 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { RawHTML } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import './editor.scss'; +import { createBlock } from '../../api'; + +export const name = 'core/nextpage'; + +export const settings = { + title: __( 'Page break' ), + + description: __( 'This block allows you to set break points on your post. Visitors of your blog are then presented with content split into multiple pages.' ), + + icon: 'admin-page', + + category: 'layout', + + keywords: [ __( 'next page' ), __( 'pagination' ) ], + + supports: { + customClassName: false, + className: false, + html: false, + }, + + attributes: {}, + + transforms: { + from: [ + { + type: 'raw', + isMatch: ( node ) => node.dataset && node.dataset.block === 'core/nextpage', + transform() { + return createBlock( 'core/nextpage', {} ); + }, + }, + ], + }, + + edit() { + return ( + <div className="wp-block-nextpage"> + <span>{ __( 'Page break' ) }</span> + </div> + ); + }, + + save() { + return ( + <RawHTML> + { '<!--nextpage-->' } + </RawHTML> + ); + }, +}; diff --git a/blocks/library/nextpage/test/__snapshots__/index.js.snap b/blocks/library/nextpage/test/__snapshots__/index.js.snap new file mode 100644 index 00000000000000..9a324d23afcca1 --- /dev/null +++ b/blocks/library/nextpage/test/__snapshots__/index.js.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`core/nextpage block edit matches snapshot 1`] = ` +<div + class="wp-block-nextpage" +> + <span> + Page break + </span> +</div> +`; diff --git a/blocks/library/nextpage/test/index.js b/blocks/library/nextpage/test/index.js new file mode 100644 index 00000000000000..3658f45050fecf --- /dev/null +++ b/blocks/library/nextpage/test/index.js @@ -0,0 +1,13 @@ +/** + * Internal dependencies + */ +import { name, settings } from '../'; +import { blockEditRender } from 'blocks/test/helpers'; + +describe( 'core/nextpage', () => { + test( 'block edit matches snapshot', () => { + const wrapper = blockEditRender( name, settings ); + + expect( wrapper ).toMatchSnapshot(); + } ); +} ); diff --git a/blocks/test/fixtures/core__nextpage.html b/blocks/test/fixtures/core__nextpage.html new file mode 100644 index 00000000000000..9bf78b8cbf7924 --- /dev/null +++ b/blocks/test/fixtures/core__nextpage.html @@ -0,0 +1,3 @@ +<!-- wp:core/nextpage --> +<!--nextpage--> +<!-- /wp:core/nextpage --> diff --git a/blocks/test/fixtures/core__nextpage.json b/blocks/test/fixtures/core__nextpage.json new file mode 100644 index 00000000000000..5ce9cb21fc6367 --- /dev/null +++ b/blocks/test/fixtures/core__nextpage.json @@ -0,0 +1,10 @@ +[ + { + "uid": "_uid_0", + "name": "core/nextpage", + "isValid": true, + "attributes": {}, + "innerBlocks": [], + "originalContent": "<!--nextpage-->" + } +] diff --git a/blocks/test/fixtures/core__nextpage.parsed.json b/blocks/test/fixtures/core__nextpage.parsed.json new file mode 100644 index 00000000000000..0dde31a4bf41ad --- /dev/null +++ b/blocks/test/fixtures/core__nextpage.parsed.json @@ -0,0 +1,12 @@ +[ + { + "blockName": "core/nextpage", + "attrs": null, + "innerBlocks": [], + "innerHTML": "\n<!--nextpage-->\n" + }, + { + "attrs": {}, + "innerHTML": "\n" + } +] diff --git a/blocks/test/fixtures/core__nextpage.serialized.html b/blocks/test/fixtures/core__nextpage.serialized.html new file mode 100644 index 00000000000000..beef64ebd56d36 --- /dev/null +++ b/blocks/test/fixtures/core__nextpage.serialized.html @@ -0,0 +1,3 @@ +<!-- wp:nextpage --> +<!--nextpage--> +<!-- /wp:nextpage --> diff --git a/components/autocomplete/README.md b/components/autocomplete/README.md new file mode 100644 index 00000000000000..a3a3f8a979b9f0 --- /dev/null +++ b/components/autocomplete/README.md @@ -0,0 +1,121 @@ +Autocomplete +============ + +This component is used to provide autocompletion support for a child input component. + +## Autocompleters + +Autocompleters enable us to offer users options for completing text input. For example, Gutenberg includes a user autocompleter that provides a list of user names and completes a selection with a user mention like `@mary`. + +Each completer declares: + +* Its name. +* The text prefix that should trigger the display of completion options. +* Raw option data. +* How to render an option's label. +* An option's keywords, words that will be used to match an option with user input. +* What the completion of an option looks like, including whether it should be inserted in the text or used to replace the current block. + +In addition, a completer may optionally declare: + +* A class name to be applied to the completion menu. +* Whether it should apply to a specified text node. +* Whether the completer applies in a given context, defined via a Range before and a Range after the autocompletion trigger and query. + +### The Completer Interface + +#### name + +The name of the completer. Useful for identifying a specific completer to be overridden via extensibility hooks. + +- Type: `String` +- Required: Yes + +#### options + +The raw options for completion. May be an array, a function that returns an array, or a function that returns a promise for an array. + +Options may be of any type or shape. The completer declares how those options are rendered and what their completions should be when selected. + +- Type: `Array|Function` +- Required: Yes + +#### triggerPrefix + +The string prefix that should trigger the completer. For example, Gutenberg's block completer is triggered when the '/' character is entered. + +- Type: `String` +- Required: Yes + +#### getOptionLabel + +A function that returns the label for a given option. A label may be a string or a mixed array of strings, elements, and components. + +- Type: `Function` +- Required: Yes + +#### getOptionKeywords + +A function that returns the keywords for the specified option. + +- Type: `Function` +- Required: Yes + +#### getOptionCompletion + +A function that takes an option and responds with how the option should be completed. By default, the result is a value to be inserted in the text. However, a completer may explicitly declare how a completion should be treated by returning an object with `action` and `value` properties. The `action` declares what should be done with the `value`. + +There are currently two supported actions: + +* "insert-at-caret" - Insert the `value` into the text (the default completion action). +* "replace" - Replace the current block with the block specified in the `value` property. + +#### allowNode + +A function that takes a text node and returns a boolean indicating whether the completer should be considered for that node. + +- Type: `Function` +- Required: No + +#### allowContext + +A function that takes a Range before and a Range after the autocomplete trigger and query text and returns a boolean indicating whether the completer should be considered for that context. + +- Type: `Function` +- Required: No + +#### className + +A class name to apply to the autocompletion popup menu. + +- Type: `String` +- Required: No + +### Examples + +The following is a contrived completer for fresh fruit. + +```jsx +const fruitCompleter = { + name: 'fruit', + // The prefix that triggers this completer + triggerPrefix: '~', + // The option data + options: [ + { visual: '🍎', name: 'Apple' }, + { visual: '🍊', name: 'Orange' }, + { visual: '🍇', name: 'Grapes' }, + ], + // Returns a label for an option like "🍊 Orange" + getOptionLabel: option => [ + <span class="icon">{ option.visual }</span>, + option.name + ], + // Declares that options should be matched by their name + getOptionKeywords: option => [ option.name ], + // Declares completions should be inserted as abbreviations + getOptionCompletion: option => ( + <abbr title={ option.name }>{ option.visual }</abbr> + ), +}; +``` diff --git a/components/autocomplete/completer-compat.js b/components/autocomplete/completer-compat.js new file mode 100644 index 00000000000000..790b692182eb7c --- /dev/null +++ b/components/autocomplete/completer-compat.js @@ -0,0 +1,61 @@ +/** + * This mod + */ + +/** + * WordPress dependencies. + */ +import { deprecated } from '@wordpress/utils'; + +const generateCompleterName = ( () => { + let count = 0; + return () => `backcompat-completer-${ count++ }`; +} )(); + +export function isDeprecatedCompleter( completer ) { + return 'onSelect' in completer; +} + +export function toCompatibleCompleter( deprecatedCompleter ) { + deprecated( 'Original autocompleter interface in wp.components.Autocomplete', { + version: '2.8', + alternative: 'latest autocompleter interface', + plugin: 'Gutenberg', + link: 'https://github.com/WordPress/gutenberg/blob/master/components/autocomplete/README.md', + } ); + + const optionalProperties = [ 'className', 'allowNode', 'allowContext' ] + .filter( key => key in deprecatedCompleter ) + .reduce( ( properties, key ) => { + return { + ...properties, + [ key ]: deprecatedCompleter[ key ], + }; + }, {} ); + + return { + name: generateCompleterName(), + triggerPrefix: deprecatedCompleter.triggerPrefix, + + options() { + return deprecatedCompleter.getOptions(); + }, + + getOptionLabel( option ) { + return option.label; + }, + + getOptionKeywords( option ) { + return option.keywords; + }, + + getOptionCompletion() { + return { + action: 'backcompat', + value: deprecatedCompleter.onSelect.bind( deprecatedCompleter ), + }; + }, + + ...optionalProperties, + }; +} diff --git a/components/button-group/index.js b/components/button-group/index.js new file mode 100644 index 00000000000000..df65e99cb8068c --- /dev/null +++ b/components/button-group/index.js @@ -0,0 +1,19 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * Internal dependencies + */ +import './style.scss'; + +function ButtonGroup( { className, ...props } ) { + const classes = classnames( 'components-button-group', className ); + + return ( + <div { ...props } className={ classes } role="group" /> + ); +} + +export default ButtonGroup; diff --git a/components/button-group/style.scss b/components/button-group/style.scss new file mode 100644 index 00000000000000..5be7e700ac197c --- /dev/null +++ b/components/button-group/style.scss @@ -0,0 +1,19 @@ +.components-button-group { + display: inline-block; + margin-bottom: 20px; + + .components-button { + border-radius: 0; + & + .components-button { + margin-left: -1px; + } + + &:first-child { + border-radius: 3px 0 0 3px; + } + + &:last-child { + border-radius: 0 3px 3px 0; + } + } +} diff --git a/components/code-editor/README.md b/components/code-editor/README.md new file mode 100644 index 00000000000000..c750c6474b6cd8 --- /dev/null +++ b/components/code-editor/README.md @@ -0,0 +1,56 @@ +CodeEditor +======= + +CodeEditor is a React component that provides the user with a code editor +that has syntax highlighting and linting. + +The components acts as a drop-in replacement for a <textarea>, and uses the +CodeMirror library that is provided as part of WordPress Core. + +## Usage + +```jsx +import { CodeEditor } from '@wordpress/components'; + +function editCode() { + return ( + <CodeEditor + value={ '<p>This is some <b>HTML</b> code that will have syntax highlighting!</p>' } + onChange={ value => console.log( value ) } + /> + ); +} +``` + +## Props + +The component accepts the following props: + +### value + +The source code to load into the code editor. + +- Type: `string` +- Required: Yes + +### focus + +Whether or not the code editor should be focused. + +- Type: `boolean` +- Required: No + +### onFocus + +The function called when the editor is focused. + +- Type: `Function` +- Required: No + +### onChange + +The function called when the user has modified the source code via the +editor. It is passed the new value as an argument. + +- Type: `Function` +- Required: No diff --git a/components/code-editor/editor.js b/components/code-editor/editor.js new file mode 100644 index 00000000000000..23ebe66a4aba86 --- /dev/null +++ b/components/code-editor/editor.js @@ -0,0 +1,105 @@ +/** + * WordPress dependencies + */ +import { Component } from '@wordpress/element'; +import { keycodes } from '@wordpress/utils'; + +/** + * Module constants + */ +const { UP, DOWN } = keycodes; + +class CodeEditor extends Component { + constructor() { + super( ...arguments ); + + this.onFocus = this.onFocus.bind( this ); + this.onBlur = this.onBlur.bind( this ); + this.onCursorActivity = this.onCursorActivity.bind( this ); + this.onKeyHandled = this.onKeyHandled.bind( this ); + } + + componentDidMount() { + const instance = wp.codeEditor.initialize( this.textarea, window._wpGutenbergCodeEditorSettings ); + this.editor = instance.codemirror; + + this.editor.on( 'focus', this.onFocus ); + this.editor.on( 'blur', this.onBlur ); + this.editor.on( 'cursorActivity', this.onCursorActivity ); + this.editor.on( 'keyHandled', this.onKeyHandled ); + + this.updateFocus(); + } + + componentDidUpdate( prevProps ) { + if ( this.props.value !== prevProps.value && this.editor.getValue() !== this.props.value ) { + this.editor.setValue( this.props.value ); + } + + if ( this.props.focus !== prevProps.focus ) { + this.updateFocus(); + } + } + + componentWillUnmount() { + this.editor.on( 'focus', this.onFocus ); + this.editor.off( 'blur', this.onBlur ); + this.editor.off( 'cursorActivity', this.onCursorActivity ); + this.editor.off( 'keyHandled', this.onKeyHandled ); + + this.editor.toTextArea(); + this.editor = null; + } + + onFocus() { + if ( this.props.onFocus ) { + this.props.onFocus(); + } + } + + onBlur( editor ) { + if ( this.props.onChange ) { + this.props.onChange( editor.getValue() ); + } + } + + onCursorActivity( editor ) { + this.lastCursor = editor.getCursor(); + } + + onKeyHandled( editor, name, event ) { + /* + * Pressing UP/DOWN should only move focus to another block if the cursor is + * at the start or end of the editor. + * + * We do this by stopping UP/DOWN from propagating if: + * - We know what the cursor was before this event; AND + * - This event caused the cursor to move + */ + if ( event.keyCode === UP || event.keyCode === DOWN ) { + const areCursorsEqual = ( a, b ) => a.line === b.line && a.ch === b.ch; + if ( this.lastCursor && ! areCursorsEqual( editor.getCursor(), this.lastCursor ) ) { + event.stopImmediatePropagation(); + } + } + } + + updateFocus() { + if ( this.props.focus && ! this.editor.hasFocus() ) { + // Need to wait for the next frame to be painted before we can focus the editor + window.requestAnimationFrame( () => { + this.editor.focus(); + } ); + } + + if ( ! this.props.focus && this.editor.hasFocus() ) { + document.activeElement.blur(); + } + } + + render() { + return <textarea ref={ ref => ( this.textarea = ref ) } value={ this.props.value } />; + } +} + +export default CodeEditor; diff --git a/components/code-editor/index.js b/components/code-editor/index.js new file mode 100644 index 00000000000000..8e9bebe5f5fac3 --- /dev/null +++ b/components/code-editor/index.js @@ -0,0 +1,95 @@ +/** + * WordPress dependencies + */ +import { Component } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import CodeEditor from './editor'; +import Placeholder from '../placeholder'; +import Spinner from '../spinner'; + +function loadScript() { + return new Promise( ( resolve, reject ) => { + const handles = [ 'wp-codemirror', 'code-editor', 'htmlhint', 'csslint', 'jshint' ]; + + // Don't load htmlhint-kses unless we need it + if ( window._wpGutenbergCodeEditorSettings.htmlhint.kses ) { + handles.push( 'htmlhint-kses' ); + } + + const script = document.createElement( 'script' ); + script.src = `/wp-admin/load-scripts.php?load=${ handles.join( ',' ) }`; + script.onload = resolve; + script.onerror = reject; + + document.head.appendChild( script ); + } ); +} + +function loadStyle() { + return new Promise( ( resolve, reject ) => { + const handles = [ 'wp-codemirror', 'code-editor' ]; + + const style = document.createElement( 'link' ); + style.rel = 'stylesheet'; + style.href = `/wp-admin/load-styles.php?load=${ handles.join( ',' ) }`; + style.onload = resolve; + style.onerror = reject; + + document.head.appendChild( style ); + } ); +} + +let hasAlreadyLoadedAssets = false; + +function loadAssets() { + if ( hasAlreadyLoadedAssets ) { + return Promise.resolve(); + } + + return Promise.all( [ loadScript(), loadStyle() ] ).then( () => { + hasAlreadyLoadedAssets = true; + } ); +} + +class LazyCodeEditor extends Component { + constructor() { + super( ...arguments ); + + this.state = { + status: 'pending', + }; + } + + componentDidMount() { + loadAssets().then( + () => { + this.setState( { status: 'success' } ); + }, + () => { + this.setState( { status: 'error' } ); + } + ); + } + + render() { + if ( this.state.status === 'pending' ) { + return ( + <Placeholder> + <Spinner /> + </Placeholder> + ); + } + + if ( this.state.status === 'error' ) { + return <Placeholder>{ __( 'An unknown error occurred.' ) }</Placeholder>; + } + + return <CodeEditor { ...this.props } />; + } +} + +export default LazyCodeEditor; diff --git a/components/code-editor/test/__snapshots__/editor.js.snap b/components/code-editor/test/__snapshots__/editor.js.snap new file mode 100644 index 00000000000000..145b69b94d7d07 --- /dev/null +++ b/components/code-editor/test/__snapshots__/editor.js.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CodeEditor should render without an error 1`] = ` +<textarea + value="<b>wowee</b>" +/> +`; diff --git a/components/code-editor/test/editor.js b/components/code-editor/test/editor.js new file mode 100644 index 00000000000000..8579eb42b2f4fd --- /dev/null +++ b/components/code-editor/test/editor.js @@ -0,0 +1,24 @@ +/** + * External dependencies + */ +import { shallow } from 'enzyme'; +import { set, noop } from 'lodash'; + +/** + * Internal dependencies + */ +import CodeEditor from '../editor'; + +describe( 'CodeEditor', () => { + it( 'should render without an error', () => { + set( global, 'wp.codeEditor.initialize', () => ( { + codemirror: { + on: noop, + hasFocus: () => false, + }, + } ) ); + + const wrapper = shallow( <CodeEditor value={ '<b>wowee</b>' } /> ); + expect( wrapper ).toMatchSnapshot(); + } ); +} ); diff --git a/components/dashicon/style.scss b/components/dashicon/style.scss new file mode 100644 index 00000000000000..086fe34394e161 --- /dev/null +++ b/components/dashicon/style.scss @@ -0,0 +1,4 @@ +svg.dashicon { + fill: currentColor; + outline: none; +} diff --git a/components/disabled/README.md b/components/disabled/README.md new file mode 100644 index 00000000000000..6c7e394c2e364d --- /dev/null +++ b/components/disabled/README.md @@ -0,0 +1,33 @@ +Disabled +======== + +Disabled is a component which disables descendant tabbable elements and prevents pointer interaction. + +## Usage + +Assuming you have a form component, you can disable all form inputs by wrapping the form with `<Disabled>`. + +```jsx +const DisableToggleForm = withState( { + isDisabled: true, +} )( ( { isDisabled, setState } ) => { + let form = <form><input /></form>; + + if ( isDisabled ) { + form = <Disabled>{ form }</Disabled>; + } + + const toggleDisabled = setState( ( state ) => ( { + isDisabled: ! state.isDisabled, + } ) ); + + return ( + <div> + { form } + <button onClick={ toggleDisabled }> + Toggle Disabled + </button> + </div> + ); +} ) +``` diff --git a/components/disabled/index.js b/components/disabled/index.js new file mode 100644 index 00000000000000..15da09d4bcb29a --- /dev/null +++ b/components/disabled/index.js @@ -0,0 +1,93 @@ +/** + * External dependencies + */ +import { includes, debounce } from 'lodash'; + +/** + * WordPress dependencies + */ +import { Component } from '@wordpress/element'; +import { focus } from '@wordpress/utils'; + +/** + * Internal dependencies + */ +import './style.scss'; + +/** + * Names of control nodes which qualify for disabled behavior. + * + * See WHATWG HTML Standard: 4.10.18.5: "Enabling and disabling form controls: the disabled attribute". + * + * @link https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#enabling-and-disabling-form-controls:-the-disabled-attribute + * + * @type {string[]} + */ +const DISABLED_ELIGIBLE_NODE_NAMES = [ + 'BUTTON', + 'FIELDSET', + 'INPUT', + 'OPTGROUP', + 'OPTION', + 'SELECT', + 'TEXTAREA', +]; + +class Disabled extends Component { + constructor() { + super( ...arguments ); + + this.bindNode = this.bindNode.bind( this ); + this.disable = this.disable.bind( this ); + + // Debounce re-disable since disabling process itself will incur + // additional mutations which should be ignored. + this.debouncedDisable = debounce( this.disable, { leading: true } ); + } + + componentDidMount() { + this.disable(); + + this.observer = new window.MutationObserver( this.debouncedDisable ); + this.observer.observe( this.node, { + childList: true, + attributes: true, + subtree: true, + } ); + } + + componentWillUnmount() { + this.observer.disconnect(); + this.debouncedDisable.cancel(); + } + + bindNode( node ) { + this.node = node; + } + + disable() { + focus.focusable.find( this.node ).forEach( ( focusable ) => { + if ( includes( DISABLED_ELIGIBLE_NODE_NAMES, focusable.nodeName ) ) { + focusable.setAttribute( 'disabled', '' ); + } + + if ( focusable.hasAttribute( 'tabindex' ) ) { + focusable.removeAttribute( 'tabindex' ); + } + + if ( focusable.hasAttribute( 'contenteditable' ) ) { + focusable.setAttribute( 'contenteditable', 'false' ); + } + } ); + } + + render() { + return ( + <div ref={ this.bindNode } className="components-disabled"> + { this.props.children } + </div> + ); + } +} + +export default Disabled; diff --git a/components/disabled/style.scss b/components/disabled/style.scss new file mode 100644 index 00000000000000..1e6c51a62ff30c --- /dev/null +++ b/components/disabled/style.scss @@ -0,0 +1,13 @@ +.components-disabled { + position: relative; + pointer-events: none; + + &:after { + content: ''; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + } +} diff --git a/components/disabled/test/index.js b/components/disabled/test/index.js new file mode 100644 index 00000000000000..c6c6167cf85f3a --- /dev/null +++ b/components/disabled/test/index.js @@ -0,0 +1,97 @@ +/** + * External dependencies + */ +import { mount } from 'enzyme'; + +/** + * Internal dependencies + */ +import Disabled from '../'; + +jest.mock( '@wordpress/utils', () => { + const focus = require.requireActual( '@wordpress/utils' ).focus; + + return { + focus: { + ...focus, + focusable: { + ...focus.focusable, + find( context ) { + // In JSDOM, all elements have zero'd widths and height. + // This is a metric for focusable's `isVisible`, so find + // and apply an arbitrary non-zero width. + [ ...context.querySelectorAll( '*' ) ].forEach( ( element ) => { + Object.defineProperties( element, { + offsetWidth: { + get: () => 1, + }, + } ); + } ); + + return focus.focusable.find( ...arguments ); + }, + }, + }, + }; +} ); + +describe( 'Disabled', () => { + let MutationObserver; + + beforeAll( () => { + MutationObserver = window.MutationObserver; + window.MutationObserver = function() {}; + window.MutationObserver.prototype = { + observe() {}, + disconnect() {}, + }; + } ); + + afterAll( () => { + window.MutationObserver = MutationObserver; + } ); + + const Form = () => <form><input /><div contentEditable tabIndex="0" /></form>; + + it( 'will disable all fields', () => { + const wrapper = mount( <Disabled><Form /></Disabled> ); + + const input = wrapper.find( 'input' ).getDOMNode(); + const div = wrapper.find( '[contentEditable]' ).getDOMNode(); + + expect( input.hasAttribute( 'disabled' ) ).toBe( true ); + expect( div.getAttribute( 'contenteditable' ) ).toBe( 'false' ); + expect( div.hasAttribute( 'tabindex' ) ).toBe( false ); + expect( div.hasAttribute( 'disabled' ) ).toBe( false ); + } ); + + it( 'should cleanly un-disable via reconciliation', () => { + // If this test suddenly starts failing, it means React has become + // smarter about reusing children into grandfather element when the + // parent is dropped, so we'd need to find another way to restore + // original form state. + function MaybeDisable( { isDisabled = true } ) { + const element = <Form />; + return isDisabled ? <Disabled>{ element }</Disabled> : element; + } + + const wrapper = mount( <MaybeDisable /> ); + wrapper.setProps( { isDisabled: false } ); + + const input = wrapper.find( 'input' ).getDOMNode(); + const div = wrapper.find( '[contentEditable]' ).getDOMNode(); + + expect( input.hasAttribute( 'disabled' ) ).toBe( false ); + expect( div.getAttribute( 'contenteditable' ) ).toBe( 'true' ); + expect( div.hasAttribute( 'tabindex' ) ).toBe( true ); + } ); + + // Ideally, we'd have two more test cases here: + // + // - it( 'will disable all fields on component render change' ) + // - it( 'will disable all fields on sneaky DOM manipulation' ) + // + // Alas, JSDOM does not support MutationObserver: + // + // https://github.com/jsdom/jsdom/issues/639 +} ); diff --git a/components/draggable/README.md b/components/draggable/README.md new file mode 100644 index 00000000000000..974f1eab06db62 --- /dev/null +++ b/components/draggable/README.md @@ -0,0 +1,37 @@ +# Draggable + +`Draggable` is a Component that can wrap any element to make it draggable. When used, a cross-browser (including IE) customisable drag image is created. The component clones the specified element on drag-start and uses the clone as a drag image during drag-over. Discards the clone on drag-end. + +## Props + +The component accepts the following props: + +### elementId + +The HTML id of the element to clone on drag + +- Type: `string` +- Required: Yes + +### transferData + +Arbitrary data object attached to the drag and drop event. + +- Type: `Object` +- Required: Yes + +### onDragStart + +The function called when dragging starts. + +- Type: `Function` +- Required: No +- Default: `noop` + +### onDragEnd + +The function called when dragging ends. + +- Type: `Function` +- Required: No +- Default: `noop` diff --git a/components/draggable/index.js b/components/draggable/index.js new file mode 100644 index 00000000000000..01b67d44b92e46 --- /dev/null +++ b/components/draggable/index.js @@ -0,0 +1,169 @@ +/** + * External dependencies + */ +import { noop } from 'lodash'; +import classnames from 'classnames'; + +/** + * WordPress Dependencies + */ +import { Component } from '@wordpress/element'; + +/** + * Internal Dependencies + */ +import withSafeTimeout from '../higher-order/with-safe-timeout'; +import './style.scss'; + +const dragImageClass = 'components-draggable__invisible-drag-image'; +const cloneWrapperClass = 'components-draggable__clone'; +const cloneHeightTransformationBreakpoint = 700; +const clonePadding = 20; + +class Draggable extends Component { + constructor() { + super( ...arguments ); + + this.onDragStart = this.onDragStart.bind( this ); + this.onDragOver = this.onDragOver.bind( this ); + this.onDragEnd = this.onDragEnd.bind( this ); + this.resetDragState = this.resetDragState.bind( this ); + } + + componentWillUnmount() { + this.resetDragState(); + } + + /** + * Removes the element clone, resets cursor, and removes drag listener. + * @param {Object} event The non-custom DragEvent. + */ + onDragEnd( event ) { + const { onDragEnd = noop } = this.props; + event.preventDefault(); + + this.resetDragState(); + + this.props.setTimeout( onDragEnd ); + } + + /* + * Updates positioning of element clone based on mouse movement during dragging. + * @param {Object} event The non-custom DragEvent. + */ + onDragOver( event ) { + this.cloneWrapper.style.top = + `${ parseInt( this.cloneWrapper.style.top, 10 ) + event.clientY - this.cursorTop }px`; + this.cloneWrapper.style.left = + `${ parseInt( this.cloneWrapper.style.left, 10 ) + event.clientX - this.cursorLeft }px`; + + // Update cursor coordinates. + this.cursorLeft = event.clientX; + this.cursorTop = event.clientY; + } + + /** + * - Clones the current element and spawns clone over original element. + * - Adds a fake temporary drag image to avoid browser defaults. + * - Sets transfer data. + * - Adds dragover listener. + * @param {Object} event The non-custom DragEvent. + * @param {string} elementId The HTML id of the element to be dragged. + * @param {Object} transferData The data to be set to the event's dataTransfer - to be accessible in any later drop logic. + */ + onDragStart( event ) { + const { elementId, transferData, onDragStart = noop } = this.props; + const element = document.getElementById( elementId ); + if ( ! element ) { + event.preventDefault(); + return; + } + + // Set a fake drag image to avoid browser defaults. Remove from DOM + // right after. event.dataTransfer.setDragImage is not supported yet in + // IE, we need to check for its existence first. + if ( 'function' === typeof event.dataTransfer.setDragImage ) { + const dragImage = document.createElement( 'div' ); + dragImage.id = `drag-image-${ elementId }`; + dragImage.classList.add( dragImageClass ); + document.body.appendChild( dragImage ); + event.dataTransfer.setDragImage( dragImage, 0, 0 ); + this.props.setTimeout( () => { + document.body.removeChild( dragImage ); + } ); + } + + event.dataTransfer.setData( 'text', JSON.stringify( transferData ) ); + + // Prepare element clone and append to element wrapper. + const elementRect = element.getBoundingClientRect(); + const elementWrapper = element.parentNode; + const elementTopOffset = parseInt( elementRect.top, 10 ); + const elementLeftOffset = parseInt( elementRect.left, 10 ); + const clone = element.cloneNode( true ); + clone.id = `clone-${ elementId }`; + this.cloneWrapper = document.createElement( 'div' ); + this.cloneWrapper.classList.add( cloneWrapperClass ); + this.cloneWrapper.style.width = `${ elementRect.width + ( clonePadding * 2 ) }px`; + + if ( elementRect.height > cloneHeightTransformationBreakpoint ) { + // Scale down clone if original element is larger than 700px. + this.cloneWrapper.style.transform = 'scale(0.5)'; + this.cloneWrapper.style.transformOrigin = 'top left'; + // Position clone near the cursor. + this.cloneWrapper.style.top = `${ event.clientY - 100 }px`; + this.cloneWrapper.style.left = `${ event.clientX }px`; + } else { + // Position clone right over the original element (20px padding). + this.cloneWrapper.style.top = `${ elementTopOffset - clonePadding }px`; + this.cloneWrapper.style.left = `${ elementLeftOffset - clonePadding }px`; + } + + // Hack: Remove iFrames as it's causing the embeds drag clone to freeze + [ ...clone.querySelectorAll( 'iframe' ) ].forEach( child => child.parentNode.removeChild( child ) ); + + this.cloneWrapper.appendChild( clone ); + elementWrapper.appendChild( this.cloneWrapper ); + + // Mark the current cursor coordinates. + this.cursorLeft = event.clientX; + this.cursorTop = event.clientY; + // Update cursor to 'grabbing', document wide. + document.body.classList.add( 'is-dragging-components-draggable' ); + document.addEventListener( 'dragover', this.onDragOver ); + + this.props.setTimeout( onDragStart ); + } + + /** + * Cleans up drag state when drag has completed, or component unmounts + * while dragging. + */ + resetDragState() { + // Remove drag clone + document.removeEventListener( 'dragover', this.onDragOver ); + if ( this.cloneWrapper && this.cloneWrapper.parentNode ) { + this.cloneWrapper.parentNode.removeChild( this.cloneWrapper ); + this.cloneWrapper = null; + } + + // Reset cursor. + document.body.classList.remove( 'is-dragging-components-draggable' ); + } + + render() { + const { children, className } = this.props; + return ( + <div + className={ classnames( 'components-draggable', className ) } + onDragStart={ this.onDragStart } + onDragEnd={ this.onDragEnd } + draggable + > + { children } + </div> + ); + } +} + +export default withSafeTimeout( Draggable ); diff --git a/components/draggable/style.scss b/components/draggable/style.scss new file mode 100644 index 00000000000000..6d4f9845840db5 --- /dev/null +++ b/components/draggable/style.scss @@ -0,0 +1,20 @@ +body.is-dragging-components-draggable { + cursor: move;/* Fallback for IE/Edge < 14 */ + cursor: grabbing !important; +} + +.components-draggable__invisible-drag-image { + position: fixed; + left: -1000px; + height: 50px; + width: 50px; +} + +.components-draggable__clone { + position: fixed; + padding: 20px; + background: transparent; + pointer-events: none; + z-index: z-index( '.components-draggable__clone' ); + opacity: 0.8; +} diff --git a/components/drop-zone/README.md b/components/drop-zone/README.md new file mode 100644 index 00000000000000..acec85b06275b5 --- /dev/null +++ b/components/drop-zone/README.md @@ -0,0 +1,47 @@ +# DropZone + +`DropZone` is a Component creating a drop zone area taking the full size of its parent element. It supports dropping files, HTML content or any other HTML drop event. To work properly this components needs to be wrapped in a `DropZoneProvider`. + +## Usage + +```jsx +import { DropZoneProvider, DropZone } from '@wordpress/components'; + +function MyComponent() { + return ( + <DropZoneProvider> + <div> + <DropZone onDrop={ () => console.log( 'do something' ) } /> + </div> + </DropZoneProvider> + ); +} +``` + +## Props + +The component accepts the following props: + +### onFilesDrop + +The function is called when dropping a file into the `DropZone`. It receives two arguments: an array of dropped files and a position object which the following shape: `{ x: 'left|right', y: 'top|bottom' }`. The position object indicates whether the drop event happened closer to the top or bottom edges and left or right ones. + +- Type: `Function` +- Required: No +- Default: `noop` + +### onHTMLDrop + +The function is called when dropping a file into the `DropZone`. It receives two arguments: the HTML being dropped and a position object. + +- Type: `Function` +- Required: No +- Default: `noop` + +### onDrop + +The function is generic drop handler called if the `onFilesDrop` or `onHTMLDrop` are not called. It receives two arguments: The drop `event` object and the position object. + +- Type: `Function` +- Required: No +- Default: `noop` diff --git a/components/focusable-iframe/README.md b/components/focusable-iframe/README.md new file mode 100644 index 00000000000000..b1b09a74974a01 --- /dev/null +++ b/components/focusable-iframe/README.md @@ -0,0 +1,39 @@ +Focusable Iframe +================ + +`<FocusableIframe />` is a component rendering an `iframe` element enhanced to support focus events. By default, it is not possible to detect when an iframe is focused or clicked within. This enhanced component uses a technique which checks whether the target of a window `blur` event is the iframe, inferring that this has resulted in the focus of the iframe. It dispatches an emulated [`FocusEvent`](https://developer.mozilla.org/en-US/docs/Web/API/FocusEvent) on the iframe element with event bubbling, so a parent component binding its own `onFocus` event will account for focus transitioning within the iframe. + +## Usage + +Use as you would a standard `iframe`. You may pass `onFocus` directly as the callback to be invoked when the iframe receives focus, or on an ancestor component since the event will bubble. + +```jsx +import { FocusableIframe } from '@wordpress/components'; + +function MyIframe() { + return ( + <FocusableIframe + src="https://example.com" + onFocus={ /* ... */ } + /> + ); +} +``` + +## Props + +Any props aside from those listed below will be passed to the `FocusableIframe` will be passed through to the underlying `iframe` element. + +### `onFocus` + +- Type: `Function` +- Required: No + +Callback to invoke when iframe receives focus. Passes an emulated `FocusEvent` object as the first argument. + +### `iframeRef` + +- Type: `wp.element.Ref` +- Required: No + +If a reference to the underlying DOM element is needed, pass `iframeRef` as the result of a `wp.element.createRef` called from your component. diff --git a/components/focusable-iframe/index.js b/components/focusable-iframe/index.js new file mode 100644 index 00000000000000..2d6a1a2b177655 --- /dev/null +++ b/components/focusable-iframe/index.js @@ -0,0 +1,69 @@ +/** + * External dependencies + */ +import { omit } from 'lodash'; + +/** + * WordPress dependencies + */ +import { Component, createRef } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import withGlobalEvents from '../higher-order/with-global-events'; + +/** + * Browser dependencies + */ + +const { FocusEvent } = window; + +class FocusableIframe extends Component { + constructor( props ) { + super( ...arguments ); + + this.checkFocus = this.checkFocus.bind( this ); + + this.node = props.iframeRef || createRef(); + } + + /** + * Checks whether the iframe is the activeElement, inferring that it has + * then received focus, and calls the `onFocus` prop callback. + */ + checkFocus() { + const iframe = this.node.current; + + if ( document.activeElement !== iframe ) { + return; + } + + const focusEvent = new FocusEvent( 'focus', { bubbles: true } ); + iframe.dispatchEvent( focusEvent ); + + const { onFocus } = this.props; + if ( onFocus ) { + onFocus( focusEvent ); + } + } + + render() { + // Disable reason: The rendered iframe is a pass-through component, + // assigning props inherited from the rendering parent. It's the + // responsibility of the parent to assign a title. + + /* eslint-disable jsx-a11y/iframe-has-title */ + return ( + <iframe + ref={ this.node } + { ...omit( this.props, [ 'iframeRef', 'onFocus' ] ) } + /> + ); + /* eslint-enable jsx-a11y/iframe-has-title */ + } +} + +export default withGlobalEvents( { + blur: 'checkFocus', +} )( FocusableIframe ); diff --git a/components/higher-order/if-condition/README.md b/components/higher-order/if-condition/README.md new file mode 100644 index 00000000000000..0792fc25185448 --- /dev/null +++ b/components/higher-order/if-condition/README.md @@ -0,0 +1,20 @@ +If Condition +============ + +`ifCondition` is a higher-order component creator, used for creating a new component which renders if the given condition is satisfied. + +## Usage + +`ifCondition`, passed with a predicate function, will render the underlying component only if the predicate returns a truthy value. The predicate is passed the component's own original props as an argument. + +```jsx +function MyEvenNumber( { number } ) { + // This is only reached if the `number` prop is even. Otherwise, nothing + // will be rendered. + return <strong>{ number }</strong>; +} + +MyEvenNumber = ifCondition( + ( { number } ) => number % 2 === 0 +)( MyEvenNumber ); +``` diff --git a/components/higher-order/if-condition/index.js b/components/higher-order/if-condition/index.js new file mode 100644 index 00000000000000..0ae3099810a1c4 --- /dev/null +++ b/components/higher-order/if-condition/index.js @@ -0,0 +1,25 @@ +/** + * WordPress dependencies + */ +import { createHigherOrderComponent } from '@wordpress/element'; + +/** + * Higher-order component creator, creating a new component which renders if + * the given condition is satisfied or with the given optional prop name. + * + * @param {Function} predicate Function to test condition. + * + * @return {Function} Higher-order component. + */ +const ifCondition = ( predicate ) => createHigherOrderComponent( + ( WrappedComponent ) => ( props ) => { + if ( ! predicate( props ) ) { + return null; + } + + return <WrappedComponent { ...props } />; + }, + 'ifCondition' +); + +export default ifCondition; diff --git a/components/higher-order/with-global-events/README.md b/components/higher-order/with-global-events/README.md new file mode 100644 index 00000000000000..7bcf4c3044d057 --- /dev/null +++ b/components/higher-order/with-global-events/README.md @@ -0,0 +1,31 @@ +withGlobalEvents +================ + +`withGlobalEvents` is a higher-order component used to facilitate responding to global events, where one would otherwise use `window.addEventListener`. + +On behalf of the consuming developer, the higher-order component manages: + +- Unbinding when the component unmounts. +- Binding at most a single event handler for the entire application. + +## Usage + +Pass an object where keys correspond to the DOM event type, the value the name of the method on the original component's instance which handles the event. + +```js +import { withGlobalEvents } from '@wordpress/components'; + +class ResizingComponent extends Component { + handleResize() { + // ... + } + + render() { + // ... + } +} + +export default withGlobalEvents( { + resize: 'handleResize', +} )( ResizingComponent ); +``` diff --git a/components/higher-order/with-global-events/index.js b/components/higher-order/with-global-events/index.js new file mode 100644 index 00000000000000..a04b1d8020b351 --- /dev/null +++ b/components/higher-order/with-global-events/index.js @@ -0,0 +1,64 @@ +/** + * External dependencies + */ +import { forEach } from 'lodash'; + +/** + * WordPress dependencies + */ +import { + Component, + createRef, + createHigherOrderComponent, +} from '@wordpress/element'; + +/** + * Internal dependencies + */ +import Listener from './listener'; + +/** + * Listener instance responsible for managing document event handling. + * + * @type {Listener} + */ +const listener = new Listener(); + +function withGlobalEvents( eventTypesToHandlers ) { + return createHigherOrderComponent( ( WrappedComponent ) => { + return class extends Component { + constructor() { + super( ...arguments ); + + this.handleEvent = this.handleEvent.bind( this ); + + this.ref = createRef(); + } + + componentDidMount() { + forEach( eventTypesToHandlers, ( handler, eventType ) => { + listener.add( eventType, this ); + } ); + } + + componentWillUnmount() { + forEach( eventTypesToHandlers, ( handler, eventType ) => { + listener.remove( eventType, this ); + } ); + } + + handleEvent( event ) { + const handler = eventTypesToHandlers[ event.type ]; + if ( typeof this.ref.current[ handler ] === 'function' ) { + this.ref.current[ handler ]( event ); + } + } + + render() { + return <WrappedComponent ref={ this.ref } { ...this.props } />; + } + }; + }, 'withGlobalEvents' ); +} + +export default withGlobalEvents; diff --git a/components/higher-order/with-global-events/listener.js b/components/higher-order/with-global-events/listener.js new file mode 100644 index 00000000000000..fa469e7ce1a0da --- /dev/null +++ b/components/higher-order/with-global-events/listener.js @@ -0,0 +1,45 @@ +/** + * External dependencies + */ +import { forEach, without } from 'lodash'; + +/** + * Class responsible for orchestrating event handling on the global window, + * binding a single event to be shared across all handling instances, and + * removing the handler when no instances are listening for the event. + */ +class Listener { + constructor() { + this.listeners = {}; + + this.handleEvent = this.handleEvent.bind( this ); + } + + add( eventType, instance ) { + if ( ! this.listeners[ eventType ] ) { + // Adding first listener for this type, so bind event. + window.addEventListener( eventType, this.handleEvent ); + this.listeners[ eventType ] = []; + } + + this.listeners[ eventType ].push( instance ); + } + + remove( eventType, instance ) { + this.listeners[ eventType ] = without( this.listeners[ eventType ], instance ); + + if ( ! this.listeners[ eventType ].length ) { + // Removing last listener for this type, so unbind event. + window.removeEventListener( eventType, this.handleEvent ); + delete this.listeners[ eventType ]; + } + } + + handleEvent( event ) { + forEach( this.listeners[ event.type ], ( instance ) => { + instance.handleEvent( event ); + } ); + } +} + +export default Listener; diff --git a/components/higher-order/with-global-events/test/index.js b/components/higher-order/with-global-events/test/index.js new file mode 100644 index 00000000000000..9054e5515d2524 --- /dev/null +++ b/components/higher-order/with-global-events/test/index.js @@ -0,0 +1,93 @@ +/** + * External dependencies + */ +import { mount } from 'enzyme'; + +/** + * External dependencies + */ +import { Component } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import withGlobalEvents from '../'; +import Listener from '../listener'; + +jest.mock( '../listener', () => { + const ActualListener = require.requireActual( '../listener' ).default; + + return class extends ActualListener { + constructor() { + super( ...arguments ); + + this.constructor._instance = this; + + jest.spyOn( this, 'add' ); + jest.spyOn( this, 'remove' ); + } + }; +} ); + +describe( 'withGlobalEvents', () => { + let wrapper; + + class OriginalComponent extends Component { + handleResize( event ) { + this.props.onResize( event ); + } + + render() { + return <div>{ this.props.children }</div>; + } + } + + beforeAll( () => { + jest.spyOn( OriginalComponent.prototype, 'handleResize' ); + } ); + + beforeEach( () => { + jest.clearAllMocks(); + } ); + + afterEach( () => { + if ( wrapper ) { + wrapper.unmount(); + wrapper = null; + } + } ); + + function mountEnhancedComponent( props ) { + const EnhancedComponent = withGlobalEvents( { + resize: 'handleResize', + } )( OriginalComponent ); + + wrapper = mount( <EnhancedComponent { ...props }>Hello</EnhancedComponent> ); + } + + it( 'renders with original component', () => { + mountEnhancedComponent(); + + expect( wrapper.childAt( 0 ).childAt( 0 ).type() ).toBe( 'div' ); + expect( wrapper.childAt( 0 ).text() ).toBe( 'Hello' ); + } ); + + it( 'binds events from passed object', () => { + mountEnhancedComponent(); + + expect( Listener._instance.add ).toHaveBeenCalledWith( 'resize', wrapper.instance() ); + } ); + + it( 'handles events', () => { + const onResize = jest.fn(); + + mountEnhancedComponent( { onResize } ); + + const event = { type: 'resize' }; + + Listener._instance.handleEvent( event ); + + expect( OriginalComponent.prototype.handleResize ).toHaveBeenCalledWith( event ); + expect( onResize ).toHaveBeenCalledWith( event ); + } ); +} ); diff --git a/components/higher-order/with-global-events/test/listener.js b/components/higher-order/with-global-events/test/listener.js new file mode 100644 index 00000000000000..53f5db9a3298de --- /dev/null +++ b/components/higher-order/with-global-events/test/listener.js @@ -0,0 +1,87 @@ +/** + * Internal dependencies + */ +import Listener from '../listener'; + +describe( 'Listener', () => { + const createHandler = () => ( { handleEvent: jest.fn() } ); + + let listener, _addEventListener, _removeEventListener; + beforeAll( () => { + _addEventListener = global.window.addEventListener; + _removeEventListener = global.window.removeEventListener; + global.window.addEventListener = jest.fn(); + global.window.removeEventListener = jest.fn(); + } ); + + beforeEach( () => { + listener = new Listener(); + jest.clearAllMocks(); + } ); + + afterAll( () => { + global.window.addEventListener = _addEventListener; + global.window.removeEventListener = _removeEventListener; + } ); + + describe( '#add()', () => { + it( 'adds an event listener on first listener', () => { + listener.add( 'resize', createHandler() ); + + expect( window.addEventListener ).toHaveBeenCalledWith( 'resize', expect.any( Function ) ); + } ); + + it( 'does not add event listener on subsequent listeners', () => { + listener.add( 'resize', createHandler() ); + listener.add( 'resize', createHandler() ); + + expect( window.addEventListener ).toHaveBeenCalledTimes( 1 ); + } ); + } ); + + describe( '#remove()', () => { + it( 'removes an event listener on last listener', () => { + const handler = createHandler(); + listener.add( 'resize', handler ); + listener.remove( 'resize', handler ); + + expect( window.removeEventListener ).toHaveBeenCalledWith( 'resize', expect.any( Function ) ); + } ); + + it( 'does not remove event listener on remaining listeners', () => { + const firstHandler = createHandler(); + const secondHandler = createHandler(); + listener.add( 'resize', firstHandler ); + listener.add( 'resize', secondHandler ); + listener.remove( 'resize', firstHandler ); + + expect( window.removeEventListener ).not.toHaveBeenCalled(); + } ); + } ); + + describe( '#handleEvent()', () => { + it( 'calls concerned listeners', () => { + const handler = createHandler(); + listener.add( 'resize', handler ); + + const event = { type: 'resize' }; + + listener.handleEvent( event ); + + expect( handler.handleEvent ).toHaveBeenCalledWith( event ); + } ); + + it( 'calls all added handlers', () => { + const handler = createHandler(); + listener.add( 'resize', handler ); + listener.add( 'resize', handler ); + listener.add( 'resize', handler ); + + const event = { type: 'resize' }; + + listener.handleEvent( event ); + + expect( handler.handleEvent ).toHaveBeenCalledTimes( 3 ); + } ); + } ); +} ); diff --git a/components/menu-group/index.js b/components/menu-group/index.js new file mode 100644 index 00000000000000..ef11c27434f4ec --- /dev/null +++ b/components/menu-group/index.js @@ -0,0 +1,50 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { Children } from '@wordpress/element'; +import { applyFilters } from '@wordpress/hooks'; + +/** + * Internal dependencies + */ +import './style.scss'; +import { NavigableMenu } from '../navigable-container'; +import withInstanceId from '../higher-order/with-instance-id'; + +export function MenuGroup( { + children, + className = '', + filterName, + instanceId, + label, +} ) { + const childrenArray = Children.toArray( children ); + const menuItems = filterName ? + applyFilters( filterName, childrenArray ) : + childrenArray; + + if ( ! Array.isArray( menuItems ) || ! menuItems.length ) { + return null; + } + + const labelId = `components-menu-group-label-${ instanceId }`; + const classNames = classnames( className, 'components-menu-group' ); + + return ( + <div className={ classNames }> + { label && + <div className="components-menu-group__label" id={ labelId }>{ label }</div> + } + <NavigableMenu orientation="vertical" aria-labelledby={ labelId }> + { menuItems } + </NavigableMenu> + </div> + ); +} + +export default withInstanceId( MenuGroup ); diff --git a/components/menu-group/style.scss b/components/menu-group/style.scss new file mode 100644 index 00000000000000..5d0952d468f54b --- /dev/null +++ b/components/menu-group/style.scss @@ -0,0 +1,9 @@ +.components-menu-group { + width: 100%; + padding: 10px; +} + +.components-menu-group__label { + margin-bottom: 10px; + color: $dark-gray-300; +} diff --git a/components/menu-group/test/__snapshots__/index.js.snap b/components/menu-group/test/__snapshots__/index.js.snap new file mode 100644 index 00000000000000..537a7b921b7738 --- /dev/null +++ b/components/menu-group/test/__snapshots__/index.js.snap @@ -0,0 +1,24 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MenuGroup should match snapshot 1`] = ` +<div + className="components-menu-group" +> + <div + className="components-menu-group__label" + id="components-menu-group-label-1" + > + My group + </div> + <NavigableMenu + aria-labelledby="components-menu-group-label-1" + orientation="vertical" + > + <p + key=".0" + > + My item + </p> + </NavigableMenu> +</div> +`; diff --git a/components/menu-group/test/index.js b/components/menu-group/test/index.js new file mode 100644 index 00000000000000..7f5a476614b22d --- /dev/null +++ b/components/menu-group/test/index.js @@ -0,0 +1,30 @@ +/** + * External dependencies + */ +import { shallow } from 'enzyme'; + +/** + * Internal dependencies + */ +import { MenuGroup } from '../'; + +describe( 'MenuGroup', () => { + test( 'should render null when no children provided', () => { + const wrapper = shallow( <MenuGroup /> ); + + expect( wrapper.html() ).toBe( null ); + } ); + + test( 'should match snapshot', () => { + const wrapper = shallow( + <MenuGroup + label="My group" + instanceId="1" + > + <p>My item</p> + </MenuGroup> + ); + + expect( wrapper ).toMatchSnapshot(); + } ); +} ); diff --git a/components/menu-item/index.js b/components/menu-item/index.js new file mode 100644 index 00000000000000..842d3131d8ad26 --- /dev/null +++ b/components/menu-item/index.js @@ -0,0 +1,64 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; +import { isString } from 'lodash'; + +/** + * WordPress dependencies + */ +import { cloneElement } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import './style.scss'; +import Button from '../button'; +import Shortcut from './shortcut'; +import IconButton from '../icon-button'; + +/** + * Renders a generic menu item for use inside the more menu. + * + * @return {WPElement} More menu item. + */ +function MenuItem( { children, className, icon, onClick, shortcut, isSelected = false } ) { + className = classnames( 'components-menu-item__button', className, { + 'has-icon': icon, + } ); + + if ( icon ) { + if ( ! isString( icon ) ) { + icon = cloneElement( icon, { + className: 'components-menu-items__item-icon', + height: 20, + width: 20, + } ); + } + + return ( + <IconButton + className={ className } + icon={ icon } + onClick={ onClick } + aria-pressed={ isSelected } + > + { children } + <Shortcut shortcut={ shortcut } /> + </IconButton> + ); + } + + return ( + <Button + className={ className } + onClick={ onClick } + aria-pressed={ isSelected } + > + { children } + <Shortcut shortcut={ shortcut } /> + </Button> + ); +} + +export default MenuItem; diff --git a/components/menu-item/shortcut.js b/components/menu-item/shortcut.js new file mode 100644 index 00000000000000..3124c354687fd5 --- /dev/null +++ b/components/menu-item/shortcut.js @@ -0,0 +1,15 @@ +/** + * Internal dependencies + */ +import './style.scss'; + +function MenuItemsShortcut( { shortcut } ) { + if ( ! shortcut ) { + return null; + } + return ( + <span className="components-menu-item__shortcut">{ shortcut }</span> + ); +} + +export default MenuItemsShortcut; diff --git a/components/menu-item/style.scss b/components/menu-item/style.scss new file mode 100644 index 00000000000000..4c9f9f1b352c09 --- /dev/null +++ b/components/menu-item/style.scss @@ -0,0 +1,35 @@ +.components-menu-item__button, +.components-menu-item__button.components-icon-button { + width: 100%; + padding: 8px; + text-align: left; + padding-left: 25px; + color: $dark-gray-500; + + .dashicon, + .components-menu-items__item-icon { + margin-right: 5px; + } + + .components-menu-items__item-icon { + display: inline-block; + flex: 0 0 auto; + } + + &.has-icon { + padding-left: 0; + } + + &:hover { + @include menu-style__neutral; + } + + &:focus { + @include menu-style__focus; + } +} + +.components-menu-item__shortcut { + float: right; + opacity: .5; +} diff --git a/components/menu-item/test/__snapshots__/index.js.snap b/components/menu-item/test/__snapshots__/index.js.snap new file mode 100644 index 00000000000000..214cc157d630ac --- /dev/null +++ b/components/menu-item/test/__snapshots__/index.js.snap @@ -0,0 +1,25 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MenuItem should match snapshot when all props provided 1`] = ` +<IconButton + aria-pressed={true} + className="components-menu-item__button my-class has-icon" + icon="wordpress" + onClick={[Function]} +> + My item + <MenuItemsShortcut + shortcut="mod+shift+alt+w" + /> +</IconButton> +`; + +exports[`MenuItem should match snapshot when only label provided 1`] = ` +<Button + aria-pressed={false} + className="components-menu-item__button" +> + My item + <MenuItemsShortcut /> +</Button> +`; diff --git a/components/menu-item/test/index.js b/components/menu-item/test/index.js new file mode 100644 index 00000000000000..f21ff7ab176dcf --- /dev/null +++ b/components/menu-item/test/index.js @@ -0,0 +1,37 @@ +/** + * External dependencies + */ +import { shallow } from 'enzyme'; + +/** + * Internal dependencies + */ +import MenuItem from '../'; + +describe( 'MenuItem', () => { + test( 'should match snapshot when only label provided', () => { + const wrapper = shallow( + <MenuItem> + My item + </MenuItem> + ); + + expect( wrapper ).toMatchSnapshot(); + } ); + + test( 'should match snapshot when all props provided', () => { + const wrapper = shallow( + <MenuItem + className="my-class" + icon="wordpress" + isSelected={ true } + onClick={ () => {} } + shortcut="mod+shift+alt+w" + > + My item + </MenuItem> + ); + + expect( wrapper ).toMatchSnapshot(); + } ); +} ); diff --git a/components/menu-items-choice/index.js b/components/menu-items-choice/index.js new file mode 100644 index 00000000000000..50edc61df89505 --- /dev/null +++ b/components/menu-items-choice/index.js @@ -0,0 +1,29 @@ +/** + * Internal dependencies + */ +import MenuItem from '../menu-item'; + +export default function MenuItemsChoice( { + choices = [], + onSelect, + value, +} ) { + return choices.map( ( item ) => { + const isSelected = value === item.value; + return ( + <MenuItem + key={ item.value } + icon={ isSelected && 'yes' } + isSelected={ isSelected } + shortcut={ item.shortcut } + onClick={ () => { + if ( ! isSelected ) { + onSelect( item.value ); + } + } } + > + { item.label } + </MenuItem> + ); + } ); +} diff --git a/components/query-controls/category-select.js b/components/query-controls/category-select.js new file mode 100644 index 00000000000000..978dd9da84e1c9 --- /dev/null +++ b/components/query-controls/category-select.js @@ -0,0 +1,20 @@ +/** + * WordPress dependencies + */ +import { buildTermsTree } from '@wordpress/utils'; + +/** + * Internal dependencies + */ +import TreeSelect from '../tree-select'; + +export default function CategorySelect( { label, noOptionLabel, categoriesList, selectedCategoryId, onChange } ) { + const termsTree = buildTermsTree( categoriesList ); + return ( + <TreeSelect + { ...{ label, noOptionLabel, onChange } } + tree={ termsTree } + selectedId={ selectedCategoryId } + /> + ); +} diff --git a/components/query-controls/index.js b/components/query-controls/index.js new file mode 100644 index 00000000000000..4ed57e468eb691 --- /dev/null +++ b/components/query-controls/index.js @@ -0,0 +1,85 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { RangeControl, SelectControl } from '../'; +import CategorySelect from './category-select'; + +const DEFAULT_MIN_ITEMS = 1; +const DEFAULT_MAX_ITEMS = 100; + +export default function QueryControls( { + categoriesList, + selectedCategoryId, + numberOfItems, + order, + orderBy, + maxItems = DEFAULT_MAX_ITEMS, + minItems = DEFAULT_MIN_ITEMS, + onCategoryChange, + onNumberOfItemsChange, + onOrderChange, + onOrderByChange, +} ) { + return [ + ( onOrderChange && onOrderByChange ) && ( + <SelectControl + key="query-controls-order-select" + label={ __( 'Order by' ) } + value={ `${ orderBy }/${ order }` } + options={ [ + { + label: __( 'Newest to Oldest' ), + value: 'date/desc', + }, + { + label: __( 'Oldest to Newest' ), + value: 'date/asc', + }, + { + /* translators: label for ordering posts by title in ascending order */ + label: __( 'A → Z' ), + value: 'title/asc', + }, + { + /* translators: label for ordering posts by title in descending order */ + label: __( 'Z → A' ), + value: 'title/desc', + }, + ] } + onChange={ ( value ) => { + const [ newOrderBy, newOrder ] = value.split( '/' ); + if ( newOrder !== order ) { + onOrderChange( newOrder ); + } + if ( newOrderBy !== orderBy ) { + onOrderByChange( newOrderBy ); + } + } } + /> + ), + onCategoryChange && ( + <CategorySelect + key="query-controls-category-select" + categoriesList={ categoriesList } + label={ __( 'Category' ) } + noOptionLabel={ __( 'All' ) } + selectedCategoryId={ selectedCategoryId } + onChange={ onCategoryChange } + /> ), + onNumberOfItemsChange && ( + <RangeControl + key="query-controls-range-control" + label={ __( 'Number of items' ) } + value={ numberOfItems } + onChange={ onNumberOfItemsChange } + min={ minItems } + max={ maxItems } + /> + ), + ]; +} diff --git a/components/scroll-lock/README.md b/components/scroll-lock/README.md new file mode 100644 index 00000000000000..d74373e9d2e928 --- /dev/null +++ b/components/scroll-lock/README.md @@ -0,0 +1,21 @@ +ScrollLock +========== + +ScrollLock is a content-free React component for declaratively preventing scroll bleed from modal UI to the page body. This component applies a `lockscroll` class to the `document.documentElement` and `document.scrollingElement` elements to stop the body from scrolling. When it is present, the lock is applied. + +## Usage + +Declare scroll locking as part of modal UI. + +```jsx +import { ScrollLock } from '@wordpress/components'; + +function Sidebar( { isMobile } ) { + return ( + <div> + Sidebar Content! + { isMobile && <ScrollLock /> } + </div> + ); +} +``` diff --git a/components/scroll-lock/index.js b/components/scroll-lock/index.js new file mode 100644 index 00000000000000..aaea8ce84f4195 --- /dev/null +++ b/components/scroll-lock/index.js @@ -0,0 +1,114 @@ +/** + * WordPress dependencies + */ +import { Component } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import './index.scss'; + +/** + * Creates a ScrollLock component bound to the specified document. + * + * This function creates a ScrollLock component for the specified document + * and is exposed so we can create an isolated component for unit testing. + * + * @param {Object} args Keyword args. + * @param {HTMLDocument} args.htmlDocument The document to lock the scroll for. + * @param {string} args.className The name of the class used to lock scrolling. + * @return {Component} The bound ScrollLock component. + */ +export function createScrollLockComponent( { + htmlDocument = document, + className = 'lockscroll', +} = {} ) { + let lockCounter = 0; + + /* + * Setting `overflow: hidden` on html and body elements resets body scroll in iOS. + * Save scroll top so we can restore it after locking scroll. + * + * NOTE: It would be cleaner and possibly safer to find a localized solution such + * as preventing default on certain touchmove events. + */ + let previousScrollTop = 0; + + /** + * Locks and unlocks scroll depending on the boolean argument. + * + * @param {boolean} locked Whether or not scroll should be locked. + */ + function setLocked( locked ) { + const scrollingElement = htmlDocument.scrollingElement || htmlDocument.body; + + if ( locked ) { + previousScrollTop = scrollingElement.scrollTop; + } + + const methodName = locked ? 'add' : 'remove'; + scrollingElement.classList[ methodName ]( className ); + + // Adding the class to the document element seems to be necessary in iOS. + htmlDocument.documentElement.classList[ methodName ]( className ); + + if ( ! locked ) { + scrollingElement.scrollTop = previousScrollTop; + } + } + + /** + * Requests scroll lock. + * + * This function tracks requests for scroll lock. It locks scroll on the first + * request and counts each request so `releaseLock` can unlock scroll when + * all requests have been released. + */ + function requestLock() { + if ( lockCounter === 0 ) { + setLocked( true ); + } + + ++lockCounter; + } + + /** + * Releases a request for scroll lock. + * + * This function tracks released requests for scroll lock. When all requests + * have been released, it unlocks scroll. + */ + function releaseLock() { + if ( lockCounter === 1 ) { + setLocked( false ); + } + + --lockCounter; + } + + return class ScrollLock extends Component { + /** + * Requests scroll lock on mount. + */ + componentDidMount() { + requestLock(); + } + /** + * Releases scroll lock before unmount. + */ + componentWillUnmount() { + releaseLock(); + } + + /** + * Render nothing as this component is merely a way to declare scroll lock. + * + * @return {null} Render nothing by returning `null`. + */ + render() { + return null; + } + }; +} + +export default createScrollLockComponent(); diff --git a/components/scroll-lock/index.scss b/components/scroll-lock/index.scss new file mode 100644 index 00000000000000..e1c8cebda053ff --- /dev/null +++ b/components/scroll-lock/index.scss @@ -0,0 +1,4 @@ +html.lockscroll, +body.lockscroll { + overflow: hidden; +} diff --git a/components/scroll-lock/test/index.js b/components/scroll-lock/test/index.js new file mode 100644 index 00000000000000..c86daf178cb7b2 --- /dev/null +++ b/components/scroll-lock/test/index.js @@ -0,0 +1,53 @@ +/** + * External dependencies + */ +import { mount } from 'enzyme'; + +/** + * Internal dependencies + */ +import { createScrollLockComponent } from '..'; + +describe( 'scroll-lock', () => { + const lockingClassName = 'test-lock-scroll'; + + // Use a separate document to reduce the risk of test side-effects. + let testDocument = null; + let ScrollLock = null; + let wrapper = null; + + function expectLocked( locked ) { + expect( testDocument.documentElement.classList.contains( lockingClassName ) ).toBe( locked ); + // Assert against `body` because `scrollingElement` does not exist on our test DOM implementation. + expect( testDocument.body.classList.contains( lockingClassName ) ).toBe( locked ); + } + + beforeEach( () => { + testDocument = document.implementation.createHTMLDocument( 'Test scroll-lock' ); + ScrollLock = createScrollLockComponent( { + htmlDocument: testDocument, + className: lockingClassName, + } ); + } ); + + afterEach( () => { + testDocument = null; + + if ( wrapper ) { + wrapper.unmount(); + wrapper = null; + } + } ); + + it( 'locks when mounted', () => { + expectLocked( false ); + wrapper = mount( <ScrollLock /> ); + expectLocked( true ); + } ); + it( 'unlocks when unmounted', () => { + wrapper = mount( <ScrollLock /> ); + expectLocked( true ); + wrapper.unmount(); + expectLocked( false ); + } ); +} ); diff --git a/components/slot-fill/test/index.js b/components/slot-fill/test/index.js new file mode 100644 index 00000000000000..041c8337a34b7a --- /dev/null +++ b/components/slot-fill/test/index.js @@ -0,0 +1,28 @@ +/** + * External dependecies + */ +import { shallow } from 'enzyme'; + +/** + * Internal dependencies + */ +import { createSlotFill, Fill, Slot } from '../'; + +describe( 'createSlotFill', () => { + const SLOT_NAME = 'MyFill'; + const MyFill = createSlotFill( SLOT_NAME ); + + test( 'should match snapshot for Fill', () => { + const wrapper = shallow( <MyFill /> ); + + expect( wrapper.type() ).toBe( Fill ); + expect( wrapper ).toHaveProp( 'name', SLOT_NAME ); + } ); + + test( 'should match snapshot for Slot', () => { + const wrapper = shallow( <MyFill.Slot /> ); + + expect( wrapper.type() ).toBe( Slot ); + expect( wrapper ).toHaveProp( 'name', SLOT_NAME ); + } ); +} ); diff --git a/core-data/README.md b/core-data/README.md new file mode 100644 index 00000000000000..987d5b429e9d44 --- /dev/null +++ b/core-data/README.md @@ -0,0 +1,49 @@ +Core Data +========= + +Core Data is a [data module](../data) intended to simplify access to and manipulation of core WordPress entities. It registers its own store and provides a number of selectors which resolve data from the WordPress REST API automatically, along with dispatching action creators to manipulate data. + +Used in combination with features of the data module such as [`subscribe`](https://github.com/WordPress/gutenberg/tree/master/data#subscribe-function) or [higher-order components](https://github.com/WordPress/gutenberg/tree/master/data#higher-order-components), it enables a developer to easily add data into the logic and display of their plugin. + +## Example + +Below is an example of a component which simply renders a list of categories: + +```jsx +const { withSelect } = wp.data; + +function MyCategoriesList( { categories, isRequesting } ) { + if ( isRequesting ) { + return 'Loading…'; + } + + return ( + <ul> + { categories.map( ( category ) => ( + <li key={ category.id }>{ category.name }</li> + ) ) } + </ul> + ); +} + +MyCategoriesList = withSelect( ( select ) => { + const { getCategories, isRequestingCategories } = select( 'core' ); + + return { + categories: getCategories(), + isRequesting: isRequestingCategories(), + }; +} ); +``` + +## Actions + +The following set of dispatching action creators are available on the object returned by `wp.data.dispatch( 'core' )`: + +_Refer to `actions.js` for the full set of dispatching action creators. In the future, this documentation will be automatically generated to detail all available dispatching action creators._ + +## Selectors + +The following selectors are available on the object returned by `wp.data.select( 'core' )`: + +_Refer to `selectors.js` for the full set of selectors. In the future, this documentation will be automatically generated to detail all available selectors._ diff --git a/core-data/actions.js b/core-data/actions.js new file mode 100644 index 00000000000000..cd74beb40dfa1d --- /dev/null +++ b/core-data/actions.js @@ -0,0 +1,66 @@ +/** + * External dependencies + */ +import { castArray } from 'lodash'; + +/** + * Returns an action object used in signalling that the request for a given + * data type has been made. + * + * @param {string} dataType Data type requested. + * @param {?string} subType Optional data sub-type. + * + * @return {Object} Action object. + */ +export function setRequested( dataType, subType ) { + return { + type: 'SET_REQUESTED', + dataType, + subType, + }; +} + +/** + * Returns an action object used in signalling that terms have been received + * for a given taxonomy. + * + * @param {string} taxonomy Taxonomy name. + * @param {Object[]} terms Terms received. + * + * @return {Object} Action object. + */ +export function receiveTerms( taxonomy, terms ) { + return { + type: 'RECEIVE_TERMS', + taxonomy, + terms, + }; +} + +/** + * Returns an action object used in signalling that media have been received. + * + * @param {Array|Object} media Media received. + * + * @return {Object} Action object. + */ +export function receiveMedia( media ) { + return { + type: 'RECEIVE_MEDIA', + media: castArray( media ), + }; +} + +/** + * Returns an action object used in signalling that post types have been received. + * + * @param {Array|Object} postTypes Post Types received. + * + * @return {Object} Action object. + */ +export function receivePostTypes( postTypes ) { + return { + type: 'RECEIVE_POST_TYPES', + postTypes: castArray( postTypes ), + }; +} diff --git a/core-data/index.js b/core-data/index.js new file mode 100644 index 00000000000000..1b82d772a43438 --- /dev/null +++ b/core-data/index.js @@ -0,0 +1,21 @@ +/** + * WordPress Dependencies + */ +import { registerStore } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import reducer from './reducer'; +import * as selectors from './selectors'; +import * as actions from './actions'; +import * as resolvers from './resolvers'; + +const store = registerStore( 'core', { + reducer, + actions, + selectors, + resolvers, +} ); + +export default store; diff --git a/core-data/reducer.js b/core-data/reducer.js new file mode 100644 index 00000000000000..6ca7f358335e16 --- /dev/null +++ b/core-data/reducer.js @@ -0,0 +1,89 @@ +/** + * External dependencies + */ +import { keyBy } from 'lodash'; + +/** + * WordPress dependencies + */ +import { combineReducers } from '@wordpress/data'; + +/** + * Reducer managing terms state. Keyed by taxonomy slug, the value is either + * undefined (if no request has been made for given taxonomy), null (if a + * request is in-flight for given taxonomy), or the array of terms for the + * taxonomy. + * + * @param {Object} state Current state. + * @param {Object} action Dispatched action. + * + * @return {Object} Updated state. + */ +export function terms( state = {}, action ) { + switch ( action.type ) { + case 'RECEIVE_TERMS': + return { + ...state, + [ action.taxonomy ]: action.terms, + }; + + case 'SET_REQUESTED': + const { dataType, subType: taxonomy } = action; + if ( dataType !== 'terms' || state.hasOwnProperty( taxonomy ) ) { + return state; + } + + return { + ...state, + [ taxonomy ]: null, + }; + } + + return state; +} + +/** + * Reducer managing media state. Keyed by id. + * + * @param {Object} state Current state. + * @param {Object} action Dispatched action. + * + * @return {Object} Updated state. + */ +export function media( state = {}, action ) { + switch ( action.type ) { + case 'RECEIVE_MEDIA': + return { + ...state, + ...keyBy( action.media, 'id' ), + }; + } + + return state; +} + +/** + * Reducer managing post types state. Keyed by slug. + * + * @param {Object} state Current state. + * @param {Object} action Dispatched action. + * + * @return {Object} Updated state. + */ +export function postTypes( state = {}, action ) { + switch ( action.type ) { + case 'RECEIVE_POST_TYPES': + return { + ...state, + ...keyBy( action.postTypes, 'slug' ), + }; + } + + return state; +} + +export default combineReducers( { + terms, + media, + postTypes, +} ); diff --git a/core-data/resolvers.js b/core-data/resolvers.js new file mode 100644 index 00000000000000..7870a42bb68813 --- /dev/null +++ b/core-data/resolvers.js @@ -0,0 +1,46 @@ +/** + * WordPress dependencies + */ +import apiRequest from '@wordpress/api-request'; + +/** + * Internal dependencies + */ +import { + setRequested, + receiveTerms, + receiveMedia, + receivePostTypes, +} from './actions'; + +/** + * Requests categories from the REST API, yielding action objects on request + * progress. + */ +export async function* getCategories() { + yield setRequested( 'terms', 'categories' ); + const categories = await apiRequest( { path: '/wp/v2/categories' } ); + yield receiveTerms( 'categories', categories ); +} + +/** + * Requests a media element from the REST API. + * + * @param {Object} state State tree + * @param {number} id Media id + */ +export async function* getMedia( state, id ) { + const media = await apiRequest( { path: `/wp/v2/media/${ id }` } ); + yield receiveMedia( media ); +} + +/** + * Requests a post type element from the REST API. + * + * @param {Object} state State tree + * @param {number} slug Post Type slug + */ +export async function* getPostType( state, slug ) { + const postType = await apiRequest( { path: `/wp/v2/types/${ slug }?context=edit` } ); + yield receivePostTypes( postType ); +} diff --git a/core-data/selectors.js b/core-data/selectors.js new file mode 100644 index 00000000000000..560708c6c5dfc1 --- /dev/null +++ b/core-data/selectors.js @@ -0,0 +1,71 @@ +/** + * Returns all the available terms for the given taxonomy. + * + * @param {Object} state Data state. + * @param {string} taxonomy Taxonomy name. + * + * @return {Array} Categories list. + */ +export function getTerms( state, taxonomy ) { + return state.terms[ taxonomy ]; +} + +/** + * Returns all the available categories. + * + * @param {Object} state Data state. + * + * @return {Array} Categories list. + */ +export function getCategories( state ) { + return getTerms( state, 'categories' ); +} + +/** + * Returns true if a request is in progress for terms data of a given taxonomy, + * or false otherwise. + * + * @param {Object} state Data state. + * @param {string} taxonomy Taxonomy name. + * + * @return {boolean} Whether a request is in progress for taxonomy's terms. + */ +export function isRequestingTerms( state, taxonomy ) { + return state.terms[ taxonomy ] === null; +} + +/** + * Returns true if a request is in progress for categories data, or false + * otherwise. + * + * @param {Object} state Data state. + * + * @return {boolean} Whether a request is in progress for categories. + */ +export function isRequestingCategories( state ) { + return isRequestingTerms( state, 'categories' ); +} + +/** + * Returns the media object by id. + * + * @param {Object} state Data state. + * @param {number} id Media id. + * + * @return {Object?} Media object. + */ +export function getMedia( state, id ) { + return state.media[ id ]; +} + +/** + * Returns the Post Type object by slug. + * + * @param {Object} state Data state. + * @param {number} slug Post Type slug. + * + * @return {Object?} Post Type object. + */ +export function getPostType( state, slug ) { + return state.postTypes[ slug ]; +} diff --git a/core-data/test/__mocks__/@wordpress/api-request.js b/core-data/test/__mocks__/@wordpress/api-request.js new file mode 100644 index 00000000000000..6ee585673edbb6 --- /dev/null +++ b/core-data/test/__mocks__/@wordpress/api-request.js @@ -0,0 +1 @@ +module.exports = jest.fn(); diff --git a/core-data/test/reducer.js b/core-data/test/reducer.js new file mode 100644 index 00000000000000..524926579ad25f --- /dev/null +++ b/core-data/test/reducer.js @@ -0,0 +1,111 @@ +/** + * External dependencies + */ +import deepFreeze from 'deep-freeze'; + +/** + * Internal dependencies + */ +import { terms, media, postTypes } from '../reducer'; + +describe( 'terms()', () => { + it( 'returns an empty object by default', () => { + const state = terms( undefined, {} ); + + expect( state ).toEqual( {} ); + } ); + + it( 'returns with received terms', () => { + const originalState = deepFreeze( {} ); + const state = terms( originalState, { + type: 'RECEIVE_TERMS', + taxonomy: 'categories', + terms: [ { id: 1 } ], + } ); + + expect( state ).toEqual( { + categories: [ { id: 1 } ], + } ); + } ); + + it( 'assigns requested taxonomy to null', () => { + const originalState = deepFreeze( {} ); + const state = terms( originalState, { + type: 'SET_REQUESTED', + dataType: 'terms', + subType: 'categories', + } ); + + expect( state ).toEqual( { + categories: null, + } ); + } ); + + it( 'does not assign requested taxonomy to null if received', () => { + const originalState = deepFreeze( { + categories: [ { id: 1 } ], + } ); + const state = terms( originalState, { + type: 'SET_REQUESTED', + dataType: 'terms', + subType: 'categories', + } ); + + expect( state ).toEqual( { + categories: [ { id: 1 } ], + } ); + } ); + + it( 'does not assign requested taxonomy if not terms data type', () => { + const originalState = deepFreeze( {} ); + const state = terms( originalState, { + type: 'SET_REQUESTED', + dataType: 'foo', + subType: 'categories', + } ); + + expect( state ).toEqual( {} ); + } ); +} ); + +describe( 'media', () => { + it( 'returns an empty object by default', () => { + const state = media( undefined, {} ); + + expect( state ).toEqual( {} ); + } ); + + it( 'returns with received media by id', () => { + const originalState = deepFreeze( {} ); + const state = media( originalState, { + type: 'RECEIVE_MEDIA', + media: [ { id: 1, title: 'beach' }, { id: 2, title: 'sun' } ], + } ); + + expect( state ).toEqual( { + 1: { id: 1, title: 'beach' }, + 2: { id: 2, title: 'sun' }, + } ); + } ); +} ); + +describe( 'postTypes', () => { + it( 'returns an empty object by default', () => { + const state = postTypes( undefined, {} ); + + expect( state ).toEqual( {} ); + } ); + + it( 'returns with received post types by slug', () => { + const originalState = deepFreeze( {} ); + const state = postTypes( originalState, { + type: 'RECEIVE_POST_TYPES', + postTypes: [ { slug: 'b', title: 'beach' }, { slug: 's', title: 'sun' } ], + } ); + + expect( state ).toEqual( { + b: { slug: 'b', title: 'beach' }, + s: { slug: 's', title: 'sun' }, + } ); + } ); +} ); diff --git a/core-data/test/resolvers.js b/core-data/test/resolvers.js new file mode 100644 index 00000000000000..435fe2443c30b2 --- /dev/null +++ b/core-data/test/resolvers.js @@ -0,0 +1,68 @@ +/** + * WordPress dependencies + */ +import apiRequest from '@wordpress/api-request'; + +/** + * Internal dependencies + */ +import { getCategories, getMedia, getPostType } from '../resolvers'; +import { setRequested, receiveTerms, receiveMedia, receivePostTypes } from '../actions'; + +jest.mock( '@wordpress/api-request' ); + +describe( 'getCategories', () => { + const CATEGORIES = [ { id: 1 } ]; + + beforeAll( () => { + apiRequest.mockImplementation( ( options ) => { + if ( options.path === '/wp/v2/categories' ) { + return Promise.resolve( CATEGORIES ); + } + } ); + } ); + + it( 'yields with requested terms', async () => { + const fulfillment = getCategories(); + const requested = ( await fulfillment.next() ).value; + expect( requested.type ).toBe( setRequested().type ); + const received = ( await fulfillment.next() ).value; + expect( received ).toEqual( receiveTerms( 'categories', CATEGORIES ) ); + } ); +} ); + +describe( 'getMedia', () => { + const MEDIA = { id: 1 }; + + beforeAll( () => { + apiRequest.mockImplementation( ( options ) => { + if ( options.path === '/wp/v2/media/1' ) { + return Promise.resolve( MEDIA ); + } + } ); + } ); + + it( 'yields with requested media', async () => { + const fulfillment = getMedia( {}, 1 ); + const received = ( await fulfillment.next() ).value; + expect( received ).toEqual( receiveMedia( MEDIA ) ); + } ); +} ); + +describe( 'getPostType', () => { + const POST_TYPE = { slug: 'post' }; + + beforeAll( () => { + apiRequest.mockImplementation( ( options ) => { + if ( options.path === '/wp/v2/types/post?context=edit' ) { + return Promise.resolve( POST_TYPE ); + } + } ); + } ); + + it( 'yields with requested post type', async () => { + const fulfillment = getPostType( {}, 'post' ); + const received = ( await fulfillment.next() ).value; + expect( received ).toEqual( receivePostTypes( POST_TYPE ) ); + } ); +} ); diff --git a/core-data/test/selectors.js b/core-data/test/selectors.js new file mode 100644 index 00000000000000..6b069fe5d0a13e --- /dev/null +++ b/core-data/test/selectors.js @@ -0,0 +1,94 @@ +/** + * External dependencies + */ +import deepFreeze from 'deep-freeze'; + +/** + * Internal dependencies + */ +import { getTerms, isRequestingTerms, getMedia, getPostType } from '../selectors'; + +describe( 'getTerms()', () => { + it( 'returns value of terms by taxonomy', () => { + let state = deepFreeze( { + terms: {}, + } ); + expect( getTerms( state, 'categories' ) ).toBe( undefined ); + + state = deepFreeze( { + terms: { + categories: [ { id: 1 } ], + }, + } ); + expect( getTerms( state, 'categories' ) ).toEqual( [ { id: 1 } ] ); + } ); +} ); + +describe( 'isRequestingTerms()', () => { + it( 'returns false if never requested', () => { + const state = deepFreeze( { + terms: {}, + } ); + + const result = isRequestingTerms( state, 'categories' ); + expect( result ).toBe( false ); + } ); + + it( 'returns false if terms received', () => { + const state = deepFreeze( { + terms: { + categories: [ { id: 1 } ], + }, + } ); + + const result = isRequestingTerms( state, 'categories' ); + expect( result ).toBe( false ); + } ); + + it( 'returns true if requesting', () => { + const state = deepFreeze( { + terms: { + categories: null, + }, + } ); + + const result = isRequestingTerms( state, 'categories' ); + expect( result ).toBe( true ); + } ); +} ); + +describe( 'getMedia', () => { + it( 'should return undefined for unknown media', () => { + const state = deepFreeze( { + media: {}, + } ); + expect( getMedia( state, 1 ) ).toBe( undefined ); + } ); + + it( 'should return a media element by id', () => { + const state = deepFreeze( { + media: { + 1: { id: 1 }, + }, + } ); + expect( getMedia( state, 1 ) ).toEqual( { id: 1 } ); + } ); +} ); + +describe( 'getPostType', () => { + it( 'should return undefined for unknown post type', () => { + const state = deepFreeze( { + postTypes: {}, + } ); + expect( getPostType( state, 'post' ) ).toBe( undefined ); + } ); + + it( 'should return a post type by slug', () => { + const state = deepFreeze( { + postTypes: { + post: { slug: 'post' }, + }, + } ); + expect( getPostType( state, 'post' ) ).toEqual( { slug: 'post' } ); + } ); +} ); diff --git a/docs/deprecated.md b/docs/deprecated.md new file mode 100644 index 00000000000000..97b9d25eff1475 --- /dev/null +++ b/docs/deprecated.md @@ -0,0 +1,29 @@ +Gutenberg's deprecation policy is intended to support backwards-compatibility for two minor 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. + +## 2.8.0 + +- `Original autocompleter interface in wp.components.Autocomplete` updated. Please use `latest autocompleter interface` instead. See: https://github.com/WordPress/gutenberg/blob/master/components/autocomplete/README.md. +- `getInserterItems`: the `allowedBlockTypes` argument is now mandatory. +- `getFrecentInserterItems`: the `allowedBlockTypes` argument is now mandatory. + +## 2.7.0 + +- `wp.element.getWrapperDisplayName` function removed. Please use `wp.element.createHigherOrderComponent` instead. + +## 2.6.0 + + - `wp.blocks.getBlockDefaultClassname` function removed. Please use `wp.blocks.getBlockDefaultClassName` instead. + - `wp.blocks.Editable` component removed. Please use the `wp.blocks.RichText` component instead. + +## 2.5.0 + + - Returning raw HTML from block `save` is unsupported. Please use the `wp.element.RawHTML` component instead. + - `wp.data.query` higher-order component removed. Please use `wp.data.withSelect` instead. + +## 2.4.0 + + - `wp.blocks.BlockDescription` component removed. Please use the `description` block property instead. + - `wp.blocks.InspectorControls.*` components removed. Please use `wp.components.*` components instead. + - `wp.blocks.source.*` matchers removed. Please use the declarative attributes instead. See: https://wordpress.org/gutenberg/handbook/block-api/attributes/. + - `wp.data.select( 'selector', ...args )` removed. Please use `wp.data.select( reducerKey' ).*` instead. + - `wp.blocks.MediaUploadButton` component removed. Please use `wp.blocks.MediaUpload` component instead. diff --git a/docs/extensibility/autocomplete.md b/docs/extensibility/autocomplete.md new file mode 100644 index 00000000000000..8549ac1995ec38 --- /dev/null +++ b/docs/extensibility/autocomplete.md @@ -0,0 +1,85 @@ +Autocomplete +============ + +Gutenberg provides a `blocks.Autocomplete.completers` filter for extending and overriding the list of autocompleters used by blocks. + +The `Autocomplete` component found in `@wordpress/blocks` 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/blocks`. + +### Example + +Here is an example of using the `blocks.Autocomplete.completers` filter to add an acronym completer. You can find full documentation for the autocompleter interface with the `Autocomplete` component in the `@wordpress/components` package. + +{% codetabs %} +{% ES5 %} +```js +// Our completer +var acronymCompleter = { + name: 'acronyms', + triggerPrefix: '::', + options: [ + { letters: 'FYI', expansion: 'For Your Information' }, + { letters: 'AFAIK', expansion: 'As Far As I Know' }, + { letters: 'IIRC', expansion: 'If I Recall Correctly' }, + ], + getOptionKeywords: function( abbr ) { + var expansionWords = abbr.expansion.split( /\s+/ ); + return [ abbr.letters ].concat( expansionWords ); + }, + getOptionLabel: function( acronym ) { + return acronym.letters; + }, + getOptionCompletion: function( abbr ) { + return wp.element.createElement( + 'abbr', + { title: abbr.expansion }, + abbr.letters + ); + }, +}; + +// Our filter function +function appendAcronymCompleter( completers ) { + return completers.concat( acronymCompleter ); +} + +// Adding the filter +wp.hooks.addFilter( + 'blocks.Autocomplete.completers', + 'my-plugin/autocompleters/acronyms', + appendAcronymCompleter +); +``` +{% ESNext %} +```jsx +// Our completer +const acronymCompleter = { + name: 'acronyms', + triggerPrefix: '::', + options: [ + { letters: 'FYI', expansion: 'For Your Information' }, + { letters: 'AFAIK', expansion: 'As Far As I Know' }, + { letters: 'IIRC', expansion: 'If I Recall Correctly' }, + ], + getOptionKeywords( { letters, expansion } ) { + const expansionWords = expansion.split( /\s+/ ); + return [ letters, ...expansionWords ]; + }, + getOptionLabel: acronym => acronym.letters, + getOptionCompletion: ( { letters, expansion } ) => ( + <abbr title={ expansion }>{ letters }</abbr>, + ), +}; + +// Our filter function +function appendAcronymCompleter( completers ) { + return [ ...completers, acronymCompleter ]; +} + +// Adding the filter +wp.hooks.addFilter( + 'blocks.Autocomplete.completers', + 'my-plugin/autocompleters/acronym', + appendAcronymCompleter +); +``` +{% end %} diff --git a/docs/extensibility/extending-blocks.md b/docs/extensibility/extending-blocks.md new file mode 100644 index 00000000000000..106afa5ad7e4e5 --- /dev/null +++ b/docs/extensibility/extending-blocks.md @@ -0,0 +1,129 @@ +# 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 + +To modify the behavior of existing blocks, Gutenberg exposes the following Filters: + +#### `blocks.registerBlockType` + +Used to filter the block settings. It receives the block settings and the name of the block the registered block as arguments. + +#### `blocks.BlockEdit` + +Used to modify the block's `edit` component. It receives the original block `edit` component and returns a new wrapped component. + +#### `blocks.getSaveElement` + +A filter that applies to the result of a block's `save` function. This filter is used to replace or extend the element, for example using `wp.element.cloneElement` to modify the element's props or replace its children, or returning an entirely new element. + +#### `blocks.getSaveContent.extraProps` + +A filter that applies to all blocks returning a WP Element in the `save` function. This filter is used to add extra props to the root element of the `save` function. For example: to add a className, an id, or any valid prop for this element. It receives the current props of the `save` element, the block type and the block attributes as arguments. + +_Example:_ + +Adding a background by default to all blocks. + +```js +function addBackgroundColorStyle( props ) { + return Object.assign( props, { style: { backgroundColor: 'red' } } ); +} + +wp.hooks.addFilter( + 'blocks.getSaveContent.extraProps', + 'my-plugin/add-background-color-style', + addBackgroundColorStyle +); +``` + +_Note:_ This filter must always be run on every page load, and not in your browser's developer tools console. Otherwise, a [block validation](https://wordpress.org/gutenberg/handbook/block-api/block-edit-save/#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` + +Generated HTML classes for blocks follow the `wp-block-{name}` nomenclature. This filter allows to provide an alternative class name. + +_Example:_ + +```js +// Our filter function +function setBlockCustomClassName( className, blockName ) { + return blockName === 'core/code' ? + 'my-plugin-code' : + className; +} + +// Adding the filter +wp.hooks.addFilter( + 'blocks.getBlockDefaultClassName', + 'my-plugin/set-block-custom-class-name', + setBlockCustomClassName +); +``` + +#### `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. + +## Removing Blocks + +### Using a blacklist + +Adding blocks is easy enough, removing them is as easy. Plugin or theme authors have the possibility to "unregister" blocks. + +```js +// myplugin.js + +wp.blocks.unregisterBlockType( 'core/verse' ); +``` + +and load this script in the Editor + +```php +<?php +// myplugin.php + +function myplugin_blacklist_blocks() { + wp_enqueue_script( + 'myplugin-blacklist-blocks', + plugins_url( 'myplugin.js', __FILE__ ), + array( 'wp-blocks' ) + ); +} +add_action( 'enqueue_block_editor_assets', 'myplugin_blacklist_blocks' ); +``` + +### Using a whitelist + +If you want to disable all blocks except a whitelisted list, you can adapt the script above like so: + +```js +// myplugin.js +var allowedBlocks = [ + 'core/paragraph', + 'core/image', + 'core/html', + 'core/freeform' +]; + +wp.blocks.getBlockTypes().forEach( function( blockType ) { + if ( allowedBlocks.indexOf( blockType.name ) === -1 ) { + wp.blocks.unregisterBlockType( blockType.name ); + } +} ); +``` + +## Hiding blocks from the inserter + +On the server, you can filter the list of blocks shown in the inserter using the `allowed_block_types` filter. you can return either true (all block types supported), false (no block types supported), or an array of block type names to allow. + +```php +add_filter( 'allowed_block_types', function() { + return [ 'core/paragraph' ]; +} ); +``` diff --git a/docs/extensibility/meta-box.md b/docs/extensibility/meta-box.md new file mode 100644 index 00000000000000..ca5e9783b8be90 --- /dev/null +++ b/docs/extensibility/meta-box.md @@ -0,0 +1,84 @@ +# Meta Boxes + +This is a brief document detailing how meta box support works in Gutenberg. With the superior developer and user experience of blocks, especially once block templates are available, **porting PHP meta boxes to blocks is highly encouraged!** + +### Testing, Converting, and Maintaining Existing Meta Boxes + +Before converting meta boxes to blocks, it may be easier to test if a meta box works with Gutenberg, and explicitly mark it as such. + +If a meta box *doesn't* work with in Gutenberg, and updating it to work correctly is not an option, the next step is to add the `__block_editor_compatible_meta_box` argument to the meta box declaration: + +```php +add_meta_box( 'my-meta-box', 'My Meta Box', 'my_meta_box_callback', + null, 'normal', 'high', + array( + '__block_editor_compatible_meta_box' => false, + ) +); +``` + +WordPress will fall back to the Classic editor, where the meta box will continue working as before. + +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: + +```php +add_meta_box( 'my-meta-box', 'My Meta Box', 'my_meta_box_callback', + null, 'normal', 'high', + array( + '__back_compat_meta_box' => false, + ) +); +``` + +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. + +### Meta Box Data Collection + +On each Gutenberg page load, we register an action that collects the meta box data to determine if an area is empty. The original global state is reset upon collection of meta box data. + +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. + +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`. + +Ideally, this could be done at instantiation of the editor and help simplify this flow. However, it is not possible to know the meta box state before `admin_enqueue_scripts`, where we are calling `initializeEditor()`. This will have to do, unless we want to move `initializeEditor()` to fire in the footer or at some point after `admin_head`. With recent changes to editor bootstrapping this might now be possible. Test with ACF to make sure. + +### Redux and React Meta Box Management + +When rendering the Gutenberg Page, the meta boxes are rendered to a hidden div `#metaboxes`. + +*The Redux store will hold all meta boxes as inactive by default*. When +`INITIALIZE_META_BOX_STATE` comes in, the store will update any active meta box areas by setting the `isActive` flag to `true`. Once this happens React will check for the new props sent in by Redux on the `MetaBox` component. If that `MetaBox` is now active, instead of rendering null, a `MetaBoxArea` component will be rendered. The `MetaBox` component is the container component that mediates between the `MetaBoxArea` and the Redux Store. *If no meta boxes are active, nothing happens. This will be the default behavior, as all core meta boxes have been stripped.* + +#### MetaBoxArea Component + +When the component renders it will store a reference to the meta boxes container and retrieve the meta boxes HTML from the prefetch location. + +When the post is updated, only meta box areas that are active will be submitted. This prevents unnecessary requests. No extra revisions are created by the meta box submissions. A Redux action will trigger on `REQUEST_POST_UPDATE` for any active meta box. See `editor/effects.js`. The `REQUEST_META_BOX_UPDATES` action will set that meta box's state to `isUpdating`. The `isUpdating` prop will be sent into the `MetaBoxArea` and cause a form submission. + +When the meta box area is saving, we display an updating overlay, to prevent users from changing the form values while a save is in progress. + +After the new block editor is made into the default editor, it will be necessary to provide the classic-editor flag to access the meta box partial page. + +`gutenberg_meta_box_save()` saves meta box changes. A `meta_box` request parameter should be present and should match one of `'advanced'`, `'normal'`, or `'side'`. This value will determine which meta box area is served. + +So an example url would look like: + +`mysite.com/wp-admin/post.php?post=1&action=edit&meta_box=$location&classic-editor` + +This url is automatically passed into React via a `_wpMetaBoxUrl` global variable. + +This page page mimics the `post.php` post form, so when it is submitted it will fire all of the normal hooks and actions, and have the proper global state to correctly fire any PHP meta box mumbo jumbo without needing to modify any existing code. On successful submission, React will signal a `handleMetaBoxReload` to remove the updating overlay. + +### Common Compatibility Issues + +Most PHP meta boxes should continue to work in Gutenberg, but some meta boxes that include advanced functionality could break. Here are some common reasons why meta boxes might not work as expected in Gutenberg: + +- 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". diff --git a/docs/extensibility/theme-support.md b/docs/extensibility/theme-support.md new file mode 100644 index 00000000000000..353626a856c557 --- /dev/null +++ b/docs/extensibility/theme-support.md @@ -0,0 +1,55 @@ +# 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 they can provide no styles at all, and rely fully on what the theme provides. + +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. + +To opt-in for one of these features, call `add_theme_support` in the `functions.php` file of the theme. For example: + +```php +function mytheme_setup_theme_supported_features() { + add_theme_support( 'editor-color-palette', + '#a156b4', + '#d0a5db', + '#eee', + '#444' + ); +} + +add_action( 'after_setup_theme', 'mytheme_setup_theme_supported_features' ); +``` + +## Opt-in features + +### Wide Alignment: + +Some blocks such as the image block have the possibility to define a "wide" or "full" alignment by adding the corresponding classname to the block's wrapper ( `alignwide` or `alignfull` ). A theme can opt-in for this feature by calling: + +```php +add_theme_support( 'align-wide' ); +``` + +### Block Color Palettes: + +Different blocks have the possibility of customizing colors. Gutenberg provides a default palette, but a theme can overwrite it and provide its own: + +```php +add_theme_support( 'editor-color-palette', + '#a156b4', + '#d0a5db', + '#eee', + '#444' +); +``` + +The colors will be shown in order on the palette, and there's no limit to how many can be specified. + +### Disabling custom colors in block Color Palettes + +By default, the color palette offered to blocks, allows the user to select a custom color different from the editor or theme default colors. +Themes can disable this feature using: +```php +add_theme_support( 'disable-custom-colors' ); +``` + +This flag will make sure users are only able to choose colors from the `editor-color-palette` the theme provided or from the editor default colors if the theme did not provide one. diff --git a/edit-post/README.md b/edit-post/README.md new file mode 100644 index 00000000000000..e040a84248455e --- /dev/null +++ b/edit-post/README.md @@ -0,0 +1,111 @@ +## 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. + +Experimental components can be found under `wp.editPost.__experimental`. Experimental components are still being evaluated and can change in a future version. + +### `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 [`PluginMoreMenuItem`](#pluginmoremenuitem) component or the `wp.data.dispatch` API: +```js +wp.data.dispatch( 'core/edit-post' ).openGeneralSidebar( 'plugin-name/sidebar-name' ); +``` + +_Example:_ + +```jsx +const { PanelBody } = wp.components; +const { PluginSidebar } = wp.editPost; + +const MyPluginSidebar = () => ( + <PluginSidebar + name="sidebar-name" + title="Sidebar title" + > + <PanelBody> + My sidebar content + </PanelBody> + </PluginSidebar> +); +``` + +#### 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 + + +### `PluginMoreMenuItem` +**Experimental** + +Renders a menu item in the more menu drop down, and can be used to activate other plugin UI components. +The text within the component appears as the menu item label. + +_Example:_ + +```jsx +const { PluginMoreMenuItem } = wp.editPost; + +const MyPluginMenuItem = () => ( + <PluginMoreMenuItem + name="my-plugin" + icon="yes" + type="sidebar" + target="my-sidebar" + > + My Sidebar + </PluginMoreMenuItem> +); +``` + +#### Props + +##### name + +A string identifying the menu item. Must be unique for every menu item registered within the scope of your plugin. + +- Type: `String` +- Required: Yes + +##### type + +A string identifying the type of UI element you wish this menu item to activate. Can be: `sidebar`. + +- Type: `String` +- Required: Yes + +##### target + +A string identifying the UI element you wish to be activated by this menu item. Must be the same as the `name` prop you have given to that UI element. + +- 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 + + diff --git a/edit-post/components/keyboard-shortcuts/index.js b/edit-post/components/keyboard-shortcuts/index.js new file mode 100644 index 00000000000000..59dcb2513b6413 --- /dev/null +++ b/edit-post/components/keyboard-shortcuts/index.js @@ -0,0 +1,50 @@ +/** + * WordPress dependencies + */ +import { Component, compose } from '@wordpress/element'; +import { KeyboardShortcuts } from '@wordpress/components'; +import { withSelect, withDispatch } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import shortcuts from '../../keyboard-shortcuts'; + +class EditorModeKeyboardShortcuts extends Component { + constructor() { + super( ...arguments ); + + this.toggleMode = this.toggleMode.bind( this ); + } + + toggleMode() { + const { mode, switchMode } = this.props; + switchMode( mode === 'visual' ? 'text' : 'visual' ); + } + + render() { + return ( + <KeyboardShortcuts + bindGlobal + shortcuts={ { + [ shortcuts.toggleEditorMode.value ]: this.toggleMode, + } } + /> + ); + } +} + +export default compose( [ + withSelect( ( select ) => { + return { + mode: select( 'core/edit-post' ).getEditorMode(), + }; + } ), + withDispatch( ( dispatch ) => { + return { + switchMode: ( mode ) => { + dispatch( 'core/edit-post' ).switchEditorMode( mode ); + }, + }; + } ), +] )( EditorModeKeyboardShortcuts ); diff --git a/edit-post/components/plugin-more-menu-group/index.js b/edit-post/components/plugin-more-menu-group/index.js new file mode 100644 index 00000000000000..ad7251385d12d7 --- /dev/null +++ b/edit-post/components/plugin-more-menu-group/index.js @@ -0,0 +1,28 @@ +/** + * WordPress dependencies + */ +import { compose } from '@wordpress/element'; +import { withContext, MenuGroup } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import PluginMoreMenuItem, { SLOT_NAME } from '../plugin-more-menu-item'; + +const PluginMoreMenuGroup = ( { getFills, fillProps } ) => { + // We don't want the plugins menu items group to be rendered if there are no fills. + if ( ! getFills( SLOT_NAME ).length ) { + return null; + } + return ( + <MenuGroup + label={ __( 'Plugins' ) } > + <PluginMoreMenuItem.Slot name={ SLOT_NAME } fillProps={ fillProps } /> + </MenuGroup> + ); +}; + +export default compose( [ + withContext( 'getFills' )(), +] )( PluginMoreMenuGroup ); diff --git a/edit-post/components/plugin-more-menu-item/index.js b/edit-post/components/plugin-more-menu-item/index.js new file mode 100644 index 00000000000000..279ec06b351355 --- /dev/null +++ b/edit-post/components/plugin-more-menu-item/index.js @@ -0,0 +1,56 @@ +/** + * External dependencies + */ +import { flow, noop } from 'lodash'; + +/** + * WordPress dependencies + */ +import { Slot, Fill } from '@wordpress/components'; +import { PluginContext } from '@wordpress/plugins'; + +/** + * Internal dependencies + */ +import PluginSidebarMoreMenuItem from './plugin-sidebar-more-menu-item'; + +/** + * Name of slot in which the more menu items should fill. + * + * @type {string} + */ +export const SLOT_NAME = 'PluginMoreMenuItem'; + +const PluginMoreMenuItem = ( props ) => ( + <PluginContext.Consumer> + { ( { pluginName } ) => ( + <Fill name={ SLOT_NAME }> + { ( fillProps ) => { + const { + target, + type, + onClick = noop, + } = props; + + const newProps = { + ...props, + onClick: flow( onClick, fillProps.onClose ), + target: `${ pluginName }/${ target }`, + }; + + switch ( type ) { + case 'sidebar': + return <PluginSidebarMoreMenuItem { ...newProps } />; + } + return null; + } } + </Fill> + ) } + </PluginContext.Consumer> +); + +PluginMoreMenuItem.Slot = ( { fillProps } ) => ( + <Slot name={ SLOT_NAME } fillProps={ fillProps } /> +); + +export default PluginMoreMenuItem; diff --git a/edit-post/components/plugin-more-menu-item/plugin-sidebar-more-menu-item.js b/edit-post/components/plugin-more-menu-item/plugin-sidebar-more-menu-item.js new file mode 100644 index 00000000000000..57755be8ae0d17 --- /dev/null +++ b/edit-post/components/plugin-more-menu-item/plugin-sidebar-more-menu-item.js @@ -0,0 +1,41 @@ +/** + * WordPress dependencies + */ +import { compose } from '@wordpress/element'; +import { withSelect, withDispatch } from '@wordpress/data'; +import { MenuItem } from '@wordpress/components'; + +const PluginSidebarMoreMenuItem = ( { children, isSelected, icon, onClick } ) => ( + <MenuItem + icon={ isSelected ? 'yes' : icon } + isSelected={ isSelected } + onClick={ onClick } + > + { children } + </MenuItem> +); + +export default compose( [ + withSelect( ( select, ownProps ) => { + const { target } = ownProps; + return { + isSelected: select( 'core/edit-post' ).getActiveGeneralSidebarName() === target, + }; + } ), + withDispatch( ( dispatch, ownProps ) => { + const { target, isSelected } = ownProps; + const { + closeGeneralSidebar, + openGeneralSidebar, + } = dispatch( 'core/edit-post' ); + const onClick = isSelected ? + closeGeneralSidebar : + () => openGeneralSidebar( target ); + return { + onClick: () => { + ownProps.onClick(); + onClick(); + }, + }; + } ), +] )( PluginSidebarMoreMenuItem ); diff --git a/edit-post/components/sidebar/block-sidebar/index.js b/edit-post/components/sidebar/block-sidebar/index.js new file mode 100644 index 00000000000000..4bb871e973bcd3 --- /dev/null +++ b/edit-post/components/sidebar/block-sidebar/index.js @@ -0,0 +1,31 @@ +/** + * WordPress dependencies + */ +import { Panel, PanelBody } from '@wordpress/components'; +import { BlockInspector } from '@wordpress/editor'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal Dependencies + */ +import './style.scss'; +import SettingsHeader from '../settings-header'; +import Sidebar from '../'; + +const SIDEBAR_NAME = 'edit-post/block'; + +const BlockSidebar = () => ( + <Sidebar + name={ SIDEBAR_NAME } + label={ __( 'Editor block settings' ) } + > + <SettingsHeader sidebarName={ SIDEBAR_NAME } /> + <Panel> + <PanelBody className="edit-post-block-sidebar__panel"> + <BlockInspector /> + </PanelBody> + </Panel> + </Sidebar> +); + +export default BlockSidebar; diff --git a/edit-post/components/sidebar/block-sidebar/style.scss b/edit-post/components/sidebar/block-sidebar/style.scss new file mode 100644 index 00000000000000..8df820bfdc56ef --- /dev/null +++ b/edit-post/components/sidebar/block-sidebar/style.scss @@ -0,0 +1,21 @@ +.edit-post-block-sidebar__panel .components-panel__body { + border: none; + margin: 0 -16px; + + .components-base-control { + margin: 0 0 1em 0; + } + + .components-panel__body-toggle { + color: $dark-gray-500; + } + + &:first-child { + border-top: 1px solid $light-gray-500; + margin-top: 16px; + } + + &:last-child { + margin-bottom: -16px; + } +} diff --git a/edit-post/components/sidebar/document-sidebar/index.js b/edit-post/components/sidebar/document-sidebar/index.js new file mode 100644 index 00000000000000..ecf5113d5621c9 --- /dev/null +++ b/edit-post/components/sidebar/document-sidebar/index.js @@ -0,0 +1,44 @@ +/** + * WordPress dependencies + */ +import { Panel } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal Dependencies + */ +import PostStatus from '../post-status'; +import PostExcerpt from '../post-excerpt'; +import PostTaxonomies from '../post-taxonomies'; +import FeaturedImage from '../featured-image'; +import DiscussionPanel from '../discussion-panel'; +import LastRevision from '../last-revision'; +import PageAttributes from '../page-attributes'; +import DocumentOutlinePanel from '../document-outline-panel'; +import MetaBoxes from '../../meta-boxes'; +import SettingsHeader from '../settings-header'; +import Sidebar from '../'; + +const SIDEBAR_NAME = 'edit-post/document'; + +const DocumentSidebar = () => ( + <Sidebar + name={ SIDEBAR_NAME } + label={ __( 'Editor block settings' ) } + > + <SettingsHeader sidebarName={ SIDEBAR_NAME } /> + <Panel> + <PostStatus /> + <LastRevision /> + <PostTaxonomies /> + <FeaturedImage /> + <PostExcerpt /> + <DiscussionPanel /> + <PageAttributes /> + <DocumentOutlinePanel /> + <MetaBoxes location="side" usePanel /> + </Panel> + </Sidebar> +); + +export default DocumentSidebar; diff --git a/edit-post/components/sidebar/plugin-sidebar/index.js b/edit-post/components/sidebar/plugin-sidebar/index.js new file mode 100644 index 00000000000000..3ce7f5b87ec4bc --- /dev/null +++ b/edit-post/components/sidebar/plugin-sidebar/index.js @@ -0,0 +1,41 @@ +/** + * WordPress dependencies + */ +import { Panel } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { PluginContext } from '@wordpress/plugins'; + +/** + * Internal dependencies + */ +import Sidebar from '../'; +import SidebarHeader from '../sidebar-header'; + +/** + * Renders the plugin sidebar component. + * + * @return {WPElement} Plugin sidebar component. + */ +function PluginSidebar( { name, title, children } ) { + return ( + <PluginContext.Consumer> + { ( { pluginName } ) => ( + <Sidebar + name={ `${ pluginName }/${ name }` } + label={ __( 'Editor plugins' ) } + > + <SidebarHeader + closeLabel={ __( 'Close plugin' ) } + > + <strong>{ title }</strong> + </SidebarHeader> + <Panel> + { children } + </Panel> + </Sidebar> + ) } + </PluginContext.Consumer> + ); +} + +export default PluginSidebar; diff --git a/edit-post/components/sidebar/post-taxonomies/taxonomy-panel.js b/edit-post/components/sidebar/post-taxonomies/taxonomy-panel.js new file mode 100644 index 00000000000000..048458882ac09c --- /dev/null +++ b/edit-post/components/sidebar/post-taxonomies/taxonomy-panel.js @@ -0,0 +1,46 @@ +/** + * External Dependencies + */ +import { get } from 'lodash'; + +/** + * WordPress dependencies + */ +import { compose } from '@wordpress/element'; +import { PanelBody } from '@wordpress/components'; +import { withSelect, withDispatch } from '@wordpress/data'; + +function TaxonomyPanel( { taxonomy, isOpened, onTogglePanel, children } ) { + const taxonomyMenuName = get( taxonomy, [ 'labels', 'menu_name' ] ); + if ( ! taxonomyMenuName ) { + return null; + } + return ( + <PanelBody + title={ taxonomyMenuName } + opened={ isOpened } + onToggle={ onTogglePanel } + > + { children } + </PanelBody> + ); +} + +export default compose( + withSelect( ( select, ownProps ) => { + const slug = get( ownProps.taxonomy, [ 'slug' ] ); + const panelName = slug ? `taxonomy-panel-${ slug }` : ''; + return { + panelName, + isOpened: slug ? + select( 'core/edit-post' ).isEditorSidebarPanelOpened( panelName ) : + false, + }; + } ), + withDispatch( ( dispatch, ownProps ) => ( { + onTogglePanel: () => { + dispatch( 'core/edit-post' ). + toggleGeneralSidebarEditorPanel( ownProps.panelName ); + }, + } ) ), +)( TaxonomyPanel ); diff --git a/edit-post/components/sidebar/settings-header/index.js b/edit-post/components/sidebar/settings-header/index.js new file mode 100644 index 00000000000000..8049a93c210056 --- /dev/null +++ b/edit-post/components/sidebar/settings-header/index.js @@ -0,0 +1,48 @@ +/** + * WordPress dependencies + */ +import { compose } from '@wordpress/element'; +import { __, _n, sprintf } from '@wordpress/i18n'; +import { withDispatch, withSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import './style.scss'; +import SidebarHeader from '../sidebar-header'; + +const SettingsHeader = ( { count, openSidebar, sidebarName } ) => { + // Do not display "0 Blocks". + count = count === 0 ? 1 : count; + + return ( + <SidebarHeader + className="edit-post-sidebar__panel-tabs" + closeLabel={ __( 'Close settings' ) } + > + <button + onClick={ () => openSidebar( 'edit-post/document' ) } + className={ `edit-post-sidebar__panel-tab ${ sidebarName === 'edit-post/document' ? 'is-active' : '' }` } + aria-label={ __( 'Document settings' ) } + > + { __( 'Document' ) } + </button> + <button + onClick={ () => openSidebar( 'edit-post/block' ) } + className={ `edit-post-sidebar__panel-tab ${ sidebarName === 'edit-post/block' ? 'is-active' : '' }` } + aria-label={ __( 'Block settings' ) } + > + { sprintf( _n( 'Block', '%d Blocks', count ), count ) } + </button> + </SidebarHeader> + ); +}; + +export default compose( + withSelect( ( select ) => ( { + count: select( 'core/editor' ).getSelectedBlockCount(), + } ) ), + withDispatch( ( dispatch ) => ( { + openSidebar: dispatch( 'core/edit-post' ).openGeneralSidebar, + } ) ), +)( SettingsHeader ); diff --git a/edit-post/components/sidebar/settings-header/style.scss b/edit-post/components/sidebar/settings-header/style.scss new file mode 100644 index 00000000000000..d5d7ac579ad9aa --- /dev/null +++ b/edit-post/components/sidebar/settings-header/style.scss @@ -0,0 +1,29 @@ +.components-panel__header.edit-post-sidebar__panel-tabs { + justify-content: flex-start; + padding-left: 0; + padding-right: $panel-padding / 2; + border-top: 0; +} + +.edit-post-sidebar__panel-tab { + background: transparent; + border: none; + border-radius: 0; + cursor: pointer; + height: 50px; + padding: 3px 15px; // Use padding to offset the is-active border, this benefits Windows High Contrast mode + margin-left: 0; + font-weight: 400; + color: $dark-gray-900; + @include square-style__neutral; + + &.is-active { + padding-bottom: 0; + border-bottom: 3px solid $blue-medium-500; + font-weight: 600; + } + + &:focus { + @include square-style__focus; + } +} diff --git a/edit-post/components/sidebar/sidebar-header/index.js b/edit-post/components/sidebar/sidebar-header/index.js new file mode 100644 index 00000000000000..e4fa4a0559f080 --- /dev/null +++ b/edit-post/components/sidebar/sidebar-header/index.js @@ -0,0 +1,51 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { Fragment, compose } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { IconButton } from '@wordpress/components'; +import { withDispatch, withSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import './style.scss'; + +const SidebarHeader = ( { children, className, closeLabel, closeSidebar, title } ) => { + return ( + <Fragment> + <div className="components-panel__header edit-post-sidebar-header__small"> + <span className="edit-post-sidebar-header__title"> + { title || __( '(no title)' ) } + </span> + <IconButton + onClick={ closeSidebar } + icon="no-alt" + label={ closeLabel } + /> + </div> + <div className={ classnames( 'components-panel__header edit-post-sidebar-header', className ) }> + { children } + <IconButton + onClick={ closeSidebar } + icon="no-alt" + label={ closeLabel } + /> + </div> + </Fragment> + ); +}; + +export default compose( + withSelect( ( select ) => ( { + title: select( 'core/editor' ).getEditedPostAttribute( 'title' ), + } ) ), + withDispatch( ( dispatch ) => ( { + closeSidebar: dispatch( 'core/edit-post' ).closeGeneralSidebar, + } ) ), +)( SidebarHeader ); diff --git a/edit-post/components/sidebar/sidebar-header/style.scss b/edit-post/components/sidebar/sidebar-header/style.scss new file mode 100644 index 00000000000000..3fef4be85c43a3 --- /dev/null +++ b/edit-post/components/sidebar/sidebar-header/style.scss @@ -0,0 +1,29 @@ +/* Text Editor specific */ +.components-panel__header.edit-post-sidebar-header__small { + background: $white; + padding-right: $panel-padding / 2; + + .edit-post-sidebar__title { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + width: 100%; + } + + @include break-medium() { + display: none; + } +} + +.components-panel__header.edit-post-sidebar-header { + padding-right: $panel-padding / 2; + + .components-icon-button { + display: none; + margin-left: auto; + + @include break-medium() { + display: flex; + } + } +} diff --git a/edit-post/components/text-editor/index.js b/edit-post/components/text-editor/index.js new file mode 100644 index 00000000000000..3a3e69b2176ef8 --- /dev/null +++ b/edit-post/components/text-editor/index.js @@ -0,0 +1,22 @@ +/** + * WordPress dependencies + */ +import { PostTextEditor, PostTitle } from '@wordpress/editor'; + +/** + * Internal dependencies + */ +import './style.scss'; + +function TextEditor() { + return ( + <div className="edit-post-text-editor"> + <div className="edit-post-text-editor__body"> + <PostTitle /> + <PostTextEditor /> + </div> + </div> + ); +} + +export default TextEditor; diff --git a/edit-post/components/text-editor/style.scss b/edit-post/components/text-editor/style.scss new file mode 100644 index 00000000000000..39e7c8cb1943b6 --- /dev/null +++ b/edit-post/components/text-editor/style.scss @@ -0,0 +1,40 @@ +.edit-post-text-editor__body { + padding-top: 40px; + + @include break-small() { + padding-top: 40px + $admin-bar-height-big; + } + + @include break-medium() { + padding-top: 40px + $admin-bar-height; + } +} + +// Use padding to center text in the textarea, this allows you to click anywhere to focus it +.edit-post-text-editor { + padding-left: 20px; + padding-right: 20px; + + @include break-large() { + padding-left: calc( 50% - #{ $text-editor-max-width / 2 } ); + padding-right: calc( 50% - #{ $text-editor-max-width / 2 } ); + } + + .edit-post-post-text-editor__toolbar { + width: 100%; + max-width: $text-editor-max-width; + margin: 0 auto; + } + + // Always show outlines in code editor + .editor-post-title div, + .editor-post-text-editor { + border: 1px solid $light-gray-500; + } + + .editor-post-text-editor { + padding: $block-padding; + min-height: 200px; + line-height: 1.8; + } +} diff --git a/edit-post/components/visual-editor/block-inspector-button.js b/edit-post/components/visual-editor/block-inspector-button.js new file mode 100644 index 00000000000000..d5e17d40f4acbf --- /dev/null +++ b/edit-post/components/visual-editor/block-inspector-button.js @@ -0,0 +1,55 @@ +/** + * External dependencies + */ +import { flow, noop } from 'lodash'; + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { IconButton, withSpokenMessages } from '@wordpress/components'; +import { withSelect, withDispatch } from '@wordpress/data'; +import { compose } from '@wordpress/element'; + +export function BlockInspectorButton( { + areAdvancedSettingsOpened, + closeSidebar, + openEditorSidebar, + onClick = noop, + small = false, + speak, + role, +} ) { + const speakMessage = () => { + if ( areAdvancedSettingsOpened ) { + speak( __( 'Block settings closed' ) ); + } else { + speak( __( 'Additional settings are now available in the Editor block settings sidebar' ) ); + } + }; + + const label = areAdvancedSettingsOpened ? __( 'Hide Block Settings' ) : __( 'Show Block Settings' ); + + return ( + <IconButton + className="editor-block-settings-menu__control" + onClick={ flow( areAdvancedSettingsOpened ? closeSidebar : openEditorSidebar, speakMessage, onClick ) } + icon="admin-generic" + label={ small ? label : undefined } + role={ role } + > + { ! small && label } + </IconButton> + ); +} + +export default compose( + withSelect( ( select ) => ( { + areAdvancedSettingsOpened: select( 'core/edit-post' ).getActiveGeneralSidebarName() === 'edit-post/block', + } ) ), + withDispatch( ( dispatch ) => ( { + openEditorSidebar: () => dispatch( 'core/edit-post' ).openGeneralSidebar( 'edit-post/block' ), + closeSidebar: dispatch( 'core/edit-post' ).closeGeneralSidebar, + } ) ), + withSpokenMessages, +)( BlockInspectorButton ); diff --git a/edit-post/components/visual-editor/index.js b/edit-post/components/visual-editor/index.js new file mode 100644 index 00000000000000..c72aee8223a9f1 --- /dev/null +++ b/edit-post/components/visual-editor/index.js @@ -0,0 +1,47 @@ +/** + * WordPress dependencies + */ +import { + BlockList, + CopyHandler, + PostTitle, + WritingFlow, + ObserveTyping, + EditorGlobalKeyboardShortcuts, + BlockSelectionClearer, + MultiSelectScrollIntoView, +} from '@wordpress/editor'; +import { Fragment } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import './style.scss'; +import BlockInspectorButton from './block-inspector-button'; + +function VisualEditorBlockMenu( { children, onClose } ) { + return ( + <Fragment> + <BlockInspectorButton onClick={ onClose } role="menuitem" /> + { children } + </Fragment> + ); +} + +function VisualEditor() { + return ( + <BlockSelectionClearer className="edit-post-visual-editor"> + <EditorGlobalKeyboardShortcuts /> + <CopyHandler /> + <MultiSelectScrollIntoView /> + <WritingFlow> + <ObserveTyping> + <PostTitle /> + <BlockList renderBlockMenu={ VisualEditorBlockMenu } /> + </ObserveTyping> + </WritingFlow> + </BlockSelectionClearer> + ); +} + +export default VisualEditor; diff --git a/edit-post/components/visual-editor/style.scss b/edit-post/components/visual-editor/style.scss new file mode 100644 index 00000000000000..7412f27d4e74d0 --- /dev/null +++ b/edit-post/components/visual-editor/style.scss @@ -0,0 +1,124 @@ +.edit-post-visual-editor { + position: relative; + padding: 50px 0; + + &, + & p { + font-family: $editor-font; + font-size: $editor-font-size; + line-height: $editor-line-height; + } + + & ul, + & ol { + margin: 0; + padding: 0; + } + + & ul:not(.wp-block-gallery) { + list-style-type: disc; + } + + & ol { + list-style-type: decimal; + } + + & .button { + font-family: $default-font; + } +} + +.edit-post-visual-editor .editor-writing-flow__click-redirect { + // Collapse to minimum height of 50px, to fully occupy editor bottom pad. + height: 50px; + width: 100%; + max-width: $content-width; + // Offset for: Visual editor padding, block (default appender) margin. + margin: #{ -1 * $block-spacing } auto -50px; +} + +.edit-post-visual-editor .editor-block-list__block { + margin-left: auto; + margin-right: auto; + max-width: $content-width; + + @include break-small() { + .editor-block-list__block-edit { + margin-left: -$block-side-ui-padding; + margin-right: -$block-side-ui-padding; + } + + &[data-align="full"] > .editor-block-contextual-toolbar, + &[data-align="wide"] > .editor-block-contextual-toolbar { // don't affect nested block toolbars + max-width: $content-width + 2; // 1px border left and right + margin-left: auto; + margin-right: auto; + } + } + + &[data-align="wide"] { + max-width: 1100px; + } + + &[data-align="full"] { + max-width: none; + } +} + +// This is a focus style shown for blocks that need an indicator even when in an isEditing state +// like for example an image block that receives arrowkey focus. +.edit-post-visual-editor .editor-block-list__block:not( .is-selected ) { + .editor-block-list__block-edit { + box-shadow: 0 0 0 0 $white, 0 0 0 0 $dark-gray-900; + transition: .1s box-shadow .05s; + } + + &:focus .editor-block-list__block-edit { + box-shadow: 0 0 0 1px $white, 0 0 0 3px $dark-gray-900; + } +} + +.edit-post-visual-editor .editor-post-title { + margin-left: auto; + margin-right: auto; + max-width: $content-width + ( 2 * $block-side-ui-padding ); + + .editor-post-permalink { + left: $block-padding; + right: $block-padding; + } + + @include break-small() { + padding: 5px #{ $block-side-ui-padding - 1px }; // subtract 1px border, because this is an outline + + .editor-post-permalink { + left: $block-side-ui-padding; + right: $block-side-ui-padding; + } + } +} + +.edit-post-visual-editor .editor-default-block-appender { + // Default to centered and content-width, like blocks + max-width: $content-width; + margin-left: auto; + margin-right: auto; + position: relative; + + &[data-root-uid=""] .editor-default-block-appender__content:hover { + // Outline on root-level default block appender is redundant with the + // WritingFlow click redirector. + outline: 1px solid transparent; + } + + @include break-small() { + .editor-default-block-appender__content { + padding: 0 $block-padding; + } + } +} + +.edit-post-visual-editor .editor-block-list__layout > .editor-block-list__insertion-point { + max-width: $content-width; + position: relative; +} diff --git a/edit-post/editor.js b/edit-post/editor.js new file mode 100644 index 00000000000000..26eb9e39b2b093 --- /dev/null +++ b/edit-post/editor.js @@ -0,0 +1,24 @@ +/** + * WordPress dependencies + */ +import { withSelect } from '@wordpress/data'; +import { EditorProvider, ErrorBoundary } from '@wordpress/editor'; + +/** + * Internal dependencies + */ +import Layout from './components/layout'; + +function Editor( { settings, hasFixedToolbar, onError, ...props } ) { + return ( + <EditorProvider settings={ { ...settings, hasFixedToolbar } } { ...props }> + <ErrorBoundary onError={ onError }> + <Layout /> + </ErrorBoundary> + </EditorProvider> + ); +} + +export default withSelect( select => ( { + hasFixedToolbar: select( 'core/edit-post' ).isFeatureActive( 'fixedToolbar' ), +} ) )( Editor ); diff --git a/edit-post/hooks/blocks/index.js b/edit-post/hooks/blocks/index.js new file mode 100644 index 00000000000000..35b318203784e7 --- /dev/null +++ b/edit-post/hooks/blocks/index.js @@ -0,0 +1,17 @@ +/** + * WordPress dependencies + */ +import { addFilter } from '@wordpress/hooks'; + +/** + * Internal dependencies + */ +import MediaUpload from './media-upload'; + +const replaceMediaUpload = () => MediaUpload; + +addFilter( + 'blocks.MediaUpload', + 'core/edit-post/blocks/media-upload/replaceMediaUpload', + replaceMediaUpload +); diff --git a/edit-post/hooks/blocks/media-upload/index.js b/edit-post/hooks/blocks/media-upload/index.js new file mode 100644 index 00000000000000..bc6b4922bfe174 --- /dev/null +++ b/edit-post/hooks/blocks/media-upload/index.js @@ -0,0 +1,175 @@ +/** + * External Dependencies + */ +import { pick } from 'lodash'; + +/** + * WordPress dependencies + */ +import { parseWithAttributeSchema } from '@wordpress/blocks'; +import { Component } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; + +// Getter for the sake of unit tests. +const getGalleryDetailsMediaFrame = () => { + /** + * Custom gallery details frame. + * + * @link https://github.com/xwp/wp-core-media-widgets/blob/905edbccfc2a623b73a93dac803c5335519d7837/wp-admin/js/widgets/media-gallery-widget.js + * @class GalleryDetailsMediaFrame + * @constructor + */ + return wp.media.view.MediaFrame.Post.extend( { + + /** + * Create the default states. + * + * @return {void} + */ + createStates: function createStates() { + this.states.add( [ + new wp.media.controller.Library( { + id: 'gallery', + title: wp.media.view.l10n.createGalleryTitle, + priority: 40, + toolbar: 'main-gallery', + filterable: 'uploaded', + multiple: 'add', + editable: false, + + library: wp.media.query( _.defaults( { + type: 'image', + }, this.options.library ) ), + } ), + + new wp.media.controller.GalleryEdit( { + library: this.options.selection, + editing: this.options.editing, + menu: 'gallery', + displaySettings: false, + } ), + + new wp.media.controller.GalleryAdd(), + ] ); + }, + } ); +}; + +// the media library image object contains numerous attributes +// we only need this set to display the image in the library +const slimImageObject = ( img ) => { + const attrSet = [ 'sizes', 'mime', 'type', 'subtype', 'id', 'url', 'alt', 'link', 'caption' ]; + return pick( img, attrSet ); +}; + +class MediaUpload extends Component { + constructor( { multiple = false, type, gallery = false, title = __( 'Select or Upload Media' ), modalClass } ) { + super( ...arguments ); + this.openModal = this.openModal.bind( this ); + this.onSelect = this.onSelect.bind( this ); + this.onUpdate = this.onUpdate.bind( this ); + this.onOpen = this.onOpen.bind( this ); + this.processMediaCaption = this.processMediaCaption.bind( this ); + const frameConfig = { + title, + button: { + text: __( 'Select' ), + }, + multiple, + selection: new wp.media.model.Selection( [] ), + }; + if ( !! type ) { + frameConfig.library = { type }; + } + + if ( gallery ) { + const GalleryDetailsMediaFrame = getGalleryDetailsMediaFrame(); + this.frame = new GalleryDetailsMediaFrame( { + frame: 'select', + mimeType: type, + state: 'gallery', + } ); + wp.media.frame = this.frame; + } else { + this.frame = wp.media( frameConfig ); + } + + if ( modalClass ) { + this.frame.$el.addClass( modalClass ); + } + + // When an image is selected in the media frame... + this.frame.on( 'select', this.onSelect ); + this.frame.on( 'update', this.onUpdate ); + this.frame.on( 'open', this.onOpen ); + } + + componentWillUnmount() { + this.frame.remove(); + } + + onUpdate( selections ) { + const { onSelect, multiple = false } = this.props; + const state = this.frame.state(); + const selectedImages = selections || state.get( 'selection' ); + + if ( ! selectedImages || ! selectedImages.models.length ) { + return; + } + + if ( multiple ) { + onSelect( selectedImages.models.map( ( model ) => this.processMediaCaption( slimImageObject( model.toJSON() ) ) ) ); + } else { + onSelect( this.processMediaCaption( slimImageObject( ( selectedImages.models[ 0 ].toJSON() ) ) ) ); + } + } + + onSelect() { + const { onSelect, multiple = false } = this.props; + // Get media attachment details from the frame state + const attachment = this.frame.state().get( 'selection' ).toJSON(); + onSelect( + multiple ? + attachment.map( this.processMediaCaption ) : + this.processMediaCaption( attachment[ 0 ] ) + ); + } + + onOpen() { + const selection = this.frame.state().get( 'selection' ); + const addMedia = ( id ) => { + const attachment = wp.media.attachment( id ); + attachment.fetch(); + selection.add( attachment ); + }; + + if ( ! this.props.value ) { + return; + } + + if ( this.props.multiple ) { + this.props.value.map( addMedia ); + } else { + addMedia( this.props.value ); + } + } + + openModal() { + this.frame.open(); + } + + processMediaCaption( mediaObject ) { + return ! mediaObject.caption ? + mediaObject : + { ...mediaObject, caption: parseWithAttributeSchema( mediaObject.caption, { + source: 'children', + } ) }; + } + + render() { + return this.props.render( { open: this.openModal } ); + } +} + +export default MediaUpload; + diff --git a/edit-post/store/utils.js b/edit-post/store/utils.js new file mode 100644 index 00000000000000..86b043327e291d --- /dev/null +++ b/edit-post/store/utils.js @@ -0,0 +1,18 @@ +/** + * Given a selector returns a functions that returns the listener only + * if the returned value from the selector changes. + * + * @param {function} selector Selector. + * @param {function} listener Listener. + * @return {function} Listener creator. + */ +export const onChangeListener = ( selector, listener ) => { + let previousValue = selector(); + return () => { + const selectedValue = selector(); + if ( selectedValue !== previousValue ) { + previousValue = selectedValue; + listener( selectedValue ); + } + }; +}; diff --git a/editor/components/block-drop-zone/style.scss b/editor/components/block-drop-zone/style.scss new file mode 100644 index 00000000000000..efb544ea7d1794 --- /dev/null +++ b/editor/components/block-drop-zone/style.scss @@ -0,0 +1,29 @@ + // Dropzones +.editor-block-drop-zone { + border: none; + border-radius: 0; + + .components-drop-zone__content { + display: none; + } + + &.is-close-to-bottom { + background: none; + border-bottom: 3px solid $blue-medium-500; + } + + &.is-close-to-top, + &.is-appender.is-close-to-top, + &.is-appender.is-close-to-bottom { + background: none; + border-top: 3px solid $blue-medium-500; + border-bottom: none; + } + + &.is-dragging-html, + &.is-dragging-default { + .components-drop-zone__content { + display: none; + } + } +} diff --git a/editor/components/block-list/block-draggable.js b/editor/components/block-list/block-draggable.js new file mode 100644 index 00000000000000..3c6ccabaf99a70 --- /dev/null +++ b/editor/components/block-list/block-draggable.js @@ -0,0 +1,31 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { Draggable } from '@wordpress/components'; + +function BlockDraggable( { rootUID, index, uid, layout, isDragging, ...props } ) { + const className = classnames( 'editor-block-list__block-draggable', { + 'is-visible': isDragging, + } ); + + const transferData = { + type: 'block', + fromIndex: index, + rootUID, + uid, + layout, + }; + + return ( + <Draggable className={ className } transferData={ transferData } { ...props }> + <div className="editor-block-list__block-draggable-inner"></div> + </Draggable> + ); +} + +export default BlockDraggable; diff --git a/editor/components/block-list/breadcrumb.js b/editor/components/block-list/breadcrumb.js new file mode 100644 index 00000000000000..ce310e2616b7d7 --- /dev/null +++ b/editor/components/block-list/breadcrumb.js @@ -0,0 +1,100 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { compose, Component } from '@wordpress/element'; +import { Dashicon, Tooltip, Toolbar, Button } from '@wordpress/components'; +import { withDispatch, withSelect } from '@wordpress/data'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import NavigableToolbar from '../navigable-toolbar'; +import BlockTitle from '../block-title'; + +/** + * Block breadcrumb component, displaying the label of the block. If the block + * descends from a root block, a button is displayed enabling the user to select + * the root block. + * + * @param {string} props.uid UID of block. + * @param {string} props.rootUID UID of block's root. + * @param {Function} props.selectRootBlock Callback to select root block. + */ +export class BlockBreadcrumb extends Component { + constructor() { + super( ...arguments ); + this.state = { + isFocused: false, + }; + this.onFocus = this.onFocus.bind( this ); + this.onBlur = this.onBlur.bind( this ); + } + + onFocus( event ) { + this.setState( { + isFocused: true, + } ); + + // This is used for improved interoperability + // with the block's `onFocus` handler which selects the block, thus conflicting + // with the intention to select the root block. + event.stopPropagation(); + } + + onBlur() { + this.setState( { + isFocused: false, + } ); + } + + render( ) { + const { uid, rootUID, selectRootBlock, isHidden } = this.props; + const { isFocused } = this.state; + + return ( + <NavigableToolbar className={ classnames( 'editor-block-list__breadcrumb', { + 'is-visible': ! isHidden || isFocused, + } ) }> + <Toolbar> + { rootUID && ( + <Tooltip text={ __( 'Select parent block' ) }> + <Button + onClick={ selectRootBlock } + onFocus={ this.onFocus } + onBlur={ this.onBlur } + > + <Dashicon icon="arrow-left-alt" uid={ uid } /> + </Button> + </Tooltip> + ) } + <BlockTitle uid={ uid } /> + </Toolbar> + </NavigableToolbar> + ); + } +} + +export default compose( [ + withSelect( ( select, ownProps ) => { + const { getBlockRootUID } = select( 'core/editor' ); + const { uid } = ownProps; + + return { + rootUID: getBlockRootUID( uid ), + }; + } ), + withDispatch( ( dispatch, ownProps ) => { + const { rootUID } = ownProps; + const { selectBlock } = dispatch( 'core/editor' ); + + return { + selectRootBlock: () => selectBlock( rootUID ), + }; + } ), +] )( BlockBreadcrumb ); diff --git a/editor/components/block-list/with-hover-areas.js b/editor/components/block-list/with-hover-areas.js new file mode 100644 index 00000000000000..a894f491307467 --- /dev/null +++ b/editor/components/block-list/with-hover-areas.js @@ -0,0 +1,62 @@ +/** + * WordPress dependencies + */ +import { Component, findDOMNode, createHigherOrderComponent } from '@wordpress/element'; + +const withHoverAreas = createHigherOrderComponent( ( WrappedComponent ) => { + class WithHoverAreasComponent extends Component { + constructor() { + super( ...arguments ); + this.state = { + hoverArea: null, + }; + this.onMouseLeave = this.onMouseLeave.bind( this ); + this.onMouseMove = this.onMouseMove.bind( this ); + } + + componentDidMount() { + // Disable reason: We use findDOMNode to avoid unnecessary extra dom Nodes + // eslint-disable-next-line react/no-find-dom-node + this.container = findDOMNode( this ); + this.container.addEventListener( 'mousemove', this.onMouseMove ); + this.container.addEventListener( 'mouseleave', this.onMouseLeave ); + } + + componentWillUnmount() { + this.container.removeEventListener( 'mousemove', this.onMouseMove ); + this.container.removeEventListener( 'mouseleave', this.onMouseLeave ); + } + + onMouseLeave() { + if ( this.state.hoverArea ) { + this.setState( { hoverArea: null } ); + } + } + + onMouseMove( event ) { + const { width, left, right } = this.container.getBoundingClientRect(); + + let hoverArea = null; + if ( ( event.clientX - left ) < width / 3 ) { + hoverArea = 'left'; + } else if ( ( right - event.clientX ) < width / 3 ) { + hoverArea = 'right'; + } + + if ( hoverArea !== this.state.hoverArea ) { + this.setState( { hoverArea } ); + } + } + + render() { + const { hoverArea } = this.state; + return ( + <WrappedComponent { ...this.props } hoverArea={ hoverArea } /> + ); + } + } + + return WithHoverAreasComponent; +} ); + +export default withHoverAreas; diff --git a/editor/components/block-mover/arrows.js b/editor/components/block-mover/arrows.js new file mode 100644 index 00000000000000..1d4dfbc9130038 --- /dev/null +++ b/editor/components/block-mover/arrows.js @@ -0,0 +1,11 @@ +export const upArrow = ( + <svg width="18" height="18" xmlns="http://www.w3.org/2000/svg" aria-hidden role="img" focusable="false"> + <path d="M12.293 12.207L9 8.914l-3.293 3.293-1.414-1.414L9 6.086l4.707 4.707z" /> + </svg> +); + +export const downArrow = ( + <svg width="18" height="18" xmlns="http://www.w3.org/2000/svg" aria-hidden role="img" focusable="false"> + <path d="M12.293 6.086L9 9.379 5.707 6.086 4.293 7.5 9 12.207 13.707 7.5z" /> + </svg> +); diff --git a/editor/components/block-mover/mover-description.js b/editor/components/block-mover/mover-description.js new file mode 100644 index 00000000000000..e10d5b2d5f8654 --- /dev/null +++ b/editor/components/block-mover/mover-description.js @@ -0,0 +1,111 @@ +/** + * Wordpress dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; + +/** + * Return a label for the block movement controls depending on block position. + * + * @param {number} selectedCount Number of blocks selected. + * @param {string} type Block type - in the case of a single block, should + * define its 'type'. I.e. 'Text', 'Heading', 'Image' etc. + * @param {number} firstIndex The index (position - 1) of the first block selected. + * @param {boolean} isFirst This is the first block. + * @param {boolean} isLast This is the last block. + * @param {number} dir Direction of movement (> 0 is considered to be going + * down, < 0 is up). + * + * @return {string} Label for the block movement controls. + */ +export function getBlockMoverDescription( selectedCount, type, firstIndex, isFirst, isLast, dir ) { + const position = ( firstIndex + 1 ); + + if ( selectedCount > 1 ) { + return getMultiBlockMoverDescription( selectedCount, firstIndex, isFirst, isLast, dir ); + } + + if ( isFirst && isLast ) { + // translators: %s: Type of block (i.e. Text, Image etc) + return sprintf( __( 'Block %s is the only block, and cannot be moved' ), type ); + } + + if ( dir > 0 && ! isLast ) { + // moving down + return sprintf( + __( 'Move %(type)s block from position %(position)d down to position %(newPosition)d' ), + { + type, + position, + newPosition: ( position + 1 ), + } + ); + } + + if ( dir > 0 && isLast ) { + // moving down, and is the last item + // translators: %s: Type of block (i.e. Text, Image etc) + return sprintf( __( 'Block %s is at the end of the content and can’t be moved down' ), type ); + } + + if ( dir < 0 && ! isFirst ) { + // moving up + return sprintf( + __( 'Move %(type)s block from position %(position)d up to position %(newPosition)d' ), + { + type, + position, + newPosition: ( position - 1 ), + } + ); + } + + if ( dir < 0 && isFirst ) { + // moving up, and is the first item + // translators: %s: Type of block (i.e. Text, Image etc) + return sprintf( __( 'Block %s is at the beginning of the content and can’t be moved up' ), type ); + } +} + +/** + * Return a label for the block movement controls depending on block position. + * + * @param {number} selectedCount Number of blocks selected. + * @param {number} firstIndex The index (position - 1) of the first block selected. + * @param {boolean} isFirst This is the first block. + * @param {boolean} isLast This is the last block. + * @param {number} dir Direction of movement (> 0 is considered to be going + * down, < 0 is up). + * + * @return {string} Label for the block movement controls. + */ +export function getMultiBlockMoverDescription( selectedCount, firstIndex, isFirst, isLast, dir ) { + const position = ( firstIndex + 1 ); + + if ( dir < 0 && isFirst ) { + return __( 'Blocks cannot be moved up as they are already at the top' ); + } + + if ( dir > 0 && isLast ) { + return __( 'Blocks cannot be moved down as they are already at the bottom' ); + } + + if ( dir < 0 && ! isFirst ) { + return sprintf( + __( 'Move %(selectedCount)d blocks from position %(position)d up by one place' ), + { + selectedCount, + position, + } + ); + } + + if ( dir > 0 && ! isLast ) { + return sprintf( + __( 'Move %(selectedCount)d blocks from position %(position)s down by one place' ), + { + selectedCount, + position, + } + ); + } +} diff --git a/editor/components/block-mover/test/mover-description.js b/editor/components/block-mover/test/mover-description.js new file mode 100644 index 00000000000000..63fa6ad52a3ba5 --- /dev/null +++ b/editor/components/block-mover/test/mover-description.js @@ -0,0 +1,112 @@ +/** + * Internal dependencies + */ +import { getBlockMoverDescription, getMultiBlockMoverDescription } from '../mover-description'; + +describe( 'block mover', () => { + const dirUp = -1, + dirDown = 1; + + describe( 'getBlockMoverDescription', () => { + const type = 'TestType'; + + it( 'Should generate a title for the first item moving up', () => { + expect( getBlockMoverDescription( + 1, + type, + 0, + true, + false, + dirUp, + ) ).toBe( + `Block ${ type } is at the beginning of the content and can’t be moved up` + ); + } ); + + it( 'Should generate a title for the last item moving down', () => { + expect( getBlockMoverDescription( + 1, + type, + 3, + false, + true, + dirDown, + ) ).toBe( `Block ${ type } is at the end of the content and can’t be moved down` ); + } ); + + it( 'Should generate a title for the second item moving up', () => { + expect( getBlockMoverDescription( + 1, + type, + 1, + false, + false, + dirUp, + ) ).toBe( `Move ${ type } block from position 2 up to position 1` ); + } ); + + it( 'Should generate a title for the second item moving down', () => { + expect( getBlockMoverDescription( + 1, + type, + 1, + false, + false, + dirDown, + ) ).toBe( `Move ${ type } block from position 2 down to position 3` ); + } ); + + it( 'Should generate a title for the only item in the list', () => { + expect( getBlockMoverDescription( + 1, + type, + 0, + true, + true, + dirDown, + ) ).toBe( `Block ${ type } is the only block, and cannot be moved` ); + } ); + } ); + + describe( 'getMultiBlockMoverDescription', () => { + it( 'Should generate a title moving multiple blocks up', () => { + expect( getMultiBlockMoverDescription( + 4, + 1, + false, + true, + dirUp, + ) ).toBe( 'Move 4 blocks from position 2 up by one place' ); + } ); + + it( 'Should generate a title moving multiple blocks down', () => { + expect( getMultiBlockMoverDescription( + 4, + 0, + true, + false, + dirDown, + ) ).toBe( 'Move 4 blocks from position 1 down by one place' ); + } ); + + it( 'Should generate a title for a selection of blocks at the top', () => { + expect( getMultiBlockMoverDescription( + 4, + 1, + true, + true, + dirUp, + ) ).toBe( 'Blocks cannot be moved up as they are already at the top' ); + } ); + + it( 'Should generate a title for a selection of blocks at the bottom', () => { + expect( getMultiBlockMoverDescription( + 4, + 2, + false, + true, + dirDown, + ) ).toBe( 'Blocks cannot be moved down as they are already at the bottom' ); + } ); + } ); +} ); diff --git a/editor/components/block-settings-menu/block-duplicate-button.js b/editor/components/block-settings-menu/block-duplicate-button.js new file mode 100644 index 00000000000000..d905e20679702e --- /dev/null +++ b/editor/components/block-settings-menu/block-duplicate-button.js @@ -0,0 +1,64 @@ +/** + * External dependencies + */ +import { flow, noop, last, every, first } from 'lodash'; + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { IconButton } from '@wordpress/components'; +import { compose } from '@wordpress/element'; +import { withSelect, withDispatch } from '@wordpress/data'; +import { cloneBlock, getBlockType, withEditorSettings } from '@wordpress/blocks'; + +export function BlockDuplicateButton( { blocks, onDuplicate, onClick = noop, isLocked, small = false, role } ) { + const canDuplicate = every( blocks, block => { + const type = getBlockType( block.name ); + return ! type.useOnce; + } ); + if ( isLocked || ! canDuplicate ) { + return null; + } + + const label = __( 'Duplicate' ); + + return ( + <IconButton + className="editor-block-settings-menu__control" + onClick={ flow( onDuplicate, onClick ) } + icon="admin-page" + label={ small ? label : undefined } + role={ role } + > + { ! small && label } + </IconButton> + ); +} + +export default compose( + withSelect( ( select, { uids, rootUID } ) => ( { + blocks: select( 'core/editor' ).getBlocksByUID( uids ), + index: select( 'core/editor' ).getBlockIndex( last( uids ), rootUID ), + } ) ), + withDispatch( ( dispatch, { blocks, index, rootUID } ) => ( { + onDuplicate() { + const clonedBlocks = blocks.map( block => cloneBlock( block ) ); + dispatch( 'core/editor' ).insertBlocks( + clonedBlocks, + index + 1, + rootUID + ); + if ( clonedBlocks.length > 1 ) { + dispatch( 'core/editor' ).multiSelect( first( clonedBlocks ).uid, last( clonedBlocks ).uid ); + } + }, + } ) ), + withEditorSettings( ( settings ) => { + const { templateLock } = settings; + + return { + isLocked: !! templateLock, + }; + } ), +)( BlockDuplicateButton ); diff --git a/editor/components/block-settings-menu/shared-block-settings.js b/editor/components/block-settings-menu/shared-block-settings.js new file mode 100644 index 00000000000000..50ad4fb401f533 --- /dev/null +++ b/editor/components/block-settings-menu/shared-block-settings.js @@ -0,0 +1,92 @@ +/** + * External dependencies + */ +import { noop } from 'lodash'; + +/** + * WordPress dependencies + */ +import { Fragment, compose } from '@wordpress/element'; +import { IconButton } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { isSharedBlock } from '@wordpress/blocks'; +import { withSelect, withDispatch } from '@wordpress/data'; + +export function SharedBlockSettings( { sharedBlock, onConvertToStatic, onConvertToShared, onDelete, itemsRole } ) { + return ( + <Fragment> + { ! sharedBlock && ( + <IconButton + className="editor-block-settings-menu__control" + icon="controls-repeat" + onClick={ onConvertToShared } + role={ itemsRole } + > + { __( 'Convert to Shared Block' ) } + </IconButton> + ) } + { sharedBlock && ( + <div className="editor-block-settings-menu__section"> + <IconButton + className="editor-block-settings-menu__control" + icon="controls-repeat" + onClick={ onConvertToStatic } + role={ itemsRole } + > + { __( 'Convert to Regular Block' ) } + </IconButton> + <IconButton + className="editor-block-settings-menu__control" + icon="no" + disabled={ sharedBlock.isTemporary } + onClick={ () => onDelete( sharedBlock.id ) } + role={ itemsRole } + > + { __( 'Delete Shared Block' ) } + </IconButton> + </div> + ) } + </Fragment> + ); +} + +export default compose( [ + withSelect( ( select, { uid } ) => { + const { getBlock, getSharedBlock } = select( 'core/editor' ); + const block = getBlock( uid ); + return { + sharedBlock: block && isSharedBlock( block ) ? getSharedBlock( block.attributes.ref ) : null, + }; + } ), + withDispatch( ( dispatch, { uid, onToggle = noop } ) => { + const { + convertBlockToShared, + convertBlockToStatic, + deleteSharedBlock, + } = dispatch( 'core/editor' ); + + return { + onConvertToStatic() { + convertBlockToStatic( uid ); + onToggle(); + }, + onConvertToShared() { + convertBlockToShared( uid ); + onToggle(); + }, + onDelete( id ) { + // TODO: Make this a <Confirm /> component or similar + // eslint-disable-next-line no-alert + const hasConfirmed = window.confirm( __( + 'Are you sure you want to delete this Shared Block?\n\n' + + 'It will be permanently removed from all posts and pages that use it.' + ) ); + + if ( hasConfirmed ) { + deleteSharedBlock( id ); + onToggle(); + } + }, + }; + } ), +] )( SharedBlockSettings ); diff --git a/editor/components/block-settings-menu/test/shared-block-settings.js b/editor/components/block-settings-menu/test/shared-block-settings.js new file mode 100644 index 00000000000000..904d5c7732b7ae --- /dev/null +++ b/editor/components/block-settings-menu/test/shared-block-settings.js @@ -0,0 +1,59 @@ +/** + * External dependencies + */ +import { shallow } from 'enzyme'; + +/** + * Internal dependencies + */ +import { SharedBlockSettings } from '../shared-block-settings'; + +describe( 'SharedBlockSettings', () => { + it( 'should allow converting a static block to a shared block', () => { + const onConvert = jest.fn(); + const wrapper = shallow( + <SharedBlockSettings + sharedBlock={ null } + onConvertToShared={ onConvert } + /> + ); + + const text = wrapper.find( 'IconButton' ).children().text(); + expect( text ).toEqual( 'Convert to Shared Block' ); + + wrapper.find( 'IconButton' ).simulate( 'click' ); + expect( onConvert ).toHaveBeenCalled(); + } ); + + it( 'should allow converting a shared block to static', () => { + const onConvert = jest.fn(); + const wrapper = shallow( + <SharedBlockSettings + sharedBlock={ {} } + onConvertToStatic={ onConvert } + /> + ); + + const text = wrapper.find( 'IconButton' ).first().children().text(); + expect( text ).toEqual( 'Convert to Regular Block' ); + + wrapper.find( 'IconButton' ).first().simulate( 'click' ); + expect( onConvert ).toHaveBeenCalled(); + } ); + + it( 'should allow deleting a shared block', () => { + const onDelete = jest.fn(); + const wrapper = shallow( + <SharedBlockSettings + sharedBlock={ { id: 123 } } + onDelete={ onDelete } + /> + ); + + const text = wrapper.find( 'IconButton' ).last().children().text(); + expect( text ).toEqual( 'Delete Shared Block' ); + + wrapper.find( 'IconButton' ).last().simulate( 'click' ); + expect( onDelete ).toHaveBeenCalledWith( 123 ); + } ); +} ); diff --git a/editor/components/block-switcher/test/__snapshots__/index.js.snap b/editor/components/block-switcher/test/__snapshots__/index.js.snap new file mode 100644 index 00000000000000..3c19baa2d4fd1f --- /dev/null +++ b/editor/components/block-switcher/test/__snapshots__/index.js.snap @@ -0,0 +1,10 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`BlockSwitcher should render switcher with blocks 1`] = ` +<Dropdown + className="editor-block-switcher" + contentClassName="editor-block-switcher__popover" + renderContent={[Function]} + renderToggle={[Function]} +/> +`; diff --git a/editor/components/block-switcher/test/__snapshots__/multi-blocks-switcher.js.snap b/editor/components/block-switcher/test/__snapshots__/multi-blocks-switcher.js.snap new file mode 100644 index 00000000000000..30fe0ea13763f5 --- /dev/null +++ b/editor/components/block-switcher/test/__snapshots__/multi-blocks-switcher.js.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MultiBlocksSwitcher should return a BlockSwitcher element matching the snapshot. 1`] = ` +<WithSelect(WithDispatch(WithEditorSettings(BlockSwitcher))) + key="switcher" + uids={ + Array [ + "an-uid", + "another-uid", + ] + } +/> +`; diff --git a/editor/components/block-switcher/test/index.js b/editor/components/block-switcher/test/index.js new file mode 100644 index 00000000000000..156f419a72f22b --- /dev/null +++ b/editor/components/block-switcher/test/index.js @@ -0,0 +1,158 @@ +/** + * External dependencies + */ +import { shallow } from 'enzyme'; + +/** + * WordPress dependencies + */ +import { registerCoreBlocks } from '@wordpress/blocks'; +import { keycodes } from '@wordpress/utils'; + +/** + * Internal dependencies + */ +import { BlockSwitcher } from '../'; + +const { DOWN } = keycodes; + +describe( 'BlockSwitcher', () => { + const headingBlock1 = { + attributes: { + content: [ 'How are you?' ], + nodeName: 'H2', + }, + isValid: true, + name: 'core/heading', + originalContent: '<h2>How are you?</h2>', + uid: 'a1303fd6-3e60-4fff-a770-0e0ea656c5b9', + }; + + const textBlock = { + attributes: { + content: [ 'I am great!' ], + nodeName: 'P', + }, + isValid: true, + name: 'core/text', + originalContent: '<p>I am great!</p>', + uid: 'b1303fdb-3e60-43faf-a770-2e1ea656c5b8', + }; + + const headingBlock2 = { + attributes: { + content: [ 'I am the greatest!' ], + nodeName: 'H3', + }, + isValid: true, + name: 'core/text', + originalContent: '<h3>I am the greatest!</h3>', + uid: 'c2403fd2-4e63-5ffa-b71c-1e0ea656c5b0', + }; + + beforeAll( () => { + registerCoreBlocks(); + } ); + + test( 'should not render block switcher without blocks', () => { + const wrapper = shallow( <BlockSwitcher /> ); + + expect( wrapper.html() ).toBeNull(); + } ); + + test( 'should render switcher with blocks', () => { + const blocks = [ + headingBlock1, + ]; + const wrapper = shallow( <BlockSwitcher blocks={ blocks } /> ); + + expect( wrapper ).toMatchSnapshot(); + } ); + + test( 'should not render block switcher with multi block of different types.', () => { + const blocks = [ + headingBlock1, + textBlock, + ]; + const wrapper = shallow( <BlockSwitcher blocks={ blocks } /> ); + + expect( wrapper.html() ).toBeNull(); + } ); + + test( 'should not render a component when the multi selected types of blocks match.', () => { + const blocks = [ + headingBlock1, + headingBlock2, + ]; + const wrapper = shallow( <BlockSwitcher blocks={ blocks } /> ); + + expect( wrapper.html() ).toBeNull(); + } ); + + describe( 'Dropdown', () => { + const blocks = [ + headingBlock1, + ]; + + const onTransformStub = jest.fn(); + const getDropdown = () => { + const blockSwitcher = shallow( <BlockSwitcher blocks={ blocks } onTransform={ onTransformStub } /> ); + return blockSwitcher.find( 'Dropdown' ); + }; + + test( 'should dropdown exist', () => { + expect( getDropdown() ).toHaveLength( 1 ); + } ); + + describe( '.renderToggle', () => { + const onToggleStub = jest.fn(); + const mockKeyDown = { + preventDefault: () => {}, + stopPropagation: () => {}, + keyCode: DOWN, + }; + + afterEach( () => { + onToggleStub.mockReset(); + } ); + + test( 'should simulate a keydown event, which should call onToggle and open transform toggle.', () => { + const toggleClosed = shallow( getDropdown().props().renderToggle( { onToggle: onToggleStub, isOpen: false } ) ); + const iconButtonClosed = toggleClosed.find( 'IconButton' ); + + iconButtonClosed.simulate( 'keydown', mockKeyDown ); + + expect( onToggleStub ).toHaveBeenCalledTimes( 1 ); + } ); + + test( 'should simulate a click event, which should call onToggle.', () => { + const toggleOpen = shallow( getDropdown().props().renderToggle( { onToggle: onToggleStub, isOpen: true } ) ); + const iconButtonOpen = toggleOpen.find( 'IconButton' ); + + iconButtonOpen.simulate( 'keydown', mockKeyDown ); + + expect( onToggleStub ).toHaveBeenCalledTimes( 0 ); + } ); + } ); + + describe( '.renderContent', () => { + const onCloseStub = jest.fn(); + + const getIconButtons = () => { + const content = shallow( getDropdown().props().renderContent( { onClose: onCloseStub } ) ); + return content.find( 'IconButton' ); + }; + + test( 'should create the iconButtons for the chosen block. A heading block will have 3 items', () => { + expect( getIconButtons() ).toHaveLength( 3 ); + } ); + + test( 'should simulate the click event by closing the switcher and causing a block transform on iconButtons.', () => { + getIconButtons().first().simulate( 'click' ); + + expect( onCloseStub ).toHaveBeenCalledTimes( 1 ); + expect( onTransformStub ).toHaveBeenCalledTimes( 1 ); + } ); + } ); + } ); +} ); diff --git a/editor/components/block-switcher/test/multi-blocks-switcher.js b/editor/components/block-switcher/test/multi-blocks-switcher.js new file mode 100644 index 00000000000000..37935f47788f63 --- /dev/null +++ b/editor/components/block-switcher/test/multi-blocks-switcher.js @@ -0,0 +1,42 @@ +/** + * External dependencies + */ +import { shallow } from 'enzyme'; + +/** + * Internal dependencies + */ +import { MultiBlocksSwitcher } from '../multi-blocks-switcher'; + +describe( 'MultiBlocksSwitcher', () => { + test( 'should return null when the selection is not a multi block selection.', () => { + const isMultiBlockSelection = false; + const selectedBlockUids = [ + 'an-uid', + ]; + const wrapper = shallow( + <MultiBlocksSwitcher + isMultiBlockSelection={ isMultiBlockSelection } + selectedBlockUids={ selectedBlockUids } + /> + ); + + expect( wrapper.html() ).toBeNull(); + } ); + + test( 'should return a BlockSwitcher element matching the snapshot.', () => { + const isMultiBlockSelection = true; + const selectedBlockUids = [ + 'an-uid', + 'another-uid', + ]; + const wrapper = shallow( + <MultiBlocksSwitcher + isMultiBlockSelection={ isMultiBlockSelection } + selectedBlockUids={ selectedBlockUids } + /> + ); + + expect( wrapper ).toMatchSnapshot(); + } ); +} ); diff --git a/editor/components/block-title/README.md b/editor/components/block-title/README.md new file mode 100644 index 00000000000000..9bf6964f44d118 --- /dev/null +++ b/editor/components/block-title/README.md @@ -0,0 +1,10 @@ +Block Title +=========== + +Renders the block's configured title as a string, or empty if the title cannot be determined. + +## Usage + +```jsx +<BlockTitle uid="afd1cb17-2c08-4e7a-91be-007ba7ddc3a1" /> +``` diff --git a/editor/components/block-title/index.js b/editor/components/block-title/index.js new file mode 100644 index 00000000000000..0dd1c1232f768b --- /dev/null +++ b/editor/components/block-title/index.js @@ -0,0 +1,41 @@ +/** + * WordPress dependencies + */ +import { withSelect } from '@wordpress/data'; +import { getBlockType } from '@wordpress/blocks'; + +/** + * Renders the block's configured title as a string, or empty if the title + * cannot be determined. + * + * @example + * + * ```jsx + * <BlockTitle uid="afd1cb17-2c08-4e7a-91be-007ba7ddc3a1" /> + * ``` + * + * @param {?string} props.name Block name. + * + * @return {?string} Block title. + */ +export function BlockTitle( { name } ) { + if ( ! name ) { + return null; + } + + const blockType = getBlockType( name ); + if ( ! blockType ) { + return null; + } + + return blockType.title; +} + +export default withSelect( ( select, ownProps ) => { + const { getBlockName } = select( 'core/editor' ); + const { uid } = ownProps; + + return { + name: getBlockName( uid ), + }; +} )( BlockTitle ); diff --git a/editor/components/block-title/test/index.js b/editor/components/block-title/test/index.js new file mode 100644 index 00000000000000..4306ae6247b93b --- /dev/null +++ b/editor/components/block-title/test/index.js @@ -0,0 +1,43 @@ +/** + * External dependencies + */ +import { shallow } from 'enzyme'; + +/** + * Internal dependencies + */ +import { BlockTitle } from '../'; + +jest.mock( '@wordpress/blocks', () => { + return { + getBlockType( name ) { + switch ( name ) { + case 'name-not-exists': + return null; + + case 'name-exists': + return { title: 'Block Title' }; + } + }, + }; +} ); + +describe( 'BlockTitle', () => { + it( 'renders nothing if name is falsey', () => { + const wrapper = shallow( <BlockTitle /> ); + + expect( wrapper.type() ).toBe( null ); + } ); + + it( 'renders nothing if block type does not exist', () => { + const wrapper = shallow( <BlockTitle name="name-not-exists" /> ); + + expect( wrapper.type() ).toBe( null ); + } ); + + it( 'renders title if block type exists', () => { + const wrapper = shallow( <BlockTitle name="name-exists" /> ); + + expect( wrapper.text() ).toBe( 'Block Title' ); + } ); +} ); diff --git a/editor/components/observe-typing/README.md b/editor/components/observe-typing/README.md new file mode 100644 index 00000000000000..06f5e85bfdc878 --- /dev/null +++ b/editor/components/observe-typing/README.md @@ -0,0 +1,18 @@ +Observe Typing +============== + +`<ObserveTyping />` is a component used in managing the editor's internal typing flag. When used to wrap content — typically the top-level block list — it observes keyboard and mouse events to set and unset the typing flag. The typing flag is used in considering whether the block border and controls should be visible. While typing, these elements are hidden for a distraction-free experience. + +## Usage + +Wrap the component where blocks are to be rendered with `<ObserveTyping />`: + +```jsx +function VisualEditor() { + return ( + <ObserveTyping> + <BlockList /> + </ObserveTyping> + ); +} +``` diff --git a/editor/components/observe-typing/index.js b/editor/components/observe-typing/index.js new file mode 100644 index 00000000000000..9ba35d7ce7d2ef --- /dev/null +++ b/editor/components/observe-typing/index.js @@ -0,0 +1,194 @@ +/** + * External dependencies + */ +import { includes } from 'lodash'; + +/** + * WordPress dependencies + */ +import { Component, compose } from '@wordpress/element'; +import { withSelect, withDispatch } from '@wordpress/data'; +import { isTextField, keycodes } from '@wordpress/utils'; +import { withSafeTimeout } from '@wordpress/components'; + +const { UP, RIGHT, DOWN, LEFT, ENTER, BACKSPACE } = keycodes; + +/** + * Set of key codes upon which typing is to be initiated on a keydown event. + * + * @type {number[]} + */ +const KEY_DOWN_ELIGIBLE_KEY_CODES = [ UP, RIGHT, DOWN, LEFT, ENTER, BACKSPACE ]; + +/** + * Returns true if a given keydown event can be inferred as intent to start + * typing, or false otherwise. A keydown is considered eligible if it is a + * text navigation without shift active. + * + * @param {KeyboardEvent} event Keydown event to test. + * + * @return {boolean} Whether event is eligible to start typing. + */ +function isKeyDownEligibleForStartTyping( event ) { + const { keyCode, shiftKey } = event; + return ! shiftKey && includes( KEY_DOWN_ELIGIBLE_KEY_CODES, keyCode ); +} + +class ObserveTyping extends Component { + constructor() { + super( ...arguments ); + + this.stopTypingOnSelectionUncollapse = this.stopTypingOnSelectionUncollapse.bind( this ); + this.stopTypingOnMouseMove = this.stopTypingOnMouseMove.bind( this ); + this.startTypingInTextField = this.startTypingInTextField.bind( this ); + this.stopTypingOnNonTextField = this.stopTypingOnNonTextField.bind( this ); + + this.lastMouseMove = null; + } + + componentDidMount() { + this.toggleEventBindings( this.props.isTyping ); + } + + componentDidUpdate( prevProps ) { + if ( this.props.isTyping !== prevProps.isTyping ) { + this.toggleEventBindings( this.props.isTyping ); + } + } + + componentWillUnmount() { + this.toggleEventBindings( false ); + } + + /** + * Bind or unbind events to the document when typing has started or stopped + * respectively, or when component has become unmounted. + * + * @param {boolean} isBound Whether event bindings should be applied. + */ + toggleEventBindings( isBound ) { + const bindFn = isBound ? 'addEventListener' : 'removeEventListener'; + document[ bindFn ]( 'selectionchange', this.stopTypingOnSelectionUncollapse ); + document[ bindFn ]( 'mousemove', this.stopTypingOnMouseMove ); + } + + /** + * On mouse move, unset typing flag if user has moved cursor. + * + * @param {MouseEvent} event Mousemove event. + */ + stopTypingOnMouseMove( event ) { + const { clientX, clientY } = event; + + // We need to check that the mouse really moved because Safari triggers + // mousemove events when shift or ctrl are pressed. + if ( this.lastMouseMove ) { + const { + clientX: lastClientX, + clientY: lastClientY, + } = this.lastMouseMove; + + if ( lastClientX !== clientX || lastClientY !== clientY ) { + this.props.onStopTyping(); + } + } + + this.lastMouseMove = { clientX, clientY }; + } + + /** + * On selection change, unset typing flag if user has made an uncollapsed + * (shift) selection. + */ + stopTypingOnSelectionUncollapse() { + const selection = window.getSelection(); + const isCollapsed = selection.rangeCount > 0 && selection.getRangeAt( 0 ).collapsed; + + if ( ! isCollapsed ) { + this.props.onStopTyping(); + } + } + + /** + * Handles a keypress or keydown event to infer intention to start typing. + * + * @param {KeyboardEvent} event Keypress or keydown event to interpret. + */ + startTypingInTextField( event ) { + const { isTyping, onStartTyping } = this.props; + const { type, target } = event; + + // Abort early if already typing, or key press is incurred outside a + // text field (e.g. arrow-ing through toolbar buttons). + if ( isTyping || ! isTextField( target ) ) { + return; + } + + // Special-case keydown because certain keys do not emit a keypress + // event. Conversely avoid keydown as the canonical event since there + // are many keydown which are explicitly not targeted for typing. + if ( type === 'keydown' && ! isKeyDownEligibleForStartTyping( event ) ) { + return; + } + + onStartTyping(); + } + + /** + * Stops typing when focus transitions to a non-text field element. + * + * @param {FocusEvent} event Focus event. + */ + stopTypingOnNonTextField( event ) { + event.persist(); + + // Since focus to a non-text field via arrow key will trigger before + // the keydown event, wait until after current stack before evaluating + // whether typing is to be stopped. Otherwise, typing will re-start. + this.props.setTimeout( () => { + const { isTyping, onStopTyping } = this.props; + const { target } = event; + if ( isTyping && ! isTextField( target ) ) { + onStopTyping(); + } + } ); + } + + render() { + const { children } = this.props; + + // Disable reason: This component is responsible for capturing bubbled + // keyboard events which are interpreted as typing intent. + + /* eslint-disable jsx-a11y/no-static-element-interactions */ + return ( + <div + onFocus={ this.stopTypingOnNonTextField } + onKeyPress={ this.startTypingInTextField } + onKeyDown={ this.startTypingInTextField } + > + { children } + </div> + ); + /* eslint-enable jsx-a11y/no-static-element-interactions */ + } +} + +export default compose( [ + withSelect( ( select ) => { + const { isTyping } = select( 'core/editor' ); + + return { + isTyping: isTyping(), + }; + } ), + withDispatch( ( dispatch ) => { + const { startTyping, stopTyping } = dispatch( 'core/editor' ); + + return { + onStartTyping: startTyping, + onStopTyping: stopTyping, + }; + } ), + withSafeTimeout, +] )( ObserveTyping ); diff --git a/editor/components/preserve-scroll-in-reorder/index.js b/editor/components/preserve-scroll-in-reorder/index.js new file mode 100644 index 00000000000000..400243b2fbc0b9 --- /dev/null +++ b/editor/components/preserve-scroll-in-reorder/index.js @@ -0,0 +1,80 @@ +/** + * WordPress dependencies + */ +import { Component } from '@wordpress/element'; +import { withSelect } from '@wordpress/data'; +import { getScrollContainer } from '@wordpress/utils'; + +/** + * Internal dependencies + */ +import { getBlockDOMNode } from '../../utils/dom'; + +/** + * Non-visual component which preserves offset of selected block within nearest + * scrollable container while reordering. + * + * @example + * + * ```jsx + * <PreserveScrollInReorder /> + * ``` + */ +class PreserveScrollInReorder extends Component { + componentWillUpdate( nextProps ) { + const { blockOrder, selectionStart } = nextProps; + if ( blockOrder !== this.props.blockOrder && selectionStart ) { + this.setPreviousOffset( selectionStart ); + } + } + + componentDidUpdate() { + if ( this.previousOffset ) { + this.restorePreviousOffset(); + } + } + + /** + * Given the block UID of the start of the selection, saves the block's + * top offset as an instance property before a reorder is to occur. + * + * @param {string} selectionStart UID of selected block. + */ + setPreviousOffset( selectionStart ) { + const blockNode = getBlockDOMNode( selectionStart ); + if ( ! blockNode ) { + return; + } + + this.previousOffset = blockNode.getBoundingClientRect().top; + } + + /** + * After a block reordering, restores the previous viewport top offset. + */ + restorePreviousOffset() { + const { selectionStart } = this.props; + const blockNode = getBlockDOMNode( selectionStart ); + if ( blockNode ) { + const scrollContainer = getScrollContainer( blockNode ); + if ( scrollContainer ) { + scrollContainer.scrollTop = scrollContainer.scrollTop + + blockNode.getBoundingClientRect().top - + this.previousOffset; + } + } + + delete this.previousOffset; + } + + render() { + return null; + } +} + +export default withSelect( ( select ) => { + return { + blockOrder: select( 'core/editor' ).getBlockOrder(), + selectionStart: select( 'core/editor' ).getBlockSelectionStart(), + }; +} )( PreserveScrollInReorder ); diff --git a/editor/components/skip-to-selected-block/index.js b/editor/components/skip-to-selected-block/index.js new file mode 100644 index 00000000000000..5ed7efa569d19c --- /dev/null +++ b/editor/components/skip-to-selected-block/index.js @@ -0,0 +1,31 @@ +/** + * WordPress dependencies + */ +import { withSelect } from '@wordpress/data'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal Dependencies + */ +import './style.scss'; +import { getBlockFocusableWrapper } from '../../utils/dom'; + +const SkipToSelectedBlock = ( { selectedBlockUID } ) => { + const onClick = () => { + const selectedBlockElement = getBlockFocusableWrapper( selectedBlockUID ); + selectedBlockElement.focus(); + }; + + return ( + selectedBlockUID && + <button type="button" className="button editor-skip-to-selected-block" onClick={ onClick }> + { __( 'Skip to the selected block' ) } + </button> + ); +}; + +export default withSelect( ( select ) => { + return { + selectedBlockUID: select( 'core/editor' ).getBlockSelectionStart(), + }; +} )( SkipToSelectedBlock ); diff --git a/editor/components/skip-to-selected-block/style.scss b/editor/components/skip-to-selected-block/style.scss new file mode 100644 index 00000000000000..01d2b5e61efd34 --- /dev/null +++ b/editor/components/skip-to-selected-block/style.scss @@ -0,0 +1,20 @@ +.editor-skip-to-selected-block { + position: absolute; + top: -9999em; + + &:focus { + height: auto; + width: auto; + display: block; + font-size: 14px; + font-weight: 600; + padding: 15px 23px 14px; + background: #f1f1f1; + color: $blue-wordpress; + line-height: normal; + box-shadow: 0 0 2px 2px rgba(0,0,0,.6); + text-decoration: none; + outline: none; + z-index: z-index( '.skip-to-selected-block' ); + } +} diff --git a/editor/components/template-validation-notice/index.js b/editor/components/template-validation-notice/index.js new file mode 100644 index 00000000000000..63bbaf133331ae --- /dev/null +++ b/editor/components/template-validation-notice/index.js @@ -0,0 +1,50 @@ +/** + * External dependencies + */ +import { connect } from 'react-redux'; + +/** + * WordPress dependencies + */ +import { Notice, Button } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import './style.scss'; +import { isValidTemplate } from '../../store/selectors'; +import { setTemplateValidity, synchronizeTemplate } from '../../store/actions'; + +function TemplateValidationNotice( { isValid, ...props } ) { + if ( isValid ) { + return null; + } + + const confirmSynchronization = () => { + // eslint-disable-next-line no-alert + if ( window.confirm( __( 'Resetting the template may result in loss of content, do you want to continue?' ) ) ) { + props.synchronizeTemplate(); + } + }; + + return ( + <Notice className="editor-template-validation-notice" isDismissible={ false } status="warning"> + <p>{ __( 'The content of your post doesn\'t match the template assigned to your post type.' ) }</p> + <div> + <Button className="button" onClick={ props.resetTemplateValidity }>{ __( 'Keep it as is' ) }</Button> + <Button onClick={ confirmSynchronization } isPrimary>{ __( 'Reset the template' ) }</Button> + </div> + </Notice> + ); +} + +export default connect( + ( state ) => ( { + isValid: isValidTemplate( state ), + } ), + { + resetTemplateValidity: () => setTemplateValidity( true ), + synchronizeTemplate, + } +)( TemplateValidationNotice ); diff --git a/editor/components/template-validation-notice/style.scss b/editor/components/template-validation-notice/style.scss new file mode 100644 index 00000000000000..dde63f7e36b14c --- /dev/null +++ b/editor/components/template-validation-notice/style.scss @@ -0,0 +1,9 @@ +.editor-template-validation-notice { + display: flex; + justify-content: space-between; + align-items: center; + + .components-button { + margin-left: 5px; + } +} diff --git a/editor/components/writing-flow/style.scss b/editor/components/writing-flow/style.scss new file mode 100644 index 00000000000000..06a7d77c1a69cc --- /dev/null +++ b/editor/components/writing-flow/style.scss @@ -0,0 +1,10 @@ +.editor-writing-flow { + height: 100%; + display: flex; + flex-direction: column; +} + +.editor-writing-flow__click-redirect { + flex-basis: 100%; + cursor: text; +} diff --git a/editor/store/array.js b/editor/store/array.js new file mode 100644 index 00000000000000..176d8936450afa --- /dev/null +++ b/editor/store/array.js @@ -0,0 +1,41 @@ +/** + * External dependencies + */ +import { castArray } from 'lodash'; + +/** + * Insert one or multiple elements into a given position of an array. + * + * @param {Array} array Source array. + * @param {*} elements Elements to insert. + * @param {number} index Insert Position. + * + * @return {Array} Result. + */ +export function insertAt( array, elements, index ) { + return [ + ...array.slice( 0, index ), + ...castArray( elements ), + ...array.slice( index ), + ]; +} + +/** + * Moves an element in an array. + * + * @param {Array} array Source array. + * @param {number} from Source index. + * @param {number} to Destination index. + * @param {number} count Number of elements to move. + * + * @return {Array} Result. + */ +export function moveTo( array, from, to, count = 1 ) { + const withoutMovedElements = [ ...array ]; + withoutMovedElements.splice( from, count ); + return insertAt( + withoutMovedElements, + array.slice( from, from + count ), + to, + ); +} diff --git a/editor/store/test/array.js b/editor/store/test/array.js new file mode 100644 index 00000000000000..5edcad46ad80f5 --- /dev/null +++ b/editor/store/test/array.js @@ -0,0 +1,45 @@ +/** + * Internal dependencies + */ +import { insertAt, moveTo } from '../array'; + +describe( 'array', () => { + describe( 'insertAt', () => { + it( 'should insert a unique item at a given position', () => { + const array = [ 'a', 'b', 'd' ]; + expect( insertAt( array, 'c', 2 ) ).toEqual( + [ 'a', 'b', 'c', 'd' ] + ); + } ); + + it( 'should insert multiple items at a given position', () => { + const array = [ 'a', 'b', 'e' ]; + expect( insertAt( array, [ 'c', 'd' ], 2 ) ).toEqual( + [ 'a', 'b', 'c', 'd', 'e' ] + ); + } ); + } ); + + describe( 'moveTo', () => { + it( 'should move an item to a given position', () => { + const array = [ 'a', 'b', 'd', 'c' ]; + expect( moveTo( array, 3, 2 ) ).toEqual( + [ 'a', 'b', 'c', 'd' ] + ); + } ); + + it( 'should move an item upwards to a given position', () => { + const array = [ 'b', 'a', 'c', 'd' ]; + expect( moveTo( array, 0, 1 ) ).toEqual( + [ 'a', 'b', 'c', 'd' ] + ); + } ); + + it( 'should move multiple items to a given position', () => { + const array = [ 'a', 'c', 'd', 'b', 'e' ]; + expect( moveTo( array, 1, 2, 2 ) ).toEqual( + [ 'a', 'b', 'c', 'd', 'e' ] + ); + } ); + } ); +} ); diff --git a/editor/utils/block-list.js b/editor/utils/block-list.js new file mode 100644 index 00000000000000..0f68869c783d8a --- /dev/null +++ b/editor/utils/block-list.js @@ -0,0 +1,69 @@ +/** + * External dependencies + */ +import { noop } from 'lodash'; + +/** + * WordPress dependencies + */ +import { Component } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import BlockList from '../components/block-list'; + +/** + * An object of cached BlockList components + * + * @type {Object} + */ +const INNER_BLOCK_LIST_CACHE = {}; + +/** + * Returns a BlockList component which is already pre-bound to render with a + * given UID as its rootUID prop. It is necessary to cache these components + * because otherwise the rendering of a nested BlockList will cause ancestor + * blocks to re-mount, leading to an endless cycle of remounting inner blocks. + * + * @param {string} uid Block UID to use as root UID of + * BlockList component. + * @param {Function} renderBlockMenu Render function for block menu of + * nested BlockList. + * + * @return {Component} Pre-bound BlockList component + */ +export function createInnerBlockList( uid, renderBlockMenu = noop ) { + if ( ! INNER_BLOCK_LIST_CACHE[ uid ] ) { + INNER_BLOCK_LIST_CACHE[ uid ] = [ + // The component class: + class extends Component { + componentWillMount() { + INNER_BLOCK_LIST_CACHE[ uid ][ 1 ]++; + } + + componentWillUnmount() { + // If, after decrementing the tracking count, there are no + // remaining instances of the component, remove from cache. + if ( ! INNER_BLOCK_LIST_CACHE[ uid ][ 1 ]-- ) { + delete INNER_BLOCK_LIST_CACHE[ uid ]; + } + } + + render() { + return ( + <BlockList + rootUID={ uid } + renderBlockMenu={ renderBlockMenu } + { ...this.props } /> + ); + } + }, + + // A counter tracking active mounted instances: + 0, + ]; + } + + return INNER_BLOCK_LIST_CACHE[ uid ][ 0 ]; +} diff --git a/editor/utils/dom.js b/editor/utils/dom.js new file mode 100644 index 00000000000000..28f27a9d51e1af --- /dev/null +++ b/editor/utils/dom.js @@ -0,0 +1,54 @@ +/** + * External dependencies + */ +import 'element-closest'; + +/** + * Given a block UID, returns the corresponding DOM node for the block, if + * exists. As much as possible, this helper should be avoided, and used only + * in cases where isolated behaviors need remote access to a block node. + * + * @param {string} uid Block UID. + * + * @return {Element} Block DOM node. + */ +export function getBlockDOMNode( uid ) { + return document.querySelector( '[data-block="' + uid + '"]' ); +} + +/** + * Given a block UID, returns the corresponding DOM node for the block focusable + * wrapper, if exists. As much as possible, this helper should be avoided, and + * used only in cases where isolated behaviors need remote access to a block node. + * + * @param {string} uid Block UID. + * + * @return {Element} Block DOM node. + */ +export function getBlockFocusableWrapper( uid ) { + return getBlockDOMNode( uid ).closest( '.editor-block-list__block' ); +} + +/** + * Returns true if the given HTMLElement is a block focus stop. Blocks without + * their own text fields rely on the focus stop to be keyboard navigable. + * + * @param {HTMLElement} element Element to test. + * + * @return {boolean} Whether element is a block focus stop. + */ +export function isBlockFocusStop( element ) { + return element.classList.contains( 'editor-block-list__block' ); +} + +/** + * Returns true if two elements are contained within the same block. + * + * @param {HTMLElement} a First element. + * @param {HTMLElement} b Second element. + * + * @return {boolean} Whether elements are in the same block. + */ +export function isInSameBlock( a, b ) { + return a.closest( '[data-block]' ) === b.closest( '[data-block]' ); +} diff --git a/element/serialize.js b/element/serialize.js new file mode 100644 index 00000000000000..91746f0fb57d45 --- /dev/null +++ b/element/serialize.js @@ -0,0 +1,565 @@ +/** + * Parts of this source were derived and modified from fast-react-render, + * released under the MIT license. + * + * https://github.com/alt-j/fast-react-render + * + * Copyright (c) 2016 Andrey Morozov + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +/** + * External dependencies + */ +import { isEmpty, castArray, omit, kebabCase } from 'lodash'; + +/** + * Internal dependencies + */ +import { Fragment, RawHTML } from './'; + +/** + * Valid attribute types. + * + * @type {Set} + */ +const ATTRIBUTES_TYPES = new Set( [ + 'string', + 'boolean', + 'number', +] ); + +/** + * Element tags which can be self-closing. + * + * @type {Set} + */ +const SELF_CLOSING_TAGS = new Set( [ + 'area', + 'base', + 'br', + 'col', + 'command', + 'embed', + 'hr', + 'img', + 'input', + 'keygen', + 'link', + 'meta', + 'param', + 'source', + 'track', + 'wbr', +] ); + +/** + * Boolean attributes are attributes whose presence as being assigned is + * meaningful, even if only empty. + * + * See: https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#boolean-attributes + * Extracted from: https://html.spec.whatwg.org/multipage/indices.html#attributes-3 + * + * Object.keys( [ ...document.querySelectorAll( '#attributes-1 > tbody > tr' ) ] + * .filter( ( tr ) => tr.lastChild.textContent.indexOf( 'Boolean attribute' ) !== -1 ) + * .reduce( ( result, tr ) => Object.assign( result, { + * [ tr.firstChild.textContent.trim() ]: true + * } ), {} ) ).sort(); + * + * @type {Set} + */ +const BOOLEAN_ATTRIBUTES = new Set( [ + 'allowfullscreen', + 'allowpaymentrequest', + 'allowusermedia', + 'async', + 'autofocus', + 'autoplay', + 'checked', + 'controls', + 'default', + 'defer', + 'disabled', + 'formnovalidate', + 'hidden', + 'ismap', + 'itemscope', + 'loop', + 'multiple', + 'muted', + 'nomodule', + 'novalidate', + 'open', + 'playsinline', + 'readonly', + 'required', + 'reversed', + 'selected', + 'typemustmatch', +] ); + +/** + * Enumerated attributes are attributes which must be of a specific value form. + * Like boolean attributes, these are meaningful if specified, even if not of a + * valid enumerated value. + * + * See: https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#enumerated-attribute + * Extracted from: https://html.spec.whatwg.org/multipage/indices.html#attributes-3 + * + * Object.keys( [ ...document.querySelectorAll( '#attributes-1 > tbody > tr' ) ] + * .filter( ( tr ) => /^("(.+?)";?\s*)+/.test( tr.lastChild.textContent.trim() ) ) + * .reduce( ( result, tr ) => Object.assign( result, { + * [ tr.firstChild.textContent.trim() ]: true + * } ), {} ) ).sort(); + * + * Some notable omissions: + * + * - `alt`: https://blog.whatwg.org/omit-alt + * + * @type {Set} + */ +const ENUMERATED_ATTRIBUTES = new Set( [ + 'autocapitalize', + 'autocomplete', + 'charset', + 'contenteditable', + 'crossorigin', + 'decoding', + 'dir', + 'draggable', + 'enctype', + 'formenctype', + 'formmethod', + 'http-equiv', + 'inputmode', + 'kind', + 'method', + 'preload', + 'scope', + 'shape', + 'spellcheck', + 'translate', + 'type', + 'wrap', +] ); + +/** + * Set of CSS style properties which support assignment of unitless numbers. + * Used in rendering of style properties, where `px` unit is assumed unless + * property is included in this set or value is zero. + * + * Generated via: + * + * Object.entries( document.createElement( 'div' ).style ) + * .filter( ( [ key ] ) => ( + * ! /^(webkit|ms|moz)/.test( key ) && + * ( e.style[ key ] = 10 ) && + * e.style[ key ] === '10' + * ) ) + * .map( ( [ key ] ) => key ) + * .sort(); + * + * @type {Set} + */ +const CSS_PROPERTIES_SUPPORTS_UNITLESS = new Set( [ + 'animation', + 'animationIterationCount', + 'baselineShift', + 'borderImageOutset', + 'borderImageSlice', + 'borderImageWidth', + 'columnCount', + 'cx', + 'cy', + 'fillOpacity', + 'flexGrow', + 'flexShrink', + 'floodOpacity', + 'fontWeight', + 'gridColumnEnd', + 'gridColumnStart', + 'gridRowEnd', + 'gridRowStart', + 'lineHeight', + 'opacity', + 'order', + 'orphans', + 'r', + 'rx', + 'ry', + 'shapeImageThreshold', + 'stopOpacity', + 'strokeDasharray', + 'strokeDashoffset', + 'strokeMiterlimit', + 'strokeOpacity', + 'strokeWidth', + 'tabSize', + 'widows', + 'x', + 'y', + 'zIndex', + 'zoom', +] ); + +/** + * Returns an escaped attribute value. + * + * @link https://w3c.github.io/html/syntax.html#elements-attributes + * + * "[...] the text cannot contain an ambiguous ampersand [...] must not contain + * any literal U+0022 QUOTATION MARK characters (")" + * + * @param {string} value Attribute value. + * + * @return {string} Escaped attribute value. + */ +function escapeAttribute( value ) { + return value.replace( /&/g, '&amp;' ).replace( /"/g, '&quot;' ); +} + +/** + * Returns an escaped HTML element value. + * + * @link https://w3c.github.io/html/syntax.html#writing-html-documents-elements + * @link https://w3c.github.io/html/syntax.html#ambiguous-ampersand + * + * "the text must not contain the character U+003C LESS-THAN SIGN (<) or an + * ambiguous ampersand." + * + * @param {string} value Element value. + * + * @return {string} Escaped HTML element value. + */ +function escapeHTML( value ) { + return value.replace( /&/g, '&amp;' ).replace( /</g, '&lt;' ); +} + +/** + * Returns true if the specified string is prefixed by one of an array of + * possible prefixes. + * + * @param {string} string String to check. + * @param {string[]} prefixes Possible prefixes. + * + * @return {boolean} Whether string has prefix. + */ +export function hasPrefix( string, prefixes ) { + return prefixes.some( ( prefix ) => string.indexOf( prefix ) === 0 ); +} + +/** + * Returns true if the given prop name should be ignored in attributes + * serialization, or false otherwise. + * + * @param {string} attribute Attribute to check. + * + * @return {boolean} Whether attribute should be ignored. + */ +function isInternalAttribute( attribute ) { + return 'key' === attribute || 'children' === attribute; +} + +/** + * Returns the normal form of the element's attribute value for HTML. + * + * @param {string} attribute Attribute name. + * @param {*} value Non-normalized attribute value. + * + * @return {string} Normalized attribute value. + */ +function getNormalAttributeValue( attribute, value ) { + switch ( attribute ) { + case 'style': + return renderStyle( value ); + } + + return value; +} + +/** + * Returns the normal form of the element's attribute name for HTML. + * + * @param {string} attribute Non-normalized attribute name. + * + * @return {string} Normalized attribute name. + */ +function getNormalAttributeName( attribute ) { + switch ( attribute ) { + case 'htmlFor': + return 'for'; + + case 'className': + return 'class'; + } + + return attribute.toLowerCase(); +} + +/** + * Returns the normal form of the style property value for HTML. Appends a + * default pixel unit if numeric, not a unitless property, and not zero. + * + * @param {string} property Property name. + * @param {*} value Non-normalized property value. + * + * @return {*} Normalized property value. + */ +function getNormalStyleValue( property, value ) { + if ( typeof value === 'number' && 0 !== value && + ! CSS_PROPERTIES_SUPPORTS_UNITLESS.has( property ) ) { + return value + 'px'; + } + + return value; +} + +/** + * Serializes a React element to string. + * + * @param {WPElement} element Element to serialize. + * @param {?Object} context Context object. + * + * @return {string} Serialized element. + */ +export function renderElement( element, context = {} ) { + if ( null === element || undefined === element || false === element ) { + return ''; + } + + if ( Array.isArray( element ) ) { + return renderChildren( element, context ); + } + + switch ( typeof element ) { + case 'string': + return escapeHTML( element ); + + case 'number': + return element.toString(); + } + + const { type: tagName, props } = element; + + switch ( tagName ) { + case Fragment: + return renderChildren( props.children, context ); + + case RawHTML: + const { children, ...wrapperProps } = props; + + return renderNativeComponent( + isEmpty( wrapperProps ) ? null : 'div', + { + ...wrapperProps, + dangerouslySetInnerHTML: { __html: children }, + }, + context + ); + } + + switch ( typeof tagName ) { + case 'string': + return renderNativeComponent( tagName, props, context ); + + case 'function': + if ( tagName.prototype && typeof tagName.prototype.render === 'function' ) { + return renderComponent( tagName, props, context ); + } + + return renderElement( tagName( props, context ), context ); + } + + return ''; +} + +/** + * Serializes a native component type to string. + * + * @param {?string} type Native component type to serialize, or null if + * rendering as fragment of children content. + * @param {Object} props Props object. + * @param {?Object} context Context object. + * + * @return {string} Serialized element. + */ +export function renderNativeComponent( type, props, context = {} ) { + let content = ''; + if ( type === 'textarea' && props.hasOwnProperty( 'value' ) ) { + // Textarea children can be assigned as value prop. If it is, render in + // place of children. Ensure to omit so it is not assigned as attribute + // as well. + content = renderChildren( [ props.value ], context ); + props = omit( props, 'value' ); + } else if ( props.dangerouslySetInnerHTML && + typeof props.dangerouslySetInnerHTML.__html === 'string' ) { + // Dangerous content is left unescaped. + content = props.dangerouslySetInnerHTML.__html; + } else if ( typeof props.children !== 'undefined' ) { + content = renderChildren( castArray( props.children ), context ); + } + + if ( ! type ) { + return content; + } + + const attributes = renderAttributes( props ); + + if ( SELF_CLOSING_TAGS.has( type ) ) { + return '<' + type + attributes + '/>'; + } + + return '<' + type + attributes + '>' + content + '</' + type + '>'; +} + +/** + * Serializes a non-native component type to string. + * + * @param {Function} Component Component type to serialize. + * @param {Object} props Props object. + * @param {?Object} context Context object. + * + * @return {string} Serialized element + */ +export function renderComponent( Component, props, context = {} ) { + const instance = new Component( props, context ); + + if ( typeof instance.componentWillMount === 'function' ) { + instance.componentWillMount(); + } + + if ( typeof instance.getChildContext === 'function' ) { + Object.assign( context, instance.getChildContext() ); + } + + const html = renderElement( instance.render(), context ); + + return html; +} + +/** + * Serializes an array of children to string. + * + * @param {Array} children Children to serialize. + * @param {?Object} context Context object. + * + * @return {string} Serialized children. + */ +function renderChildren( children, context = {} ) { + let result = ''; + + for ( let i = 0; i < children.length; i++ ) { + const child = children[ i ]; + + result += renderElement( child, context ); + } + + return result; +} + +/** + * Renders a props object as a string of HTML attributes. + * + * @param {Object} props Props object. + * + * @return {string} Attributes string. + */ +export function renderAttributes( props ) { + let result = ''; + + for ( const key in props ) { + const attribute = getNormalAttributeName( key ); + let value = getNormalAttributeValue( key, props[ key ] ); + + // If value is not of serializeable type, skip. + if ( ! ATTRIBUTES_TYPES.has( typeof value ) ) { + continue; + } + + // Don't render internal attribute names. + if ( isInternalAttribute( key ) ) { + continue; + } + + const isBooleanAttribute = BOOLEAN_ATTRIBUTES.has( attribute ); + + // Boolean attribute should be omitted outright if its value is false. + if ( isBooleanAttribute && value === false ) { + continue; + } + + const isMeaningfulAttribute = ( + isBooleanAttribute || + hasPrefix( key, [ 'data-', 'aria-' ] ) || + ENUMERATED_ATTRIBUTES.has( attribute ) + ); + + // Only write boolean value as attribute if meaningful. + if ( typeof value === 'boolean' && ! isMeaningfulAttribute ) { + continue; + } + + result += ' ' + attribute; + + // Boolean attributes should write attribute name, but without value. + // Mere presence of attribute name is effective truthiness. + if ( isBooleanAttribute ) { + continue; + } + + if ( typeof value === 'string' ) { + value = escapeAttribute( value ); + } + + result += '="' + value + '"'; + } + + return result; +} + +/** + * Renders a style object as a string attribute value. + * + * @param {Object} style Style object. + * + * @return {string} Style attribute value. + */ +export function renderStyle( style ) { + let result; + + for ( const property in style ) { + const value = style[ property ]; + if ( null === value || undefined === value ) { + continue; + } + + if ( result ) { + result += ';'; + } else { + result = ''; + } + + result += kebabCase( property ) + ':' + getNormalStyleValue( property, value ); + } + + return result; +} + +export default renderElement; diff --git a/element/test/serialize.js b/element/test/serialize.js new file mode 100644 index 00000000000000..610e8e29563533 --- /dev/null +++ b/element/test/serialize.js @@ -0,0 +1,524 @@ +/** + * External dependencies + */ +import { noop } from 'lodash'; + +/** + * Internal dependencies + */ +import { Component, Fragment, RawHTML } from '../'; +import serialize, { + hasPrefix, + renderElement, + renderNativeComponent, + renderComponent, + renderAttributes, + renderStyle, +} from '../serialize'; + +describe( 'serialize()', () => { + it( 'should render with context', () => { + class Provider extends Component { + getChildContext() { + return { + greeting: 'Hello!', + }; + } + + render() { + return this.props.children; + } + } + + Provider.childContextTypes = { + greeting: noop, + }; + + // NOTE: Technically, a component should only receive context if it + // explicitly defines `contextTypes`. This requirement is ignored in + // our implementation. + + function FunctionComponent( props, context ) { + return 'FunctionComponent: ' + context.greeting; + } + + class ClassComponent extends Component { + render() { + return 'ClassComponent: ' + this.context.greeting; + } + } + + const result = serialize( + <Provider> + <FunctionComponent /> + <ClassComponent /> + </Provider> + ); + + expect( result ).toBe( + 'FunctionComponent: Hello!' + + 'ClassComponent: Hello!' + ); + } ); + + describe( 'empty attributes', () => { + it( 'should not render a null attribute value', () => { + const result = serialize( <video src={ undefined } /> ); + + expect( result ).toBe( '<video></video>' ); + } ); + + it( 'should not render an undefined attribute value', () => { + const result = serialize( <video src={ null } /> ); + + expect( result ).toBe( '<video></video>' ); + } ); + + it( 'should an explicitly empty string attribute', () => { + const result = serialize( <video className="" /> ); + + expect( result ).toBe( '<video class=""></video>' ); + } ); + + it( 'should not render an empty object style', () => { + const result = serialize( <video style={ {} } /> ); + + expect( result ).toBe( '<video></video>' ); + } ); + } ); + + describe( 'boolean attributes', () => { + it( 'should render elements with false boolean attributes', () => { + [ false, null, undefined ].forEach( ( controls ) => { + const result = serialize( <video src="/" controls={ controls } /> ); + + expect( result ).toBe( '<video src="/"></video>' ); + } ); + } ); + + it( 'should render elements with true boolean attributes', () => { + [ true, 'true', 'false', '' ].forEach( ( controls ) => { + const result = serialize( <video src="/" controls={ controls } /> ); + + expect( result ).toBe( '<video src="/" controls></video>' ); + } ); + } ); + + it( 'should not render non-boolean-attribute with boolean value', () => { + const result = serialize( <video src controls /> ); + + expect( result ).toBe( '<video controls></video>' ); + } ); + } ); +} ); + +describe( 'hasPrefix()', () => { + it( 'returns true if prefixed', () => { + const result = hasPrefix( 'Hello World', [ 'baz', 'Hello' ] ); + + expect( result ).toBe( true ); + } ); + + it( 'returns false if not contains', () => { + const result = hasPrefix( 'World', [ 'Hello' ] ); + + expect( result ).toBe( false ); + } ); + + it( 'returns false if contains but not prefix', () => { + const result = hasPrefix( 'World Hello', [ 'Hello' ] ); + + expect( result ).toBe( false ); + } ); +} ); + +describe( 'renderElement()', () => { + it( 'renders empty content as empty string', () => { + [ null, undefined, false ].forEach( ( element ) => { + const result = renderElement( element ); + + expect( result ).toBe( '' ); + } ); + } ); + + it( 'renders an array of mixed content', () => { + const result = renderElement( [ 'hello', <div key="div" /> ] ); + + expect( result ).toBe( 'hello<div></div>' ); + } ); + + it( 'renders escaped string element', () => { + const result = renderElement( 'hello & world &amp; friends <img/>' ); + + expect( result ).toBe( 'hello &amp; world &amp;amp; friends &lt;img/>' ); + } ); + + it( 'renders numeric element as string', () => { + const result = renderElement( 10 ); + + expect( result ).toBe( '10' ); + } ); + + it( 'renders native component', () => { + const result = renderElement( <div className="greeting">Hello</div> ); + + expect( result ).toBe( '<div class="greeting">Hello</div>' ); + } ); + + it( 'renders function component', () => { + function Greeting() { + return <div className="greeting">Hello</div>; + } + + const result = renderElement( <Greeting /> ); + + expect( result ).toBe( '<div class="greeting">Hello</div>' ); + } ); + + it( 'renders class component', () => { + class Greeting extends Component { + render() { + return <div className="greeting">Hello</div>; + } + } + + const result = renderElement( <Greeting /> ); + + expect( result ).toBe( '<div class="greeting">Hello</div>' ); + } ); + + it( 'renders empty string for indeterminite types', () => { + const result = renderElement( {} ); + + expect( result ).toBe( '' ); + } ); + + it( 'renders Fragment as its inner children', () => { + const result = renderElement( <Fragment>Hello</Fragment> ); + + expect( result ).toBe( 'Hello' ); + } ); + + it( 'renders RawHTML as its unescaped children', () => { + const result = renderElement( <RawHTML>{ '<img/>' }</RawHTML> ); + + expect( result ).toBe( '<img/>' ); + } ); + + it( 'renders RawHTML with wrapper if props passed', () => { + const result = renderElement( <RawHTML className="foo">{ '<img/>' }</RawHTML> ); + + expect( result ).toBe( '<div class="foo"><img/></div>' ); + } ); + + it( 'renders RawHTML with empty children as empty string', () => { + const result = renderElement( <RawHTML /> ); + + expect( result ).toBe( '' ); + } ); + + it( 'renders RawHTML with wrapper and empty children', () => { + const result = renderElement( <RawHTML className="foo" /> ); + + expect( result ).toBe( '<div class="foo"></div>' ); + } ); +} ); + +describe( 'renderNativeComponent()', () => { + describe( 'textarea', () => { + it( 'should render textarea value as its content', () => { + const result = renderNativeComponent( 'textarea', { value: 'Hello', children: [] } ); + + expect( result ).toBe( '<textarea>Hello</textarea>' ); + } ); + + it( 'should render textarea children as its content', () => { + const result = renderNativeComponent( 'textarea', { children: [ 'Hello' ] } ); + + expect( result ).toBe( '<textarea>Hello</textarea>' ); + } ); + } ); + + describe( 'escaping', () => { + it( 'should escape children', () => { + const result = renderNativeComponent( 'div', { children: [ '<img/>' ] } ); + + expect( result ).toBe( '<div>&lt;img/></div>' ); + } ); + + it( 'should not render invalid dangerouslySetInnerHTML', () => { + const result = renderNativeComponent( 'div', { dangerouslySetInnerHTML: { __html: undefined } } ); + + expect( result ).toBe( '<div></div>' ); + } ); + + it( 'should not escape children with dangerouslySetInnerHTML', () => { + const result = renderNativeComponent( 'div', { dangerouslySetInnerHTML: { __html: '<img/>' } } ); + + expect( result ).toBe( '<div><img/></div>' ); + } ); + } ); + + describe( 'self-closing', () => { + it( 'should render self-closing elements', () => { + const result = renderNativeComponent( 'img', { src: 'foo.png' } ); + + expect( result ).toBe( '<img src="foo.png"/>' ); + } ); + + it( 'should ignore self-closing elements children', () => { + const result = renderNativeComponent( 'img', { src: 'foo.png', children: [ 'Surprise!' ] } ); + + expect( result ).toBe( '<img src="foo.png"/>' ); + } ); + } ); + + describe( 'with children', () => { + it( 'should render single literal child', () => { + const result = renderNativeComponent( 'div', { children: 'Hello' } ); + + expect( result ).toBe( '<div>Hello</div>' ); + } ); + + it( 'should render array of children', () => { + const result = renderNativeComponent( 'div', { children: [ + 'Hello ', + <Fragment key="toWhom">World</Fragment>, + ] } ); + + expect( result ).toBe( '<div>Hello World</div>' ); + } ); + } ); +} ); + +describe( 'renderComponent()', () => { + it( 'calls constructor and componentWillMount', () => { + class Example extends Component { + constructor() { + super( ...arguments ); + + this.constructed = 'constructed'; + } + + componentWillMount() { + this.willMounted = 'willMounted'; + } + + render() { + return this.constructed + this.willMounted; + } + } + + const result = renderComponent( Example, {} ); + + expect( result ).toBe( 'constructedwillMounted' ); + } ); + + it( 'does not call componentDidMount', () => { + class Example extends Component { + constructor() { + super( ...arguments ); + + this.state = {}; + } + + componentDidMount() { + this.setState( { didMounted: 'didMounted' } ); + } + + render() { + return this.state.didMounted; + } + } + + const result = renderComponent( Example, {} ); + + expect( result ).toBe( '' ); + } ); +} ); + +describe( 'renderAttributes()', () => { + describe( 'boolean attributes', () => { + it( 'should return boolean attributes false as omitted', () => { + const result = renderAttributes( { controls: false } ); + + expect( result ).toBe( '' ); + } ); + + it( 'should return boolean attributes non-false as present', () => { + [ true, 'true', 'false', '' ].forEach( ( controls ) => { + const result = renderAttributes( { controls } ); + + expect( result ).toBe( ' controls' ); + } ); + } ); + + it( 'should consider normalized boolean attribute name', () => { + const result = renderAttributes( { allowFullscreen: true } ); + + expect( result ).toBe( ' allowfullscreen' ); + } ); + } ); + + describe( 'prefixed attributes', () => { + it( 'should not render if nullish', () => { + [ null, undefined ].forEach( ( value ) => { + const result = renderAttributes( { 'data-foo': value } ); + + expect( result ).toBe( '' ); + } ); + } ); + + it( 'should return in its string form unmodified', () => { + let result = renderAttributes( { + 'aria-hidden': '', + } ); + + expect( result ).toBe( ' aria-hidden=""' ); + + result = renderAttributes( { + 'aria-hidden': true, + } ); + + expect( result ).toBe( ' aria-hidden="true"' ); + + result = renderAttributes( { + 'aria-hidden': false, + } ); + + expect( result ).toBe( ' aria-hidden="false"' ); + } ); + } ); + + describe( 'normalized attribute names', () => { + it( 'should return with normal attribute names', () => { + const result = renderAttributes( { + htmlFor: 'foo', + className: 'bar', + contentEditable: true, + } ); + + expect( result ).toBe( ' for="foo" class="bar" contenteditable="true"' ); + } ); + } ); + + describe( 'string escaping', () => { + it( 'should escape string attributes', () => { + const result = renderAttributes( { + style: { + background: 'url("foo.png")', + }, + href: '/index.php?foo=bar&qux=<"scary">', + } ); + + expect( result ).toBe( ' style="background:url(&quot;foo.png&quot;)" href="/index.php?foo=bar&amp;qux=<&quot;scary&quot;>"' ); + } ); + + it( 'should render numeric attributes', () => { + const result = renderAttributes( { + size: 10, + } ); + + expect( result ).toBe( ' size="10"' ); + } ); + } ); + + describe( 'ignored attributes', () => { + it( 'does not render nullish attributes', () => { + const result = renderAttributes( { + className: null, + htmlFor: undefined, + } ); + + expect( result ).toBe( '' ); + } ); + + it( 'does not render attributes of invalid types', () => { + const result = renderAttributes( { + onClick: () => {}, + className: [], + } ); + + expect( result ).toBe( '' ); + } ); + + it( 'does not render internal attributes', () => { + const result = renderAttributes( { + key: 'foo', + children: [ 'hello' ], + } ); + + expect( result ).toBe( '' ); + } ); + } ); +} ); + +describe( 'renderStyle()', () => { + it( 'should return undefined if empty', () => { + const result = renderStyle( {} ); + + expect( result ).toBe( undefined ); + } ); + + it( 'should render without trailing semi-colon', () => { + const result = renderStyle( { + color: 'red', + } ); + + expect( result ).toBe( 'color:red' ); + } ); + + it( 'should not render nullish value', () => { + const result = renderStyle( { + border: null, + backgroundColor: undefined, + color: 'red', + } ); + + expect( result ).toBe( 'color:red' ); + } ); + + it( 'should render a semi-colon delimited set', () => { + const result = renderStyle( { + color: 'red', + border: '1px dotted green', + } ); + + expect( result ).toBe( 'color:red;border:1px dotted green' ); + } ); + + it( 'should kebab-case style properties', () => { + const result = renderStyle( { + color: 'red', + backgroundColor: 'green', + } ); + + expect( result ).toBe( 'color:red;background-color:green' ); + } ); + + describe( 'value unit', () => { + it( 'should not render zero unit', () => { + const result = renderStyle( { + borderWidth: 0, + } ); + + expect( result ).toBe( 'border-width:0' ); + } ); + + it( 'should render numeric units', () => { + const result = renderStyle( { + borderWidth: 10, + } ); + + expect( result ).toBe( 'border-width:10px' ); + } ); + + it( 'should not render numeric units for unitless properties', () => { + const result = renderStyle( { + order: 10, + } ); + + expect( result ).toBe( 'order:10' ); + } ); + } ); +} ); diff --git a/eslint/config.js b/eslint/config.js new file mode 100644 index 00000000000000..42b075a74a7680 --- /dev/null +++ b/eslint/config.js @@ -0,0 +1,161 @@ +module.exports = { + parser: 'babel-eslint', + extends: [ + 'wordpress', + 'plugin:wordpress/esnext', + 'plugin:react/recommended', + 'plugin:jsx-a11y/recommended', + ], + env: { + browser: false, + es6: true, + node: true, + }, + parserOptions: { + sourceType: 'module', + ecmaFeatures: { + jsx: true, + }, + }, + globals: { + wp: true, + window: true, + document: true, + }, + plugins: [ + 'wordpress', + 'react', + 'jsx-a11y', + ], + settings: { + react: { + pragma: 'wp', + }, + }, + rules: { + 'array-bracket-spacing': [ 'error', 'always' ], + 'brace-style': [ 'error', '1tbs' ], + camelcase: [ 'error', { properties: 'never' } ], + 'comma-dangle': [ 'error', 'always-multiline' ], + 'comma-spacing': 'error', + 'comma-style': 'error', + 'computed-property-spacing': [ 'error', 'always' ], + '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', + 'no-alert': 'error', + 'no-bitwise': 'error', + 'no-caller': 'error', + 'no-console': 'error', + 'no-debugger': 'error', + 'no-dupe-args': 'error', + 'no-dupe-keys': 'error', + 'no-duplicate-case': 'error', + 'no-else-return': 'error', + 'no-eval': 'error', + 'no-extra-semi': 'error', + 'no-fallthrough': 'error', + 'no-lonely-if': '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-undef': 'error', + 'no-undef-init': 'error', + 'no-unreachable': 'error', + 'no-unsafe-negation': 'error', + 'no-unused-expressions': 'error', + 'no-unused-vars': 'error', + 'no-useless-return': 'error', + 'no-whitespace-before-property': 'error', + 'object-curly-spacing': [ 'error', 'always' ], + 'padded-blocks': [ 'error', 'never' ], + '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', + semi: 'error', + 'semi-spacing': 'error', + 'space-before-blocks': [ 'error', 'always' ], + 'space-before-function-paren': [ 'error', { + anonymous: 'never', + named: 'never', + asyncArrow: 'always', + } ], + 'space-in-parens': [ 'error', 'always' ], + 'space-infix-ops': [ 'error', { int32Hint: false } ], + 'space-unary-ops': [ 'error', { + overrides: { + '!': true, + yield: true, + }, + } ], + 'valid-jsdoc': [ 'error', { + prefer: { + arg: 'param', + argument: 'param', + extends: 'augments', + returns: 'return', + }, + preferType: { + array: 'Array', + bool: 'boolean', + Boolean: 'boolean', + float: 'number', + Float: 'number', + int: 'number', + integer: 'number', + Integer: 'number', + Number: 'number', + object: 'Object', + String: 'string', + Void: 'void', + }, + requireParamDescription: false, + requireReturn: false, + } ], + 'valid-typeof': 'error', + yoda: 'off', + }, +}; diff --git a/phpunit/class-gutenberg-rest-api-test.php b/phpunit/class-gutenberg-rest-api-test.php new file mode 100644 index 00000000000000..bd09db6142a4b8 --- /dev/null +++ b/phpunit/class-gutenberg-rest-api-test.php @@ -0,0 +1,99 @@ +<?php +/** + * WP_Block_Type_Registry Tests + * + * @package Gutenberg + */ + +/** + * Tests for WP_Block_Type_Registry + */ +class Gutenberg_REST_API_Test extends WP_UnitTestCase { + function setUp() { + parent::setUp(); + + $this->administrator = $this->factory->user->create( array( + 'role' => 'administrator', + ) ); + $this->author = $this->factory->user->create( array( + 'role' => 'author', + ) ); + $this->editor = $this->factory->user->create( array( + 'role' => 'editor', + ) ); + } + + function tearDown() { + parent::tearDown(); + } + + /** + * Should return an extra visibility field on response when in edit context. + */ + function test_visibility_field() { + wp_set_current_user( $this->administrator ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/taxonomies/category' ); + $request->set_param( 'context', 'edit' ); + $response = rest_do_request( $request ); + + $result = $response->get_data(); + + $this->assertTrue( isset( $result['visibility'] ) ); + $this->assertInternalType( 'array', $result['visibility'] ); + $this->assertArrayHasKey( 'public', $result['visibility'] ); + $this->assertArrayHasKey( 'publicly_queryable', $result['visibility'] ); + $this->assertArrayHasKey( 'show_ui', $result['visibility'] ); + $this->assertArrayHasKey( 'show_admin_column', $result['visibility'] ); + $this->assertArrayHasKey( 'show_in_nav_menus', $result['visibility'] ); + $this->assertArrayHasKey( 'show_in_quick_edit', $result['visibility'] ); + } + + /** + * Should return an extra visibility field on response. + */ + function test_visibility_field_for_non_admin_roles() { + wp_set_current_user( $this->editor ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/taxonomies/category' ); + $request->set_param( 'context', 'edit' ); + $response = rest_do_request( $request ); + + $result = $response->get_data(); + + $this->assertTrue( isset( $result['visibility'] ) ); + $this->assertInternalType( 'array', $result['visibility'] ); + $this->assertArrayHasKey( 'public', $result['visibility'] ); + $this->assertArrayHasKey( 'publicly_queryable', $result['visibility'] ); + $this->assertArrayHasKey( 'show_ui', $result['visibility'] ); + $this->assertArrayHasKey( 'show_admin_column', $result['visibility'] ); + $this->assertArrayHasKey( 'show_in_nav_menus', $result['visibility'] ); + $this->assertArrayHasKey( 'show_in_quick_edit', $result['visibility'] ); + + /** + * See https://github.com/WordPress/gutenberg/issues/2545 + * + * Until that is resolved authors will not be able to set taxonomies. + * This should definitely be resolved though. + */ + wp_set_current_user( $this->author ); + + $response = rest_do_request( $request ); + + $result = $response->get_data(); + + $this->assertFalse( isset( $result['visibility'] ) ); + } + + /** + * Should not return an extra visibility field without context set. + */ + function test_visibility_field_without_context() { + $request = new WP_REST_Request( 'GET', '/wp/v2/taxonomies/category' ); + $response = rest_do_request( $request ); + + $result = $response->get_data(); + + $this->assertFalse( isset( $result['visibility'] ) ); + } +} diff --git a/plugins/README.md b/plugins/README.md new file mode 100644 index 00000000000000..7bcadda1b94014 --- /dev/null +++ b/plugins/README.md @@ -0,0 +1,83 @@ +# Plugins + +### Plugins API + +The plugins API contains the following methods: + +#### `wp.plugins.registerPlugin( name: string, settings: Object )` + +This method registers a new plugin. + +This method takes two arguments: + +1. `name`: A string identifying the plugin. Must be unique across all registered plugins. +2. `settings`: An object containing the following data: + - `render`: A component containing the UI elements to be rendered. + +See [the edit-post module documentation](../edit-post/) for available components. + +_Example:_ + +```jsx +const { Fragment } = wp.element; +const { PluginSidebar } = wp.editPost; +const { PluginMoreMenuItem } = wp.editPost.__experimental; +const { registerPlugin } = wp.plugins; + +const Component = () => ( + <Fragment> + <PluginMoreMenuItem + name="menu-item-name" + type="sidebar" + target="sidebar-name" + > + My Sidebar + </PluginMoreMenuItem> + <PluginSidebar + name="sidebar-name" + title="My Sidebar" + > + Content of the sidebar + </PluginSidebar> + </Fragment> +); + +registerPlugin( 'plugin-name', { + render: Component, +} ); +``` + +#### `wp.plugins.unregisterPlugin( name: string )` + +This method unregisters an existing plugin. + +This method takes one argument: + +1. `name`: A string identifying the plugin. + +_Example:_ + +```js +const { unregisterPlugin } = wp.plugins; + +unregisterPlugin( 'plugin-name' ); +``` + +### Components + +#### `PluginArea` + +A component that renders all registered plugins in a hidden div. + +_Example:_ + +```jsx +const { PluginArea } = wp.plugins; + +const Layout = () => ( + <div> + Content of the page + <PluginArea /> + </div> +); +``` diff --git a/plugins/api/index.js b/plugins/api/index.js new file mode 100644 index 00000000000000..46d474512ee683 --- /dev/null +++ b/plugins/api/index.js @@ -0,0 +1,112 @@ +/* eslint no-console: [ 'error', { allow: [ 'error' ] } ] */ + +/** + * WordPress dependencies + */ +import { applyFilters, doAction } from '@wordpress/hooks'; + +/** + * External dependencies + */ +import { isFunction } from 'lodash'; + +/** + * Plugin definitions keyed by plugin name. + * + * @type {Object.<string,WPPlugin>} + */ +const plugins = {}; + +/** + * Registers a plugin to the editor. + * + * @param {string} name The name of the plugin. + * @param {Object} settings The settings for this plugin. + * @param {Function} settings.render The function that renders the plugin. + * + * @return {Object} The final plugin settings object. + */ +export function registerPlugin( name, settings ) { + if ( typeof settings !== 'object' ) { + console.error( + 'No settings object provided!' + ); + return null; + } + if ( typeof name !== 'string' ) { + console.error( + 'Plugin names must be strings.' + ); + return null; + } + if ( ! /^[a-z][a-z0-9-]*$/.test( name ) ) { + console.error( + 'Plugin names must include only lowercase alphanumeric characters or dashes, and start with a letter. Example: "my-plugin".' + ); + return null; + } + if ( plugins[ name ] ) { + console.error( + `Plugin "${ name }" is already registered.` + ); + } + if ( ! isFunction( settings.render ) ) { + console.error( + 'The "render" property must be specified and must be a valid function.' + ); + return null; + } + + settings.name = name; + + settings = applyFilters( 'plugins.registerPlugin', settings, name ); + + plugins[ settings.name ] = settings; + + doAction( 'plugins.pluginRegistered', settings, name ); + + return settings; +} + +/** + * Unregisters a plugin by name. + * + * @param {string} name Plugin name. + * + * @return {?WPPlugin} The previous plugin settings object, if it has been + * successfully unregistered; otherwise `undefined`. + */ +export function unregisterPlugin( name ) { + if ( ! plugins[ name ] ) { + console.error( + 'Plugin "' + name + '" is not registered.' + ); + return; + } + const oldPlugin = plugins[ name ]; + delete plugins[ name ]; + + doAction( 'plugins.pluginUnregistered', oldPlugin, name ); + + return oldPlugin; +} + +/** + * Returns a registered plugin settings. + * + * @param {string} name Plugin name. + * + * @return {?Object} Plugin setting. + */ +export function getPlugin( name ) { + return plugins[ name ]; +} + +/** + * Returns all registered plugins. + * + * @return {Array} Plugin settings. + */ +export function getPlugins() { + return Object.values( plugins ); +} diff --git a/plugins/api/test/index.js b/plugins/api/test/index.js new file mode 100644 index 00000000000000..60aa1e72ec89d7 --- /dev/null +++ b/plugins/api/test/index.js @@ -0,0 +1,57 @@ +/** + * Internal dependencies + */ +import { + registerPlugin, + unregisterPlugin, + getPlugins, +} from '../'; + +describe( 'registerPlugin', () => { + afterEach( () => { + getPlugins().forEach( ( plugin ) => { + unregisterPlugin( plugin.name ); + } ); + } ); + + it( 'successfully registers a plugin', () => { + registerPlugin( 'plugin', { + render: () => 'plugin content', + } ); + } ); + + it( 'fails to register a plugin without a settings object', () => { + registerPlugin(); + expect( console ).toHaveErroredWith( 'No settings object provided!' ); + } ); + + it( 'fails to register a plugin with special character in the name', () => { + registerPlugin( 'plugin/with/special/characters', { + render: () => {}, + } ); + expect( console ).toHaveErroredWith( 'Plugin names must include only lowercase alphanumeric characters or dashes, and start with a letter. Example: "my-plugin".' ); + } ); + + it( 'fails to register a plugin with a non-string name', () => { + registerPlugin( {}, { + render: () => {}, + } ); + expect( console ).toHaveErroredWith( 'Plugin names must be strings.' ); + } ); + + it( 'fails to register a plugin without a render function', () => { + registerPlugin( 'another-plugin', {} ); + expect( console ).toHaveErroredWith( 'The "render" property must be specified and must be a valid function.' ); + } ); + + it( 'fails to register a plugin that was already been registered', () => { + registerPlugin( 'plugin', { + render: () => 'plugin content', + } ); + registerPlugin( 'plugin', { + render: () => 'plugin content', + } ); + console.log( console ); // eslint-disable-line + expect( console ).toHaveErroredWith( 'Plugin "plugin" is already registered.' ); + } ); +} ); diff --git a/plugins/components/index.js b/plugins/components/index.js new file mode 100644 index 00000000000000..21b12588e3bf35 --- /dev/null +++ b/plugins/components/index.js @@ -0,0 +1,2 @@ +export { default as PluginArea } from './plugin-area'; +export { default as PluginContext } from './plugin-context'; diff --git a/plugins/components/plugin-area/index.js b/plugins/components/plugin-area/index.js new file mode 100644 index 00000000000000..1793ba32f89ad6 --- /dev/null +++ b/plugins/components/plugin-area/index.js @@ -0,0 +1,75 @@ +/** + * External dependencies + */ +import { map } from 'lodash'; + +/** + * WordPress dependencies + */ +import { Component } from '@wordpress/element'; +import { addAction, removeAction } from '@wordpress/hooks'; + +/** + * Internal dependencies + */ +import PluginContext from '../plugin-context'; +import { getPlugins } from '../../api'; + +/** + * A component that renders all plugin fills in a hidden div. + * + * @return {WPElement} Plugin area. + */ +class PluginArea extends Component { + constructor() { + super( ...arguments ); + + this.setPlugins = this.setPlugins.bind( this ); + this.state = this.getCurrentPluginsState(); + } + + getCurrentPluginsState() { + return { + plugins: map( getPlugins(), ( { name, render } ) => { + return { + name, + Plugin: render, + context: { + pluginName: name, + }, + }; + } ), + }; + } + + componentDidMount() { + addAction( 'plugins.pluginRegistered', 'core/plugins/plugin-area/plugins-registered', this.setPlugins ); + addAction( 'plugins.pluginUnregistered', 'core/plugins/plugin-area/plugins-unregistered', this.setPlugins ); + } + + componentWillUnmount() { + removeAction( 'plugins.pluginRegistered', 'core/plugins/plugin-area/plugins-registered' ); + removeAction( 'plugins.pluginUnregistered', 'core/plugins/plugin-area/plugins-unregistered' ); + } + + setPlugins() { + this.setState( this.getCurrentPluginsState ); + } + + render() { + return ( + <div style={ { display: 'none' } }> + { map( this.state.plugins, ( { context, name, Plugin } ) => ( + <PluginContext.Provider + key={ name } + value={ context } + > + <Plugin /> + </PluginContext.Provider> + ) ) } + </div> + ); + } +} + +export default PluginArea; diff --git a/plugins/components/plugin-context/index.js b/plugins/components/plugin-context/index.js new file mode 100644 index 00000000000000..4d5e8bd547a621 --- /dev/null +++ b/plugins/components/plugin-context/index.js @@ -0,0 +1,8 @@ +/** + * WordPress dependencies + */ +import { createContext } from '@wordpress/element'; + +const PluginContext = createContext( { pluginName: null } ); + +export default PluginContext; diff --git a/plugins/index.js b/plugins/index.js new file mode 100644 index 00000000000000..3d5bec67d6d1f9 --- /dev/null +++ b/plugins/index.js @@ -0,0 +1,2 @@ +export * from './components'; +export * from './api'; diff --git a/test/e2e/jest.config.json b/test/e2e/jest.config.json new file mode 100644 index 00000000000000..a8889633f29b98 --- /dev/null +++ b/test/e2e/jest.config.json @@ -0,0 +1,7 @@ +{ + "rootDir": "../../", + "preset": "@wordpress/jest-preset-default", + "setupFiles": [], + "testMatch": [ "<rootDir>/test/e2e/specs/**/(*.)(spec|test).js?(x)" ], + "timers": "real" +} diff --git a/test/e2e/specs/__snapshots__/adding-blocks.test.js.snap b/test/e2e/specs/__snapshots__/adding-blocks.test.js.snap new file mode 100644 index 00000000000000..571013e6decb06 --- /dev/null +++ b/test/e2e/specs/__snapshots__/adding-blocks.test.js.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`adding blocks Should insert content using the placeholder and the regular inserter 1`] = ` +"<!-- wp:paragraph --> +<p>Paragraph block</p> +<!-- /wp:paragraph --> + +<!-- wp:paragraph --> +<p>Second paragraph</p> +<!-- /wp:paragraph --> + +<!-- wp:quote --> +<blockquote class=\\"wp-block-quote\\"> + <p>Quote block</p> +</blockquote> +<!-- /wp:quote --> + +<!-- wp:code --> +<pre class=\\"wp-block-code\\"><code>Code block</code></pre> +<!-- /wp:code -->" +`; diff --git a/test/e2e/specs/__snapshots__/splitting-merging.test.js.snap b/test/e2e/specs/__snapshots__/splitting-merging.test.js.snap new file mode 100644 index 00000000000000..98d163a480fbd4 --- /dev/null +++ b/test/e2e/specs/__snapshots__/splitting-merging.test.js.snap @@ -0,0 +1,17 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`splitting and merging blocks Should split and merge paragraph blocks using Enter and Backspace 1`] = ` +"<!-- wp:paragraph --> +<p>First</p> +<!-- /wp:paragraph --> + +<!-- wp:paragraph --> +<p>Second</p> +<!-- /wp:paragraph -->" +`; + +exports[`splitting and merging blocks Should split and merge paragraph blocks using Enter and Backspace 2`] = ` +"<!-- wp:paragraph --> +<p>FirstSecond</p> +<!-- /wp:paragraph -->" +`; diff --git a/test/e2e/specs/__snapshots__/templates.test.js.snap b/test/e2e/specs/__snapshots__/templates.test.js.snap new file mode 100644 index 00000000000000..a0c644e428d91d --- /dev/null +++ b/test/e2e/specs/__snapshots__/templates.test.js.snap @@ -0,0 +1,27 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Using a CPT with a predefined template Should add a custom post types with a predefined template 1`] = ` +"<!-- wp:image --> +<figure class=\\"wp-block-image\\"><img alt=\\"\\" /></figure> +<!-- /wp:image --> + +<!-- wp:paragraph {\\"placeholder\\":\\"Add a book description\\"} --> +<p></p> +<!-- /wp:paragraph --> + +<!-- wp:quote --> +<blockquote class=\\"wp-block-quote\\"></blockquote> +<!-- /wp:quote --> + +<!-- wp:columns --> +<div class=\\"wp-block-columns has-2-columns\\"> + <!-- wp:image {\\"layout\\":\\"column-1\\"} --> + <figure class=\\"wp-block-image layout-column-1\\"><img alt=\\"\\" /></figure> + <!-- /wp:image --> + + <!-- wp:paragraph {\\"placeholder\\":\\"Add a inner paragraph\\",\\"layout\\":\\"column-2\\"} --> + <p class=\\"layout-column-2\\"></p> + <!-- /wp:paragraph --> +</div> +<!-- /wp:columns -->" +`; diff --git a/test/e2e/specs/a11y.test.js b/test/e2e/specs/a11y.test.js new file mode 100644 index 00000000000000..c90224fadd09d7 --- /dev/null +++ b/test/e2e/specs/a11y.test.js @@ -0,0 +1,26 @@ +/** + * Internal dependencies + */ +import '../support/bootstrap'; +import { newPost, newDesktopBrowserPage } from '../support/utils'; + +describe( 'a11y', () => { + beforeAll( async () => { + await newDesktopBrowserPage(); + await newPost(); + } ); + + it( 'tabs header bar', async () => { + await page.keyboard.down( 'Control' ); + await page.keyboard.press( '~' ); + await page.keyboard.up( 'Control' ); + + await page.keyboard.press( 'Tab' ); + + const isFocusedToggle = await page.$eval( ':focus', ( focusedElement ) => { + return focusedElement.classList.contains( 'editor-inserter__toggle' ); + } ); + + expect( isFocusedToggle ).toBe( true ); + } ); +} ); diff --git a/test/e2e/specs/adding-blocks.test.js b/test/e2e/specs/adding-blocks.test.js new file mode 100644 index 00000000000000..973063c4613f40 --- /dev/null +++ b/test/e2e/specs/adding-blocks.test.js @@ -0,0 +1,98 @@ +/** + * Internal dependencies + */ +import '../support/bootstrap'; +import { newPost, newDesktopBrowserPage } from '../support/utils'; + +describe( 'adding blocks', () => { + beforeAll( async () => { + await newDesktopBrowserPage(); + await newPost(); + } ); + + /** + * Given a Puppeteer ElementHandle, clicks around the center-right point. + * + * TEMPORARY: This is a mild hack to work around a bug in the application + * which prevents clicking at center of the inserter, due to conflicting + * overlap of focused block contextual toolbar. + * + * @see Puppeteer.ElementHandle#click + * + * @link https://github.com/WordPress/gutenberg/pull/5658#issuecomment-376943568 + * + * @param {Puppeteer.ElementHandle} elementHandle Element handle. + * + * @return {Promise} Promise resolving when element clicked. + */ + async function clickAtRightish( elementHandle ) { + await elementHandle._scrollIntoViewIfNeeded(); + const box = await elementHandle._assertBoundingBox(); + const x = box.x + ( box.width * 0.75 ); + const y = box.y + ( box.height / 2 ); + return page.mouse.click( x, y ); + } + + /** + * Given a Puppeteer ElementHandle, clicks below its bounding box. + * + * @param {Puppeteer.ElementHandle} elementHandle Element handle. + * + * @return {Promise} Promise resolving when click occurs. + */ + async function clickBelow( elementHandle ) { + const box = await elementHandle.boundingBox(); + const x = box.x + ( box.width / 2 ); + const y = box.y + box.height + 1; + return page.mouse.click( x, y ); + } + + it( 'Should insert content using the placeholder and the regular inserter', async () => { + // Click below editor to focus last field (block appender) + await clickBelow( await page.$( '.editor-default-block-appender' ) ); + expect( await page.$( '[data-type="core/paragraph"]' ) ).not.toBeNull(); + + // Up to return back to title. Assumes that appender results in focus + // to a new block. + // TODO: Backspace should be sufficient to return to title. + await page.keyboard.press( 'ArrowUp' ); + + // Post is empty, the newly created paragraph has been removed on focus + // out because default block is provisional. + expect( await page.$( '[data-type="core/paragraph"]' ) ).toBeNull(); + + // Using the placeholder + await page.click( '.editor-default-block-appender' ); + await page.keyboard.type( 'Paragraph block' ); + + // Using the slash command + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( '/quote' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( 'Quote block' ); + + // Using the regular inserter + await page.click( '.edit-post-header [aria-label="Add block"]' ); + await page.keyboard.type( 'code' ); + await page.keyboard.press( 'Tab' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( 'Code block' ); + + // Using the between inserter + await page.mouse.move( 200, 300 ); + await page.mouse.move( 250, 350 ); + const inserter = await page.$( '[data-type="core/quote"] .editor-block-list__insertion-point-inserter' ); + await clickAtRightish( inserter ); + await page.keyboard.type( 'Second paragraph' ); + + // Switch to Text Mode to check HTML Output + await page.click( '.edit-post-more-menu [aria-label="More"]' ); + const codeEditorButton = ( await page.$x( '//button[contains(text(), \'Code Editor\')]' ) )[ 0 ]; + await codeEditorButton.click( 'button' ); + + // Assertions + const textEditorContent = await page.$eval( '.editor-post-text-editor', ( element ) => element.value ); + + expect( textEditorContent ).toMatchSnapshot(); + } ); +} ); diff --git a/test/e2e/specs/hello.test.js b/test/e2e/specs/hello.test.js new file mode 100644 index 00000000000000..080478b850d621 --- /dev/null +++ b/test/e2e/specs/hello.test.js @@ -0,0 +1,31 @@ +/** + * Internal dependencies + */ +import '../support/bootstrap'; +import { newPost, visitAdmin, newDesktopBrowserPage } from '../support/utils'; + +describe( 'hello', () => { + beforeAll( async () => { + await newDesktopBrowserPage(); + await newPost(); + } ); + + it( 'Should show the New Post Page in Gutenberg', async () => { + expect( page.url() ).toEqual( expect.stringContaining( 'post-new.php' ) ); + const title = await page.$( '[placeholder="Add title"]' ); + expect( title ).not.toBeNull(); + } ); + + it( 'Should have no history', async () => { + const undoButton = await page.$( '.editor-history__undo:not( :disabled )' ); + const redoButton = await page.$( '.editor-history__redo:not( :disabled )' ); + + expect( undoButton ).toBeNull(); + expect( redoButton ).toBeNull(); + } ); + + it( 'Should not prompt to confirm unsaved changes', async () => { + await visitAdmin( 'edit.php' ); + expect( page.url() ).not.toEqual( expect.stringContaining( 'post-new.php' ) ); + } ); +} ); diff --git a/test/e2e/specs/managing-links.test.js b/test/e2e/specs/managing-links.test.js new file mode 100644 index 00000000000000..a88766b3b528fe --- /dev/null +++ b/test/e2e/specs/managing-links.test.js @@ -0,0 +1,66 @@ +/** + * Internal dependencies + */ +import '../support/bootstrap'; +import { newPost, newDesktopBrowserPage } from '../support/utils'; + +describe( 'Managing links', () => { + beforeEach( async () => { + await newDesktopBrowserPage(); + await newPost(); + } ); + + const setFixedToolbar = async ( b ) => { + await page.click( '.edit-post-more-menu button' ); + const button = ( await page.$x( '//button[contains(text(), \'Fix Toolbar to Top\')]' ) )[ 0 ]; + const buttonClassNameProperty = await button.getProperty( 'className' ); + const buttonClassName = await buttonClassNameProperty.jsonValue(); + const isSelected = buttonClassName.indexOf( 'is-selected' ) !== -1; + if ( isSelected !== b ) { + await button.click(); + } else { + await page.click( '.edit-post-more-menu button' ); + } + }; + + it( 'Pressing Left and Esc in Link Dialog in "Fixed to Toolbar" mode', async () => { + await setFixedToolbar( true ); + + await page.click( '.editor-default-block-appender' ); + await page.keyboard.type( 'Text' ); + await page.click( 'button[aria-label="Link"]' ); + + // Typing "left" should not close the dialog + await page.keyboard.press( 'ArrowLeft' ); + let modal = await page.$( '.blocks-format-toolbar__link-modal' ); + expect( modal ).not.toBeNull(); + + // Escape should close the dialog still. + await page.keyboard.press( 'Escape' ); + modal = await page.$( '.blocks-format-toolbar__link-modal' ); + expect( modal ).toBeNull(); + } ); + + it( 'Pressing Left and Esc in Link Dialog in "Docked Toolbar" mode', async () => { + setFixedToolbar( false ); + + await page.click( '.editor-default-block-appender' ); + await page.keyboard.type( 'Text' ); + + // we need to trigger isTyping = false + await page.mouse.move( 200, 300 ); + await page.mouse.move( 250, 350 ); + + await page.click( 'button[aria-label="Link"]' ); + + // Typing "left" should not close the dialog + await page.keyboard.press( 'ArrowLeft' ); + let modal = await page.$( '.blocks-format-toolbar__link-modal' ); + expect( modal ).not.toBeNull(); + + // Escape should close the dialog still. + await page.keyboard.press( 'Escape' ); + modal = await page.$( '.blocks-format-toolbar__link-modal' ); + expect( modal ).toBeNull(); + } ); +} ); diff --git a/test/e2e/specs/meta-boxes.test.js b/test/e2e/specs/meta-boxes.test.js new file mode 100644 index 00000000000000..9f5aa97e878567 --- /dev/null +++ b/test/e2e/specs/meta-boxes.test.js @@ -0,0 +1,41 @@ +/** + * Internal dependencies + */ +import '../support/bootstrap'; +import { newPost, newDesktopBrowserPage } from '../support/utils'; +import { activatePlugin, deactivatePlugin } from '../support/plugins'; + +describe( 'Meta boxes', () => { + beforeAll( async () => { + await newDesktopBrowserPage(); + await activatePlugin( 'gutenberg-test-plugin-meta-box' ); + await newPost(); + } ); + + afterAll( async () => { + await newDesktopBrowserPage(); + await deactivatePlugin( 'gutenberg-test-plugin-meta-box' ); + } ); + + it( 'Should save the post', async () => { + // Save should not be an option for new empty post. + expect( await page.$( '.editor-post-save-draft' ) ).toBe( null ); + + // Add title to enable valid non-empty post save. + await page.type( '.editor-post-title__input', 'Hello Meta' ); + expect( await page.$( '.editor-post-save-draft' ) ).not.toBe( null ); + + await Promise.all( [ + // Transitions between three states "Saving..." -> "Saved" -> "Save + // Draft" (the button is always visible while meta are present). + page.waitForSelector( '.editor-post-saved-state.is-saving' ), + page.waitForSelector( '.editor-post-saved-state.is-saved' ), + page.waitForSelector( '.editor-post-save-draft' ), + + // Keyboard shortcut Ctrl+S save. + page.keyboard.down( 'Meta' ), + page.keyboard.press( 'S' ), + page.keyboard.up( 'Meta' ), + ] ); + } ); +} ); diff --git a/test/e2e/specs/multi-block-selection.test.js b/test/e2e/specs/multi-block-selection.test.js new file mode 100644 index 00000000000000..09142e94846b08 --- /dev/null +++ b/test/e2e/specs/multi-block-selection.test.js @@ -0,0 +1,78 @@ +/** + * Internal dependencies + */ +import '../support/bootstrap'; +import { newPost, newDesktopBrowserPage } from '../support/utils'; + +describe( 'Multi-block selection', () => { + beforeAll( async () => { + await newDesktopBrowserPage(); + await newPost(); + } ); + + it( 'Should select/unselect multiple blocks', async () => { + const firstBlockSelector = '[data-type="core/paragraph"]'; + const secondBlockSelector = '[data-type="core/image"]'; + const thirdBlockSelector = '[data-type="core/quote"]'; + const multiSelectedCssClass = 'is-multi-selected'; + + // Creating test blocks + await page.click( '.editor-default-block-appender' ); + await page.keyboard.type( 'First Paragraph' ); + await page.click( '.edit-post-header [aria-label="Add block"]' ); + await page.keyboard.type( 'Image' ); + await page.keyboard.press( 'Tab' ); + await page.keyboard.press( 'Enter' ); + await page.click( '.edit-post-header [aria-label="Add block"]' ); + await page.keyboard.type( 'Quote' ); + await page.keyboard.press( 'Tab' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( 'Quote Block' ); + + const blocks = [ firstBlockSelector, secondBlockSelector, thirdBlockSelector ]; + const expectMultiSelected = ( selectors, areMultiSelected ) => { + selectors.forEach( async ( selector ) => { + const className = await page.$eval( selector, ( element ) => element.className ); + if ( areMultiSelected ) { + expect( className ).toEqual( expect.stringContaining( multiSelectedCssClass ) ); + } else { + expect( className ).not.toEqual( expect.stringContaining( multiSelectedCssClass ) ); + } + } ); + }; + + // Default: No selection + expectMultiSelected( blocks, false ); + + // Multiselect via Shift + click + await page.mouse.move( 200, 300 ); + await page.click( firstBlockSelector ); + await page.keyboard.down( 'Shift' ); + await page.click( thirdBlockSelector ); + await page.keyboard.up( 'Shift' ); + + // Verify selection + expectMultiSelected( blocks, true ); + + // Unselect + await page.click( secondBlockSelector ); + + // No selection + expectMultiSelected( blocks, false ); + + // Multiselect via keyboard + await page.click( 'body' ); + await page.keyboard.down( 'Meta' ); + await page.keyboard.press( 'a' ); + await page.keyboard.up( 'Meta' ); + + // Verify selection + expectMultiSelected( blocks, true ); + + // Unselect + await page.keyboard.press( 'Escape' ); + + // No selection + expectMultiSelected( blocks, false ); + } ); +} ); diff --git a/test/e2e/specs/splitting-merging.test.js b/test/e2e/specs/splitting-merging.test.js new file mode 100644 index 00000000000000..4e78b0366eba28 --- /dev/null +++ b/test/e2e/specs/splitting-merging.test.js @@ -0,0 +1,55 @@ +/** + * Internal dependencies + */ +import '../support/bootstrap'; +import { newPost, newDesktopBrowserPage } from '../support/utils'; + +describe( 'splitting and merging blocks', () => { + beforeAll( async () => { + await newDesktopBrowserPage(); + await newPost(); + } ); + + it( 'Should split and merge paragraph blocks using Enter and Backspace', async () => { + //Use regular inserter to add paragraph block and text + await page.click( '.edit-post-header [aria-label="Add block"]' ); + await page.keyboard.type( 'paragraph' ); + await page.keyboard.press( 'Tab' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( 'FirstSecond' ); + + //Move caret between 'First' and 'Second' and press Enter to split paragraph blocks + for ( let i = 0; i < 6; i++ ) { + await page.keyboard.press( 'ArrowLeft' ); + } + await page.keyboard.press( 'Enter' ); + + //Switch to Code Editor to check HTML output + await page.click( '.edit-post-more-menu [aria-label="More"]' ); + let codeEditorButton = ( await page.$x( '//button[contains(text(), \'Code Editor\')]' ) )[ 0 ]; + await codeEditorButton.click( 'button' ); + + //Assert that there are now two paragraph blocks with correct content + let textEditorContent = await page.$eval( '.editor-post-text-editor', ( element ) => element.value ); + expect( textEditorContent ).toMatchSnapshot(); + + //Switch to Visual Editor to continue testing + await page.click( '.edit-post-more-menu [aria-label="More"]' ); + const visualEditorButton = ( await page.$x( '//button[contains(text(), \'Visual Editor\')]' ) )[ 0 ]; + await visualEditorButton.click( 'button' ); + + //Press Backspace to merge paragraph blocks + await page.click( '.is-selected' ); + await page.keyboard.press( 'Home' ); + await page.keyboard.press( 'Backspace' ); + + //Switch to Code Editor to check HTML output + await page.click( '.edit-post-more-menu [aria-label="More"]' ); + codeEditorButton = ( await page.$x( '//button[contains(text(), \'Code Editor\')]' ) )[ 0 ]; + await codeEditorButton.click( 'button' ); + + //Assert that there is now one paragraph with correct content + textEditorContent = await page.$eval( '.editor-post-text-editor', ( element ) => element.value ); + expect( textEditorContent ).toMatchSnapshot(); + } ); +} ); diff --git a/test/e2e/specs/templates.test.js b/test/e2e/specs/templates.test.js new file mode 100644 index 00000000000000..15d86445d7f243 --- /dev/null +++ b/test/e2e/specs/templates.test.js @@ -0,0 +1,30 @@ +/** + * Internal dependencies + */ +import '../support/bootstrap'; +import { newPost, newDesktopBrowserPage } from '../support/utils'; +import { activatePlugin, deactivatePlugin } from '../support/plugins'; + +describe( 'Using a CPT with a predefined template', () => { + beforeAll( async () => { + await newDesktopBrowserPage(); + await activatePlugin( 'gutenberg-test-plugin-templates' ); + await newPost( 'book' ); + } ); + + afterAll( async () => { + await newDesktopBrowserPage(); + await deactivatePlugin( 'gutenberg-test-plugin-templates' ); + } ); + + it( 'Should add a custom post types with a predefined template', async () => { + //Switch to Code Editor to check HTML output + await page.click( '.edit-post-more-menu [aria-label="More"]' ); + const codeEditorButton = ( await page.$x( '//button[contains(text(), \'Code Editor\')]' ) )[ 0 ]; + await codeEditorButton.click( 'button' ); + + // Assert that the post already contains the template defined blocks + const textEditorContent = await page.$eval( '.editor-post-text-editor', ( element ) => element.value ); + expect( textEditorContent ).toMatchSnapshot(); + } ); +} ); diff --git a/test/e2e/support/bootstrap.js b/test/e2e/support/bootstrap.js new file mode 100644 index 00000000000000..38ee03e2c7b57a --- /dev/null +++ b/test/e2e/support/bootstrap.js @@ -0,0 +1,20 @@ +/** + * External dependencies + */ +import puppeteer from 'puppeteer'; + +const { PUPPETEER_HEADLESS, PUPPETEER_SLOWMO } = process.env; + +// The Jest timeout is increased because these tests are a bit slow +jest.setTimeout( 100000 ); + +beforeAll( async () => { + global.browser = await puppeteer.launch( { + headless: PUPPETEER_HEADLESS !== 'false', + slowMo: parseInt( PUPPETEER_SLOWMO, 10 ) || 0, + } ); +} ); + +afterAll( async () => { + await browser.close(); +} ); diff --git a/test/e2e/support/plugins.js b/test/e2e/support/plugins.js new file mode 100644 index 00000000000000..f6ad89e49dded6 --- /dev/null +++ b/test/e2e/support/plugins.js @@ -0,0 +1,60 @@ +/** + * Node dependencies + */ +import { visitAdmin } from './utils'; + +/** + * Install a plugin from the WP.org repository. + * + * @param {string} slug Plugin slug. + * @param {string?} searchTerm If the plugin is not findable by its slug use an alternative term to search. + */ +export async function installPlugin( slug, searchTerm ) { + await 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 + '"]' ); +} + +/** + * Activates an installed plugin. + * + * @param {string} slug Plugin slug. + */ +export async function activatePlugin( slug ) { + await visitAdmin( 'plugins.php' ); + await page.click( 'tr[data-slug="' + slug + '"] .activate a' ); + await page.waitForSelector( 'tr[data-slug="' + slug + '"] .deactivate a' ); +} + +/** + * Dectivates an active plugin. + * + * @param {string} slug Plugin slug. + */ +export async function deactivatePlugin( slug ) { + await visitAdmin( 'plugins.php' ); + await page.click( 'tr[data-slug="' + slug + '"] .deactivate a' ); + await page.waitForSelector( 'tr[data-slug="' + slug + '"] .delete a' ); +} + +/** + * Uninstall a plugin. + * + * @param {string} slug Plugin slug. + */ +export async function uninstallPlugin( slug ) { + await visitAdmin( 'plugins.php' ); + const confirmPromise = new Promise( resolve => { + const confirmDialog = ( dialog ) => { + dialog.accept(); + page.removeListener( 'dialog', confirmDialog ); + resolve(); + }; + page.on( 'dialog', confirmDialog ); + } ); + await Promise.all( [ + confirmPromise, + page.click( 'tr[data-slug="' + slug + '"] .delete a' ), + ] ); + await page.waitForSelector( 'tr[data-slug="' + slug + '"].deleted' ); +} diff --git a/test/e2e/support/utils.js b/test/e2e/support/utils.js new file mode 100644 index 00000000000000..ae4050789f64eb --- /dev/null +++ b/test/e2e/support/utils.js @@ -0,0 +1,60 @@ +/** + * Node dependencies + */ +import { join } from 'path'; +import { URL } from 'url'; + +const { + WP_BASE_URL = 'http://localhost:8888', + WP_USERNAME = 'admin', + WP_PASSWORD = 'password', +} = process.env; + +function getUrl( WPPath, query = '' ) { + const url = new URL( WP_BASE_URL ); + + url.pathname = join( url.pathname, WPPath ); + url.search = query; + + return url.href; +} + +function isWPPath( WPPath, query = '' ) { + const currentUrl = new URL( page.url() ); + + currentUrl.search = query; + + return getUrl( WPPath ) === currentUrl.href; +} + +async function goToWPPath( WPPath, query ) { + await page.goto( getUrl( WPPath, query ) ); +} + +async function login() { + await page.type( '#user_login', WP_USERNAME ); + await page.type( '#user_pass', WP_PASSWORD ); + + await Promise.all( [ + page.waitForNavigation(), + page.click( '#wp-submit' ), + ] ); +} + +export async function visitAdmin( adminPath, query ) { + await goToWPPath( join( 'wp-admin', adminPath ), query ); + + if ( isWPPath( 'wp-login.php' ) ) { + await login(); + return visitAdmin( adminPath, query ); + } +} + +export async function newPost( postType ) { + await visitAdmin( 'post-new.php', postType ? 'post_type=' + postType : '' ); +} + +export async function newDesktopBrowserPage() { + global.page = await browser.newPage(); + await page.setViewport( { width: 1000, height: 700 } ); +} diff --git a/test/e2e/test-plugins/meta-box.php b/test/e2e/test-plugins/meta-box.php new file mode 100644 index 00000000000000..8d959b50ed80b9 --- /dev/null +++ b/test/e2e/test-plugins/meta-box.php @@ -0,0 +1,25 @@ +<?php + +/** + * Plugin Name: Gutenberg Test Plugin, Meta Box + * Plugin URI: https://github.com/WordPress/gutenberg + * Author: Gutenberg Team + * + * @package gutenberg-test-meta-box + */ + +function gutenberg_test_meta_box_render_meta_box() { + echo 'Hello World'; +} + +function gutenberg_test_meta_box_add_meta_box() { + add_meta_box( + 'gutenberg-test-meta-box', + 'Gutenberg Test Meta Box', + 'gutenberg_test_meta_box_render_meta_box', + 'post', + 'normal', + 'high' + ); +} +add_action( 'add_meta_boxes', 'gutenberg_test_meta_box_add_meta_box' ); diff --git a/test/e2e/test-plugins/templates.php b/test/e2e/test-plugins/templates.php new file mode 100644 index 00000000000000..00ec033710cb40 --- /dev/null +++ b/test/e2e/test-plugins/templates.php @@ -0,0 +1,46 @@ +<?php +/** + * Plugin Name: Gutenberg Test Plugin, Templates + * Plugin URI: https://github.com/WordPress/gutenberg + * Author: Gutenberg Team + * + * @package gutenberg-test-templates + */ + +/** + * Registers a book CPT with a template + */ +function gutenberg_test_templates_register_book_type() { + $args = array( + 'public' => true, + 'label' => 'Books', + 'show_in_rest' => true, + 'template' => array( + array( 'core/image' ), + array( + 'core/paragraph', + array( + 'placeholder' => 'Add a book description', + ), + ), + array( 'core/quote' ), + array( + 'core/columns', + array(), + array( + array( 'core/image', array( 'layout' => 'column-1' ) ), + array( + 'core/paragraph', + array( + 'placeholder' => 'Add a inner paragraph', + 'layout' => 'column-2', + ), + ), + ), + ), + ), + ); + register_post_type( 'book', $args ); +} + +add_action( 'init', 'gutenberg_test_templates_register_book_type' ); diff --git a/test/unit/jest.config.json b/test/unit/jest.config.json new file mode 100644 index 00000000000000..0146f21fa39734 --- /dev/null +++ b/test/unit/jest.config.json @@ -0,0 +1,22 @@ +{ + "rootDir": "../../", + "collectCoverageFrom": [ + "(blocks|components|date|editor|element|data|utils|edit-post|viewport|plugins|core-data)/**/*.js" + ], + "moduleNameMapper": { + "@wordpress\\/(blocks|components|date|editor|element|data|utils|edit-post|viewport|plugins|core-data)": "$1" + }, + "preset": "@wordpress/jest-preset-default", + "setupFiles": [ + "core-js/fn/symbol/async-iterator", + "<rootDir>/test/unit/setup-blocks.js", + "<rootDir>/test/unit/setup-wp-aliases.js" + ], + "transform": { + "\\.pegjs$": "<rootDir>/test/unit/pegjs-transform.js" + }, + "testPathIgnorePatterns": [ + "/node_modules/", + "/test/e2e" + ] +} diff --git a/viewport/README.md b/viewport/README.md new file mode 100644 index 00000000000000..7ff4c22c7f3957 --- /dev/null +++ b/viewport/README.md @@ -0,0 +1,65 @@ +Viewport +======== + +Viewport is a module for responding to changes in the browser viewport size. It registers its own [data module](https://github.com/WordPress/gutenberg/tree/master/data), updated in response to browser media queries on a standard set of supported breakpoints. This data and the included higher-order components can be used in your own modules and components to implement viewport-dependent behaviors. + +## Breakpoints + +The standard set of breakpoint thresholds is as follows: + +Name|Pixel Width +---|--- +`huge`|1440 +`wide`|1280 +`large`|960 +`medium`|782 +`small`|600 +`mobile`|480 + +## Data Module + +The Viewport module registers itself under the `core/viewport` data namespace. + +```js +const isSmall = select( 'core/viewport' ).isViewportMatch( '< medium' ); +``` + +The `isViewportMatch` selector accepts a single string argument `query`. It consists of an optional operator and breakpoint name, separated with a space. The operator can be `<` or `>=`, defaulting to `>=`. + +```js +const { isViewportMatch } = select( 'core/viewport' ); +const isSmall = isViewportMatch( '< medium' ); +const isWideOrHuge = isViewportMatch( '>= wide' ); +// Equivalent: +// const isWideOrHuge = isViewportMatch( 'wide' ); +``` + +## `ifViewportMatches` Higher-Order Component + +If you are authoring a component which should only be shown under a specific viewport condition, you can leverage the `ifViewportMatches` higher-order component to achieve this requirement. + +Pass a viewport query to render the component only when the query is matched: + +```jsx +function MyMobileComponent() { + return <div>I'm only rendered on mobile viewports!</div>; +} + +MyMobileComponent = ifViewportMatches( '< small' )( MyMobileComponent ); +``` + +## `withViewportMatch` Higher-Order Component + +If you are authoring a component which should vary its rendering behavior depending upon the matching viewport, you can leverage the `withViewportMatch` higher-order component to achieve this requirement. + +Pass an object, where each key is a prop name and its value a viewport query. The component will be rendered with your prop(s) assigned with the result(s) of the query match: + +```jsx +function MyComponent( { isMobile } ) { + return ( + <div>Currently: { isMobile ? 'Mobile' : 'Not Mobile' }</div> + ); +} + +MyComponent = withViewportMatch( { isMobile: '< small' } )( MyComponent ); +``` diff --git a/viewport/if-viewport-matches.js b/viewport/if-viewport-matches.js new file mode 100644 index 00000000000000..6df11907fcbaf9 --- /dev/null +++ b/viewport/if-viewport-matches.js @@ -0,0 +1,32 @@ +/** + * WordPress dependencies + */ +import { compose, createHigherOrderComponent } from '@wordpress/element'; +import { ifCondition } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import withViewportMatch from './with-viewport-match'; + +/** + * Higher-order component creator, creating a new component which renders if + * the viewport query is satisfied. + * + * @param {string} query Viewport query. + * + * @see withViewportMatches + * + * @return {Function} Higher-order component. + */ +const ifViewportMatches = ( query ) => createHigherOrderComponent( + compose( [ + withViewportMatch( { + isViewportMatch: query, + } ), + ifCondition( ( props ) => props.isViewportMatch ), + ] ), + 'ifViewportMatches' +); + +export default ifViewportMatches; diff --git a/viewport/index.js b/viewport/index.js new file mode 100644 index 00000000000000..a8b19616e5c8c6 --- /dev/null +++ b/viewport/index.js @@ -0,0 +1,78 @@ +/** + * External dependencies + */ +import { reduce, forEach, debounce, mapValues, property } from 'lodash'; + +/** + * WordPress dependencies + */ +import { dispatch } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import './store'; + +export { default as ifViewportMatches } from './if-viewport-matches'; +export { default as withViewportMatch } from './with-viewport-match'; + +/** + * Hash of breakpoint names with pixel width at which it becomes effective. + * + * @see _breakpoints.scss + * + * @type {Object} + */ +const BREAKPOINTS = { + huge: 1440, + wide: 1280, + large: 960, + medium: 782, + small: 600, + mobile: 480, +}; + +/** + * Hash of query operators with corresponding condition for media query. + * + * @type {Object} + */ +const OPERATORS = { + '<': 'max-width', + '>=': 'min-width', +}; + +/** + * Callback invoked when media query state should be updated. Is invoked a + * maximum of one time per call stack. + */ +const setIsMatching = debounce( () => { + const values = mapValues( queries, property( 'matches' ) ); + dispatch( 'core/viewport' ).setIsMatching( values ); +}, { leading: true } ); + +/** + * Hash of breakpoint names with generated MediaQueryList for corresponding + * media query. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia + * @see https://developer.mozilla.org/en-US/docs/Web/API/MediaQueryList + * + * @type {Object<string,MediaQueryList>} + */ +const queries = reduce( BREAKPOINTS, ( result, width, name ) => { + forEach( OPERATORS, ( condition, operator ) => { + const list = window.matchMedia( `(${ condition }: ${ width }px)` ); + list.addListener( setIsMatching ); + + const key = [ operator, name ].join( ' ' ); + result[ key ] = list; + } ); + + return result; +}, {} ); + +window.addEventListener( 'orientationchange', setIsMatching ); + +// Set initial values +setIsMatching(); diff --git a/viewport/store/actions.js b/viewport/store/actions.js new file mode 100644 index 00000000000000..0da1576677c0e1 --- /dev/null +++ b/viewport/store/actions.js @@ -0,0 +1,15 @@ +/** + * Returns an action object used in signalling that viewport queries have been + * updated. Values are specified as an object of breakpoint query keys where + * value represents whether query matches. + * + * @param {Object} values Breakpoint query matches. + * + * @return {Object} Action object. + */ +export function setIsMatching( values ) { + return { + type: 'SET_IS_MATCHING', + values, + }; +} diff --git a/viewport/store/index.js b/viewport/store/index.js new file mode 100644 index 00000000000000..88c7cba476165e --- /dev/null +++ b/viewport/store/index.js @@ -0,0 +1,17 @@ +/** + * WordPress dependencies + */ +import { registerStore } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import reducer from './reducer'; +import * as actions from './actions'; +import * as selectors from './selectors'; + +export default registerStore( 'core/viewport', { + reducer, + actions, + selectors, +} ); diff --git a/viewport/store/reducer.js b/viewport/store/reducer.js new file mode 100644 index 00000000000000..22efbda63191e5 --- /dev/null +++ b/viewport/store/reducer.js @@ -0,0 +1,19 @@ +/** + * Reducer returning the viewport state, as keys of breakpoint queries with + * boolean value representing whether query is matched. + * + * @param {Object} state Current state. + * @param {Object} action Dispatched action. + * + * @return {Object} Updated state. + */ +function reducer( state = {}, action ) { + switch ( action.type ) { + case 'SET_IS_MATCHING': + return action.values; + } + + return state; +} + +export default reducer; diff --git a/viewport/store/selectors.js b/viewport/store/selectors.js new file mode 100644 index 00000000000000..ea8308d6ee156a --- /dev/null +++ b/viewport/store/selectors.js @@ -0,0 +1,28 @@ +/** + * External dependencies + */ +import { takeRight } from 'lodash'; + +/** + * Returns true if the viewport matches the given query, or false otherwise. + * + * @param {Object} state Viewport state object. + * @param {string} query Query string. Includes operator and breakpoint name, + * space separated. Operator defaults to >=. + * + * @example + * + * ```js + * isViewportMatch( state, '< huge' ); + * isViewPortMatch( state, 'medium' ); + * ``` + * + * @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( ' ' ); + + return !! state[ key ]; +} diff --git a/viewport/store/test/reducer.js b/viewport/store/test/reducer.js new file mode 100644 index 00000000000000..ffec6ca706c316 --- /dev/null +++ b/viewport/store/test/reducer.js @@ -0,0 +1,31 @@ +/** + * External dependencies + */ +import deepFreeze from 'deep-freeze'; + +/** + * Internal dependencies + */ +import reducer from '../reducer'; + +describe( 'reducer', () => { + it( 'defaults to an empty object', () => { + const state = reducer( undefined, {} ); + + expect( state ).toEqual( {} ); + } ); + + it( 'replaces its state in response to new matching values', () => { + const original = deepFreeze( reducer( undefined, {} ) ); + const state = reducer( original, { + type: 'SET_IS_MATCHING', + values: { + huge: true, + }, + } ); + + expect( state ).toEqual( { + huge: true, + } ); + } ); +} ); diff --git a/viewport/store/test/selectors.js b/viewport/store/test/selectors.js new file mode 100644 index 00000000000000..d05406ddfe6799 --- /dev/null +++ b/viewport/store/test/selectors.js @@ -0,0 +1,26 @@ +/** + * Internal dependencies + */ +import { isViewportMatch } from '../selectors'; + +describe( 'selectors', () => { + describe( 'isViewportMatch()', () => { + it( 'should return with omitted operator defaulting to >=', () => { + const result = isViewportMatch( { + '>= wide': true, + '< wide': false, + }, 'wide' ); + + expect( result ).toBe( true ); + } ); + + it( 'should return with known query value', () => { + const result = isViewportMatch( { + '>= wide': false, + '< wide': true, + }, '< wide' ); + + expect( result ).toBe( true ); + } ); + } ); +} ); diff --git a/viewport/test/if-viewport-matches.js b/viewport/test/if-viewport-matches.js new file mode 100644 index 00000000000000..d1f2c1a247d497 --- /dev/null +++ b/viewport/test/if-viewport-matches.js @@ -0,0 +1,39 @@ +/** + * External dependencies + */ +import { mount } from 'enzyme'; + +/** + * WordPress dependencies + */ +import { dispatch } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import '../store'; +import ifViewportMatches from '../if-viewport-matches'; + +describe( 'ifViewportMatches()', () => { + const Component = () => <div>Hello</div>; + + it( 'should not render if query does not match', () => { + dispatch( 'core/viewport' ).setIsMatching( { '> wide': false } ); + const EnhancedComponent = ifViewportMatches( '> wide' )( Component ); + const wrapper = mount( <EnhancedComponent /> ); + + expect( wrapper.find( Component ) ).toHaveLength( 0 ); + + wrapper.unmount(); + } ); + + it( 'should render if query does match', () => { + dispatch( 'core/viewport' ).setIsMatching( { '> wide': true } ); + const EnhancedComponent = ifViewportMatches( '> wide' )( Component ); + const wrapper = mount( <EnhancedComponent /> ); + + expect( wrapper.find( Component ).childAt( 0 ).type() ).toBe( 'div' ); + + wrapper.unmount(); + } ); +} ); diff --git a/viewport/test/with-viewport-match.js b/viewport/test/with-viewport-match.js new file mode 100644 index 00000000000000..a2d17ded6e956f --- /dev/null +++ b/viewport/test/with-viewport-match.js @@ -0,0 +1,29 @@ +/** + * External dependencies + */ +import { mount } from 'enzyme'; + +/** + * WordPress dependencies + */ +import { dispatch } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import '../store'; +import withViewportMatch from '../with-viewport-match'; + +describe( 'withViewportMatch()', () => { + const Component = () => <div>Hello</div>; + + it( 'should render with result of query as custom prop name', () => { + dispatch( 'core/viewport' ).setIsMatching( { '> wide': true } ); + const EnhancedComponent = withViewportMatch( { isWide: '> wide' } )( Component ); + const wrapper = mount( <EnhancedComponent /> ); + + expect( wrapper.find( Component ).props() ).toEqual( { isWide: true } ); + + wrapper.unmount(); + } ); +} ); diff --git a/viewport/with-viewport-match.js b/viewport/with-viewport-match.js new file mode 100644 index 00000000000000..238d2082ec730c --- /dev/null +++ b/viewport/with-viewport-match.js @@ -0,0 +1,32 @@ +/** + * External dependencies + */ +import { mapValues } from 'lodash'; + +/** + * WordPress dependencies + */ +import { createHigherOrderComponent } from '@wordpress/element'; +import { withSelect } from '@wordpress/data'; + +/** + * Higher-order component creator, creating a new component which renders with + * the given prop names, where the value passed to the underlying component is + * the result of the query assigned as the object's value. + * + * @param {Object} queries Object of prop name to viewport query. + * + * @see isViewportMatch + * + * @return {Function} Higher-order component. + */ +const withViewportMatch = ( queries ) => createHigherOrderComponent( + withSelect( ( select ) => { + return mapValues( queries, ( query ) => { + return select( 'core/viewport' ).isViewportMatch( query ); + } ); + } ), + 'withViewportMatch' +); + +export default withViewportMatch;