Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement File block #7622

Merged
merged 20 commits into from Jul 3, 2018
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 10 additions & 1 deletion components/clipboard-button/index.js
Expand Up @@ -83,8 +83,17 @@ class ClipboardButton extends Component {
const classes = classnames( 'components-clipboard-button', className );
const ComponentToUse = icon ? IconButton : Button;

// Workaround for inconsistent behavior in Safari, where <textarea> is not
// the document.activeElement at the moment when the copy event fires.
// This causes documentHasSelection() in the copy-handler component to
// mistakenly override the ClipboardButton, and copy a serialized string
// of the current block instead.
const focusOnCopyEventTarget = ( event ) => {
event.target.focus();
};

return (
<span ref={ this.bindContainer }>
<span ref={ this.bindContainer } onCopy={ focusOnCopyEventTarget }>
<ComponentToUse { ...buttonProps } className={ classes }>
{ children }
</ComponentToUse>
Expand Down
235 changes: 235 additions & 0 deletions core-blocks/file/edit.js
@@ -0,0 +1,235 @@
/**
* External depedencies
*/
import classnames from 'classnames';

/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import { getBlobByURL, revokeBlobURL } from '@wordpress/utils';
import {
ClipboardButton,
IconButton,
Toolbar,
withNotices,
} from '@wordpress/components';
import { withSelect } from '@wordpress/data';
import { Component, compose, Fragment } from '@wordpress/element';
import {
MediaUpload,
MediaPlaceholder,
BlockControls,
RichText,
editorMediaUpload,
} from '@wordpress/editor';

/**
* Internal dependencies
*/
import './editor.scss';
import FileBlockInspector from './inspector';
import FileBlockEditableLink from './editable-link';

class FileEdit extends Component {
constructor() {
super( ...arguments );

this.onSelectFile = this.onSelectFile.bind( this );
this.confirmCopyURL = this.confirmCopyURL.bind( this );
this.resetCopyConfirmation = this.resetCopyConfirmation.bind( this );
this.changeLinkDestinationOption = this.changeLinkDestinationOption.bind( this );
this.changeOpenInNewWindow = this.changeOpenInNewWindow.bind( this );
this.changeShowDownloadButton = this.changeShowDownloadButton.bind( this );

this.state = {
showCopyConfirmation: false,
};
}

componentDidMount() {
const { href } = this.props.attributes;

// Upload a file drag-and-dropped into the editor
if ( this.isBlobURL( href ) ) {
getBlobByURL( href )
.then( ( file ) => {
editorMediaUpload( {
allowedType: '*',
filesList: [ file ],
onFileChange: ( [ media ] ) => this.onSelectFile( media ),
} );
revokeBlobURL( href );
} );
}
}

componentDidUpdate( prevProps ) {
// Reset copy confirmation state when block is deselected
if ( prevProps.isSelected && ! this.props.isSelected ) {
this.setState( { showCopyConfirmation: false } );
}
}

onSelectFile( media ) {
if ( media && media.url ) {
this.props.setAttributes( {
href: media.url,
fileName: media.title,
textLinkHref: media.url,
id: media.id,
} );
}
}

isBlobURL( url = '' ) {
return url.indexOf( 'blob:' ) === 0;
}

confirmCopyURL() {
this.setState( { showCopyConfirmation: true } );
}

resetCopyConfirmation() {
this.setState( { showCopyConfirmation: false } );
}

changeLinkDestinationOption( newHref ) {
// Choose Media File or Attachment Page (when file is in Media Library)
this.props.setAttributes( { textLinkHref: newHref } );
}

changeOpenInNewWindow( newValue ) {
this.props.setAttributes( {
textLinkTarget: newValue ? '_blank' : false,
} );
}

changeShowDownloadButton( newValue ) {
this.props.setAttributes( { showDownloadButton: newValue } );
}

render() {
const {
className,
isSelected,
attributes,
setAttributes,
noticeUI,
noticeOperations,
media,
} = this.props;
const {
fileName,
href,
textLinkHref,
textLinkTarget,
showDownloadButton,
downloadButtonText,
id,
} = attributes;
const { showCopyConfirmation } = this.state;
const attachmentPage = media && media.link;

const classes = classnames( className, {
'is-transient': this.isBlobURL( href ),
} );

if ( ! href ) {
return (
<MediaPlaceholder
icon="media-default"
labels={ {
title: __( 'File' ),
name: __( 'a file' ),
} }
onSelect={ this.onSelectFile }
notices={ noticeUI }
onError={ noticeOperations.createErrorNotice }
accept="*"
type="*"
/>
);
}

return (
<Fragment>
<FileBlockInspector
hrefs={ { href, textLinkHref, attachmentPage } }
{ ...{
openInNewWindow: !! textLinkTarget,
showDownloadButton,
changeLinkDestinationOption: this.changeLinkDestinationOption,
changeOpenInNewWindow: this.changeOpenInNewWindow,
changeShowDownloadButton: this.changeShowDownloadButton,
} }
/>
<BlockControls>
<Toolbar>
<MediaUpload
onSelect={ this.onSelectFile }
type="*"
value={ id }
render={ ( { open } ) => (
<IconButton
className="components-toolbar__control"
label={ __( 'Edit file' ) }
onClick={ open }
icon="edit"
/>
) }
/>
</Toolbar>
</BlockControls>
<div className={ classes }>
<div>
<FileBlockEditableLink
className={ className }
placeholder={ __( 'Write file name…' ) }
text={ fileName }
href={ textLinkHref }
updateFileName={ ( text ) => setAttributes( { fileName: text } ) }
/>
{ showDownloadButton &&
<div className={ `${ className }__button-richtext-wrapper` }>
{ /* Using RichText here instead of PlainText so that it can be styled like a button */ }
<RichText
tagName="div" // must be block-level or else cursor disappears
className={ `${ className }__button` }
value={ downloadButtonText }
formattingControls={ [] } // disable controls
placeholder={ __( 'Add text…' ) }
keepPlaceholderOnFocus
multiline="false"
onChange={ ( text ) => setAttributes( { downloadButtonText: text } ) }
/>
</div>
}
</div>
{ isSelected &&
<ClipboardButton
isDefault
text={ href }
className={ `${ className }__copy-url-button` }
onCopy={ this.confirmCopyURL }
onFinishCopy={ this.resetCopyConfirmation }
>
{ showCopyConfirmation ? __( 'Copied!' ) : __( 'Copy URL' ) }
</ClipboardButton>
}
</div>
</Fragment>
);
}
}

export default compose( [
withSelect( ( select, props ) => {
const { getMedia } = select( 'core' );
const { id } = props.attributes;
return {
media: id === undefined ? undefined : getMedia( id ),
};
} ),
withNotices,
] )( FileEdit );
79 changes: 79 additions & 0 deletions core-blocks/file/editable-link.js
@@ -0,0 +1,79 @@
/**
* WordPress dependencies
*/
import { Component, Fragment } from '@wordpress/element';

export default class FileBlockEditableLink extends Component {
constructor() {
super( ...arguments );

this.copyLinkToClipboard = this.copyLinkToClipboard.bind( this );
this.showPlaceholderIfEmptyString = this.showPlaceholderIfEmptyString.bind( this );

this.state = {
showPlaceholder: ! this.props.text,
};
}

componentDidUpdate( prevProps ) {
if ( prevProps.text !== this.props.text ) {
this.setState( { showPlaceholder: ! this.props.text } );
}
}

copyLinkToClipboard( event ) {
const selectedText = document.getSelection().toString();
const htmlLink = `<a href="${ this.props.href }">${ selectedText }</a>`;
event.clipboardData.setData( 'text/plain', selectedText );
event.clipboardData.setData( 'text/html', htmlLink );
}

forcePlainTextPaste( event ) {
event.preventDefault();

const selection = document.getSelection();
const clipboard = event.clipboardData.getData( 'text/plain' ).replace( /[\n\r]/g, '' );
const textNode = document.createTextNode( clipboard );

selection.getRangeAt( 0 ).insertNode( textNode );
selection.collapseToEnd();
}

showPlaceholderIfEmptyString( event ) {
this.setState( { showPlaceholder: event.target.innerText === '' } );
}

render() {
const { className, placeholder, text, href, updateFileName } = this.props;
const { showPlaceholder } = this.state;

return (
<Fragment>
<a
aria-label={ placeholder }
className={ `${ className }__textlink` }
href={ href }
onBlur={ ( event ) => updateFileName( event.target.innerText ) }
onInput={ this.showPlaceholderIfEmptyString }
onCopy={ this.copyLinkToClipboard }
onCut={ this.copyLinkToClipboard }
onPaste={ this.forcePlainTextPaste }
contentEditable
Copy link
Member

Choose a reason for hiding this comment

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

Warning:

Warning: A component is contentEditable and contains children managed by React. It is now your responsibility to guarantee that none of those nodes are unexpectedly modified or duplicated. This is probably not intentional.

Should this be using the RichText component instead? There are many complexities of contentEditable that are meant to be abstracted into this component for general usage like this.

Copy link
Member

Choose a reason for hiding this comment

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

Issue at #8023

>
{ text }
</a>
{ showPlaceholder &&
// Disable reason: Only useful for mouse users
/* eslint-disable jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */
<span
className={ `${ className }__textlink-placeholder` }
onClick={ ( event ) => event.target.previousSibling.focus() }
>
{ placeholder }
</span>
/* eslint-enable jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */
}
</Fragment>
);
}
}
35 changes: 35 additions & 0 deletions core-blocks/file/editor.scss
@@ -0,0 +1,35 @@
.wp-block-file {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0;

&.is-transient {
@include loading_fade;
}

&__textlink {
color: $blue-medium-700;
min-width: 1em;
text-decoration: underline;

&:focus {
box-shadow: none;
color: $blue-medium-700;
}
}

&__textlink-placeholder {
opacity: .5;
text-decoration: underline;
}

&__button-richtext-wrapper {
display: inline-block;
margin-left: 0.75em;
}

&__copy-url-button {
margin-left: 1em;
}
}