diff --git a/packages/editor/src/components/rich-text/format-toolbar/index.js b/packages/editor/src/components/rich-text/format-toolbar/index.js
index ea1a0ff185ce6f..3814e2b75678a0 100644
--- a/packages/editor/src/components/rich-text/format-toolbar/index.js
+++ b/packages/editor/src/components/rich-text/format-toolbar/index.js
@@ -68,6 +68,7 @@ function computeDerivedState( props ) {
settingsVisible: false,
opensInNewWindow: !! props.formats.link && !! props.formats.link.target,
linkValue: '',
+ isEditingLink: false,
};
}
@@ -151,8 +152,7 @@ class FormatToolbar extends Component {
editLink( event ) {
event.preventDefault();
- this.props.onChange( { link: { ...this.props.formats.link, isAdding: true } } );
- this.setState( { linkValue: this.props.formats.link.value } );
+ this.setState( { linkValue: this.props.formats.link.value, isEditingLink: true } );
}
submitLink( event ) {
@@ -165,7 +165,7 @@ class FormatToolbar extends Component {
value,
} } );
- this.setState( { linkValue: value } );
+ this.setState( { linkValue: value, isEditingLink: false } );
if ( ! this.props.formats.link.value ) {
this.props.speak( __( 'Link added.' ), 'assertive' );
}
@@ -177,7 +177,7 @@ class FormatToolbar extends Component {
render() {
const { formats, enabledControls = DEFAULT_CONTROLS, customControls = [], selectedNodeId } = this.props;
- const { linkValue, settingsVisible, opensInNewWindow } = this.state;
+ const { linkValue, settingsVisible, opensInNewWindow, isEditingLink } = this.state;
const isAddingLink = formats.link && formats.link.isAdding;
const toolbarControls = FORMATTING_CONTROLS.concat( customControls )
@@ -202,6 +202,13 @@ class FormatToolbar extends Component {
};
} );
+ let linkUIToShow = 'none';
+ if ( isAddingLink || isEditingLink ) {
+ linkUIToShow = 'editing';
+ } else if ( formats.link ) {
+ linkUIToShow = 'previewing';
+ }
+
const linkSettings = settingsVisible && (
- { ( isAddingLink || formats.link ) && (
+ { linkUIToShow !== 'none' && (
- { isAddingLink && (
- // Disable reason: KeyPress must be suppressed so the block doesn't hide the toolbar
- /* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
+ { linkUIToShow === 'editing' && (
+ // 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 */
+ /* eslint-enable jsx-a11y/no-noninteractive-element-interactions */
) }
- { formats.link && ! isAddingLink && (
- // Disable reason: KeyPress must be suppressed so the block doesn't hide the toolbar
- /* eslint-disable jsx-a11y/no-static-element-interactions */
+ { linkUIToShow === 'previewing' && (
+ // Disable reason: KeyPress must be suppressed so the block doesn't hide the toolbar
+ /* eslint-disable jsx-a11y/no-static-element-interactions */
{ linkSettings }
- /* eslint-enable jsx-a11y/no-static-element-interactions */
+ /* eslint-enable jsx-a11y/no-static-element-interactions */
) }
diff --git a/packages/editor/src/components/rich-text/index.js b/packages/editor/src/components/rich-text/index.js
index 55eec3c5e2b471..dffd2fe1e319df 100644
--- a/packages/editor/src/components/rich-text/index.js
+++ b/packages/editor/src/components/rich-text/index.js
@@ -30,6 +30,7 @@ import { withSelect } from '@wordpress/data';
import { rawHandler, children } from '@wordpress/blocks';
import { withInstanceId, withSafeTimeout, compose } from '@wordpress/compose';
import deprecated from '@wordpress/deprecated';
+import { isURL } from '@wordpress/url';
/**
* Internal dependencies
@@ -309,11 +310,10 @@ export class RichText extends Component {
// There is a selection, check if a URL is pasted.
if ( ! this.editor.selection.isCollapsed() ) {
- const linkRegExp = /^(?:https?:)?\/\/\S+$/i;
const pastedText = ( html || plainText ).replace( /<[^>]+>/g, '' ).trim();
// A URL was pasted, turn the selection into a link
- if ( linkRegExp.test( pastedText ) ) {
+ if ( isURL( pastedText ) ) {
this.editor.execCommand( 'mceInsertLink', false, {
href: this.editor.dom.decode( pastedText ),
} );
@@ -840,9 +840,18 @@ export class RichText extends Component {
const { isAdding, value: href, target } = formatValue;
const isSelectionCollapsed = this.editor.selection.isCollapsed();
- // Bail early if the link is still being added. will ask the user
- // for a URL and then update `formats.link`.
+ // Are we creating a new link?
if ( isAdding ) {
+ // If the selected text is a URL, instantly turn it into a link.
+ const selectedText = this.editor.selection.getContent( { format: 'text' } );
+ if ( isURL( selectedText ) ) {
+ formatValue.isAdding = false;
+ this.editor.execCommand( 'mceInsertLink', false, {
+ href: selectedText,
+ } );
+ return;
+ }
+
// Create a placeholder so that there's something to indicate which
// text will become a link. Placeholder links are stripped from
// getContent() and removed when the selection changes.
@@ -853,6 +862,9 @@ export class RichText extends Component {
'data-mce-bogus': true,
} );
}
+
+ // Bail early if the link is still being added. will ask the user
+ // for a URL and then update `formats.link`.
return;
}
diff --git a/packages/url/README.md b/packages/url/README.md
index e50d2f142def4f..9e7d61e50f2719 100644
--- a/packages/url/README.md
+++ b/packages/url/README.md
@@ -13,13 +13,16 @@ npm install @wordpress/url --save
## Usage
```JS
-import { addQueryArgs, prependHTTP } from '@wordpress/url';
+import { isURL, addQueryArgs, prependHTTP } from '@wordpress/url';
+
+// Checks if the argument looks like a URL
+const isURL = isURL( 'https://wordpress.org' ); // true
// Appends arguments to the query string of a given url
-const newUrl = addQueryArgs( 'https://google.com', { q: 'test' } ); // https://google.com/?q=test
+const newURL = addQueryArgs( 'https://google.com', { q: 'test' } ); // https://google.com/?q=test
// Prepends 'http://' to URLs that are probably mean to have them
-const actualUrl = prependHTTP( 'wordpress.org' ); // http://wordpress.org
+const actualURL = prependHTTP( 'wordpress.org' ); // http://wordpress.org
```

diff --git a/packages/url/src/index.js b/packages/url/src/index.js
index a002ccdeca0ca9..fccbaa420deead 100644
--- a/packages/url/src/index.js
+++ b/packages/url/src/index.js
@@ -3,9 +3,21 @@
*/
import { parse, stringify } from 'qs';
+const URL_REGEXP = /^(?:https?:)?\/\/\S+$/i;
const EMAIL_REGEXP = /^(mailto:)?[a-z0-9._%+-]+@[a-z0-9][a-z0-9.-]*\.[a-z]{2,63}$/i;
const USABLE_HREF_REGEXP = /^(?:[a-z]+:|#|\?|\.|\/)/i;
+/**
+ * Determines whether the given string looks like a URL.
+ *
+ * @param {string} url The string to scrutinise.
+ *
+ * @return {boolean} Whether or not it looks like a URL.
+ */
+export function isURL( url ) {
+ return URL_REGEXP.test( url );
+}
+
/**
* Appends arguments to the query string of the url
*
diff --git a/packages/url/src/test/index.test.js b/packages/url/src/test/index.test.js
index 40fbc4fe20d53a..0343ead1c11471 100644
--- a/packages/url/src/test/index.test.js
+++ b/packages/url/src/test/index.test.js
@@ -1,7 +1,38 @@
/**
- * Internal Dependencies
+ * External dependencies
*/
-import { addQueryArgs, prependHTTP } from '../';
+import { every } from 'lodash';
+
+/**
+ * Internal dependencies
+ */
+import { isURL, addQueryArgs, prependHTTP } from '../';
+
+describe( 'isURL', () => {
+ it( 'returns true when given things that look like a URL', () => {
+ const urls = [
+ 'http://wordpress.org',
+ 'https://wordpress.org',
+ 'HTTPS://WORDPRESS.ORG',
+ 'https://wordpress.org/foo#bar',
+ 'https://localhost/foo#bar',
+ ];
+
+ expect( every( urls, isURL ) ).toBe( true );
+ } );
+
+ it( 'returns false when given things that don\'t look like a URL', () => {
+ const urls = [
+ 'HTTP: HyperText Transfer Protocol',
+ 'URLs begin with a http:// prefix',
+ 'Go here: http://wordpress.org',
+ 'http://',
+ '',
+ ];
+
+ expect( every( urls, isURL ) ).toBe( false );
+ } );
+} );
describe( 'addQueryArgs', () => {
it( 'should append args to an URL without query string', () => {
diff --git a/test/e2e/specs/__snapshots__/links.test.js.snap b/test/e2e/specs/__snapshots__/links.test.js.snap
index 3bb19928ccb620..00077cd64cb306 100644
--- a/test/e2e/specs/__snapshots__/links.test.js.snap
+++ b/test/e2e/specs/__snapshots__/links.test.js.snap
@@ -12,6 +12,12 @@ exports[`Links can be created by selecting text and using keyboard shortcuts 1`]
"
`;
+exports[`Links can be created instantly when a URL is selected 1`] = `
+"
+This is Gutenberg: https://wordpress.org/gutenberg
+"
+`;
+
exports[`Links can be created without any text selected 1`] = `
"
This is Gutenberg: https://wordpress.org/gutenberg
diff --git a/test/e2e/specs/links.test.js b/test/e2e/specs/links.test.js
index ece6bda4413a39..7dd10ff5d664af 100644
--- a/test/e2e/specs/links.test.js
+++ b/test/e2e/specs/links.test.js
@@ -100,6 +100,27 @@ describe( 'Links', () => {
expect( await getEditedPostContent() ).toMatchSnapshot();
} );
+ it( 'can be created instantly when a URL is selected', async () => {
+ // Create a block with some text
+ await clickBlockAppender();
+ await page.keyboard.type( 'This is Gutenberg: https://wordpress.org/gutenberg' );
+
+ // Select the URL
+ await pressWithModifier( SELECT_WORD_MODIFIER_KEYS, 'ArrowLeft' );
+ await pressWithModifier( SELECT_WORD_MODIFIER_KEYS, 'ArrowLeft' );
+ await pressWithModifier( SELECT_WORD_MODIFIER_KEYS, 'ArrowLeft' );
+ await pressWithModifier( SELECT_WORD_MODIFIER_KEYS, 'ArrowLeft' );
+
+ // Click on the Link button
+ await page.click( 'button[aria-label="Link"]' );
+
+ // A placeholder link should not have been inserted
+ expect( await page.$( 'a[data-wp-placeholder]' ) ).toBeNull();
+
+ // A link with the selected URL as its href should have been inserted
+ expect( await getEditedPostContent() ).toMatchSnapshot();
+ } );
+
it( 'is not created when we click away from the link input', async () => {
// Create a block with some text
await clickBlockAppender();