diff --git a/packages/block-editor/src/components/index.js b/packages/block-editor/src/components/index.js index 1fb22e4200805..2b6cc244403c9 100644 --- a/packages/block-editor/src/components/index.js +++ b/packages/block-editor/src/components/index.js @@ -45,6 +45,7 @@ export { default as ToolSelector } from './tool-selector'; export { default as URLInput } from './url-input'; export { default as URLInputButton } from './url-input/button'; export { default as URLPopover } from './url-popover'; +export { __experimentalImageURLInputUI } from './url-popover/image-url-input-ui'; export { default as withColorContext } from './color-palette/with-color-context'; /* diff --git a/packages/block-editor/src/components/url-popover/image-url-input-ui.js b/packages/block-editor/src/components/url-popover/image-url-input-ui.js new file mode 100644 index 0000000000000..ef970081c7b41 --- /dev/null +++ b/packages/block-editor/src/components/url-popover/image-url-input-ui.js @@ -0,0 +1,312 @@ +/** + * External dependencies + */ +import { find, isEmpty, each, map } from 'lodash'; + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { useRef, useState, useCallback } from '@wordpress/element'; +import { + IconButton, + NavigableMenu, + MenuItem, + ToggleControl, + TextControl, + SVG, + Path, +} from '@wordpress/components'; +import { + LEFT, + RIGHT, + UP, + DOWN, + BACKSPACE, + ENTER, +} from '@wordpress/keycodes'; + +/** + * Internal dependencies + */ +import URLPopover from './index'; + +const LINK_DESTINATION_NONE = 'none'; +const LINK_DESTINATION_CUSTOM = 'custom'; +const LINK_DESTINATION_MEDIA = 'media'; +const LINK_DESTINATION_ATTACHMENT = 'attachment'; +const NEW_TAB_REL = [ 'noreferrer', 'noopener' ]; + +const icon = ; + +const ImageURLInputUI = ( { + linkDestination, + onChangeUrl, + url, + mediaType = 'image', + mediaUrl, + mediaLink, + linkTarget, + linkClass, + rel, +} ) => { + const [ isOpen, setIsOpen ] = useState( false ); + const openLinkUI = useCallback( () => { + setIsOpen( true ); + } ); + + const [ isEditingLink, setIsEditingLink ] = useState( false ); + const [ urlInput, setUrlInput ] = useState( null ); + + const autocompleteRef = useRef( null ); + + const stopPropagation = ( event ) => { + event.stopPropagation(); + }; + + const stopPropagationRelevantKeys = ( event ) => { + if ( [ LEFT, DOWN, RIGHT, UP, BACKSPACE, ENTER ].indexOf( event.keyCode ) > -1 ) { + // Stop the key event from propagating up to ObserveTyping.startTypingInTextField. + event.stopPropagation(); + } + }; + + const startEditLink = useCallback( () => { + if ( linkDestination === LINK_DESTINATION_MEDIA || + linkDestination === LINK_DESTINATION_ATTACHMENT + ) { + setUrlInput( '' ); + } + setIsEditingLink( true ); + } ); + + const stopEditLink = useCallback( () => { + setIsEditingLink( false ); + } ); + + const closeLinkUI = useCallback( () => { + setUrlInput( null ); + stopEditLink(); + setIsOpen( false ); + } ); + + const removeNewTabRel = ( currentRel ) => { + let newRel = currentRel; + + if ( currentRel !== undefined && ! isEmpty( newRel ) ) { + if ( ! isEmpty( newRel ) ) { + each( NEW_TAB_REL, function( relVal ) { + const regExp = new RegExp( '\\b' + relVal + '\\b', 'gi' ); + newRel = newRel.replace( regExp, '' ); + } ); + + // Only trim if NEW_TAB_REL values was replaced. + if ( newRel !== currentRel ) { + newRel = newRel.trim(); + } + + if ( isEmpty( newRel ) ) { + newRel = undefined; + } + } + } + + return newRel; + }; + + const getUpdatedLinkTargetSettings = ( value ) => { + const newLinkTarget = value ? '_blank' : undefined; + + let updatedRel; + if ( ! newLinkTarget && ! rel ) { + updatedRel = undefined; + } else { + updatedRel = removeNewTabRel( rel ); + } + + return { + linkTarget: newLinkTarget, + rel: updatedRel, + }; + }; + + const onFocusOutside = useCallback( () => { + return ( event ) => { + // The autocomplete suggestions list renders in a separate popover (in a portal), + // so onFocusOutside fails to detect that a click on a suggestion occurred in the + // LinkContainer. Detect clicks on autocomplete suggestions using a ref here, and + // return to avoid the popover being closed. + const autocompleteElement = autocompleteRef.current; + if ( autocompleteElement && autocompleteElement.contains( event.target ) ) { + return; + } + setIsOpen( false ); + setUrlInput( null ); + stopEditLink(); + }; + } ); + + const onSubmitLinkChange = useCallback( () => { + return ( event ) => { + if ( urlInput ) { + onChangeUrl( { href: urlInput } ); + } + stopEditLink(); + setUrlInput( null ); + event.preventDefault(); + }; + } ); + + const onLinkRemove = useCallback( () => { + onChangeUrl( { + linkDestination: LINK_DESTINATION_NONE, + href: '', + } ); + } ); + + const getLinkDestinations = () => { + return [ + { + linkDestination: LINK_DESTINATION_MEDIA, + title: __( 'Media File' ), + url: mediaType === 'image' ? mediaUrl : undefined, + icon, + }, + { + linkDestination: LINK_DESTINATION_ATTACHMENT, + title: __( 'Attachment Page' ), + url: mediaType === 'image' ? mediaLink : undefined, + icon: , + }, + ]; + }; + + const onSetHref = ( value ) => { + const linkDestinations = getLinkDestinations(); + let linkDestinationInput; + if ( ! value ) { + linkDestinationInput = LINK_DESTINATION_NONE; + } else { + linkDestinationInput = ( + find( linkDestinations, ( destination ) => { + return destination.url === value; + } ) || + { linkDestination: LINK_DESTINATION_CUSTOM } + ).linkDestination; + } + onChangeUrl( { + linkDestination: linkDestinationInput, + href: value, + } ); + }; + + const onSetNewTab = ( value ) => { + const updatedLinkTarget = getUpdatedLinkTargetSettings( value ); + onChangeUrl( updatedLinkTarget ); + }; + + const onSetLinkRel = ( value ) => { + onChangeUrl( { rel: value } ); + }; + + const onSetLinkClass = ( value ) => { + onChangeUrl( { linkClass: value } ); + }; + + const advancedOptions = ( + <> + + + + + ); + + const linkEditorValue = urlInput !== null ? urlInput : url; + + const urlLabel = ( find( getLinkDestinations(), [ 'linkDestination', linkDestination ] ) || {} ).title; + + return ( + <> + + { isOpen && ( + advancedOptions } + additionalControls={ ! linkEditorValue && ( + + { + map( getLinkDestinations(), ( link ) => ( + { + setUrlInput( null ); + onSetHref( link.url ); + stopEditLink(); + } } + > + { link.title } + + ) ) + } + + ) } + > + { ( ! url || isEditingLink ) && ( + + ) } + { ( url && ! isEditingLink ) && ( + <> + + + + ) } + + ) } + + ); +}; + +export { + ImageURLInputUI as __experimentalImageURLInputUI, +}; diff --git a/packages/block-library/src/image/edit.js b/packages/block-library/src/image/edit.js index 0214273dd1f2e..cb1f4094761ba 100644 --- a/packages/block-library/src/image/edit.js +++ b/packages/block-library/src/image/edit.js @@ -3,7 +3,6 @@ */ import classnames from 'classnames'; import { - find, get, isEmpty, map, @@ -20,30 +19,16 @@ import { Button, ButtonGroup, ExternalLink, - IconButton, - MenuItem, - NavigableMenu, PanelBody, - Path, ResizableBox, SelectControl, Spinner, - SVG, TextareaControl, TextControl, - ToggleControl, ToolbarGroup, withNotices, } from '@wordpress/components'; import { compose } from '@wordpress/compose'; -import { - LEFT, - RIGHT, - UP, - DOWN, - BACKSPACE, - ENTER, -} from '@wordpress/keycodes'; import { withSelect, withDispatch } from '@wordpress/data'; import { BlockAlignmentToolbar, @@ -53,14 +38,11 @@ import { InspectorAdvancedControls, MediaPlaceholder, MediaReplaceFlow, - URLPopover, RichText, + __experimentalImageURLInputUI as ImageURLInputUI, } from '@wordpress/block-editor'; import { Component, - useCallback, - useState, - useRef, } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; import { getPath } from '@wordpress/url'; @@ -72,17 +54,13 @@ import { withViewportMatch } from '@wordpress/viewport'; import { createUpgradedEmbedBlock } from '../embed/util'; import icon from './icon'; import ImageSize from './image-size'; -import { getUpdatedLinkTargetSettings, removeNewTabRel } from './utils'; - /** * Module constants */ import { MIN_SIZE, - LINK_DESTINATION_NONE, LINK_DESTINATION_MEDIA, LINK_DESTINATION_ATTACHMENT, - LINK_DESTINATION_CUSTOM, ALLOWED_MEDIA_TYPES, DEFAULT_SIZE_SLUG, } from './constants'; @@ -115,155 +93,6 @@ const isTemporaryImage = ( id, url ) => ! id && isBlobURL( url ); */ const isExternalImage = ( id, url ) => url && ! id && ! isBlobURL( url ); -const stopPropagation = ( event ) => { - event.stopPropagation(); -}; - -const stopPropagationRelevantKeys = ( event ) => { - if ( [ LEFT, DOWN, RIGHT, UP, BACKSPACE, ENTER ].indexOf( event.keyCode ) > -1 ) { - // Stop the key event from propagating up to ObserveTyping.startTypingInTextField. - event.stopPropagation(); - } -}; - -const ImageURLInputUI = ( { - advancedOptions, - linkDestination, - mediaLinks, - onChangeUrl, - url, -} ) => { - const [ isOpen, setIsOpen ] = useState( false ); - const openLinkUI = useCallback( () => { - setIsOpen( true ); - } ); - - const [ isEditingLink, setIsEditingLink ] = useState( false ); - const [ urlInput, setUrlInput ] = useState( null ); - - const startEditLink = useCallback( () => { - if ( linkDestination === LINK_DESTINATION_MEDIA || - linkDestination === LINK_DESTINATION_ATTACHMENT - ) { - setUrlInput( '' ); - } - setIsEditingLink( true ); - } ); - const stopEditLink = useCallback( () => { - setIsEditingLink( false ); - } ); - - const closeLinkUI = useCallback( () => { - setUrlInput( null ); - stopEditLink(); - setIsOpen( false ); - } ); - - const autocompleteRef = useRef( null ); - - const onClickOutside = useCallback( () => { - return ( event ) => { - // The autocomplete suggestions list renders in a separate popover (in a portal), - // so onClickOutside fails to detect that a click on a suggestion occurred in the - // LinkContainer. Detect clicks on autocomplete suggestions using a ref here, and - // return to avoid the popover being closed. - const autocompleteElement = autocompleteRef.current; - if ( autocompleteElement && autocompleteElement.contains( event.target ) ) { - return; - } - setIsOpen( false ); - setUrlInput( null ); - stopEditLink(); - }; - } ); - - const onSubmitLinkChange = useCallback( () => { - return ( event ) => { - if ( urlInput ) { - onChangeUrl( urlInput ); - } - stopEditLink(); - setUrlInput( null ); - event.preventDefault(); - }; - } ); - - const onLinkRemove = useCallback( () => { - closeLinkUI(); - onChangeUrl( '' ); - } ); - const linkEditorValue = urlInput !== null ? urlInput : url; - - const urlLabel = ( - find( mediaLinks, [ 'linkDestination', linkDestination ] ) || {} - ).title; - return ( - <> - - { isOpen && ( - advancedOptions } - additionalControls={ ! linkEditorValue && ( - - { - map( mediaLinks, ( link ) => ( - { - setUrlInput( null ); - onChangeUrl( link.url ); - stopEditLink(); - } } - > - { link.title } - - ) ) - } - - ) } - > - { ( ! url || isEditingLink ) && ( - - ) } - { ( url && ! isEditingLink ) && ( - <> - - - - ) } - - ) } - - ); -}; - export class ImageEdit extends Component { constructor() { super( ...arguments ); @@ -278,14 +107,10 @@ export class ImageEdit extends Component { this.updateHeight = this.updateHeight.bind( this ); this.updateDimensions = this.updateDimensions.bind( this ); this.onSetHref = this.onSetHref.bind( this ); - this.onSetLinkClass = this.onSetLinkClass.bind( this ); - this.onSetLinkRel = this.onSetLinkRel.bind( this ); - this.onSetNewTab = this.onSetNewTab.bind( this ); this.onSetTitle = this.onSetTitle.bind( this ); this.getFilename = this.getFilename.bind( this ); this.onUploadError = this.onUploadError.bind( this ); this.onImageError = this.onImageError.bind( this ); - this.getLinkDestinations = this.getLinkDestinations.bind( this ); this.state = { captionFocused: false, @@ -419,29 +244,8 @@ export class ImageEdit extends Component { } } - onSetHref( value ) { - const linkDestinations = this.getLinkDestinations(); - const { attributes } = this.props; - const { linkDestination } = attributes; - let linkDestinationInput; - if ( ! value ) { - linkDestinationInput = LINK_DESTINATION_NONE; - } else { - linkDestinationInput = ( - find( linkDestinations, ( destination ) => { - return destination.url === value; - } ) || - { linkDestination: LINK_DESTINATION_CUSTOM } - ).linkDestination; - } - if ( linkDestination !== linkDestinationInput ) { - this.props.setAttributes( { - linkDestination: linkDestinationInput, - href: value, - } ); - return; - } - this.props.setAttributes( { href: value } ); + onSetHref( props ) { + this.props.setAttributes( props ); } onSetTitle( value ) { @@ -449,19 +253,6 @@ export class ImageEdit extends Component { this.props.setAttributes( { title: value } ); } - onSetLinkClass( value ) { - this.props.setAttributes( { linkClass: value } ); - } - - onSetLinkRel( value ) { - this.props.setAttributes( { rel: value } ); - } - - onSetNewTab( value ) { - const updatedLinkTarget = getUpdatedLinkTargetSettings( value, this.props.attributes ); - this.props.setAttributes( updatedLinkTarget ); - } - onFocusCaption() { if ( ! this.state.captionFocused ) { this.setState( { @@ -526,24 +317,6 @@ export class ImageEdit extends Component { } } - getLinkDestinations() { - return [ - { - linkDestination: LINK_DESTINATION_MEDIA, - title: __( 'Media File' ), - url: ( this.props.image && this.props.image.source_url ) || - this.props.attributes.url, - icon, - }, - { - linkDestination: LINK_DESTINATION_ATTACHMENT, - title: __( 'Attachment Page' ), - url: this.props.image && this.props.image.link, - icon: , - }, - ]; - } - getImageSizeOptions() { const { imageSizes } = this.props; return map( imageSizes, ( { name, slug } ) => ( { value: slug, label: name } ) ); @@ -579,7 +352,6 @@ export class ImageEdit extends Component { sizeSlug, } = attributes; - const cleanRel = removeNewTabRel( rel ); const isExternal = isExternalImage( id, url ); const controls = ( @@ -600,30 +372,12 @@ export class ImageEdit extends Component { - - - - - } + mediaUrl={ this.props.image && this.props.image.source_url } + mediaLink={ this.props.image && this.props.image.link } + linkTarget={ linkTarget } + linkClass={ linkClass } + rel={ rel } /> ) } diff --git a/packages/block-library/src/media-text/block.json b/packages/block-library/src/media-text/block.json index 252bd9147fd8b..eb16cddf7d6a6 100644 --- a/packages/block-library/src/media-text/block.json +++ b/packages/block-library/src/media-text/block.json @@ -32,6 +32,36 @@ "selector": "figure video,figure img", "attribute": "src" }, + "mediaLink": { + "type": "string" + }, + "linkDestination": { + "type": "string" + }, + "linkTarget": { + "type": "string", + "source": "attribute", + "selector": "figure a", + "attribute": "target" + }, + "href": { + "type": "string", + "source": "attribute", + "selector": "figure a", + "attribute": "href" + }, + "rel": { + "type": "string", + "source": "attribute", + "selector": "figure a", + "attribute": "rel" + }, + "linkClass": { + "type": "string", + "source": "attribute", + "selector": "figure a", + "attribute": "class" + }, "mediaType": { "type": "string" }, diff --git a/packages/block-library/src/media-text/edit.js b/packages/block-library/src/media-text/edit.js index 44f15a8ce00ee..6ce0485dad665 100644 --- a/packages/block-library/src/media-text/edit.js +++ b/packages/block-library/src/media-text/edit.js @@ -8,6 +8,8 @@ import { get } from 'lodash'; * WordPress dependencies */ import { __, _x } from '@wordpress/i18n'; +import { compose } from '@wordpress/compose'; +import { withSelect } from '@wordpress/data'; import { BlockControls, BlockVerticalAlignmentToolbar, @@ -15,15 +17,16 @@ import { InspectorControls, PanelColorSettings, withColors, + __experimentalImageURLInputUI as ImageURLInputUI, } from '@wordpress/block-editor'; import { Component } from '@wordpress/element'; import { - ExternalLink, - FocalPointPicker, PanelBody, TextareaControl, ToggleControl, ToolbarGroup, + ExternalLink, + FocalPointPicker, } from '@wordpress/components'; /** * Internal dependencies @@ -40,6 +43,9 @@ const TEMPLATE = [ const WIDTH_CONSTRAINT_PERCENTAGE = 15; const applyWidthConstraints = ( width ) => Math.max( WIDTH_CONSTRAINT_PERCENTAGE, Math.min( width, 100 - WIDTH_CONSTRAINT_PERCENTAGE ) ); +export const LINK_DESTINATION_MEDIA = 'media'; +export const LINK_DESTINATION_ATTACHMENT = 'attachment'; + class MediaTextEdit extends Component { constructor() { super( ...arguments ); @@ -50,10 +56,12 @@ class MediaTextEdit extends Component { this.state = { mediaWidth: null, }; + this.onSetHref = this.onSetHref.bind( this ); } onSelectMedia( media ) { const { setAttributes } = this.props; + const { linkDestination, href } = this.props.attributes; let mediaType; let src; @@ -75,11 +83,26 @@ class MediaTextEdit extends Component { src = get( media, [ 'sizes', 'large', 'url' ] ) || get( media, [ 'media_details', 'sizes', 'large', 'source_url' ] ); } + let newHref = href; + if ( linkDestination === LINK_DESTINATION_MEDIA ) { + // Update the media link. + newHref = media.url; + } + + // Check if the image is linked to the attachment page. + if ( linkDestination === LINK_DESTINATION_ATTACHMENT ) { + // Update the media link. + newHref = media.link; + } + setAttributes( { mediaAlt: media.alt, mediaId: media.id, mediaType, mediaUrl: src || media.url, + mediaLink: media.link || undefined, + href: newHref, + imageFill: undefined, focalPoint: undefined, } ); } @@ -90,6 +113,10 @@ class MediaTextEdit extends Component { } ); } + onSetHref( props ) { + this.props.setAttributes( props ); + } + commitWidthChange( width ) { const { setAttributes } = this.props; @@ -104,7 +131,6 @@ class MediaTextEdit extends Component { renderMediaArea() { const { attributes } = this.props; const { mediaAlt, mediaId, mediaPosition, mediaType, mediaUrl, mediaWidth, imageFill, focalPoint } = attributes; - return ( ) } ); + return ( <> @@ -230,6 +264,19 @@ class MediaTextEdit extends Component { onChange={ onVerticalAlignmentChange } value={ verticalAlignment } /> + { mediaType === 'image' && ( + + ) }
{ this.renderMediaArea() } @@ -243,4 +290,13 @@ class MediaTextEdit extends Component { } } -export default withColors( 'backgroundColor' )( MediaTextEdit ); +export default compose( [ + withColors( 'backgroundColor' ), + withSelect( ( select, props ) => { + const { getMedia } = select( 'core' ); + const { attributes: { mediaId }, isSelected } = props; + return { + image: mediaId && isSelected ? getMedia( mediaId ) : null, + }; + } ), +] )( MediaTextEdit ); diff --git a/packages/block-library/src/media-text/save.js b/packages/block-library/src/media-text/save.js index 8d4529173948e..ac56a0f5babdc 100644 --- a/packages/block-library/src/media-text/save.js +++ b/packages/block-library/src/media-text/save.js @@ -2,7 +2,7 @@ * External dependencies */ import classnames from 'classnames'; -import { noop } from 'lodash'; +import { noop, isEmpty } from 'lodash'; /** * WordPress dependencies @@ -33,9 +33,34 @@ export default function save( { attributes } ) { verticalAlignment, imageFill, focalPoint, + linkClass, + href, + linkTarget, + rel, } = attributes; + const newRel = isEmpty( rel ) ? undefined : rel; + + let image = {; + + if ( href ) { + image = ( + + { image } + + ); + } + const mediaTypeRenders = { - image: () => {, + image: () => image, video: () =>