-
Notifications
You must be signed in to change notification settings - Fork 384
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add AMP validation checking for Gutenberg blocks #1019
Changes from 5 commits
8413457
07aeee2
01691e5
27e8bec
590cd76
8218919
9c3568d
ab82e8d
ea1c37a
96297fc
ed4b03b
561af32
f64ab2e
b9bd206
bcdff5f
049e482
fe34e8e
49b66ae
743cca4
4676777
b9aa16c
e2fc9a7
6cd996a
339f69f
a02d029
15f0abe
2b03c6a
d366da3
80d32de
16388c2
f9d8575
9aa6704
dcb1e77
eb00bcf
9ecc24c
354fdcd
0bada73
34024cc
8e78904
277da63
ca3ff60
01a21a8
3e032d4
6a4cbcc
4d7dc19
21e60f0
ca6c96c
7a60509
5bf12da
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,185 @@ | ||
/*jshint esversion: 6 */ | ||
/*global _, wp:true */ | ||
/** | ||
* AMP Gutenberg integration. | ||
* | ||
* On editing a block, this checks that the content is AMP-compatible. | ||
* And it displays a notice if it's not. | ||
*/ | ||
|
||
/* exported ampBlockValidation */ | ||
var ampBlockValidation = ( function( $ ) { | ||
var module = { | ||
|
||
/** | ||
* Holds data. | ||
*/ | ||
data: {}, | ||
|
||
/** | ||
* Boot module. | ||
* | ||
* @param {Object} data Object data. | ||
* @return {void} | ||
*/ | ||
boot: function( data ) { | ||
module.data = data; | ||
$( document ).ready( function() { | ||
if ( 'undefined' !== typeof wp.blocks ) { | ||
module.processBlocks(); | ||
} | ||
} ); | ||
}, | ||
|
||
/** | ||
* Gets all of the registered blocks, and overwrites their edit() functions. | ||
* | ||
* The new edit() functions will check if the content is AMP-compliant. | ||
* If not, it will display a notice. | ||
* | ||
* @returns {void} | ||
*/ | ||
processBlocks: function() { | ||
var blocks = wp.blocks.getBlockTypes(); | ||
blocks.forEach( function( block ) { | ||
if ( block.hasOwnProperty( 'name' ) ) { | ||
module.overwriteEdit( block.name ); | ||
} | ||
} ); | ||
}, | ||
|
||
/** | ||
* Overwrites the edit() function of a block. | ||
* | ||
* Outputs the original edit function, stored in OriginalBlockEdit. | ||
* This also appends a notice to the block. | ||
* It only displays if the block's content isn't valid AMP, | ||
* | ||
* @see https://riad.blog/2017/10/16/one-thousand-and-one-way-to-extend-gutenberg-today/ | ||
* @param {string} blockType the type of the block, like 'core/paragraph'. | ||
* @returns {void} | ||
*/ | ||
overwriteEdit: function( blockType ) { | ||
let block = wp.blocks.unregisterBlockType( blockType ); | ||
let OriginalBlockEdit = block.edit; | ||
|
||
block.edit = class AMPNotice extends wp.element.Component { | ||
|
||
/** | ||
* The AMPNotice constructor. | ||
* | ||
* @param {object} props The component properties. | ||
* @returns {void} | ||
*/ | ||
constructor( props ) { | ||
props.attributes.pendingValidation = false; | ||
super( props ); | ||
this.validateAMP = _.throttle( this.validateAMP, 5000 ); | ||
this.state = { isInvalidAMP: false }; | ||
} | ||
|
||
/** | ||
* Outputs the existing block, with a Notice element below. | ||
* | ||
* The Notice only appears if the state of isInvalidAMP is false. | ||
* It also displays the name of the block. | ||
* | ||
* @returns {array} The elements. | ||
*/ | ||
render() { | ||
let originalEdit; | ||
let result; | ||
|
||
result = []; | ||
originalEdit = wp.element.createElement( OriginalBlockEdit, this.props ); | ||
if ( originalEdit ) { | ||
result.push( originalEdit ); | ||
} | ||
if ( this.state.isInvalidAMP && wp.components.hasOwnProperty( 'Notice' ) ) { | ||
result.push( wp.element.createElement( | ||
wp.components.Notice, | ||
{ | ||
status: 'warning', | ||
content: module.data.i18n.notice.replace( '%s', this.props.name ), | ||
isDismissible: false | ||
} | ||
) ); | ||
} | ||
|
||
this.props.attributes.pendingValidation = false; | ||
return result; | ||
} | ||
|
||
/** | ||
* Handler for after the component mounting. | ||
* | ||
* If validateAMP() changes the isInvalidAMP state, it will result in this method being called again. | ||
* There's no need to check if the state is valid again. | ||
* So this skips the check if pendingValidation is true. | ||
* | ||
* @returns {void} | ||
*/ | ||
componentDidMount() { | ||
if ( ! this.props.attributes.pendingValidation ) { | ||
let content = this.props.attributes.content; | ||
if ( 'string' !== typeof content ) { | ||
content = wp.element.renderToString( content ); | ||
} | ||
if ( content.length > 0 ) { | ||
this.validateAMP( this.props.attributes.content ); | ||
} | ||
} | ||
this.props.attributes.pendingValidation = false; | ||
} | ||
|
||
/** | ||
* Validates the content for AMP compliance, and sets the state of the Notice. | ||
* | ||
* Depending on the results of the validation, | ||
* it sets the Notice component's isInvalidAMP state. | ||
* This will cause the notice to either display or be hidden. | ||
* | ||
* @param {string} content The block content, from calling save(). | ||
* @returns {void} | ||
*/ | ||
validateAMP( content ) { | ||
this.setState( function() { | ||
|
||
// Changing the state can cause componentDidMount() to be called, so prevent it from calling validateAMP() again. | ||
component.props.attributes.pendingValidation = true; | ||
return { isInvalidAMP: ( Math.random() > 0.5 ) }; | ||
} ); | ||
|
||
let component = this; | ||
$.post( | ||
module.data.endpoint, | ||
{ | ||
markup: content | ||
} | ||
).done( function( data ) { | ||
if ( data.hasOwnProperty( 'removed_elements' ) && ( 0 === data.removed_elements.length ) && ( 0 === data.removed_attributes.length ) ) { | ||
component.setState( function() { | ||
|
||
// Changing the state can cause componentDidMount() to be called, so prevent it from calling validateAMP() again. | ||
component.props.attributes.pendingValidation = true; | ||
return { isInvalidAMP: false }; | ||
} ); | ||
} else { | ||
component.setState( function() { | ||
component.props.attributes.pendingValidation = true; | ||
return { isInvalidAMP: true }; | ||
} ); | ||
} | ||
} ); | ||
} | ||
|
||
}; | ||
|
||
wp.blocks.registerBlockType( blockType, block ); | ||
} | ||
|
||
}; | ||
|
||
return module; | ||
|
||
} )( window.jQuery ); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should eliminate jQuery as being a dependency for this script. Extending Gutenberg should be jQuery-free. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks, @westonruter. This commit removes the |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -152,6 +152,13 @@ class AMP_Validation_Utils { | |
*/ | ||
const VALIDATION_ERRORS_META_BOX = 'amp_validation_errors'; | ||
|
||
/** | ||
* The name of the REST API field with the AMP validation results. | ||
* | ||
* @var string | ||
*/ | ||
const REST_FIELD_NAME = 'amp_validation_errors'; | ||
|
||
/** | ||
* The errors encountered when validating. | ||
* | ||
|
@@ -204,6 +211,8 @@ public static function init() { | |
add_filter( 'dashboard_glance_items', array( __CLASS__, 'filter_dashboard_glance_items' ) ); | ||
add_action( 'rightnow_end', array( __CLASS__, 'print_dashboard_glance_styles' ) ); | ||
add_action( 'save_post', array( __CLASS__, 'handle_save_post_prompting_validation' ), 10, 2 ); | ||
add_action( 'enqueue_block_editor_assets', array( __CLASS__, 'enqueue_block_validation' ) ); | ||
add_action( 'rest_api_init', array( __CLASS__, 'add_rest_api_fields' ) ); | ||
} | ||
|
||
add_action( 'edit_form_top', array( __CLASS__, 'print_edit_form_validation_status' ), 10, 2 ); | ||
|
@@ -1869,4 +1878,60 @@ public static function get_recheck_link( $post, $redirect_url, $recheck_url = nu | |
); | ||
} | ||
|
||
/** | ||
* Enqueues the block validation script. | ||
* | ||
* @return void | ||
*/ | ||
public static function enqueue_block_validation() { | ||
$slug = 'amp-block-validation'; | ||
|
||
wp_enqueue_script( | ||
$slug, | ||
amp_get_asset_url( "js/{$slug}.js" ), | ||
array( 'jquery' ), | ||
AMP__VERSION, | ||
true | ||
); | ||
|
||
$data = wp_json_encode( array( | ||
'i18n' => array( | ||
/* translators: %s: the name of the block */ | ||
'notice' => __( 'The %s block above has invalid AMP', 'amp' ), | ||
), | ||
) ); | ||
wp_add_inline_script( $slug, sprintf( 'ampBlockValidation.boot( %s );', $data ) ); | ||
} | ||
|
||
/** | ||
* Adds fields to the REST API responses, in order to display validation errors. | ||
* | ||
* @return void | ||
*/ | ||
public static function add_rest_api_fields() { | ||
register_rest_field( | ||
array( 'post', 'page' ), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks, @westonruter. This commit implements that, and updates the tests. |
||
self::REST_FIELD_NAME, | ||
array( | ||
'get_callback' => array( __CLASS__, 'rest_field_amp_validation' ), | ||
'schema' => array( | ||
'description' => __( 'AMP validation results', 'amp' ), | ||
'type' => 'object', | ||
), | ||
) | ||
); | ||
} | ||
|
||
/** | ||
* Adds a field to the REST API responses to display the validation status. | ||
* | ||
* @param array $post_data Data for the post. | ||
* @param string $field_name The name of the field to add. | ||
* @return array|null $validation_data Validation data if it's available, or null. | ||
*/ | ||
public static function rest_field_amp_validation( $post_data, $field_name ) { | ||
$validation_post = self::get_validation_status_post( $post_data['link'] ); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Excellent re-use of this function. One key thing is that instead of Idea: If this post is empty or the post type is not viewable, should we take the post content and try to validate it? In other words, this: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good idea, @westonruter. This commit abstracts that snippet above into get_existing_validation_errors(), and reuses it here. If there are no existing errors, it validates the front-end. The only issue is that this delays the response if it validates several posts. |
||
return isset( $validation_post ) ? json_decode( $validation_post->post_content, true ) : null; | ||
} | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ES6 classes aren't going to work in IE11.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, @westonruter. This commit removes the ES6 class.