@wordpress/block-library
Version:
Block library for the WordPress editor.
602 lines (548 loc) • 15.1 kB
JavaScript
/**
* External dependencies
*/
import { View, TouchableWithoutFeedback, Text } from 'react-native';
import Clipboard from '@react-native-clipboard/clipboard';
/**
* WordPress dependencies
*/
import {
requestImageFailedRetryDialog,
requestImageUploadCancelDialog,
mediaUploadSync,
} from '@wordpress/react-native-bridge';
import {
BlockIcon,
MediaPlaceholder,
MediaUploadProgress,
RichText,
PlainText,
BlockControls,
MediaUpload,
InspectorControls,
MEDIA_TYPE_ANY,
store as blockEditorStore,
} from '@wordpress/block-editor';
import {
ToolbarButton,
ToolbarGroup,
PanelBody,
ToggleControl,
TextControl,
SelectControl,
Icon,
} from '@wordpress/components';
import {
file as icon,
replace,
button,
external,
link,
warning,
} from '@wordpress/icons';
import { Component } from '@wordpress/element';
import { __, _x } from '@wordpress/i18n';
import { compose, withPreferredColorScheme } from '@wordpress/compose';
import { withDispatch, withSelect } from '@wordpress/data';
import { getProtocol } from '@wordpress/url';
import { store as coreStore } from '@wordpress/core-data';
/**
* Internal dependencies
*/
import styles from './style.scss';
const URL_COPIED_NOTIFICATION_DURATION_MS = 1500;
const MIN_WIDTH = 40;
export class FileEdit extends Component {
constructor( props ) {
super( props );
this.state = {
isUploadInProgress: false,
isSidebarLinkSettings: false,
placeholderTextWidth: 0,
maxWidth: 0,
};
this.timerRef = null;
this.onLayout = this.onLayout.bind( this );
this.onSelectFile = this.onSelectFile.bind( this );
this.onChangeFileName = this.onChangeFileName.bind( this );
this.onChangeDownloadButtonText =
this.onChangeDownloadButtonText.bind( this );
this.updateMediaProgress = this.updateMediaProgress.bind( this );
this.finishMediaUploadWithSuccess =
this.finishMediaUploadWithSuccess.bind( this );
this.finishMediaUploadWithFailure =
this.finishMediaUploadWithFailure.bind( this );
this.getFileComponent = this.getFileComponent.bind( this );
this.onChangeDownloadButtonVisibility =
this.onChangeDownloadButtonVisibility.bind( this );
this.onCopyURL = this.onCopyURL.bind( this );
this.onChangeOpenInNewWindow =
this.onChangeOpenInNewWindow.bind( this );
this.onChangeLinkDestinationOption =
this.onChangeLinkDestinationOption.bind( this );
this.onShowLinkSettings = this.onShowLinkSettings.bind( this );
this.onFilePressed = this.onFilePressed.bind( this );
this.mediaUploadStateReset = this.mediaUploadStateReset.bind( this );
}
componentDidMount() {
const { attributes, setAttributes } = this.props;
const { downloadButtonText } = attributes;
if ( RichText.isEmpty( downloadButtonText ) ) {
setAttributes( {
downloadButtonText: _x( 'Download', 'button label' ),
} );
}
if (
attributes.id &&
attributes.url &&
getProtocol( attributes.url ) === 'file:'
) {
mediaUploadSync();
}
}
componentWillUnmount() {
clearTimeout( this.timerRef );
}
componentDidUpdate( prevProps ) {
if (
prevProps.isSidebarOpened &&
! this.props.isSidebarOpened &&
this.state.isSidebarLinkSettings
) {
this.setState( { isSidebarLinkSettings: false } );
}
}
onSelectFile( media ) {
this.props.setAttributes( {
href: media.url,
fileName: media.title,
textLinkHref: media.url,
id: media.id,
} );
}
onChangeFileName( fileName ) {
this.props.setAttributes( { fileName } );
}
onChangeDownloadButtonText( downloadButtonText ) {
this.props.setAttributes( { downloadButtonText } );
}
onChangeDownloadButtonVisibility( showDownloadButton ) {
this.props.setAttributes( { showDownloadButton } );
}
onChangeLinkDestinationOption( newHref ) {
// Choose Media File or Attachment Page (when file is in Media Library)
this.props.setAttributes( { textLinkHref: newHref } );
}
onCopyURL() {
if ( this.state.isUrlCopied ) {
return;
}
const { href } = this.props.attributes;
Clipboard.setString( href );
this.setState( { isUrlCopied: true } );
this.timerRef = setTimeout( () => {
this.setState( { isUrlCopied: false } );
}, URL_COPIED_NOTIFICATION_DURATION_MS );
}
onChangeOpenInNewWindow( newValue ) {
this.props.setAttributes( {
textLinkTarget: newValue ? '_blank' : false,
} );
}
updateMediaProgress( payload ) {
const { setAttributes } = this.props;
if ( payload.mediaUrl ) {
setAttributes( { url: payload.mediaUrl } );
}
if ( ! this.state.isUploadInProgress ) {
this.setState( { isUploadInProgress: true } );
}
}
finishMediaUploadWithSuccess( payload ) {
const { setAttributes } = this.props;
setAttributes( {
href: payload.mediaUrl,
id: payload.mediaServerId,
textLinkHref: payload.mediaUrl,
} );
this.setState( { isUploadInProgress: false } );
}
finishMediaUploadWithFailure( payload ) {
this.props.setAttributes( { id: payload.mediaId } );
this.setState( { isUploadInProgress: false } );
}
mediaUploadStateReset() {
const { setAttributes } = this.props;
setAttributes( {
id: null,
href: null,
textLinkHref: null,
fileName: null,
} );
this.setState( { isUploadInProgress: false } );
}
onShowLinkSettings() {
this.setState(
{
isSidebarLinkSettings: true,
},
this.props.openSidebar
);
}
getToolbarEditButton( open ) {
return (
<BlockControls>
<ToolbarGroup>
<ToolbarButton
title={ __( 'Edit file' ) }
icon={ replace }
onClick={ open }
/>
<ToolbarButton
title={ __( 'Link To' ) }
icon={ link }
onClick={ this.onShowLinkSettings }
/>
</ToolbarGroup>
</BlockControls>
);
}
getInspectorControls(
{ showDownloadButton, textLinkTarget, href, textLinkHref },
media,
isUploadInProgress,
isUploadFailed
) {
let linkDestinationOptions = [ { value: href, label: __( 'URL' ) } ];
const attachmentPage = media && media.link;
const { isSidebarLinkSettings } = this.state;
if ( attachmentPage ) {
linkDestinationOptions = [
{ value: href, label: __( 'Media file' ) },
{ value: attachmentPage, label: __( 'Attachment page' ) },
];
}
const actionButtonStyle = this.props.getStylesFromColorScheme(
styles.actionButton,
styles.actionButtonDark
);
const isCopyUrlDisabled = isUploadFailed || isUploadInProgress;
const dimmedActionButtonStyle = this.props.getStylesFromColorScheme(
styles.dimmedActionButton,
styles.dimmedActionButtonDark
);
const finalButtonStyle = isCopyUrlDisabled
? dimmedActionButtonStyle
: actionButtonStyle;
return (
<InspectorControls>
{ isSidebarLinkSettings || (
<PanelBody title={ __( 'File block settings' ) } />
) }
<PanelBody>
<SelectControl
icon={ link }
label={ __( 'Link to' ) }
value={ textLinkHref }
onChange={ this.onChangeLinkDestinationOption }
options={ linkDestinationOptions }
hideCancelButton
/>
<ToggleControl
icon={ external }
label={ __( 'Open in new tab' ) }
checked={ textLinkTarget === '_blank' }
onChange={ this.onChangeOpenInNewWindow }
/>
{ ! isSidebarLinkSettings && (
<ToggleControl
icon={ button }
label={ __( 'Show download button' ) }
checked={ showDownloadButton }
onChange={ this.onChangeDownloadButtonVisibility }
/>
) }
<TextControl
disabled={ isCopyUrlDisabled }
label={
this.state.isUrlCopied
? __( 'Copied!' )
: __( 'Copy file URL' )
}
labelStyle={
this.state.isUrlCopied || finalButtonStyle
}
onPress={ this.onCopyURL }
/>
</PanelBody>
</InspectorControls>
);
}
getStyleForAlignment( align ) {
const getFlexAlign = ( alignment ) => {
switch ( alignment ) {
case 'right':
return 'flex-end';
case 'center':
return 'center';
default:
return 'flex-start';
}
};
return { alignSelf: getFlexAlign( align ) };
}
getTextAlignmentForAlignment( align ) {
switch ( align ) {
case 'right':
return 'right';
case 'center':
return 'center';
default:
return 'left';
}
}
onFilePressed() {
const { attributes } = this.props;
if ( this.state.isUploadInProgress ) {
requestImageUploadCancelDialog( attributes.id );
} else if (
attributes.id &&
getProtocol( attributes.href ) === 'file:'
) {
requestImageFailedRetryDialog( attributes.id );
}
}
onLayout( { nativeEvent } ) {
const { width } = nativeEvent.layout;
const { paddingLeft, paddingRight } = styles.defaultButton;
this.setState( {
maxWidth: width - ( paddingLeft + paddingRight ),
} );
}
// Render `Text` with `placeholderText` styled as a placeholder
// to calculate its width which then is set as a `minWidth`
// This should be fixed on RNAztec level. In the mean time,
// We use the same strategy implemented in Button block.
getPlaceholderWidth( placeholderText ) {
const { maxWidth, placeholderTextWidth } = this.state;
return (
<Text
style={ styles.placeholder }
onTextLayout={ ( { nativeEvent } ) => {
const textWidth =
nativeEvent.lines[ 0 ] && nativeEvent.lines[ 0 ].width;
if ( textWidth && textWidth !== placeholderTextWidth ) {
this.setState( {
placeholderTextWidth: Math.min(
textWidth,
maxWidth
),
} );
}
} }
>
{ placeholderText }
</Text>
);
}
getFileComponent( openMediaOptions, getMediaOptions ) {
const { attributes, media, isSelected } = this.props;
const { isButtonFocused, placeholderTextWidth } = this.state;
const { fileName, downloadButtonText, id, showDownloadButton, align } =
attributes;
const minWidth =
isButtonFocused ||
( ! isButtonFocused &&
downloadButtonText &&
downloadButtonText !== '' )
? MIN_WIDTH
: placeholderTextWidth;
const placeholderText =
isButtonFocused ||
( ! isButtonFocused &&
downloadButtonText &&
downloadButtonText !== '' )
? ''
: __( 'Add text…' );
return (
<MediaUploadProgress
mediaId={ id }
onUpdateMediaProgress={ this.updateMediaProgress }
onFinishMediaUploadWithSuccess={
this.finishMediaUploadWithSuccess
}
onFinishMediaUploadWithFailure={
this.finishMediaUploadWithFailure
}
onMediaUploadStateReset={ this.mediaUploadStateReset }
renderContent={ ( { isUploadInProgress, isUploadFailed } ) => {
const dimmedStyle =
( this.state.isUploadInProgress || isUploadFailed ) &&
styles.disabledButton;
const finalButtonStyle = [
styles.defaultButton,
dimmedStyle,
];
const errorIconStyle = Object.assign(
{},
styles.errorIcon,
styles.uploadFailed
);
return (
<TouchableWithoutFeedback
accessible={ ! isSelected }
onPress={ this.onFilePressed }
disabled={ ! isSelected }
>
<View
onLayout={ this.onLayout }
testID="file-edit-container"
>
{ this.getPlaceholderWidth( placeholderText ) }
{ isUploadInProgress ||
this.getToolbarEditButton(
openMediaOptions
) }
{ getMediaOptions() }
{ isSelected &&
this.getInspectorControls(
attributes,
media,
isUploadInProgress,
isUploadFailed
) }
<View style={ styles.container }>
<RichText
withoutInteractiveFormatting
__unstableMobileNoFocusOnMount
onChange={ this.onChangeFileName }
placeholder={ __( 'File name' ) }
tagName="p"
underlineColorAndroid="transparent"
value={ fileName }
deleteEnter
textAlign={ this.getTextAlignmentForAlignment(
align
) }
/>
{ isUploadFailed && (
<View style={ styles.errorContainer }>
<Icon
icon={ warning }
style={ errorIconStyle }
/>
<PlainText
editable={ false }
value={ __( 'Error' ) }
style={ styles.uploadFailed }
/>
</View>
) }
</View>
{ showDownloadButton &&
this.state.maxWidth > 0 && (
<View
style={ [
finalButtonStyle,
this.getStyleForAlignment(
align
),
] }
>
<RichText
withoutInteractiveFormatting
__unstableMobileNoFocusOnMount
tagName="p"
textAlign="center"
minWidth={ minWidth }
maxWidth={ this.state.maxWidth }
deleteEnter
style={ styles.buttonText }
value={ downloadButtonText }
placeholder={ placeholderText }
unstableOnFocus={ () =>
this.setState( {
isButtonFocused: true,
} )
}
onBlur={ () =>
this.setState( {
isButtonFocused: false,
} )
}
selectionColor={
styles.buttonText.color
}
placeholderTextColor={
styles.placeholderTextColor
.color
}
underlineColorAndroid="transparent"
onChange={
this
.onChangeDownloadButtonText
}
/>
</View>
) }
</View>
</TouchableWithoutFeedback>
);
} }
/>
);
}
render() {
const { attributes, wasBlockJustInserted, isSelected } = this.props;
const { href } = attributes;
if ( ! href ) {
return (
<MediaPlaceholder
icon={ <BlockIcon icon={ icon } /> }
labels={ {
title: __( 'File' ),
instructions: __( 'Choose a file' ),
} }
onSelect={ this.onSelectFile }
onFocus={ this.props.onFocus }
allowedTypes={ [ MEDIA_TYPE_ANY ] }
autoOpenMediaUpload={ isSelected && wasBlockJustInserted }
/>
);
}
return (
<MediaUpload
allowedTypes={ [ MEDIA_TYPE_ANY ] }
isReplacingMedia
onSelect={ this.onSelectFile }
render={ ( { open, getMediaOptions } ) => {
return this.getFileComponent( open, getMediaOptions );
} }
/>
);
}
}
export default compose( [
withSelect( ( select, props ) => {
const { attributes, isSelected, clientId } = props;
const { id, href } = attributes;
const { isEditorSidebarOpened } = select( 'core/edit-post' );
const isNotFileHref = id && getProtocol( href ) !== 'file:';
return {
media: isNotFileHref
? select( coreStore ).getMedia( id )
: undefined,
isSidebarOpened: isSelected && isEditorSidebarOpened(),
wasBlockJustInserted: select(
blockEditorStore
).wasBlockJustInserted( clientId, 'inserter_menu' ),
};
} ),
withDispatch( ( dispatch ) => {
const { openGeneralSidebar } = dispatch( 'core/edit-post' );
return {
openSidebar: () => openGeneralSidebar( 'edit-post/block' ),
};
} ),
withPreferredColorScheme,
] )( FileEdit );