Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 20 additions & 13 deletions packages/editor/src/components/rich-text/format-toolbar/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ function computeDerivedState( props ) {
settingsVisible: false,
opensInNewWindow: !! props.formats.link && !! props.formats.link.target,
linkValue: '',
isEditingLink: false,
};
}

Expand Down Expand Up @@ -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 ) {
Expand All @@ -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' );
}
Expand All @@ -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 )
Expand All @@ -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 && (
<div className="editor-format-toolbar__link-modal-line editor-format-toolbar__link-settings">
<ToggleControl
Expand All @@ -215,17 +222,17 @@ class FormatToolbar extends Component {
<div className="editor-format-toolbar">
<Toolbar controls={ toolbarControls } />

{ ( isAddingLink || formats.link ) && (
{ linkUIToShow !== 'none' && (
<Fill name="RichText.Siblings">
<PositionedAtSelection className="editor-format-toolbar__link-container">
<Popover
position="bottom center"
focusOnMount={ isAddingLink ? 'firstElement' : false }
key={ selectedNodeId /* Used to force rerender on change */ }
>
{ 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 */
<form
className="editor-format-toolbar__link-modal"
onKeyPress={ stopKeyPropagation }
Expand All @@ -244,12 +251,12 @@ class FormatToolbar extends Component {
</div>
{ linkSettings }
</form>
/* 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 */
<div
className="editor-format-toolbar__link-modal"
onKeyPress={ stopKeyPropagation }
Expand All @@ -272,7 +279,7 @@ class FormatToolbar extends Component {
</div>
{ linkSettings }
</div>
/* eslint-enable jsx-a11y/no-static-element-interactions */
/* eslint-enable jsx-a11y/no-static-element-interactions */
) }
</Popover>
</PositionedAtSelection>
Expand Down
20 changes: 16 additions & 4 deletions packages/editor/src/components/rich-text/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 ),
} );
Expand Down Expand Up @@ -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. <RichText> 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 <a> 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.
Expand All @@ -853,6 +862,9 @@ export class RichText extends Component {
'data-mce-bogus': true,
} );
}

// Bail early if the link is still being added. <RichText> will ask the user
// for a URL and then update `formats.link`.
return;
}

Expand Down
9 changes: 6 additions & 3 deletions packages/url/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

<br/><br/><p align="center"><img src="https://s.w.org/style/images/codeispoetry.png?1" alt="Code is Poetry." /></p>
12 changes: 12 additions & 0 deletions packages/url/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 ) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice util, I think we may be able to use it in:

if ( linkRegExp.test( pastedText ) ) {

Removing a duplicate regex.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've already replaced that regex 🙂

return URL_REGEXP.test( url );
}

/**
* Appends arguments to the query string of the url
*
Expand Down
35 changes: 33 additions & 2 deletions packages/url/src/test/index.test.js
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down
6 changes: 6 additions & 0 deletions test/e2e/specs/__snapshots__/links.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ exports[`Links can be created by selecting text and using keyboard shortcuts 1`]
<!-- /wp:paragraph -->"
`;

exports[`Links can be created instantly when a URL is selected 1`] = `
"<!-- wp:paragraph -->
<p>This is Gutenberg: <a href=\\"https://wordpress.org/gutenberg\\">https://wordpress.org/gutenberg</a></p>
<!-- /wp:paragraph -->"
`;

exports[`Links can be created without any text selected 1`] = `
"<!-- wp:paragraph -->
<p>This is Gutenberg: <a href=\\"https://wordpress.org/gutenberg\\">https://wordpress.org/gutenberg</a></p>
Expand Down
21 changes: 21 additions & 0 deletions test/e2e/specs/links.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down