-
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
Editable: Adding an inline link control to the format toolbar #524
Conversation
This is not polished yet, but I wanted to have some feedback. |
e2c1f8a
to
a25fe23
Compare
Nice work! I do see that it's still in progress, but this gets us much closer to the current link behavior, and it looks good! |
An issue I'm observing:
Expected: Link to be removed Guessing something to do with focus being changed. Though strangely it does work with the same steps if in step 1 you highlight text instead of simply clicking into it. |
/** | ||
* Internal dependencies | ||
*/ | ||
import './style.scss'; |
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.
I don't think we need to import the styles here if we're already doing so in index.js
.
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.
👍
// as we're doing here; instead, we should consider a common components path. | ||
import IconButton from '../../../editor/components/icon-button'; | ||
|
||
const formattingControls = [ |
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.
Idea: Do you think there's value in a naming convention for constants. The uh, well... "screaming snake case" convention of FORMATTING_CONTROLS
?
There's precedent with the PHP styling guidelines, but not a similar mention in JavaScript (perhaps worth championing):
Constants should be in all upper-case with underscores separating words:
define( 'DOING_AJAX', true );
https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#naming-conventions
blocks/components/editable/index.js
Outdated
} else { | ||
this.editor.formatter.apply( format ); | ||
} | ||
Object.keys( formats ).forEach( format => { |
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.
Could use Lodash's forEach
which works on objects and would optimize from O(2n)
to O(n)
:
forEach( formats, ( formatValue, format ) => {
onClick: () => this.toggleFormat( control.format ), | ||
isActive: this.isFormatActive( control.format ) | ||
} ) ) } /> | ||
<FormatToolbar focusPosition={ this.state.focusPosition } formats={ this.state.formats } onChange={ this.changeFormats } /> |
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.
Might want to split these props onto multiple lines to avoid the long line length.
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.
Instead of calculating the position, would it be possible to do some absolute positioning within a relative control parent for the overlay?
Something like: https://codepen.io/aduth/pen/LyxJEb
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.
Oh, I see now we're trying to position the overlay atop the selected text. I suppose this works too, just maybe a little unexpected to have to move cursor to toolbar to add link, only to have to move it back to text to configure the link. I do see this is how it works in the current editor though.
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.
Maybe something for @jasmussen to weigh in on with regards to the placement of the link dialog. Should it be next to the selected text, or next to the now-active link toolbar control?
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.
I do like this link design (though the specific buttons are a bit quickly put together):
@iseulde should be in the loop on the link work we're doing here, because IMO links work better in the current WordPress editor than anywhere else, and she worked a lot on them. Having the dialog under as opposed to over was for a reason, though I can't recall which.
Also, here's an admittedly suboptimal design for when you are linking inside captions:
Still thinking about that one, but it's at least one argument for having links under.
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.
Yeah we put in under the link because we kept in mind that we might want to do something different above. There might be more reasons I don't remember though.
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.
Oh, I see now we're trying to position the overlay atop the selected text. I suppose this works too, just maybe a little unexpected to have to move cursor to toolbar to add link, only to have to move it back to text to configure the link. I do see this is how it works in the current editor though.
We were planning to look into putting the formatting tools inline which would solve this. If we keep them on top, it might be worth exploring other solutions though. There's also cmd+K and pasting links over words :)
blocks/components/editable/index.js
Outdated
} | ||
Object.keys( formats ).forEach( format => { | ||
const formatValue = formats[ format ]; | ||
if ( format === 'link' ) { |
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.
Based on the distinctly unique treatment between links and other formatting, I'm wondering if they ought to be handled separately. Maybe not; I do like the single onChange
prop applied to the toolbar component.
const { formats, focusPosition } = this.props; | ||
const linkStyle = focusPosition | ||
? { position: 'absolute', ...focusPosition } | ||
: {}; |
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.
Minor, and doesn't affect this case, but I tend to default to null
as the empty value, since it can be strictly compared, in case what you're applying it to happens to be a pure component.
style={ linkStyle } | ||
onSubmit={ this.submitLink }> | ||
<input type="url" value={ this.state.linkValue } onChange={ this.updateLinkValue } /> | ||
<IconButton icon="editor-break" onClick={ this.submitLink } /> |
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.
I wonder if we ought to be able to make IconButton
behave as a submit. Actually, based on the additional props behavior, we might just be able to pass type="submit"
.
blocks/components/editable/index.js
Outdated
Object.keys( formats ).forEach( format => { | ||
const formatValue = formats[ format ]; | ||
if ( format === 'link' ) { | ||
if ( formatValue !== undefined ) { |
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.
To this and the above TODO
, what's the use case for an empty link? Should we just check general truthiness here, including treating an empty string as an unlink intent?
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.
The use-case is showing the link modal at the right position when we toggle the link control in the toolbar.
To show the link modal we could rely on a state boolean but we need a dom element around our selection to compute the right position for the link modal. So my idea is to insert an empty link when we hit the link control.
this.addLink = this.addLink.bind( this ); | ||
this.dropLink = this.dropLink.bind( this ); | ||
this.submitLink = this.submitLink.bind( this ); | ||
this.updateLinkValue = this.updateLinkValue.bind( this ); |
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.
The instance property initializers feature is growing on me 😅
b3b3741
to
833b944
Compare
addLink() { | ||
if ( ! this.props.formats.link ) { | ||
// TODO find a way to add an empty link to TinyMCE | ||
this.props.onChange( { link: 'http://wordpress.org' } ); |
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.
Here's where I need an empty link @iseulde. This allows adding a "node" to show the link modal at the right place
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.
In core we put _wp_link_placeholder
as the href
and then make sure it's stripped everywhere appropriate. :)
Going to have a look in a moment. |
One thing that seems to be missing here is a state for just viewing the link. So when you put the caret inside a link, you can see what the link is (also characters would be encoded), click on it to go to the page, edit it, and remove it. When you click the edit button you have an input field with the decoded URL, a save button, and an advanced button. We were thinking of getting rid of the big link modal entirely and have a dropdown for options or something. None by default, but a place where plugins can extend. |
|
17314f3
to
3d0fbee
Compare
Instead of using the command which validates against emptiness, we could take a more direct approach with |
@aduth Good idea but unfortunately it's not working. I'm having this error. |
Based on the implementation, it won't be as easy to use as the bold / italic / strike formatting, since it requires the anchor node. Are you passing that? I'd also suggest dropping the |
93d0036
to
009849c
Compare
@aduth I was able to fix the empty link issue by disabling the URL Converter. |
this.props.onChange( { link: { value: '' } } ); | ||
|
||
// Debounce the call to avoid the reset in willReceiveProps | ||
setTimeout( () => this.setState( { isEditingLink: true } ) ); |
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.
Any time we use setTimeout
or debounce
, we can't be guaranteed that the component instance is still rendered when the callback is called. Therefore we must assign the timeout reference (setTimeout
return value) into the instance and call clearTimeout
in componentWillUnmount
. Otherwise you'll encounter errors if the component is unmounted.
As a general note, this is one of the reasons I try to discourage setTimeout
in a component (or any asynchronous action for that matter); because it's very difficult to track what's occurred in the time between when the timeout is declared and the callback is invoked.
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.
Yes, and I always forget 😕. I don't have a better alternative here.
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.
Should be ok now
|
||
return ( | ||
<div className="editable-format-toolbar"> | ||
<ul className="editor-toolbar"> |
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.
Can't we use Toolbar
component here? At the very least, I'm not particularly comfortable with inheriting styles from the editor-toolbar
class because it's hard to track them back to being used in this component.
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.
If we use Toolbar
here we'll need to update its implementation, the same way I did here #505 (comment) and I guess you don't like this implementation too.
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.
Maybe not 🤔 I'll try.
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.
The behaviour was different from my previous PR. thus, I was able to use the Toolbar
component 🎉
blocks/components/editable/index.js
Outdated
} ); | ||
|
||
this.setState( { | ||
formats: merge( this.state.formats, formats ) |
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.
You're mutating this.state
directly here. Consider passing empty object {}
as first argument instead.
Never mutate
this.state
directly, as callingsetState()
afterwards may replace the mutation you made. Treatthis.state
as if it were immutable.
https://facebook.github.io/react/docs/react-component.html#state
Note: This method mutates
object
.
if ( ! anchor ) { | ||
this.editor.formatter.remove( 'link' ); | ||
} | ||
this.editor.formatter.apply( 'link', { href: formatValue.value }, anchor ); |
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.
Should this occur if ! anchor
condition above passes and the link is removed? Seems like this ought to be:
if ( anchor ) {
// apply link
} else {
// remove link
}
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.
No, this is not the desired behaviour here.
Actually I'm not sure this is very important but I just mimic the insertLink
command here. I think this might be useful if we try to add multiple links to the same node.
74bddb9
to
4a6c207
Compare
Polish the link modal
4a6c207
to
25016bc
Compare
I'm planning to merge this today, let me know if you have any other concern. |
- clear the `setTimeout` on unmount - don't mutate the state object
25016bc
to
910d2dc
Compare
onClick: this.toggleFormat( control.format ), | ||
isActive: !! formats[ control.format ] | ||
} ) ) | ||
.concat( [ { |
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.
If we're not using FORMATTING_CONTROLS
for anything else, and we're always concatenating, why not just include the link control in the constant set?
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.
They don't have the same isActive
and onClick
prop. Do you prefer a ternary or something?
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.
Ah, I think I overlooked how we're mapping to add in instance-bound handlers. This should be fine as-is.
This PR adds the link control to the formatting toolbar.
Testing instructions