diff --git a/assets/src/stories-editor/blocks/amp-story-cta/edit.css b/assets/src/stories-editor/blocks/amp-story-cta/edit.css index 1f86319cff7..3c29d1f23bd 100644 --- a/assets/src/stories-editor/blocks/amp-story-cta/edit.css +++ b/assets/src/stories-editor/blocks/amp-story-cta/edit.css @@ -44,7 +44,7 @@ div[data-type="amp/amp-story-cta"] .amp-overlay { --cta-margin: 3px; height: calc(100% + calc(var(--cta-margin) * 2)); width: 100%; - bottom: calc(var(--cta-margin) * -1); + bottom: var(--cta-margin); } .block-editor-block-list__layout .block-editor-block-list__block[data-type="amp/amp-story-cta"] .block-editor-block-list__block-edit::before { diff --git a/assets/src/stories-editor/blocks/amp-story-cta/edit.js b/assets/src/stories-editor/blocks/amp-story-cta/edit.js index 0e852144a49..cff8e283165 100644 --- a/assets/src/stories-editor/blocks/amp-story-cta/edit.js +++ b/assets/src/stories-editor/blocks/amp-story-cta/edit.js @@ -8,7 +8,7 @@ import PropTypes from 'prop-types'; * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { Component, RawHTML } from '@wordpress/element'; +import { Component } from '@wordpress/element'; import { Dashicon, IconButton, @@ -23,9 +23,9 @@ import { */ import './edit.css'; import { select } from '@wordpress/data'; -import { getUniqueId } from '../../helpers'; +import { getUniqueId, setInputSelectionToEnd } from '../../helpers'; import { getBackgroundColorWithOpacity } from '../../../common/helpers'; -import { StoryBlockMover } from '../../components'; +import { DraggableText } from '../../components'; class CallToActionEdit extends Component { constructor( props ) { @@ -78,16 +78,7 @@ class CallToActionEdit extends Component { this.toggleOverlay( true ); } if ( this.state.isEditing && ! prevState.isEditing ) { - const textInput = document.querySelector( '.is-selected .amp-block-story-cta__link' ); - if ( textInput ) { - // Create selection, collapse it in the end of the content. - const range = document.createRange(); - range.selectNodeContents( textInput ); - range.collapse( false ); - const selection = window.getSelection(); - selection.removeAllRanges(); - selection.addRange( range ); - } + setInputSelectionToEnd( '.is-selected .amp-block-story-cta__link' ); } } @@ -132,81 +123,49 @@ class CallToActionEdit extends Component { }; return ( <> - -
-
- { isEditing && ( - setAttributes( { text: value } ) } - className={ textWrapperClass } - style={ textStyle } - /> - ) } - { /* eslint-disable-next-line jsx-a11y/click-events-have-key-events */ } - { ! isEditing &&
{ - if ( isSelected ) { - this.toggleIsEditing( true ); - } - } } - onMouseDown={ ( event ) => { - // Prevent text selection on double click. - if ( 1 < event.detail ) { - event.preventDefault(); - } - } } - > - { /* eslint-disable-next-line jsx-a11y/click-events-have-key-events */ } - { hasOverlay && (
{ - this.toggleOverlay( false ); - e.stopPropagation(); - } } - >
- ) } -
- { text && text.length ? - { text } : ( - - { placeholder } - - ) } -
-
- } -
- { isSelected && ( -
event.preventDefault() }> - - setAttributes( { url: value } ) } - autoFocus={ false /* eslint-disable-line jsx-a11y/no-autofocus */ } - /> - - +
+
+ { isEditing && ( + setAttributes( { text: value } ) } + className={ textWrapperClass } + style={ textStyle } + /> ) } + { ! isEditing && + + }
- + { isSelected && isEditing && ( +
event.preventDefault() }> + + setAttributes( { url: value } ) } + autoFocus={ false /* eslint-disable-line jsx-a11y/no-autofocus */ } + /> + + + ) } +
); } diff --git a/assets/src/stories-editor/blocks/amp-story-text/edit.css b/assets/src/stories-editor/blocks/amp-story-text/edit.css index 52fd04d190d..293dd6f5371 100644 --- a/assets/src/stories-editor/blocks/amp-story-text/edit.css +++ b/assets/src/stories-editor/blocks/amp-story-text/edit.css @@ -10,8 +10,32 @@ width: 100%; } +.is-not-editing .wp-block-amp-amp-story-text.block-editor-rich-text__editable.editor-rich-text__editable { + cursor: grab; +} + +.wp-block-amp-story-text-wrapper.is-empty-draggable-text, +.wp-block-amp-story-text-wrapper:not(.with-line-height) { + height: 100%; +} + +.wp-block-amp-story-text-wrapper .amp-overlay { + height: 100%; + width: 100%; + z-index: 1000; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; +} + +.wp-block-amp-story-text-wrapper.is-empty-draggable-text .amp-text-placeholder { + opacity: 0.62; +} + .wp-block[data-type="amp/amp-story-text"] .amp-story-resize-container .components-resizable-box__handle-right { - right: -10px; + right: -13px; } .wp-block[data-type="amp/amp-story-text"] .amp-story-resize-container .components-resizable-box__handle-right::before { @@ -19,7 +43,6 @@ } .wp-block[data-type="amp/amp-story-text"] .amp-story-resize-container .components-resizable-box__handle-bottom { - bottom: -8px; width: 50px; left: calc(50% - 25px); } @@ -67,14 +90,6 @@ div[data-type="amp/amp-story-page"] .block-editor-inner-blocks .block-editor-blo .editor-block-list__layout .wp-block[data-type="amp/amp-story-text"] .editor-block-list__block-edit::before { position: absolute; - top: 2px; - left: 2px; - right: 2px; - bottom: 2px; -} - -.wp-block[data-type="amp/amp-story-text"] div[draggable="true"] { - border: 5px solid transparent; } .wp-block[data-type="amp/amp-story-text"] .amp-story-editor__rotate-container, diff --git a/assets/src/stories-editor/blocks/amp-story-text/edit.js b/assets/src/stories-editor/blocks/amp-story-text/edit.js index d4fb8871272..86a39900f21 100644 --- a/assets/src/stories-editor/blocks/amp-story-text/edit.js +++ b/assets/src/stories-editor/blocks/amp-story-text/edit.js @@ -20,23 +20,31 @@ import { select } from '@wordpress/data'; /** * Internal dependencies */ -import { maybeUpdateFontSize, maybeUpdateBlockDimensions } from '../../helpers'; +import { maybeUpdateFontSize, maybeUpdateBlockDimensions, setInputSelectionToEnd } from '../../helpers'; import { getBackgroundColorWithOpacity } from '../../../common/helpers'; import './edit.css'; +import { DraggableText } from '../../components'; class TextBlockEdit extends Component { constructor( ...args ) { super( ...args ); + this.state = { + isEditing: false, + hasOverlay: true, + }; + this.onReplace = this.onReplace.bind( this ); + this.toggleIsEditing = this.toggleIsEditing.bind( this ); + this.toggleOverlay = this.toggleOverlay.bind( this ); } componentDidMount() { maybeUpdateFontSize( this.props ); } - componentDidUpdate( prevProps ) { - const { attributes, fontSize } = this.props; + componentDidUpdate( prevProps, prevState ) { + const { attributes, fontSize, isSelected } = this.props; const { height, width, @@ -45,6 +53,12 @@ class TextBlockEdit extends Component { ampFontFamily, } = attributes; + // If the block was unselected, make sure that it's not editing anymore. + if ( ! isSelected && prevProps.isSelected ) { + this.toggleIsEditing( false ); + this.toggleOverlay( true ); + } + const checkFontSize = ampFitText && ( prevProps.attributes.ampFitText !== ampFitText || prevProps.attributes.ampFontFamily !== ampFontFamily || @@ -67,6 +81,11 @@ class TextBlockEdit extends Component { if ( checkBlockDimensions ) { maybeUpdateBlockDimensions( this.props ); } + + // If the state changed to editing, focus on the text. + if ( this.state.isEditing && ! prevState.isEditing ) { + setInputSelectionToEnd( '.is-selected .wp-block-amp-amp-story-text' ); + } } onReplace( blocks ) { @@ -88,15 +107,37 @@ class TextBlockEdit extends Component { ) ) ); } + toggleIsEditing( enable ) { + if ( enable !== this.state.isEditing ) { + this.setState( { + isEditing: ! this.state.isEditing, + } ); + } + } + + toggleOverlay( add ) { + if ( add !== this.state.hasOverlay ) { + this.setState( { + hasOverlay: ! this.state.hasOverlay, + } ); + } + } + render() { + const { isEditing, hasOverlay } = this.state; + const { attributes, setAttributes, className, + clientId, fontSize, + isPartOfMultiSelection, + isSelected, backgroundColor, customBackgroundColor, textColor, + name, } = this.props; const { @@ -136,6 +177,21 @@ class TextBlockEdit extends Component { wrapperClass += ' ' + styleClasses.join( ' ' ); } + const textWrapperClassName = 'wp-block-amp-story-text'; + const textClassNames = { + 'has-text-color': textColor.color, + [ textColor.class ]: textColor.class, + [ fontSize.class ]: ampFitText ? undefined : fontSize.class, + 'is-amp-fit-text': ampFitText, + }; + const textStyle = { + color: textColor.color, + fontSize: ampFitText ? autoFontSize + 'px' : userFontSize, + textAlign: align, + position: ampFitText && content.length ? 'static' : undefined, + }; + + // StoryBlockMover is added here to the Text block since it depends on isEditing state. return ( <> @@ -146,30 +202,41 @@ class TextBlockEdit extends Component {
- setAttributes( { content: nextContent } ) } - // The 2 following lines are necessary for pasting to work. - onReplace={ this.onReplace } - onSplit={ () => {} } - style={ { - color: textColor.color, - fontSize: ampFitText ? autoFontSize + 'px' : userFontSize, - textAlign: align, - position: ampFitText && content.length ? 'static' : undefined, - } } - className={ classnames( className, { - 'has-text-color': textColor.color, - [ textColor.class ]: textColor.class, - [ fontSize.class ]: ampFitText ? undefined : fontSize.class, - 'is-amp-fit-text': ampFitText, - } ) } - placeholder={ placeholder || __( 'Write text…', 'amp' ) } - /> + { isEditing && + setAttributes( { content: nextContent } ) } + // The 2 following lines are necessary for pasting to work. + onReplace={ this.onReplace } + onSplit={ () => {} } + style={ textStyle } + className={ classnames( className, textClassNames ) } + placeholder={ placeholder || __( 'Write text…', 'amp' ) } + /> + } + { ! isEditing && + + }
); @@ -191,6 +258,8 @@ TextBlockEdit.propTypes = { ampFontFamily: PropTypes.string, } ).isRequired, isSelected: PropTypes.bool.isRequired, + clientId: PropTypes.string.isRequired, + isPartOfMultiSelection: PropTypes.bool, onReplace: PropTypes.func.isRequired, name: PropTypes.string.isRequired, setAttributes: PropTypes.func.isRequired, diff --git a/assets/src/stories-editor/components/draggable-text.js b/assets/src/stories-editor/components/draggable-text.js new file mode 100644 index 00000000000..f5a7d9968db --- /dev/null +++ b/assets/src/stories-editor/components/draggable-text.js @@ -0,0 +1,127 @@ +/** + * External dependencies + */ +import PropTypes from 'prop-types'; +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { RawHTML } from '@wordpress/element'; +import { ENTER } from '@wordpress/keycodes'; + +/** + * Internal dependencies + */ +import { StoryBlockMover } from './index'; + +/** + * Draggable Text: a non-editable text content which can be dragged. + * Switches to edit mode when clicked twice. + * + * @param {Object} props Component props. + * + * @return {WPElement} Rendered element. + */ +const DraggableText = ( props ) => { + const { + blockClass, + blockElementId, + clientId, + hasOverlay, + isDraggable, + isSelected, + name, + toggleIsEditing, + toggleOverlay, + text, + textStyle, + textWrapperClass, + placeholder, + } = props; + return ( + +
{ + if ( isSelected ) { + toggleIsEditing( true ); + } + } } + onMouseDown={ ( event ) => { + // Prevent text selection on double click. + if ( 1 < event.detail ) { + event.preventDefault(); + } + } } + onKeyDown={ ( event ) => { + event.stopPropagation(); + if ( ENTER === event.keyCode && isSelected ) { + toggleOverlay( false ); + toggleIsEditing( true ); + } + } } + > + { hasOverlay && (
{ + toggleOverlay( false ); + e.stopPropagation(); + } } + onKeyDown={ ( event ) => { + event.stopPropagation(); + if ( ENTER === event.keyCode && isSelected ) { + toggleOverlay( false ); + toggleIsEditing( true ); + } + } } + >
+ ) } +
+ { text && text.length ? + { text } : ( + + { placeholder } + + ) } +
+
+
+ ); +}; + +DraggableText.propTypes = { + clientId: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + blockClass: PropTypes.string, + blockElementId: PropTypes.string.isRequired, + hasOverlay: PropTypes.bool.isRequired, + isDraggable: PropTypes.bool.isRequired, + isSelected: PropTypes.bool.isRequired, + toggleIsEditing: PropTypes.func.isRequired, + toggleOverlay: PropTypes.func.isRequired, + text: PropTypes.string.isRequired, + textStyle: PropTypes.shape( { + color: PropTypes.string, + fontSize: PropTypes.string, + textAlign: PropTypes.string, + position: PropTypes.string, + } ).isRequired, + textWrapperClass: PropTypes.string.isRequired, + placeholder: PropTypes.string.isRequired, +}; + +export default DraggableText; diff --git a/assets/src/stories-editor/components/higher-order/with-amp-story-settings.js b/assets/src/stories-editor/components/higher-order/with-amp-story-settings.js index c3fb8ac52a7..6176cf40b9c 100644 --- a/assets/src/stories-editor/components/higher-order/with-amp-story-settings.js +++ b/assets/src/stories-editor/components/higher-order/with-amp-story-settings.js @@ -243,7 +243,6 @@ export default createHigherOrderComponent( } const captionAttribute = isVideoBlock ? 'ampShowCaption' : 'ampShowImageCaption'; - return ( <> { ( ! isMovableBlock ) && ( ) } @@ -283,15 +282,20 @@ export default createHigherOrderComponent( snap={ BLOCK_ROTATION_SNAPS } snapGap={ BLOCK_ROTATION_SNAP_GAP } > - + { isTextBlock && ( - + ) } + { ! isTextBlock && ( + + + + ) } ) } diff --git a/assets/src/stories-editor/components/index.js b/assets/src/stories-editor/components/index.js index ff37fba2179..dc1bed21354 100644 --- a/assets/src/stories-editor/components/index.js +++ b/assets/src/stories-editor/components/index.js @@ -3,6 +3,7 @@ export { default as AnimationOrderPicker } from './animation-order-picker'; export { default as BlockNavigation } from './block-navigation'; export { default as BlockPreview } from './block-preview'; export { default as BlockPreviewLabel } from './block-preview-label'; +export { default as DraggableText } from './draggable-text'; export { default as EditorCarousel } from './editor-carousel'; export { default as Reorderer } from './reorderer'; export { default as StoryControls } from './story-controls'; diff --git a/assets/src/stories-editor/components/resizable-box/index.js b/assets/src/stories-editor/components/resizable-box/index.js index 8b5deb52811..ad23fd82c9e 100644 --- a/assets/src/stories-editor/components/resizable-box/index.js +++ b/assets/src/stories-editor/components/resizable-box/index.js @@ -21,7 +21,7 @@ import { getRadianFromDeg, } from '../../helpers'; -import { BLOCKS_WITH_TEXT_SETTINGS, TEXT_BLOCK_BORDER, TEXT_BLOCK_PADDING } from '../../constants'; +import { BLOCKS_WITH_TEXT_SETTINGS, TEXT_BLOCK_PADDING } from '../../constants'; let lastSeenX = 0, lastSeenY = 0, @@ -57,14 +57,6 @@ const EnhancedResizableBox = ( props ) => { const isBlockWithText = BLOCKS_WITH_TEXT_SETTINGS.includes( blockName ) || 'core/code' === blockName; const isText = 'amp/amp-story-text' === blockName; - if ( isText ) { - height += TEXT_BLOCK_BORDER * 2; - width += TEXT_BLOCK_BORDER * 2; - } - - const textBlockBorderInPercentageTop = getPercentageFromPixels( 'y', TEXT_BLOCK_BORDER ); - const textBlockBorderInPercentageLeft = getPercentageFromPixels( 'x', TEXT_BLOCK_BORDER ); - return ( { let appliedWidth = width + deltaW; let appliedHeight = height + deltaH; + // Restore the full height for Text block wrapper. + if ( textBlockWrapper ) { + textBlockWrapper.style.height = '100%'; + } + // Ensure the measures not crossing limits. appliedWidth = appliedWidth < lastWidth ? lastWidth : appliedWidth; appliedHeight = appliedHeight < lastHeight ? lastHeight : appliedHeight; @@ -95,12 +92,12 @@ const EnhancedResizableBox = ( props ) => { const elementTop = parseFloat( blockElement.style.top ); const elementLeft = parseFloat( blockElement.style.left ); - const positionTop = ! isText ? Number( elementTop.toFixed( 2 ) ) : Number( ( elementTop + textBlockBorderInPercentageTop ).toFixed( 2 ) ); - const positionLeft = ! isText ? Number( elementLeft.toFixed( 2 ) ) : Number( ( elementLeft + textBlockBorderInPercentageLeft ).toFixed( 2 ) ); + const positionTop = Number( elementTop.toFixed( 2 ) ); + const positionLeft = Number( elementLeft.toFixed( 2 ) ); onResizeStop( { - width: isText ? parseInt( appliedWidth ) - ( TEXT_BLOCK_BORDER * 2 ) : parseInt( appliedWidth ), - height: isText ? parseInt( appliedHeight ) - ( TEXT_BLOCK_BORDER * 2 ) : parseInt( appliedHeight ), + width: parseInt( appliedWidth ), + height: parseInt( appliedHeight ), positionTop, positionLeft, } ); @@ -174,8 +171,8 @@ const EnhancedResizableBox = ( props ) => { } } - const scrollWidth = isText ? textElement.scrollWidth + ( TEXT_BLOCK_BORDER * 2 ) : textElement.scrollWidth; - const scrollHeight = isText ? textElement.scrollHeight + ( TEXT_BLOCK_BORDER * 2 ) : textElement.scrollHeight; + const scrollWidth = textElement.scrollWidth; + const scrollHeight = textElement.scrollHeight; if ( appliedWidth < scrollWidth || appliedHeight < scrollHeight ) { appliedWidth = lastWidth; appliedHeight = lastHeight; @@ -236,8 +233,12 @@ const EnhancedResizableBox = ( props ) => { lastWidth = appliedWidth; lastHeight = appliedHeight; - if ( textBlockWrapper && ampFitText ) { - textBlockWrapper.style.lineHeight = isText ? appliedHeight - ( TEXT_BLOCK_PADDING * 2 ) + 'px' : appliedHeight + 'px'; + if ( textBlockWrapper ) { + if ( ampFitText ) { + textBlockWrapper.style.lineHeight = appliedHeight + 'px'; + } + // Also add the height to the wrapper since the background color is set to the wrapper. + textBlockWrapper.style.height = appliedHeight + 'px'; } // If it's image, let's change the width and height of the image, too. diff --git a/assets/src/stories-editor/components/story-block-drop-zone.js b/assets/src/stories-editor/components/story-block-drop-zone.js index 74fd1ed0834..43339a50a8a 100644 --- a/assets/src/stories-editor/components/story-block-drop-zone.js +++ b/assets/src/stories-editor/components/story-block-drop-zone.js @@ -23,7 +23,6 @@ import { withDispatch } from '@wordpress/data'; import { getPercentageFromPixels } from '../helpers'; import { STORY_PAGE_INNER_HEIGHT, - TEXT_BLOCK_BORDER, } from '../constants'; const wrapperElSelector = 'div[data-amp-selected="parent"] .editor-inner-blocks'; @@ -83,8 +82,6 @@ class BlockDropZone extends Component { // We will set the new position based on where the clone was moved to, with reference being the wrapper element. // Lets take the % based on the wrapper for top and left. - const possibleDelta = 'amp/amp-story-text' === srcBlockName ? TEXT_BLOCK_BORDER : 0; - const leftPosKey = isCTABlock ? 'btnPositionLeft' : 'positionLeft'; const topPosKey = isCTABlock ? 'btnPositionTop' : 'positionTop'; @@ -94,8 +91,8 @@ class BlockDropZone extends Component { baseHeight = STORY_PAGE_INNER_HEIGHT / 5; } updateBlockAttributes( srcClientId, { - [ leftPosKey ]: getPercentageFromPixels( 'x', clonePosition.left - wrapperPosition.left + possibleDelta ), - [ topPosKey ]: getPercentageFromPixels( 'y', clonePosition.top - wrapperPosition.top + possibleDelta, baseHeight ), + [ leftPosKey ]: getPercentageFromPixels( 'x', clonePosition.left - wrapperPosition.left ), + [ topPosKey ]: getPercentageFromPixels( 'y', clonePosition.top - wrapperPosition.top, baseHeight ), } ); } diff --git a/assets/src/stories-editor/components/with-wrapper-props.js b/assets/src/stories-editor/components/with-wrapper-props.js index 67ce87387e4..6bdb1ff7862 100644 --- a/assets/src/stories-editor/components/with-wrapper-props.js +++ b/assets/src/stories-editor/components/with-wrapper-props.js @@ -6,9 +6,8 @@ import { compose } from '@wordpress/compose'; /** * Internal dependencies */ -import { ALLOWED_BLOCKS, ALLOWED_CHILD_BLOCKS, TEXT_BLOCK_BORDER } from '../constants'; +import { ALLOWED_BLOCKS, ALLOWED_CHILD_BLOCKS } from '../constants'; import { withAttributes, withBlockName, withHasSelectedInnerBlock } from './'; -import { getPercentageFromPixels } from '../helpers'; const wrapperWithSelect = compose( withAttributes, @@ -59,20 +58,10 @@ const withWrapperProps = ( BlockListBlock ) => { }; if ( ALLOWED_CHILD_BLOCKS.includes( blockName ) ) { - let style = {}; - if ( 'amp/amp-story-text' === blockName ) { - const textBlockBorderInPercentageTop = getPercentageFromPixels( 'y', TEXT_BLOCK_BORDER ); - const textBlockBorderInPercentageLeft = getPercentageFromPixels( 'x', TEXT_BLOCK_BORDER ); - style = { - top: `${ parseFloat( attributes.positionTop ) - textBlockBorderInPercentageTop }%`, - left: `${ parseFloat( attributes.positionLeft ) - textBlockBorderInPercentageLeft }%`, - }; - } else { - style = { - top: `${ attributes.positionTop }%`, - left: `${ attributes.positionLeft }%`, - }; - } + let style = { + top: `${ attributes.positionTop }%`, + left: `${ attributes.positionLeft }%`, + }; style.transform = `scale(var(--preview-scale)) translateX(var(--preview-translateX)) translateY(var(--preview-translateY)) rotate(${ attributes.rotationAngle || 0 }deg)`; if ( 'amp/amp-story-cta' === blockName ) { diff --git a/assets/src/stories-editor/constants.js b/assets/src/stories-editor/constants.js index b66d3f31de9..716698f023b 100644 --- a/assets/src/stories-editor/constants.js +++ b/assets/src/stories-editor/constants.js @@ -237,7 +237,6 @@ export const AMP_STORY_FONT_IMAGES = { Ubuntu, }; -export const TEXT_BLOCK_BORDER = 5; export const TEXT_BLOCK_PADDING = 7; export const BLOCK_ROTATION_SNAPS = [ -180, -165, -150, -135, -120, -105, -90, -75, -60, -45, -30, -15, 0, 15, 30, 45, 60, 75, 90, 105, 120, 135, 150, 165, 180 ]; diff --git a/assets/src/stories-editor/helpers/index.js b/assets/src/stories-editor/helpers/index.js index be14e1de4e4..b08600e32e8 100644 --- a/assets/src/stories-editor/helpers/index.js +++ b/assets/src/stories-editor/helpers/index.js @@ -1688,3 +1688,21 @@ export const findClosestSnap = memize( ( number, snap, snapGap ) => { return snapGap === 0 || gap < snapGap ? snapArray[ closestGapIndex ] : number; } ); + +/** + * Sets input selection to the end for being able to type to the end of the existing text. + * + * @param {string} inputSelector Text input selector. + */ +export const setInputSelectionToEnd = ( inputSelector ) => { + const textInput = document.querySelector( inputSelector ); + // Create selection, collapse it in the end of the content. + if ( textInput ) { + const range = document.createRange(); + range.selectNodeContents( textInput ); + range.collapse( false ); + const selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange( range ); + } +}; diff --git a/tests/e2e/specs/stories-editor/cta.test.js b/tests/e2e/specs/stories-editor/cta.test.js index f1507aa7ca0..b85708f4439 100644 --- a/tests/e2e/specs/stories-editor/cta.test.js +++ b/tests/e2e/specs/stories-editor/cta.test.js @@ -1,7 +1,12 @@ /** * WordPress dependencies */ -import { visitAdminPage, createNewPost, clickButton } from '@wordpress/e2e-test-utils'; +import { + visitAdminPage, + createNewPost, + clickButton, + getAllBlocks, +} from '@wordpress/e2e-test-utils'; /** * Internal dependencies @@ -45,10 +50,15 @@ describe( 'Stories Editor Screen', () => { it( 'should display validation error when CTA block is on the first Page', async () => { await createNewPost( { postType: 'amp_story' } ); + const firstPageClientId = ( await getAllBlocks() )[ 0 ].clientId; + await insertBlock( 'Page' ); await insertBlock( 'Call to Action' ); await goToPreviousPage(); + + await page.waitForSelector( `#block-${ firstPageClientId }.amp-page-active` ); + await clickButtonByLabel( 'More options' ); await clickButton( 'Remove Block' ); diff --git a/tests/e2e/specs/stories-editor/story-templates.test.js b/tests/e2e/specs/stories-editor/story-templates.test.js index f8126f22d6f..64643b28d6f 100644 --- a/tests/e2e/specs/stories-editor/story-templates.test.js +++ b/tests/e2e/specs/stories-editor/story-templates.test.js @@ -71,6 +71,9 @@ describe( 'Story Templates', () => { '//*[contains(@class,"block-editor-block-preview")]' ); + // Wait until the templates are loaded and blocks accessible. + await page.waitForSelector( '.block-editor-block-preview .wp-block' ); + // Click on the template including quote and image. await nodes[ 3 ].click(); @@ -125,7 +128,7 @@ describe( 'Story Templates', () => { // Fandom CTA. expect( templateContents[ 6 ] ).toContain( 'Start Quiz' ); // Fandom Fact. - expect( templateContents[ 7 ] ).toContain( 'Robb Start made Jon his heir
(in books)' ); + expect( templateContents[ 7 ] ).toContain( 'Robb Start made Jon his heir
(in books)' ); // Fandom Fact Text. expect( templateContents[ 8 ] ).toContain( 'One of the biggest things missing from the show is the fact that before his death, Robb Start legitimizes Jon Snow as a Stark and makes him his heir.' ); // Fandom Intro diff --git a/tests/e2e/specs/stories-editor/text-block.test.js b/tests/e2e/specs/stories-editor/text-block.test.js new file mode 100644 index 00000000000..251866f6036 --- /dev/null +++ b/tests/e2e/specs/stories-editor/text-block.test.js @@ -0,0 +1,71 @@ +/** + * WordPress dependencies + */ +import { createNewPost, dragAndResize, selectBlockByClientId } from '@wordpress/e2e-test-utils'; + +/** + * Internal dependencies + */ +import { activateExperience, deactivateExperience, selectBlockByClassName, getBlocksOnPage } from '../../utils'; + +const textBlockClass = 'wp-block-amp-story-text'; + +describe( 'Text Block', () => { + beforeAll( async () => { + await activateExperience( 'stories' ); + } ); + + afterAll( async () => { + await deactivateExperience( 'stories' ); + } ); + + beforeEach( async () => { + await createNewPost( { postType: 'amp_story' } ); + await page.waitForSelector( `.${ textBlockClass }.is-not-editing` ); + await selectBlockByClassName( textBlockClass ); + } ); + + it( 'should not be in editable mode when selecting the block', async () => { + await page.keyboard.type( 'Hello' ); + const content = await page.$eval( '.block-editor-block-list__layout .block-editor-block-list__block .wp-block-amp-amp-story-text', ( node ) => node.textContent ); + expect( content ).toStrictEqual( 'Write text…' ); + } ); + + it( 'should go to editable mode after two clicks', async () => { + const textToWrite = 'Hello'; + + await page.click( `.${ textBlockClass }` ); + await page.keyboard.type( textToWrite ); + const content = await page.$eval( '.block-editor-block-list__layout .block-editor-block-list__block .wp-block-amp-amp-story-text', ( node ) => node.textContent ); + expect( content ).toStrictEqual( textToWrite ); + } ); + + // @todo Dragging is not working for some reason. + it.skip( 'should allow dragging the Text block from anywhere when not editing', async () => { // eslint-disable-line jest/no-disabled-tests + const textBlockBefore = ( await getBlocksOnPage() )[ 0 ]; + const textBlockEl = await page.$( '.wp-block-amp-story-text-wrapper' ); + + await selectBlockByClientId( textBlockBefore.clientId ); + await dragAndResize( textBlockEl, { x: 50, y: 50 } ); + + const textBlockAfter = ( await getBlocksOnPage() )[ 0 ]; + + expect( textBlockBefore.attributes.positionTop ).not.toStrictEqual( textBlockAfter.attributes.positionTop ); + expect( textBlockBefore.attributes.positionLeft ).not.toStrictEqual( textBlockAfter.attributes.positionLeft ); + } ); + + // @todo This test is useless until dragging actually works in the previous test. + it.skip( 'should not allow dragging in editable mode', async () => { // eslint-disable-line jest/no-disabled-tests + const textBlockBefore = ( await getBlocksOnPage() )[ 0 ]; + const textBlockEl = await page.$( '.wp-block-amp-story-text-wrapper' ); + + await selectBlockByClientId( textBlockBefore.clientId ); + await page.click( `.${ textBlockClass }` ); + await dragAndResize( textBlockEl, { x: 0, y: 25 } ); + + const textBlockAfter = ( await getBlocksOnPage() )[ 0 ]; + + expect( textBlockBefore.attributes.positionTop ).toStrictEqual( textBlockAfter.attributes.positionTop ); + expect( textBlockBefore.attributes.positionLeft ).toStrictEqual( textBlockAfter.attributes.positionLeft ); + } ); +} ); diff --git a/tests/e2e/utils/get-blocks-on-page.js b/tests/e2e/utils/get-blocks-on-page.js new file mode 100644 index 00000000000..6cbb6bb4e2d --- /dev/null +++ b/tests/e2e/utils/get-blocks-on-page.js @@ -0,0 +1,14 @@ +/** + * Internal dependencies + */ +import { wpDataSelect } from './wp-data-select'; + +/** + * Returns an array with all blocks on the currently active page. + * + * @return {Promise} Promise resolving with an array containing all blocks on the current page. + */ +export async function getBlocksOnPage() { + const currentPage = await wpDataSelect( 'amp/story', 'getCurrentPage' ); + return wpDataSelect( 'core/block-editor', 'getBlocks', currentPage ); +} diff --git a/tests/e2e/utils/index.js b/tests/e2e/utils/index.js index 3ae8b898b21..c9aa97132d4 100644 --- a/tests/e2e/utils/index.js +++ b/tests/e2e/utils/index.js @@ -11,4 +11,5 @@ export { openTemplateInserter } from './open-template-inserter'; export { searchForBlock } from './search-for-block'; export { selectBlockByClassName } from './select-block-by-classname'; export { uploadMedia } from './upload-media'; +export { getBlocksOnPage } from './get-blocks-on-page'; export { goToPreviousPage } from './go-to-previous-page'; diff --git a/tests/e2e/utils/wp-data-select.js b/tests/e2e/utils/wp-data-select.js new file mode 100644 index 00000000000..fac2bebcb3c --- /dev/null +++ b/tests/e2e/utils/wp-data-select.js @@ -0,0 +1,18 @@ +/** + * Queries the WordPress data module. + * @param {string} store Store to query e.g: core/editor, core/blocks... + * @param {string} selector Selector to exectute e.g: getBlocks. + * @param {...Object} parameters Parameters to pass to the selector. + * + * @return {?Object} Result of querying. + */ +export function wpDataSelect( store, selector, ...parameters ) { + return page.evaluate( + ( _store, _selector, ..._parameters ) => { + return window.wp.data.select( _store )[ _selector ]( ..._parameters ); + }, + store, + selector, + parameters + ); +}