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

Editable: Adding an inline link control to the format toolbar #524

Merged
merged 6 commits into from
May 2, 2017

Conversation

youknowriad
Copy link
Contributor

This PR adds the link control to the formatting toolbar.

Testing instructions

  • Try adding links and navigating inside an Editable.

@youknowriad youknowriad added the Framework Issues related to broader framework topics, especially as it relates to javascript label Apr 27, 2017
@youknowriad youknowriad self-assigned this Apr 27, 2017
@youknowriad youknowriad requested a review from ellatrix April 27, 2017 12:25
@youknowriad
Copy link
Contributor Author

This is not polished yet, but I wanted to have some feedback.

@jasmussen
Copy link
Contributor

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!

@aduth aduth added the [Status] In Progress Tracking issues with work in progress label Apr 27, 2017
@aduth
Copy link
Member

aduth commented Apr 27, 2017

An issue I'm observing:

  1. Click into text block at a word
  2. Click Link button in toolbar
  3. Move focus into link URL field
  4. Click Trash icon

Expected: Link to be removed
Actual: Link not removed, but modal is dismissed

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.

Remove link

/**
* Internal dependencies
*/
import './style.scss';
Copy link
Member

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.

Copy link
Contributor Author

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 = [
Copy link
Member

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

} else {
this.editor.formatter.apply( format );
}
Object.keys( formats ).forEach( format => {
Copy link
Member

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 } />
Copy link
Member

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.

Copy link
Member

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

Copy link
Member

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.

Copy link
Member

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?

Copy link
Contributor

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):

links

@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:

screen shot 2017-04-27 at 20 41 49

Still thinking about that one, but it's at least one argument for having links under.

Copy link
Member

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.

Copy link
Member

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 :)

}
Object.keys( formats ).forEach( format => {
const formatValue = formats[ format ];
if ( format === 'link' ) {
Copy link
Member

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 }
: {};
Copy link
Member

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.

Demo: https://codepen.io/aduth/pen/JNEaRb

style={ linkStyle }
onSubmit={ this.submitLink }>
<input type="url" value={ this.state.linkValue } onChange={ this.updateLinkValue } />
<IconButton icon="editor-break" onClick={ this.submitLink } />
Copy link
Member

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".

Object.keys( formats ).forEach( format => {
const formatValue = formats[ format ];
if ( format === 'link' ) {
if ( formatValue !== undefined ) {
Copy link
Member

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?

Copy link
Contributor Author

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 );
Copy link
Member

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 😅

@youknowriad youknowriad removed their assignment Apr 27, 2017
@youknowriad youknowriad force-pushed the add/link-control-inline branch from b3b3741 to 833b944 Compare April 27, 2017 14:02
addLink() {
if ( ! this.props.formats.link ) {
// TODO find a way to add an empty link to TinyMCE
this.props.onChange( { link: 'http://wordpress.org' } );
Copy link
Contributor Author

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

Copy link
Member

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. :)

@ellatrix
Copy link
Member

Going to have a look in a moment.

@ellatrix
Copy link
Member

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.

@ellatrix
Copy link
Member

ellatrix commented Apr 28, 2017

Here's an example of on encoded ink:

link

The one in the input field would be decoded which would be a pain to read.

@youknowriad youknowriad removed the [Status] In Progress Tracking issues with work in progress label Apr 28, 2017
@youknowriad
Copy link
Contributor Author

@iseulde @jasmussen

  • I've added the "view" state in 17314f3 and the behaviour here is starting to be solid.
  • This needs some design love
  • I think we should see if we can get some help from the TinyMCE team to allow adding an empty link.

@youknowriad youknowriad force-pushed the add/link-control-inline branch from 17314f3 to 3d0fbee Compare April 28, 2017 13:14
@aduth
Copy link
Member

aduth commented Apr 28, 2017

I think we should see if we can get some help from the TinyMCE team to allow adding an empty link.

Instead of using the command which validates against emptiness, we could take a more direct approach with formatter.apply, which has the benefit of perhaps letting us be more consistent with the other formatting we apply (since we're already using formatter.apply for bold, italic, strike).

https://github.com/tinymce/tinymce/blob/8695816/src/core/src/main/js/EditorCommands.js#L545

@youknowriad
Copy link
Contributor Author

@aduth Good idea but unfortunately it's not working. I'm having this error.

screen shot 2017-04-28 at 16 55 04

@aduth
Copy link
Member

aduth commented Apr 28, 2017

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 .min suffix on the TinyMCE dependency and checking where the error's occurring in the unminified code. Aside: We should probably load this unminified version when SCRIPT_DEBUG constant is enabled.

@youknowriad youknowriad force-pushed the add/link-control-inline branch from 93d0036 to 009849c Compare May 1, 2017 08:47
@youknowriad
Copy link
Contributor Author

@aduth I was able to fix the empty link issue by disabling the URL Converter.
I've also polished the modal style a bit.

this.props.onChange( { link: { value: '' } } );

// Debounce the call to avoid the reset in willReceiveProps
setTimeout( () => this.setState( { isEditingLink: true } ) );
Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

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">
Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

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 🎉

} );

this.setState( {
formats: merge( this.state.formats, formats )
Copy link
Member

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 calling setState() afterwards may replace the mutation you made. Treat this.state as if it were immutable.

https://facebook.github.io/react/docs/react-component.html#state

Note: This method mutates object.

https://lodash.com/docs/4.17.4#merge

if ( ! anchor ) {
this.editor.formatter.remove( 'link' );
}
this.editor.formatter.apply( 'link', { href: formatValue.value }, anchor );
Copy link
Member

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
}

Copy link
Contributor Author

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.

@youknowriad youknowriad force-pushed the add/link-control-inline branch 2 times, most recently from 74bddb9 to 4a6c207 Compare May 1, 2017 15:26
@youknowriad youknowriad self-assigned this May 1, 2017
@youknowriad youknowriad force-pushed the add/link-control-inline branch from 4a6c207 to 25016bc Compare May 2, 2017 08:52
@youknowriad
Copy link
Contributor Author

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
@youknowriad youknowriad force-pushed the add/link-control-inline branch from 25016bc to 910d2dc Compare May 2, 2017 08:57
@youknowriad youknowriad merged commit 29462aa into master May 2, 2017
@youknowriad youknowriad deleted the add/link-control-inline branch May 2, 2017 14:32
onClick: this.toggleFormat( control.format ),
isActive: !! formats[ control.format ]
} ) )
.concat( [ {
Copy link
Member

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?

Copy link
Contributor Author

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?

Copy link
Member

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Framework Issues related to broader framework topics, especially as it relates to javascript
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants