@wordpress/format-library
Version:
Format library for the WordPress editor.
177 lines (155 loc) • 4.35 kB
JavaScript
/**
* External dependencies
*/
import Clipboard from '@react-native-clipboard/clipboard';
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import { Component } from '@wordpress/element';
import { withSpokenMessages } from '@wordpress/components';
import { RichTextToolbarButton } from '@wordpress/block-editor';
import {
applyFormat,
getActiveFormat,
getTextContent,
isCollapsed,
removeFormat,
slice,
} from '@wordpress/rich-text';
import { isURL } from '@wordpress/url';
import { link as linkIcon } from '@wordpress/icons';
/**
* Internal dependencies
*/
import ModalLinkUI from './modal';
import { isValidHref } from './utils';
const name = 'core/link';
export const link = {
name,
title: __( 'Link' ),
tagName: 'a',
className: null,
attributes: {
url: 'href',
target: 'target',
rel: 'rel',
},
edit: withSpokenMessages(
class LinkEdit extends Component {
constructor() {
super( ...arguments );
this.addLink = this.addLink.bind( this );
this.stopAddingLink = this.stopAddingLink.bind( this );
this.onRemoveFormat = this.onRemoveFormat.bind( this );
this.getURLFromClipboard =
this.getURLFromClipboard.bind( this );
this.state = {
addingLink: false,
};
}
addLink() {
const { value, onChange } = this.props;
const text = getTextContent( slice( value ) );
if ( text && isURL( text ) && isValidHref( text ) ) {
const newValue = applyFormat( value, {
type: name,
attributes: { url: text },
} );
newValue.start = newValue.end;
newValue.activeFormats = [];
onChange( { ...newValue, needsSelectionUpdate: true } );
} else {
this.setState( { addingLink: true } );
this.getURLFromClipboard();
}
}
stopAddingLink() {
this.setState( { addingLink: false, clipboardURL: undefined } );
}
getLinkSelection() {
const { value, isActive } = this.props;
const startFormat = getActiveFormat( value, 'core/link' );
// If the link isn't selected, get the link manually by looking around the cursor
// TODO: handle partly selected links.
if ( startFormat && isCollapsed( value ) && isActive ) {
let startIndex = value.start;
let endIndex = value.end;
while (
value.formats[ startIndex ]?.find(
( format ) => format?.type === startFormat.type
)
) {
startIndex--;
}
endIndex++;
while (
value.formats[ endIndex ]?.find(
( format ) => format?.type === startFormat.type
)
) {
endIndex++;
}
return {
...value,
start: startIndex + 1,
end: endIndex,
};
}
return value;
}
onRemoveFormat() {
const { onChange, speak, value } = this.props;
const startFormat = getActiveFormat( value, 'core/link' );
// Before we try to remove anything we check if there is something at the caret position to remove.
if ( isCollapsed( value ) && startFormat === undefined ) {
return;
}
const linkSelection = this.getLinkSelection();
onChange( removeFormat( linkSelection, name ) );
speak( __( 'Link removed.' ), 'assertive' );
}
async getURLFromClipboard() {
const clipboardText = await Clipboard.getString();
if ( ! clipboardText ) {
return;
}
// Check if pasted text is URL.
if ( ! isURL( clipboardText ) ) {
return;
}
this.setState( { clipboardURL: clipboardText } );
}
render() {
const { isActive, activeAttributes, onChange } = this.props;
const linkSelection = this.getLinkSelection();
// If no URL is set and we have a clipboard URL let's use it.
if ( ! activeAttributes.url && this.state.clipboardURL ) {
activeAttributes.url = this.state.clipboardURL;
}
return (
<>
<ModalLinkUI
isVisible={ this.state.addingLink }
isActive={ isActive }
activeAttributes={ activeAttributes }
onClose={ this.stopAddingLink }
onChange={ onChange }
onRemove={ this.onRemoveFormat }
value={ linkSelection }
/>
<RichTextToolbarButton
name="link"
icon={ linkIcon }
title={ __( 'Link' ) }
onClick={ this.addLink }
isActive={ isActive }
shortcutType="primary"
shortcutCharacter="k"
/>
</>
);
}
}
),
};