diff --git a/packages/block-editor/src/components/url-popover/index.js b/packages/block-editor/src/components/url-popover/index.js index e549c3007fb08..d3c7c874c6892 100644 --- a/packages/block-editor/src/components/url-popover/index.js +++ b/packages/block-editor/src/components/url-popover/index.js @@ -1,12 +1,24 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + /** * WordPress dependencies */ import { __ } from '@wordpress/i18n'; import { Component } from '@wordpress/element'; import { - Popover, + ExternalLink, IconButton, + Popover, } from '@wordpress/components'; +import { safeDecodeURI, filterURLForDisplay } from '@wordpress/url'; + +/** + * Internal dependencies + */ +import URLInput from '../url-input'; class URLPopover extends Component { constructor() { @@ -27,6 +39,7 @@ class URLPopover extends Component { render() { const { + additionalControls, children, renderSettings, position = 'bottom center', @@ -47,21 +60,30 @@ class URLPopover extends Component { position={ position } { ...popoverProps } > -
- { children } - { !! renderSettings && ( - +
+
+ { children } + { !! renderSettings && ( + + ) } +
+ { showSettings && ( +
+ { renderSettings() } +
) }
- { showSettings && ( -
- { renderSettings() } + { additionalControls && ! showSettings && ( +
+ { additionalControls }
) } @@ -69,6 +91,76 @@ class URLPopover extends Component { } } +const LinkEditor = ( { + autocompleteRef, + className, + onChangeInputValue, + value, + ...props +} ) => ( +
+ + + + +); + +URLPopover.__experimentalLinkEditor = LinkEditor; + +const LinkViewerUrl = ( { url, urlLabel, className } ) => { + const linkClassName = classnames( + className, + 'block-editor-url-popover__link-viewer-url' + ); + + if ( ! url ) { + return ; + } + + return ( + + { urlLabel || filterURLForDisplay( safeDecodeURI( url ) ) } + + ); +}; + +const LinkViewer = ( { + className, + url, + urlLabel, + editLink, + linkClassName, + ...props +} ) => { + return ( +
+ + +
+ ); +}; + +URLPopover.__experimentalLinkViewer = LinkViewer; + /** * @see https://github.com/WordPress/gutenberg/blob/master/packages/block-editor/src/components/url-popover/README.md */ diff --git a/packages/block-editor/src/components/url-popover/style.scss b/packages/block-editor/src/components/url-popover/style.scss index 0b98f11afcd31..268908dd01478 100644 --- a/packages/block-editor/src/components/url-popover/style.scss +++ b/packages/block-editor/src/components/url-popover/style.scss @@ -1,3 +1,15 @@ +.block-editor-url-popover__additional-controls { + border-top: $border-width solid $light-gray-500; +} + +.block-editor-url-popover__additional-controls > div[role="menu"] .components-icon-button:not(:disabled):not([aria-disabled="true"]):not(.is-default) > svg { + box-shadow: none; +} + +.block-editor-url-popover__additional-controls div[role="menu"] > .components-icon-button { + padding-left: 2px; +} + .block-editor-url-popover__row { display: flex; } @@ -50,10 +62,32 @@ } .block-editor-url-popover__settings { + display: block; padding: $panel-padding; border-top: $border-width solid $light-gray-500; + .components-base-control:last-child, .components-base-control:last-child .components-base-control__field { margin-bottom: 0; } } + +.block-editor-url-popover__link-editor, +.block-editor-url-popover__link-viewer { + display: flex; +} + +.block-editor-url-popover__link-viewer-url { + margin: $grid-size - $border-width; + flex-grow: 1; + flex-shrink: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 150px; + max-width: 500px; + + &.has-invalid-link { + color: $alert-red; + } +} diff --git a/packages/block-editor/src/components/url-popover/test/__snapshots__/index.js.snap b/packages/block-editor/src/components/url-popover/test/__snapshots__/index.js.snap index c835e54540c25..e7ba18eccd1ad 100644 --- a/packages/block-editor/src/components/url-popover/test/__snapshots__/index.js.snap +++ b/packages/block-editor/src/components/url-popover/test/__snapshots__/index.js.snap @@ -7,18 +7,22 @@ exports[`URLPopover matches the snapshot in its default state 1`] = ` position="bottom center" >
-
- Editor +
+
+ Editor +
+
-
`; @@ -30,24 +34,28 @@ exports[`URLPopover matches the snapshot when the settings are toggled open 1`] position="bottom center" >
-
- Editor +
+
+ Editor +
+
- -
-
-
- Settings +
+
+ Settings +
@@ -60,10 +68,14 @@ exports[`URLPopover matches the snapshot when there are no settings 1`] = ` position="bottom center" >
-
- Editor +
+
+ Editor +
diff --git a/packages/block-library/src/image/edit.js b/packages/block-library/src/image/edit.js index b22d63da20cb9..a50b8601aa55d 100644 --- a/packages/block-library/src/image/edit.js +++ b/packages/block-library/src/image/edit.js @@ -3,6 +3,7 @@ */ import classnames from 'classnames'; import { + find, get, isEmpty, map, @@ -17,7 +18,10 @@ import { getBlobByURL, isBlobURL, revokeBlobURL } from '@wordpress/blob'; import { Button, ButtonGroup, + ExternalLink, IconButton, + MenuItem, + NavigableMenu, PanelBody, Path, Rect, @@ -30,9 +34,16 @@ import { ToggleControl, Toolbar, withNotices, - ExternalLink, } from '@wordpress/components'; import { compose } from '@wordpress/compose'; +import { + LEFT, + RIGHT, + UP, + DOWN, + BACKSPACE, + ENTER, +} from '@wordpress/keycodes'; import { withSelect } from '@wordpress/data'; import { BlockAlignmentToolbar, @@ -40,9 +51,15 @@ import { BlockIcon, InspectorControls, MediaPlaceholder, + URLPopover, RichText, } from '@wordpress/block-editor'; -import { Component } from '@wordpress/element'; +import { + Component, + useCallback, + useState, + useRef, +} from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; import { getPath } from '@wordpress/url'; import { withViewportMatch } from '@wordpress/viewport'; @@ -95,6 +112,155 @@ 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 ) && ( + <> + + + + ) } + + ) } + + ); +}; + class ImageEdit extends Component { constructor( { attributes } ) { super( ...arguments ); @@ -108,15 +274,15 @@ class ImageEdit extends Component { this.updateWidth = this.updateWidth.bind( this ); this.updateHeight = this.updateHeight.bind( this ); this.updateDimensions = this.updateDimensions.bind( this ); - this.onSetCustomHref = this.onSetCustomHref.bind( this ); + this.onSetHref = this.onSetHref.bind( this ); this.onSetLinkClass = this.onSetLinkClass.bind( this ); this.onSetLinkRel = this.onSetLinkRel.bind( this ); - this.onSetLinkDestination = this.onSetLinkDestination.bind( this ); this.onSetNewTab = this.onSetNewTab.bind( this ); this.getFilename = this.getFilename.bind( this ); this.toggleIsEditing = this.toggleIsEditing.bind( this ); this.onUploadError = this.onUploadError.bind( this ); this.onImageError = this.onImageError.bind( this ); + this.getLinkDestinations = this.getLinkDestinations.bind( this ); this.state = { captionFocused: false, @@ -199,25 +365,6 @@ class ImageEdit extends Component { } ); } - onSetLinkDestination( value ) { - let href; - - if ( value === LINK_DESTINATION_NONE ) { - href = undefined; - } else if ( value === LINK_DESTINATION_MEDIA ) { - href = ( this.props.image && this.props.image.source_url ) || this.props.attributes.url; - } else if ( value === LINK_DESTINATION_ATTACHMENT ) { - href = this.props.image && this.props.image.link; - } else { - href = this.props.attributes.href; - } - - this.props.setAttributes( { - linkDestination: value, - href, - } ); - } - onSelectURL( newURL ) { const { url } = this.props.attributes; @@ -244,7 +391,28 @@ class ImageEdit extends Component { } } - onSetCustomHref( value ) { + 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 } ); } @@ -337,12 +505,21 @@ class ImageEdit extends Component { } } - getLinkDestinationOptions() { + getLinkDestinations() { return [ - { value: LINK_DESTINATION_NONE, label: __( 'None' ) }, - { value: LINK_DESTINATION_MEDIA, label: __( 'Media File' ) }, - { value: LINK_DESTINATION_ATTACHMENT, label: __( 'Attachment Page' ) }, - { value: LINK_DESTINATION_CUSTOM, label: __( 'Custom URL' ) }, + { + 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: , + }, ]; } @@ -400,15 +577,47 @@ class ImageEdit extends Component { onChange={ this.updateAlignment } /> { url && ( - - - + <> + + + + + + + + + + } + /> + + ) } ); @@ -458,7 +667,7 @@ class ImageEdit extends Component { } ); const isResizable = [ 'wide', 'full' ].indexOf( align ) === -1 && isLargeViewport; - const isLinkURLInputReadOnly = linkDestination !== LINK_DESTINATION_CUSTOM; + const imageSizeOptions = this.getImageSizeOptions(); const getInspectorControls = ( imageWidth, imageHeight ) => ( @@ -539,39 +748,6 @@ class ImageEdit extends Component {
) } - - - { linkDestination !== LINK_DESTINATION_NONE && ( - <> - - - - - - ) } - ); diff --git a/packages/format-library/src/link/inline.js b/packages/format-library/src/link/inline.js index c4b64e9e3c45c..564ae795fd484 100644 --- a/packages/format-library/src/link/inline.js +++ b/packages/format-library/src/link/inline.js @@ -1,22 +1,15 @@ -/** - * External dependencies - */ -import classnames from 'classnames'; - /** * WordPress dependencies */ import { __ } from '@wordpress/i18n'; import { Component, createRef, useMemo } from '@wordpress/element'; import { - ExternalLink, - IconButton, ToggleControl, withSpokenMessages, } from '@wordpress/components'; import { LEFT, RIGHT, UP, DOWN, BACKSPACE, ENTER } from '@wordpress/keycodes'; import { getRectangleFromRange } from '@wordpress/dom'; -import { prependHTTP, safeDecodeURI, filterURLForDisplay } from '@wordpress/url'; +import { prependHTTP } from '@wordpress/url'; import { create, insert, @@ -25,7 +18,7 @@ import { getTextContent, slice, } from '@wordpress/rich-text'; -import { URLInput, URLPopover } from '@wordpress/block-editor'; +import { URLPopover } from '@wordpress/block-editor'; /** * Internal dependencies @@ -38,45 +31,6 @@ function isShowingInput( props, state ) { return props.addingLink || state.editLink; } -const LinkEditor = ( { value, onChangeInputValue, onKeyDown, submitLink, autocompleteRef } ) => ( - // Disable reason: KeyPress must be suppressed so the block doesn't hide the toolbar - /* eslint-disable jsx-a11y/no-noninteractive-element-interactions */ -
- - - - /* eslint-enable jsx-a11y/no-noninteractive-element-interactions */ -); - -const LinkViewerUrl = ( { url } ) => { - const prependedURL = prependHTTP( url ); - const linkClassName = classnames( 'editor-format-toolbar__link-container-value block-editor-format-toolbar__link-container-value', { - 'has-invalid-link': ! isValidHref( prependedURL ), - } ); - - if ( ! url ) { - return ; - } - - return ( - - { filterURLForDisplay( safeDecodeURI( url ) ) } - - ); -}; - const URLPopoverAtLink = ( { isActive, addingLink, value, ...props } ) => { const anchorRect = useMemo( () => { const selection = window.getSelection(); @@ -111,21 +65,6 @@ const URLPopoverAtLink = ( { isActive, addingLink, value, ...props } ) => { return ; }; -const LinkViewer = ( { url, editLink } ) => { - return ( - // Disable reason: KeyPress must be suppressed so the block doesn't hide the toolbar - /* eslint-disable jsx-a11y/no-static-element-interactions */ -
- - -
- /* eslint-enable jsx-a11y/no-static-element-interactions */ - ); -}; - class InlineLinkUI extends Component { constructor() { super( ...arguments ); @@ -271,17 +210,22 @@ class InlineLinkUI extends Component { ) } > { showInput ? ( - ) : ( - ) }