diff --git a/assets/css/amp-editor-story-blocks.css b/assets/css/amp-editor-story-blocks.css index 4955adf229f..c671b28f6d0 100644 --- a/assets/css/amp-editor-story-blocks.css +++ b/assets/css/amp-editor-story-blocks.css @@ -18,6 +18,7 @@ 8. Block mover 9. General block editor components 10. Custom Components + 11. Text Block 100. Shame 100.1 Gutenberg - Warning div not clickable @@ -670,6 +671,11 @@ div[data-type="amp/amp-story-page"] .editor-inner-blocks .editor-block-list__lay left: -100px; top: -50px; border: none; + background: transparent; +} + +.editor-block-list__layout div[data-type="amp/amp-story-page"][data-amp-selected="parent"] > .editor-block-drop-zone .components-drop-zone__content { + display: none; } div[data-type="amp/amp-story-page"] .wp-block-image { @@ -768,11 +774,6 @@ div[data-type="amp/amp-story-page"] .wp-block-image { * Custom block mover. */ -.post-type-amp_story .editor-block-list__layout .editor-block-mover.is-visible, -.post-type-amp_story .editor-block-list__layout .editor-block-mover { - display: none; -} - .amp-story-editor-block-mover { position: absolute; width: 30px; @@ -793,6 +794,48 @@ div[data-type="amp/amp-story-page"] .wp-block:hover .amp-story-editor-block-move opacity: 1; } +/** + * 11. Text Block. + */ +.amp-story-text__resize-container .components-resizable-box__handle-right { + right: -26px; + height: 50px; + top: calc(50% - 25px); +} +.amp-story-text__resize-container .components-resizable-box__handle-right::before { + margin: 14px 0; +} +.amp-story-text__resize-container .components-resizable-box__handle-bottom { + bottom: -26px; + width: 50px; + left: calc(50% - 25px); +} +.amp-story-text__resize-container .components-resizable-box__handle-bottom::before { + margin: 0 auto; +} + +.editor-styles-wrapper #amp-story-editor .wp-block .wp-block[data-type="amp/amp-story-text"] { + width: initial; +} + +.wp-block[data-type="amp/amp-story-text"] .components-resizable-box__handle, +.wp-block.is-typing[data-type="amp/amp-story-text"] .components-resizable-box__handle{ + display: block; + opacity: 0; + transition: opacity .3s; +} + +.wp-block[data-type="amp/amp-story-text"]:hover .components-resizable-box__handle, +.wp-block[data-type="amp/amp-story-text"] .components-resizable-box__handle:hover { + opacity: 100; +} + +.wp-block[data-type="amp/amp-story-text"] .block-editor-rich-text__editable, +div[data-type="amp/amp-story-page"] .block-editor-inner-blocks .block-editor-block-list__block-edit +{ + display: inline-block; +} + /* * 100. Shame */ diff --git a/assets/css/amp-stories.css b/assets/css/amp-stories.css index 80aed1e0e76..2a18ad5a814 100644 --- a/assets/css/amp-stories.css +++ b/assets/css/amp-stories.css @@ -3,7 +3,3 @@ amp-story-grid-layer[template="fill"] .wp-block-image { margin: 0; } .has-background { padding: 10px !important; } - -amp-story-grid-layer > * { - width: auto !important; -} diff --git a/assets/src/blocks/amp-story-text/edit.js b/assets/src/blocks/amp-story-text/edit.js index c9ea0abc1d8..ab3cde265c5 100644 --- a/assets/src/blocks/amp-story-text/edit.js +++ b/assets/src/blocks/amp-story-text/edit.js @@ -7,8 +7,14 @@ import classnames from 'classnames'; * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { Fragment } from '@wordpress/element'; -import { PanelBody, SelectControl, withFallbackStyles } from '@wordpress/components'; +import { Component, Fragment } from '@wordpress/element'; +import { + PanelBody, + ResizableBox, + SelectControl, + withFallbackStyles, + ToggleControl, +} from '@wordpress/components'; import { compose } from '@wordpress/compose'; import { RichText, @@ -24,10 +30,13 @@ import { * Internal dependencies */ import { FontFamilyPicker } from '../../components'; -import { maybeEnqueueFontStyle } from '../../helpers'; +import { maybeEnqueueFontStyle, calculateFontSize } from '../../helpers'; const { getComputedStyle } = window; +const maxLimitFontSize = 54; +const minLimitFontSize = 14; + const applyFallbackStyles = withFallbackStyles( ( node, ownProps ) => { const { textColor, backgroundColor, fontSize, customFontSize } = ownProps.attributes; const editableNode = node.querySelector( '[contenteditable="true"]' ); @@ -40,108 +49,178 @@ const applyFallbackStyles = withFallbackStyles( ( node, ownProps ) => { }; } ); -function TextBlock( props ) { - const { - attributes, - setAttributes, - className, - fontSize, - setFontSize, - backgroundColor, - textColor, - setBackgroundColor, - setTextColor, - fallbackTextColor, - fallbackBackgroundColor, - } = props; +class TextBlockEdit extends Component { + componentDidUpdate() { + const { attributes, isSelected, setAttributes } = this.props; + const { + height, + width, + autoFontSize, + ampFitText, + } = attributes; - const { - placeholder, - content, - type, - ampFontFamily, - } = attributes; + if ( isSelected && ampFitText ) { + // Check if the font size is OK, if not, update the font size if not. + const element = document.querySelector( `#block-${ this.props.clientId } .block-editor-rich-text__editable` ); + if ( element ) { + const fitFontSize = calculateFontSize( element, height, width, maxLimitFontSize, minLimitFontSize ); + if ( autoFontSize !== fitFontSize ) { + setAttributes( { autoFontSize: fitFontSize } ); + } + } + } + } - return ( - - - - { - maybeEnqueueFontStyle( value ); - setAttributes( { ampFontFamily: value } ); - } } - /> - - setAttributes( { type: selected } ) } - options={ [ - { value: 'auto', label: __( 'Automatic', 'amp' ) }, - { value: 'p', label: __( 'Paragraph', 'amp' ) }, - { value: 'h1', label: __( 'Heading 1', 'amp' ) }, - { value: 'h2', label: __( 'Heading 2', 'amp' ) }, + render() { + const { + attributes, + setAttributes, + className, + fontSize, + isSelected, + setFontSize, + backgroundColor, + textColor, + setBackgroundColor, + setTextColor, + fallbackTextColor, + fallbackBackgroundColor, + toggleSelection, + } = this.props; + + const { + placeholder, + content, + type, + ampFontFamily, + ampFitText, + autoFontSize, + height, + width, + } = attributes; + + const minTextHeight = 20; + const minTextWidth = 30; + const userFontSize = fontSize.size ? fontSize.size + 'px' : undefined; + + return ( + + + + { + maybeEnqueueFontStyle( value ); + setAttributes( { ampFontFamily: value } ); + } } + /> + ( setAttributes( { ampFitText: ! ampFitText } ) ) } + /> + { ! ampFitText && ( + + ) } + setAttributes( { type: selected } ) } + options={ [ + { value: 'auto', label: __( 'Automatic', 'amp' ) }, + { value: 'p', label: __( 'Paragraph', 'amp' ) }, + { value: 'h1', label: __( 'Heading 1', 'amp' ) }, + { value: 'h2', label: __( 'Heading 2', 'amp' ) }, + ] } + /> + + - - + + + + { + setAttributes( { + width: parseInt( width + delta.width, 10 ), + height: parseInt( height + delta.height, 10 ), + } ); + toggleSelection( true ); + } } + onResizeStart={ () => { + toggleSelection( false ); + } } > - setAttributes( { content: value } ) } + style={ { backgroundColor: backgroundColor.color, - fallbackTextColor, - fallbackBackgroundColor, - fontSize: fontSize.size, + color: textColor.color, + fontSize: ampFitText ? autoFontSize : userFontSize, } } + className={ classnames( className, { + 'has-text-color': textColor.color, + 'has-background': backgroundColor.color, + [ backgroundColor.class ]: backgroundColor.class, + [ textColor.class ]: textColor.class, + [ fontSize.class ]: autoFontSize ? undefined : fontSize.class, + } ) } + placeholder={ placeholder || __( 'Write text…', 'amp' ) } /> - - - setAttributes( { content: value } ) } - style={ { - backgroundColor: backgroundColor.color, - color: textColor.color, - fontSize: fontSize.size ? fontSize.size + 'px' : undefined, - } } - className={ classnames( className, { - 'has-text-color': textColor.color, - 'has-background': backgroundColor.color, - [ backgroundColor.class ]: backgroundColor.class, - [ textColor.class ]: textColor.class, - [ fontSize.class ]: fontSize.class, - } ) } - placeholder={ placeholder || __( 'Write text…', 'amp' ) } - /> - - ); + + + ); + } } export default compose( withColors( 'backgroundColor', { textColor: 'color' } ), withFontSizes( 'fontSize' ), applyFallbackStyles -)( TextBlock ); +)( TextBlockEdit ); diff --git a/assets/src/blocks/amp-story-text/index.js b/assets/src/blocks/amp-story-text/index.js index 7c3398f02aa..6ea420cd0bf 100644 --- a/assets/src/blocks/amp-story-text/index.js +++ b/assets/src/blocks/amp-story-text/index.js @@ -18,6 +18,7 @@ import { registerBlockType } from '@wordpress/blocks'; * Internal dependencies */ import edit from './edit'; +import { getPercentageFromPixels } from '../../helpers'; export const name = 'amp/amp-story-text'; @@ -33,7 +34,7 @@ const schema = { content: { type: 'string', source: 'html', - selector: 'p,h1,h2', + selector: '.amp-text-content', default: '', }, type: { @@ -50,6 +51,13 @@ const schema = { customFontSize: { type: 'number', }, + autoFontSize: { + type: 'number', + }, + ampFitText: { + type: 'boolean', + default: true, + }, ampFontFamily: { type: 'string', }, @@ -65,6 +73,14 @@ const schema = { customBackgroundColor: { type: 'string', }, + height: { + default: 50, + type: 'number', + }, + width: { + default: 250, + type: 'number', + }, }; export const settings = { @@ -93,10 +109,14 @@ export const settings = { content, fontSize, customFontSize, + ampFitText, + autoFontSize, backgroundColor, textColor, customBackgroundColor, customTextColor, + width, + height, tagName, } = attributes; @@ -105,26 +125,43 @@ export const settings = { const fontSizeClass = getFontSizeClass( fontSize ); const className = classnames( { + 'amp-text-content': ! ampFitText, 'has-text-color': textColor || customTextColor, 'has-background': backgroundColor || customBackgroundColor, - [ fontSizeClass ]: fontSizeClass, + [ fontSizeClass ]: ampFitText ? undefined : fontSizeClass, [ textClass ]: textClass, [ backgroundClass ]: backgroundClass, } ); + const userFontSize = fontSizeClass ? undefined : customFontSize; + const styles = { backgroundColor: backgroundClass ? undefined : customBackgroundColor, color: textClass ? undefined : customTextColor, - fontSize: fontSizeClass ? undefined : customFontSize, + fontSize: ampFitText ? autoFontSize : userFontSize, + width: `${ getPercentageFromPixels( 'x', width ) }%`, + height: `${ getPercentageFromPixels( 'y', height ) }%`, }; + if ( ! ampFitText ) { + return ( + + ); + } + + const ContentTag = tagName; + return ( - + className={ className }> + { content } + ); }, }; diff --git a/assets/src/components/block-mover/drag-handle.js b/assets/src/components/block-mover/drag-handle.js index 7519317d93d..f0c5c4920b9 100644 --- a/assets/src/components/block-mover/drag-handle.js +++ b/assets/src/components/block-mover/drag-handle.js @@ -1,7 +1,3 @@ -/** - * This file is copied from core. - */ - /** * External dependencies */ @@ -17,7 +13,8 @@ export const IconDragHandle = ( { isVisible, className, icon, onDragStart, onDra return null; } - const dragHandleClassNames = classnames( 'editor-block-mover__control-drag-handle', className ); + const dragHandleClassNames = classnames( 'editor-block-mover__control-drag-handle block-editor-block-mover__control-drag-handle', className ); + return ( -
+
- - + ); } } diff --git a/assets/src/helpers.js b/assets/src/helpers.js index be1d3572cd9..da5f0cf88d3 100644 --- a/assets/src/helpers.js +++ b/assets/src/helpers.js @@ -14,7 +14,13 @@ import { _x } from '@wordpress/i18n'; /** * Internal dependencies */ -import { ALLOWED_CHILD_BLOCKS, ALLOWED_TOP_LEVEL_BLOCKS, BLOCK_TAG_MAPPING } from './constants'; +import { + ALLOWED_CHILD_BLOCKS, + ALLOWED_TOP_LEVEL_BLOCKS, + BLOCK_TAG_MAPPING, + STORY_PAGE_INNER_WIDTH, + STORY_PAGE_INNER_HEIGHT, +} from './constants'; export const maybeEnqueueFontStyle = ( name ) => { if ( ! name || 'undefined' === typeof ampStoriesFonts ) { @@ -275,3 +281,49 @@ export const getTagName = ( attributes, canUseH1 ) => { return 'p'; }; + +/** + * Calculates font size that fits to the text element based on the element's size. + * Replicates amp-fit-text's logic in the editor. + * + * @see https://github.com/ampproject/amphtml/blob/e7a1b3ff97645ec0ec482192205134bd0735943c/extensions/amp-fit-text/0.1/amp-fit-text.js + * + * @param {Object} measurer HTML element. + * @param {number} expectedHeight Maximum height. + * @param {number} expectedWidth Maximum width. + * @param {number} maxFontSize Maximum font size. + * @param {number} minFontSize Minimum font size. + * @return {number} Calculated font size. + */ +export const calculateFontSize = ( measurer, expectedHeight, expectedWidth, maxFontSize, minFontSize ) => { + maxFontSize++; + // Binomial search for the best font size. + while ( maxFontSize - minFontSize > 1 ) { + const mid = Math.floor( ( minFontSize + maxFontSize ) / 2 ); + measurer.style.fontSize = mid + 'px'; + const currentHeight = measurer.offsetHeight; + const currentWidth = measurer.offsetWidth; + if ( currentHeight > expectedHeight || currentWidth > expectedWidth ) { + maxFontSize = mid; + } else { + minFontSize = mid; + } + } + return minFontSize; +}; + +/** + * Get percentage of a distance compared to the full width / height of the page. + * + * @param {string} axis X or Y axis. + * @param {number} pixelValue Value in pixels. + * @return {number} Value in percentage. + */ +export const getPercentageFromPixels = ( axis, pixelValue ) => { + if ( 'x' === axis ) { + return Math.round( ( pixelValue / STORY_PAGE_INNER_WIDTH ) * 100 ); + } else if ( 'y' === axis ) { + return Math.round( ( pixelValue / STORY_PAGE_INNER_HEIGHT ) * 100 ); + } + return 0; +};