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 ) => (
+
+ ) )
+ }
+
+ ) }
+ >
+ { ( ! 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 ) => (
-
- ) )
- }
-
- ) }
- >
- { ( ! 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: () =>
,
};
const backgroundClass = getColorClassName( 'background-color', backgroundColor );