diff --git a/assets/css/amp-editor-story-blocks.css b/assets/css/amp-editor-story-blocks.css index 12f47115752..82b97789306 100644 --- a/assets/css/amp-editor-story-blocks.css +++ b/assets/css/amp-editor-story-blocks.css @@ -371,6 +371,7 @@ div[data-type="amp/amp-story-page"] .editor-inner-blocks .editor-block-list__lay #amp-story-controls { text-align: right; + padding-right: 20px; } #amp-story-controls .amp-story-controls-reorder { @@ -385,8 +386,8 @@ div[data-type="amp/amp-story-page"] .editor-inner-blocks .editor-block-list__lay } .amp-story-controls-reorder-cancel { - padding-top: 2px; - padding-bottom: 2px; + padding-top: 5px; + padding-bottom: 5px; margin-right: 20px; display: inline-flex; } @@ -723,6 +724,155 @@ div[data-type="amp/amp-story-page"] .wp-block-image { * 10. Custom Components */ +/* + * Template Inserter Component. + */ + +#amp-story-controls .editor-inserter__amp-inserter { + height: 48px; + width: 48px; + background-color: #0085BA; + border-radius: 50%; + margin-right: 10px; +} + +#amp-story-controls .editor-inserter__amp-inserter span:first-of-type, +.amp-story-controls-reorder span:first-of-type { + display: none; +} + +#amp-story-controls .editor-inserter__amp-inserter svg { + margin: 0 auto; +} + +.amp-story-controls-reorder { + box-sizing: border-box; + height: 49px; + width: 49px; + border: 1px solid #AAAEB3; + border-radius: 50%; +} + +.amp-stories__template-inserter__popover.components-popover .components-popover__content:not(.is-mobile) { + width: 386px; + height: 400px; + padding: 2px; + transform: translateX(-90%); +} +.components-popover:not(.is-without-arrow):not(.is-mobile).is-top::before { + border: 8px solid #e2e4e7; +} +.components-popover:not(.is-without-arrow):not(.is-mobile).is-top::after { + border: 8px solid #fff; +} +.components-popover:not(.is-without-arrow):not(.is-mobile).is-top::before, +.components-popover:not(.is-without-arrow):not(.is-mobile).is-bottom::before, +.components-popover:not(.is-without-arrow):not(.is-mobile).is-top::after, +.components-popover:not(.is-without-arrow):not(.is-mobile).is-bottom::after { + border-bottom-style: solid; + border-left-color: transparent; + border-right-color: transparent; + border-top: none; + margin-left: -15px; +} + +@media (min-width: 782px) { + .amp-stories__template-inserter__popover.block-editor-inserter__popover:not(.is-mobile) > .components-popover__content { + overflow-y: scroll; + } +} + +@media (min-width: 600px) { + .amp-stories__template-inserter__popover .block-editor-block-list__block .block-editor-block-list__block-edit { + margin: 0; + } +} + +.edit-post-layout:not(.is-sidebar-opened) .amp-stories__template-inserter__popover.components-popover .components-popover__content:not(.is-mobile) { + left: initial; + transform: none; +} + +.amp-stories__template-inserter__popover.block-editor-inserter__popover .block-editor-block-types-list { + margin: 0; + padding: 5px; +} + +.amp-stories__template-inserter__popover.components-popover.is-top .components-popover__content { + bottom: initial; +} +.amp-stories__template-inserter__popover.components-popover:not(.is-without-arrow):not(.is-mobile).is-top { + margin-top: 55px; +} + +.amp-stories__editor-inserter__results li { + display: block; + list-style-type: none; + width: 160px; + height: 268px; + float: left; + margin: 15px; +} + +.amp-stories__editor-inserter__results li amp-story-page, +.amp-stories__editor-inserter__results li amp-story-grid-layer { + display: block; + width: 100%; + height: 100%; +} + +.amp-stories__editor-inserter__results .block-editor-block-preview { + width: 160px; + height: 268px; + margin: 10px; + padding: 0; +} + +.amp-stories__editor-inserter__results .block-editor-block-preview .block-editor-block-preview__content, +.amo-stories__editor-inserter__results .components-placeholder { + padding: 0; + width: 100%; + height: 100%; +} + +.amp-stories__blank-page-inserter { + height: 100%; + width: 100%; +} + +.amp-stories__blank-page-inserter svg { + margin: 0 auto; +} + +.amp-stories__editor-inserter__results .block-editor-block-preview { + pointer-events: initial; +} + +.amp-stories__editor-inserter__results .block-list-appender, +.amo-stories__editor-inserter__results .editor-block-list__insertion-point { + display: none; +} + +.amp-stories__editor-inserter__results .block-editor-block-preview .block-editor-block-preview__content .components-disabled { + transform: initial; + text-align: initial; +} +.amp-stories__editor-inserter__results .components-disabled, +.amp-stories__editor-inserter__results .components-disabled div:first-of-type, +.amp-stories__editor-inserter__results .components-disabled .editor-inner-blocks { + padding: 0; + height: 100%; + width: 100%; +} + +.amp-stories__editor-inserter__results .block-editor-block-preview .wp-block { + transform: scale(0.48) translateX(-40%) translateY(-40%); +} + +.amp-stories__editor-inserter__results .block-editor-block-types-list { + padding: 10px; +} + /* * Preview Picker component * diff --git a/assets/images/add-template.svg b/assets/images/add-template.svg new file mode 100644 index 00000000000..34e0931fc66 --- /dev/null +++ b/assets/images/add-template.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/images/amp-story-page-icon.svg b/assets/images/amp-story-page-icon.svg new file mode 100644 index 00000000000..962789c0621 --- /dev/null +++ b/assets/images/amp-story-page-icon.svg @@ -0,0 +1 @@ + diff --git a/assets/images/reorder.svg b/assets/images/reorder.svg new file mode 100644 index 00000000000..a6786eaef01 --- /dev/null +++ b/assets/images/reorder.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/src/amp-story-editor-blocks.js b/assets/src/amp-story-editor-blocks.js index 699a3fff353..17980ba27c7 100644 --- a/assets/src/amp-story-editor-blocks.js +++ b/assets/src/amp-story-editor-blocks.js @@ -47,6 +47,10 @@ import { ALLOWED_BLOCKS, ALLOWED_TOP_LEVEL_BLOCKS, ALLOWED_CHILD_BLOCKS, MEDIA_I import store from './stores/amp-story'; import { registerPlugin } from '@wordpress/plugins'; +// Register plugin. +// @todo Consider importing automatically, especially in case of more plugins. +import './plugins/template-menu-item'; + const { getSelectedBlockClientId, getBlocksByClientId, diff --git a/assets/src/blocks/amp-story-text/index.js b/assets/src/blocks/amp-story-text/index.js index 6af30639ee2..7a3a0b84047 100644 --- a/assets/src/blocks/amp-story-text/index.js +++ b/assets/src/blocks/amp-story-text/index.js @@ -26,6 +26,7 @@ export const name = 'amp/amp-story-text'; const supports = { className: false, anchor: true, + reusable: true, }; const schema = { diff --git a/assets/src/blocks/amp-story/index.js b/assets/src/blocks/amp-story/index.js index 11dde10bb82..a3110f24d4e 100644 --- a/assets/src/blocks/amp-story/index.js +++ b/assets/src/blocks/amp-story/index.js @@ -7,8 +7,9 @@ import { InnerBlocks } from '@wordpress/block-editor'; /** * Internal dependencies */ -import { BLOCK_ICONS, IMAGE_BACKGROUND_TYPE, VIDEO_BACKGROUND_TYPE } from '../../constants'; +import { IMAGE_BACKGROUND_TYPE, VIDEO_BACKGROUND_TYPE } from '../../constants'; import EditPage from './edit'; +import blockIcon from '../../../images/amp-story-page-icon.svg'; export const name = 'amp/amp-story-page'; @@ -53,9 +54,13 @@ const schema = { export const settings = { title: __( 'Page', 'amp' ), category: 'layout', - icon: BLOCK_ICONS[ 'amp/amp-story-page' ], + icon: blockIcon( { width: 24, height: 24 } ), attributes: schema, + supports: { + reusable: true, + }, + /* * : * mandatory_parent: "AMP-STORY" diff --git a/assets/src/components/editor-carousel/block-preview.js b/assets/src/components/block-preview.js similarity index 67% rename from assets/src/components/editor-carousel/block-preview.js rename to assets/src/components/block-preview.js index cfa88cc255a..5694b287d05 100644 --- a/assets/src/components/editor-carousel/block-preview.js +++ b/assets/src/components/block-preview.js @@ -6,14 +6,16 @@ import { Disabled } from '@wordpress/components'; import { BlockEdit } from '@wordpress/block-editor'; /** - * BlockPreview component that is used within the reordering UI. + * Block Preview Component: It renders a preview given a block name and attributes. * - * @return {Object} Block preview. + * @param {Object} props Component props. + * + * @return {WPElement} Rendered element. */ -const BlockPreview = ( { clientId, name, attributes, innerBlocks } ) => { +const BlockPreview = ( { clientId, name, attributes, innerBlocks = [] } ) => { const block = createBlock( name, attributes, innerBlocks ); return ( - + - ( - - ) } - /> + diff --git a/assets/src/components/template-inserter/index.js b/assets/src/components/template-inserter/index.js new file mode 100644 index 00000000000..14651fd651d --- /dev/null +++ b/assets/src/components/template-inserter/index.js @@ -0,0 +1,158 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { Dropdown, IconButton, Button } from '@wordpress/components'; +import { Component } from '@wordpress/element'; +import { withSelect, withDispatch } from '@wordpress/data'; +import { createBlock, cloneBlock } from '@wordpress/blocks'; +import { compose } from '@wordpress/compose'; + +/** + * Internal dependencies + */ +import { BlockPreview } from '../'; +import pageIcon from '../../../images/add-page-inserter.svg'; +import addTemplateIcon from '../../../images/add-template.svg'; + +const storyPageBlockName = 'amp/amp-story-page'; + +class TemplateInserter extends Component { + constructor() { + super( ...arguments ); + + this.onToggle = this.onToggle.bind( this ); + + this.state = { + reusableBlocks: [], + }; + } + + componentDidMount() { + this.props.fetchReusableBlocks(); + } + + componentDidUpdate( prevProps ) { + // This check is needed to make sure that the blocks are loaded in time. + if ( prevProps.reusableBlocks !== this.props.reusableBlocks || prevProps.allBlocks !== this.props.allBlocks ) { + this.setState( { + reusableBlocks: this.props.reusableBlocks, + } ); + } + } + + onToggle( isOpen ) { + const { onToggle } = this.props; + + // Surface toggle callback to parent component + if ( onToggle ) { + onToggle( isOpen ); + } + } + + render() { + const { insertBlock, getBlock } = this.props; + return ( + ( + + ) } + renderContent={ ( { onClose } ) => { + const isStoryBlock = ( clientId ) => { + const block = getBlock( clientId ); + return block && storyPageBlockName === block.name; + }; + + const onSelect = ( item ) => { + const block = ! item ? createBlock( storyPageBlockName ) : getBlock( item.clientId ); + onClose(); + // Clone block to avoid duplicate ID-s. + insertBlock( cloneBlock( block ) ); + }; + + const storyTemplates = this.state.reusableBlocks.filter( ( { clientId } ) => isStoryBlock( clientId ) ); + + return ( + + + + + { + onSelect( null ); + } } + className="amp-stories__blank-page-inserter editor-block-preview__content block-editor-block-preview__content editor-styles-wrapper" + /> + + { storyTemplates && storyTemplates.map( ( item ) => ( + { + onSelect( item ); + } } + className="components-button block-editor-block-preview" + > + + + ) ) } + + + + ); + } } + /> + ); + } +} + +export default compose( + withSelect( ( select ) => { + const { + __experimentalGetReusableBlocks: getReusableBlocks, + } = select( 'core/editor' ); + + const { + getBlock, + getBlocks, + } = select( 'core/block-editor' ); + + return { + reusableBlocks: getReusableBlocks(), + getBlock, + allBlocks: getBlocks(), + }; + } ), + withDispatch( ( dispatch ) => { + const { + __experimentalFetchReusableBlocks: fetchReusableBlocks, + } = dispatch( 'core/editor' ); + + const { insertBlock } = dispatch( 'core/block-editor' ); + + return { + fetchReusableBlocks, + insertBlock, + }; + } ) +)( TemplateInserter ); diff --git a/assets/src/constants.js b/assets/src/constants.js index 690755c29e7..9a067f844f0 100644 --- a/assets/src/constants.js +++ b/assets/src/constants.js @@ -35,6 +35,8 @@ export const STORY_PAGE_WIDTH = STORY_PAGE_INNER_WIDTH + storyPageBorderWidth; export const ALLOWED_TOP_LEVEL_BLOCKS = [ 'amp/amp-story-page', + 'core/block', // Reusable blocks. + 'core/template', // Reusable blocks. ]; export const ALLOWED_CHILD_BLOCKS = [ @@ -49,6 +51,8 @@ export const ALLOWED_CHILD_BLOCKS = [ 'core/verse', 'core/video', 'amp/amp-story-text', + 'core/block', // Reusable blocks. + 'core/template', // Reusable blocks. ]; export const ALLOWED_BLOCKS = [ @@ -62,10 +66,6 @@ export const ALLOWED_MEDIA_TYPES = [ 'image', 'video' ]; export const POSTER_ALLOWED_MEDIA_TYPES = [ 'image' ]; export const MEDIA_INNER_BLOCKS = [ 'core/video', 'core/audio' ]; -export const BLOCK_ICONS = { - 'amp/amp-story-page': , -}; - export const ANIMATION_DURATION_DEFAULTS = { drop: 1600, 'fade-in': 500, diff --git a/assets/src/plugins/template-menu-item.js b/assets/src/plugins/template-menu-item.js new file mode 100644 index 00000000000..a8a7bace5c7 --- /dev/null +++ b/assets/src/plugins/template-menu-item.js @@ -0,0 +1,65 @@ +/** + * WordPress dependencies + */ +import { registerPlugin } from '@wordpress/plugins'; +import { PluginBlockSettingsMenuItem } from '@wordpress/edit-post'; +import { __ } from '@wordpress/i18n'; +import { select, dispatch } from '@wordpress/data'; +import { cloneBlock } from '@wordpress/blocks'; + +/** + * External dependencies + */ +import { uniqueId } from 'lodash'; + +const addTemplate = () => { + const { getSelectedBlockClientId, getBlock } = select( 'core/block-editor' ); + const { + __experimentalReceiveReusableBlocks: receiveReusableBlocks, + __experimentalSaveReusableBlock: saveReusableBlock, + } = dispatch( 'core/editor' ); + + // @todo Allow multi-page templates. + const parsedBlock = getBlock( getSelectedBlockClientId() ); + if ( 'amp/amp-story-page' !== parsedBlock.name ) { + return; + } + + // Clone for having a different ID. + const templateBlock = cloneBlock( parsedBlock ); + + // @todo Allow choosing name for the template. + const reusableBlock = { + id: uniqueId( 'reusable' ), + clientId: templateBlock.clientId, + title: __( 'Template', 'amp' ), + }; + + receiveReusableBlocks( [ { + reusableBlock, + parsedBlock: templateBlock, + } ] ); + + // @todo Display notice. + saveReusableBlock( reusableBlock.id ); +}; + +/** + * Register plugin for adding Templates (Reusable blocks) without converting the page itself to a reusable block. + */ +export function registerTemplateSaveMenuItem() { + // @todo Change icon. + registerPlugin( 'amp-story', { + render: ( ) => ( + + ), + } ); +} + +registerTemplateSaveMenuItem();