Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion components/clipboard-button/index.js
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
/**
* External depedencies
*/
import classnames from 'classnames';

/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import { getBlobByURL, revokeBlobURL } from '@wordpress/blob';
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 ) ) {
const file = getBlobByURL( href );

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
Original file line number Diff line number Diff line change
@@ -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
Copy Markdown
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
Copy Markdown
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
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading