-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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 #6805
Implement File block #6805
Changes from 47 commits
8459693
263e817
9dfb13f
602e2be
d528d4c
00b7b7a
2dec5a4
cab2dde
5285db7
d2b7a67
f5baad2
b924e00
b261b8e
0a122e2
b82cb37
423f4b4
d4bb0f3
826be70
6e8ef78
0f78b07
1407609
938246f
e4ce6b4
5cca7d2
b415e7c
479ccf4
6083fa2
1a8020c
f9bde62
bda121d
b628fb8
99d28b2
4aa49aa
3df9827
6aa6f60
d2874ca
aba955f
8325413
17d5a00
41b6309
4bfe800
d2e95c7
768a46d
ba01c11
c91fe9c
0be3946
677b35c
f19973b
f07f213
80c802e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,228 @@ | ||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { __ } from '@wordpress/i18n'; | ||
import { getBlobByURL, revokeBlobURL } from '@wordpress/utils'; | ||
import { | ||
ClipboardButton, | ||
IconButton, | ||
Toolbar, | ||
withAPIData, | ||
withNotices, | ||
} from '@wordpress/components'; | ||
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 ); | ||
|
||
const { | ||
showDownloadButton = true, | ||
buttonText = __( 'Download' ), | ||
} = this.props.attributes; | ||
|
||
this.onSelectFile = this.onSelectFile.bind( this ); | ||
|
||
// Initialize default values if undefined | ||
this.props.setAttributes( { | ||
showDownloadButton, | ||
buttonText, | ||
} ); | ||
|
||
this.state = { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am not sure I follow the reason we keep the attributes in the state, too. What do we gain? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You're right, I've cleaned them out. |
||
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: '*', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just to make sure I understand correctly, once #6968 gets in we will feed the list if types here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. From #5462:
I don't think that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, I was under the impression that the "media" in Even /edit-post/hooks/blocks/media-upload, which includes specific logic for when gallery == true, seems generic enough for any kind of file. Is there a reason we want to keep There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I posted a separate issue for the |
||
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 ) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This worked great for me as a user. |
||
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, | ||
}, this.props.media.get /* refetch attachment page url */ ); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Totally assumed |
||
} | ||
} | ||
|
||
isBlobURL( url = '' ) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it make sense to move this one to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, I think so. But in a separate PR, because I think I saw a couple of other places in the codebase where we’re doing blob url detection, and I’d want to change them all at once. |
||
return url.indexOf( 'blob:' ) === 0; | ||
} | ||
|
||
render() { | ||
const { | ||
fileName, | ||
href, | ||
textLinkHref, | ||
openInNewWindow, | ||
showDownloadButton, | ||
buttonText, | ||
id, | ||
} = this.props.attributes; | ||
const { | ||
className, | ||
isSelected, | ||
setAttributes, | ||
noticeUI, | ||
noticeOperations, | ||
media, | ||
} = this.props; | ||
const { showCopyConfirmation } = this.state; | ||
const attachmentPage = ( id !== undefined ) && media.data && media.data.link; | ||
|
||
const classNames = [ | ||
className, | ||
this.isBlobURL( href ) ? 'is-transient' : '', | ||
].join( ' ' ); | ||
|
||
const confirmCopyURL = () => { | ||
this.setState( { showCopyConfirmation: true } ); | ||
}; | ||
const resetCopyConfirmation = () => { | ||
this.setState( { showCopyConfirmation: false } ); | ||
}; | ||
|
||
// Choose Media File or Attachment Page (when file is in Media Library) | ||
const changeLinkDestinationOption = ( newHref ) => { | ||
setAttributes( { textLinkHref: newHref } ); | ||
}; | ||
const changeOpenInNewWindow = ( newValue ) => { | ||
setAttributes( { | ||
openInNewWindow: newValue ? '_blank' : false, | ||
} ); | ||
}; | ||
const changeShowDownloadButton = ( newValue ) => { | ||
setAttributes( { showDownloadButton: newValue } ); | ||
}; | ||
|
||
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, | ||
showDownloadButton, | ||
changeLinkDestinationOption, | ||
changeOpenInNewWindow, | ||
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={ classNames }> | ||
<div> | ||
<FileBlockEditableLink | ||
className={ className } | ||
placeholder={ __( 'Write file name…' ) } | ||
text={ fileName } | ||
href={ textLinkHref } | ||
updateFileName={ ( text ) => setAttributes( { fileName: text } ) } | ||
/> | ||
{ showDownloadButton && | ||
<div className={ `${ className }__button-richtext-wrapper` }> | ||
<RichText | ||
tagName="div" // must be block-level or else cursor disappears | ||
className={ `${ className }__button` } | ||
value={ buttonText } | ||
formattingControls={ [] } // disable controls | ||
placeholder={ __( 'Add text…' ) } | ||
keepPlaceholderOnFocus | ||
multiline="false" | ||
onChange={ ( text ) => setAttributes( { buttonText: text } ) } | ||
/> | ||
</div> | ||
} | ||
</div> | ||
{ isSelected && | ||
<ClipboardButton | ||
isDefault | ||
text={ href } | ||
className={ `${ className }__copy-url-button` } | ||
onCopy={ confirmCopyURL } | ||
onFinishCopy={ resetCopyConfirmation } | ||
> | ||
{ showCopyConfirmation ? __( 'Copied!' ) : __( 'Copy URL' ) } | ||
</ClipboardButton> | ||
} | ||
</div> | ||
</Fragment> | ||
); | ||
} | ||
} | ||
|
||
export default compose( [ | ||
withAPIData( ( props ) => ( { | ||
media: `/wp/v2/media/${ props.attributes.id }`, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. export default compose( [
withSelect( ( select, props ) => {
const { getMedia } = select( 'core' );
return {
image: getMedia( props.attributes.id ), // not sure if getMedia handles undefined values
};
} ),
withNotices,
] )( FileEdit ); There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you, I wasn't aware of this. I'll look into it. |
||
} ) ), | ||
withNotices, | ||
] )( FileEdit ); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { Component, Fragment } from '@wordpress/element'; | ||
|
||
export default class FileBlockEditableLink extends Component { | ||
constructor() { | ||
super( ...arguments ); | ||
|
||
this.state = { | ||
showPlaceholder: ! this.props.text, | ||
}; | ||
} | ||
|
||
componentDidUpdate( prevProps ) { | ||
if ( prevProps.text !== this.props.text ) { | ||
this.setState( { showPlaceholder: ! this.props.text } ); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What are the trade-offs of using state here vs. just using the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since I'm resorting to use a content-editable here, this is not a "controlled" component and therefore the |
||
} | ||
} | ||
|
||
render() { | ||
const { className, placeholder, text, href, updateFileName } = this.props; | ||
const { showPlaceholder } = this.state; | ||
|
||
const copyLinkToClipboard = ( e ) => { | ||
const selectedText = document.getSelection().toString(); | ||
const htmlLink = `<a href="${ href }">${ selectedText }</a>`; | ||
e.clipboardData.setData( 'text/plain', selectedText ); | ||
e.clipboardData.setData( 'text/html', htmlLink ); | ||
}; | ||
|
||
const forcePlainTextPaste = ( e ) => { | ||
e.preventDefault(); | ||
|
||
const selection = document.getSelection(); | ||
const clipboard = | ||
e.clipboardData.getData( 'text/plain' ).replace( /[\n\r]/g, '' ); | ||
const textNode = document.createTextNode( clipboard ); | ||
|
||
selection.getRangeAt( 0 ).insertNode( textNode ); | ||
selection.collapseToEnd(); | ||
}; | ||
|
||
const showPlaceholderIfEmptyString = ( e ) => { | ||
if ( e.target.innerText === '' ) { | ||
this.setState( { showPlaceholder: true } ); | ||
} else { | ||
this.setState( { showPlaceholder: false } ); | ||
} | ||
}; | ||
|
||
return ( | ||
<Fragment> | ||
<a | ||
aria-label={ placeholder } | ||
className={ `${ className }__textlink` } | ||
contentEditable={ true } | ||
href={ href } | ||
onBlur={ ( e ) => updateFileName( e.target.innerText ) } | ||
onInput={ showPlaceholderIfEmptyString } | ||
onCopy={ copyLinkToClipboard } | ||
onCut={ copyLinkToClipboard } | ||
onPaste={ forcePlainTextPaste } | ||
> | ||
{ 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={ ( e ) => e.target.previousSibling.focus() } | ||
> | ||
{ placeholder } | ||
</span> | ||
/* eslint-enable jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */ | ||
} | ||
</Fragment> | ||
); | ||
} | ||
} |
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; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the detailed comment, explaining why do we have the workaround.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One problem in Safari, though, is that the while block (
<!-- wp:file {"href":…
) ends up in the clipboard.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, it looks like this workaround hadn't fixed the Safari bug completely. I did some testing and narrowed down the repro condition a bit. My workaround seems to be effective for pre-existing posts (i.e. posts that were already saved when the page loaded), but not for new posts. Not sure what's going on, but that's a start... I'll have to look into it further.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed in #7106