diff --git a/assets/css/amp-editor-story-blocks.css b/assets/css/amp-editor-story-blocks.css index 9b10a367a8b..68fe179ab18 100644 --- a/assets/css/amp-editor-story-blocks.css +++ b/assets/css/amp-editor-story-blocks.css @@ -1,3 +1,162 @@ div[data-type="amp/amp-story-page"] { - background-color: #ddffdd; -} \ No newline at end of file + border: 1px solid #ffdddd; +} + +.editor-block-list__layout div[data-type="amp/amp-story-page"] { + padding: 0; + margin: 60px auto 0; +} + +@media (min-width: 600px) { + div[data-type="amp/amp-story-page"] .editor-block-list__block { + margin: 0; + padding: 0; + } +} + +.components-popover.editor-inserter__amp .components-popover__content { + height: 200px; +} + +div[data-type="amp/amp-story-page"] * { + max-width: 100%; + max-height: 100%; +} + +div[data-type="amp/amp-story-page"], +div[data-type="amp/amp-story-page"] .editor-inner-blocks .editor-block-list__layout:first-of-type { + margin: auto; + max-height: 75vh !important; + max-width: 45vh !important; + min-width: 320px !important; + min-height: 533px; +} + +.editor-block-list__layout div[data-type="amp/amp-story-cta-layer"] .editor-inner-blocks .editor-block-list__layout { + min-height: initial !important; +} + +/* Make layer be exactly on top of each other */ +.editor-block-list__layout div[data-type="amp/amp-story-page"].editor-block-list__block:first-child .editor-block-list__block-edit { + margin: 0; +} + +.editor-block-list__layout div[data-type="amp/amp-story-page"].editor-block-list__block .editor-block-list__block-edit, .editor-block-list__layout .editor-block-list__layout .editor-block-list__block .editor-block-list__block-edit { + margin: 0; +} + +div[data-type="amp/amp-story-page"] .editor-inner-blocks .editor-block-list__layout .editor-block-list__layout { + background-color: initial; +} + +.editor-block-list__layout div[data-type="amp/amp-story-grid-layer"] { + position: absolute; + height: 100%; + width: 100%; +} + +.amp-grid-template .editor-block-list__layout:first-of-type { + display: grid; + overflow: hidden; + padding: 68px 32px 32px; +} + +.amp-grid-template .editor-block-list__layout:first-of-type * { + margin: 0; + box-sizing: border-box; +} + +.amp-grid-template-horizontal .editor-block-list__layout:first-of-type { + grid-auto-flow: column !important; + grid-template-rows: 100% !important; + -ms-flex-line-pack: stretch; + align-content: stretch; + -webkit-box-align: start; + -ms-flex-align: start; + align-items: start; + grid-gap: 16px; + -webkit-box-pack: start; + -ms-flex-pack: start; + justify-content: start; +} + +.amp-grid-template-vertical .editor-block-list__layout:first-of-type { + grid-auto-flow: row; + grid-template-columns: 100%; + align-content: start; + grid-gap: 16px; + -webkit-box-pack: stretch; + -ms-flex-pack: stretch; + justify-content: stretch; + justify-items: start; +} + +.amp-grid-template-thirds .editor-block-list__layout:first-of-type { + grid-template-rows: 1fr 1fr 1fr; + grid-template-areas: "upper-third" "middle-third" "lower-third"; +} + +div[data-amp-position="upper-third"] { + grid-area: upper-third / lower-third / lower-third / lower-third; +} + +div[data-amp-position="middle-third"] { + grid-area: middle-third / lower-third / lower-third / lower-third; +} + +div[data-amp-position="lower-third"] { + grid-area: lower-third / lower-third / lower-third / lower-third; +} + + +.amp-grid-template-fill .editor-block-list__layout > :first-child { + bottom: 0; + display: block; + height: auto; + left: 0; + position: absolute; + right: 0; + top: 0; + width: auto; +} + +.amp-grid-template-fill > :not(:first-child) { + display: none; +} + +.editor-block-list__layout div[data-type="amp/amp-story-cta-layer"] { + position: absolute; + height: 20%; + width: 100%; + bottom: 0; +} + +.editor-block-list__layout div[data-type="amp/amp-story-grid-layer"].is-selected, +.editor-block-list__layout div[data-type="amp/amp-story-grid-layer"].is-selected .editor-block-list__block { + border: 1px dotted #99c2e0; +} + +.editor-block-list__layout div[data-type="amp/amp-story-grid-layer"].is-selected ~ div[data-type="amp/amp-story-grid-layer"], +.editor-block-list__layout div[data-type="amp/amp-story-grid-layer"].is-selected-parent ~ div[data-type="amp/amp-story-grid-layer"]{ + display: none; +} + +.editor-selectors { + position: absolute; + right: -150px; + bottom: 0; + width: 100px; + z-index: 80; +} + +.editor-selectors .component-editor__selector { + margin-bottom: 2px; +} + +.editor-selectors .component-editor__selector button { + cursor: pointer; +} + +.editor-selectors .is-selected.component-editor__selector button { + text-decoration: underline; +} diff --git a/assets/js/amp-story-editor-blocks.js b/assets/js/amp-story-editor-blocks.js new file mode 100644 index 00000000000..e2032db7110 --- /dev/null +++ b/assets/js/amp-story-editor-blocks.js @@ -0,0 +1,217 @@ +/* exported ampStoryEditorBlocks */ +/* global lodash */ +/* eslint no-magic-numbers: [ "error", { "ignore": [ -1 ] } ] */ + +var ampStoryEditorBlocks = ( function() { // eslint-disable-line no-unused-vars + var component, __; + + __ = wp.i18n.__; + + component = { + + /** + * Holds data. + */ + data: { + allowedBlocks: [ + 'core/code', + 'core/embed', + 'core/image', + 'core/list', + 'core/paragraph', + 'core/preformatted', + 'core/pullquote', + 'core/quote', + 'core/table', + 'core/verse', + 'core/video' + ], + ampStoryPositionOptions: [ + { + value: 'upper-third', + label: __( 'Upper Third', 'amp' ) + }, + { + value: 'middle-third', + label: __( 'Middle Third', 'amp' ) + }, + { + value: 'lower-third', + label: __( 'Lower Third', 'amp' ) + } + ] + } + }; + + /** + * Add filters. + */ + component.boot = function boot() { + wp.hooks.addFilter( 'blocks.registerBlockType', 'ampStoryEditorBlocks/addAttributes', component.addAMPAttributes ); + wp.hooks.addFilter( 'editor.BlockEdit', 'ampStoryEditorBlocks/filterEdit', component.filterBlocksEdit ); + wp.hooks.addFilter( 'editor.BlockListBlock', 'my-plugin/with-data-align', component.addWrapperProps ); + wp.hooks.addFilter( 'blocks.getSaveContent.extraProps', 'ampStoryEditorBlocks/addExtraAttributes', component.addAMPExtraProps ); + }; + + /** + * Add wrapper props to the blocks within AMP Story layers. + * + * @param {Object} BlockListBlock BlockListBlock element. + * @return {Function} Handler. + */ + component.addWrapperProps = function( BlockListBlock ) { + var el = wp.element.createElement, + select = wp.data.select( 'core/editor' ); + return function( props ) { + var parentClientId, + parentBlock, + ampStoryPosition; + if ( -1 === component.data.allowedBlocks.indexOf( props.block.name ) || ! props.block.attributes.ampStoryPosition ) { + return [ + el( BlockListBlock, _.extend( { + key: 'original' + }, props ) ) + ]; + } + + parentClientId = select.getBlockRootClientId( props.block.clientId ); + parentBlock = select.getBlock( parentClientId ); + ampStoryPosition = props.block.attributes.ampStoryPosition; + + if ( 'thirds' !== parentBlock.attributes.template ) { + ampStoryPosition = null; + } + + var newProps = lodash.assign( + {}, + props, + { + wrapperProps: lodash.assign( + {}, + props.wrapperProps, + { + 'data-amp-position': ampStoryPosition + } + ) + } + ); + + return el( + BlockListBlock, + newProps + ); + }; + }; + /** + * Add extra attributes to save to DB. + * + * @param {Object} props Properties. + * @param {Object} blockType Block type. + * @param {Object} attributes Attributes. + * @return {Object} Props. + */ + component.addAMPExtraProps = function addAMPExtraProps( props, blockType, attributes ) { + var ampAttributes = {}; + if ( -1 === component.data.allowedBlocks.indexOf( blockType.name ) ) { + return props; + } + + if ( attributes.ampStoryPosition ) { + ampAttributes[ 'grid-area' ] = attributes.ampStoryPosition; + } + + return _.extend( ampAttributes, props ); + }; + + /** + * Add AMP attributes to every allowed AMP Story block. + * + * @param {Object} settings Settings. + * @param {string} name Block name. + * @return {Object} Settings. + */ + component.addAMPAttributes = function addAMPAttributes( settings, name ) { + // Add the "thirds" template position option. + if ( -1 !== component.data.allowedBlocks.indexOf( name ) ) { + if ( ! settings.attributes ) { + settings.attributes = {}; + } + settings.attributes.ampStoryPosition = { + type: 'string' + }; + } + return settings; + }; + + /** + * Filters blocks edit function of all blocks. + * + * @param {Function} BlockEdit Edit function. + * @return {Function} Edit function. + */ + component.filterBlocksEdit = function filterBlocksEdit( BlockEdit ) { + var el = wp.element.createElement, + select = wp.data.select( 'core/editor' ); + + return function( props ) { + var attributes = props.attributes, + name = props.name, + inspectorControls, + InspectorControls = wp.editor.InspectorControls, + PanelBody = wp.components.PanelBody, + SelectControl = wp.components.SelectControl, + parentClientId = select.getBlockRootClientId( props.clientId ), + parentBlock; + + if ( -1 === component.data.allowedBlocks.indexOf( name ) ) { + // Return original. + return [ + el( BlockEdit, _.extend( { + key: 'original' + }, props ) ) + ]; + } + + parentBlock = select.getBlock( parentClientId ); + if ( 'amp/amp-story-grid-layer' !== parentBlock.name ) { + // Return original. + return [ + el( BlockEdit, _.extend( { + key: 'original' + }, props ) ) + ]; + } + + if ( 'thirds' !== parentBlock.attributes.template ) { + // Return original. + return [ + el( BlockEdit, _.extend( { + key: 'original' + }, props ) ) + ]; + } + + inspectorControls = el( InspectorControls, { key: 'inspector' }, + el( PanelBody, { title: __( 'AMP Story Settings', 'amp' ) }, + el( SelectControl, { + label: __( 'Placement', 'amp' ), + value: attributes.ampStoryPosition, + options: component.data.ampStoryPositionOptions, + onChange: function( value ) { + props.setAttributes( { ampStoryPosition: value } ); + } + } ) + ) + ); + + return [ + inspectorControls, + el( BlockEdit, _.extend( { + key: 'original' + }, props ) ) + ]; + }; + }; + + return component; +}() ); diff --git a/blocks/amp-story/amp-story-cta-layer.js b/blocks/amp-story/amp-story-cta-layer.js index fb6214a589a..9677b9ee3ec 100644 --- a/blocks/amp-story/amp-story-cta-layer.js +++ b/blocks/amp-story/amp-story-cta-layer.js @@ -50,6 +50,7 @@ export default registerBlockType( category: 'layout', icon: 'grid-view', parent: [ 'amp/amp-story-page' ], + inserter: false, /* * : diff --git a/blocks/amp-story/amp-story-grid-layer.js b/blocks/amp-story/amp-story-grid-layer.js index 62802cd1ea2..51c4ccaaa79 100644 --- a/blocks/amp-story/amp-story-grid-layer.js +++ b/blocks/amp-story/amp-story-grid-layer.js @@ -58,10 +58,12 @@ export default registerBlockType( source: 'attribute', selector: 'amp-story-grid-layer', attribute: 'template', - default: 'fill' + default: 'vertical' } }, + inserter: false, + /* * : * mandatory_ancestor: "AMP-STORY-PAGE" @@ -80,26 +82,28 @@ export default registerBlockType( value={ props.attributes.template } options={ [ { - value: 'fill', - label: __( 'Fill', 'amp' ) + value: 'vertical', + label: __( 'Vertical', 'amp' ) }, { - value: 'horizontal', - label: __( 'Horizontal', 'amp' ) + value: 'fill', + label: __( 'Fill', 'amp' ) }, { value: 'thirds', label: __( 'Thirds', 'amp' ) }, { - value: 'vertical', - label: __( 'Vertical', 'amp' ) + value: 'horizontal', + label: __( 'Horizontal', 'amp' ) } ] } onChange={ value => ( setAttributes( { template: value } ) ) } /> , - +
+ +
]; }, diff --git a/blocks/amp-story/amp-story-page.js b/blocks/amp-story/amp-story-page.js index 343b3668174..73907ae3d38 100644 --- a/blocks/amp-story/amp-story-page.js +++ b/blocks/amp-story/amp-story-page.js @@ -1,5 +1,6 @@ import memoize from 'memize'; import uuid from 'uuid/v4'; +import BlockSelector from './block-selector'; const { __ } = wp.i18n; const { @@ -29,7 +30,6 @@ const ALLOWED_BLOCKS = [ const getStoryPageTemplate = memoize( ( grids, hasCTA ) => { let template = _.times( grids, () => [ 'amp/amp-story-grid-layer', - [], [ [ 'core/paragraph', @@ -124,6 +124,7 @@ export default registerBlockType( ] } /> , + , // Get the template dynamically.
diff --git a/blocks/amp-story/block-selector.js b/blocks/amp-story/block-selector.js new file mode 100644 index 00000000000..0f2bcf430a5 --- /dev/null +++ b/blocks/amp-story/block-selector.js @@ -0,0 +1,97 @@ + +const { __, sprintf } = wp.i18n; +const { Component } = wp.element; +const { Button } = wp.components; +const { + dispatch, + select +} = wp.data; +const { + getBlock, + isBlockSelected, + hasSelectedInnerBlock, + getSelectedBlock +} = select( 'core/editor' ); +const { + selectBlock +} = dispatch( 'core/editor' ); + +import LayerInserter from './layer-inserter'; + +class BlockSelector extends Component { + render() { + if ( ! this.props.rootClientId ) { + return null; + } + + const rootBlock = getBlock( this.props.rootClientId ); + + if ( ! rootBlock.innerBlocks.length ) { + return null; + } + + let links = []; + + window.lodash.forEachRight( rootBlock.innerBlocks, function( block, index ) { + let className = 'component-editor__selector'; + if ( isBlockSelected( block.clientId ) || hasSelectedInnerBlock( block.clientId ) ) { + className += ' is-selected'; + } + + let title = sprintf( __( 'Layout %d ', 'amp' ), index + 1 ); + if ( 'amp/amp-story-cta-layer' === block.name ) { + title = __( 'CTA Layer', 'amp' ); + } + links.push( +
  • + +
  • + ); + } ); + + let className = 'component-editor__selector'; + if ( isBlockSelected( this.props.rootClientId ) ) { + className += ' is-selected'; + } + + const inserterProps = { + rootClientId: this.props.rootClientId + }; + + links.push( +
  • + +
  • + ); + + // @todo Creating a custom inserter since the default inserter doesn't allow taking the root client ID dynamically. Change if that becomes available. + return ( +
      + + { links } +
    + ); + } +} + +export default BlockSelector; diff --git a/blocks/amp-story/layer-inserter.js b/blocks/amp-story/layer-inserter.js new file mode 100644 index 00000000000..651ba1c2025 --- /dev/null +++ b/blocks/amp-story/layer-inserter.js @@ -0,0 +1,157 @@ +const { __ } = wp.i18n; +const { IconButton } = wp.components; +const { Component } = wp.element; +const { BlockIcon } = wp.editor; +const { + createBlock, + getBlockType, + getBlockMenuDefaultClassName +} = wp.blocks; + +const { + Dropdown +} = wp.components; + +const { + dispatch, + select +} = wp.data; +const { + getBlock +} = select( 'core/editor' ); +const { + insertBlock +} = dispatch( 'core/editor' ); + +class LayerInserter extends Component { + constructor() { + super( ...arguments ); + + this.onToggle = this.onToggle.bind( this ); + } + + onInsertBlock( item, rootClientId ) { + const { name } = item; + const insertedBlock = createBlock( name ); + const rootBlock = getBlock( rootClientId ); + const index = rootBlock.innerBlocks.length ? rootBlock.innerBlocks.length : 0; + + insertBlock( insertedBlock, index, rootClientId ); + } + + onToggle( isOpen ) { + const { onToggle } = this.props; + + // Surface toggle callback to parent component + if ( onToggle ) { + onToggle( isOpen ); + } + } + + render() { + const { + rootClientId + } = this.props; + + const { + getInserterItems + } = wp.data.select( 'core/editor' ); + let items = getInserterItems( rootClientId ); + + if ( items.length === 0 ) { + return null; + } + + const onInsertBlock = this.onInsertBlock; + + return ( + ( + + + ) } + renderContent={ ( { onClose } ) => { + const onSelect = ( item ) => { + onInsertBlock( item, rootClientId ); + + onClose(); + }; + + // @todo If CTA layer is already added, don't display it here. + items = [ + getBlockType( 'amp/amp-story-grid-layer' ), + getBlockType( 'amp/amp-story-cta-layer' ) + ]; + + return ( +
    +
    +
      + { items.map( ( item ) => { + const itemIconStyle = item.icon ? { + backgroundColor: item.icon.background, + color: item.icon.foreground + } : {}; + const itemIconStackStyle = item.icon && item.icon.shadowColor ? { + backgroundColor: item.icon.shadowColor + } : {}; + + const className = 'editor-block-types-list__item ' + getBlockMenuDefaultClassName( item.name ); + + return ( +
    • + +
    • + ); + } ) } +
    + +
    +
    + ); + } } + /> + ); + } +} + +export default LayerInserter; diff --git a/includes/admin/class-amp-post-meta-box.php b/includes/admin/class-amp-post-meta-box.php index 38ce013e81c..9d148092ea1 100644 --- a/includes/admin/class-amp-post-meta-box.php +++ b/includes/admin/class-amp-post-meta-box.php @@ -150,7 +150,8 @@ public function enqueue_admin_assets() { self::ASSETS_HANDLE, amp_get_asset_url( 'js/amp-post-meta-box.js' ), array( 'jquery' ), - AMP__VERSION + AMP__VERSION, + false ); if ( current_theme_supports( 'amp' ) ) { diff --git a/includes/amp-helper-functions.php b/includes/amp-helper-functions.php index 957db06eab4..8202588eac0 100644 --- a/includes/amp-helper-functions.php +++ b/includes/amp-helper-functions.php @@ -272,6 +272,11 @@ function is_amp_endpoint() { _doing_it_wrong( __FUNCTION__, sprintf( esc_html__( "is_amp_endpoint() was called before the 'parse_query' hook was called. This function will always return 'false' before the 'parse_query' hook is called.", 'amp' ) ), '0.4.2' ); } + // AMP Stories are always an AMP endpoint. + if ( is_singular( AMP_Story_Post_Type::POST_TYPE_SLUG ) ) { + return true; + } + $has_amp_query_var = ( isset( $_GET[ amp_get_slug() ] ) // WPCS: CSRF OK. || diff --git a/includes/class-amp-story-post-type.php b/includes/class-amp-story-post-type.php index b649c90eaaa..3e5214075c6 100644 --- a/includes/class-amp-story-post-type.php +++ b/includes/class-amp-story-post-type.php @@ -147,6 +147,19 @@ public static function enqueue_block_editor_assets() { AMP__VERSION ); + // @todo Name the script better to distinguish. + wp_enqueue_script( + 'amp-story-editor-blocks', + amp_get_asset_url( 'js/amp-story-editor-blocks.js' ), + array( 'wp-blocks', 'lodash', 'wp-i18n', 'wp-element', 'wp-components' ), + AMP__VERSION + ); + + wp_add_inline_script( + 'amp-story-editor-blocks', + 'ampStoryEditorBlocks.boot();' + ); + wp_enqueue_script( 'amp-editor-story-blocks-build', amp_get_asset_url( 'js/amp-story-blocks-compiled.js' ), diff --git a/includes/sanitizers/class-amp-allowed-tags-generated.php b/includes/sanitizers/class-amp-allowed-tags-generated.php index c12d90067c7..93e1a868212 100644 --- a/includes/sanitizers/class-amp-allowed-tags-generated.php +++ b/includes/sanitizers/class-amp-allowed-tags-generated.php @@ -13371,6 +13371,7 @@ class AMP_Allowed_Tags_Generated { '', ), ), + 'grid-area' => array(), 'hidden' => array( 'value' => array( '', diff --git a/includes/templates/single-amp_story.php b/includes/templates/single-amp_story.php index e6243d93853..5e9b17a0056 100644 --- a/includes/templates/single-amp_story.php +++ b/includes/templates/single-amp_story.php @@ -13,7 +13,11 @@ <?php echo esc_html( wp_get_document_title() ); ?> - do_items( array( 'amp-story' ) ); ?> + do_items( array( 'amp-runtime' ) ); // @todo Duplicate with AMP_Theme_Support::enqueue_assets(). + wp_styles()->do_items( array( 'wp-block-library' ) ); // @todo We need to allow a theme to enqueue their own AMP story styles. + ?>