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

Text formatting UI #460

Merged
merged 7 commits into from
Apr 24, 2017
Merged

Text formatting UI #460

merged 7 commits into from
Apr 24, 2017

Conversation

ellatrix
Copy link
Member

See #452

Still needs a styling fix and a look into Range errors, but wanted to push it here to see what others think.

@youknowriad
Copy link
Contributor

Tested and is working pretty well but I have several questions/remarks here.

  • How do you think we would handle multi-editable blocks like quote?

  • Something that bothers me with this approach is its "imperativeness". React is declarative by default, which could mean props for a component are description of a state or an option but not an imperative command. which brings me to the requestFormat prop which if I understand correctly is a set of commands to trigger inside the Editable component and right after the command is triggered we call a callback onPathChange that resets these commands.

Could we pass the formats state instead and compare with the previous passed state in componentWillReceiveProps to trigger TinyMCE's commands? This would allow us to pass scalar values as well (think Link Control)

  • Also, you're extracting the formats values (toggled or not) by passing a path to the VisualBlock component. I think the VisualBlock should not know about paths and stuff like that (these are TinyMCE specific). Could we just move this behavior inside the Editable component and directly pass the formats state as a paramter to onFormatChange prop or similar (as opposed to the current onPathChange prop)

@ellatrix
Copy link
Member Author

How do you think we would handle multi-editable blocks like quote?

As discussed earlier in Slack, I think we should mark some areas as the main text area or a field that needs controls repositioned. We'll need a way to distinguish between the two.

Something that bothers me with this approach is its "imperativeness". React is declarative by default, which could mean props for a component are description of a state or an option but not an imperative command. which brings me to the requestFormat prop which if I understand correctly is a set of commands to trigger inside the Editable component and right after the command is triggered we call a callback onPathChange that resets these commands.

Suggestions welcome! If we use formats, how do we distinguish between a path change and a command?

@youknowriad
Copy link
Contributor

Suggestions welcome! If we use formats, how do we distinguish between a path change and a command?

Yep, it's probably a bad name, the most important for me is having an object containing all the inline controls data.

const inlineState = {
  bold: true,
  italic: false,
  link: { url: 'http://google.com', target: '_blank' }
}

@ellatrix
Copy link
Member Author

Sure, that's the same but a different name. What if the state changes by changing the selection, not by clicking the button. How does Editable know what happened?

@youknowriad
Copy link
Contributor

that's the onFormatChange I was talking about above (the name could be different). computing the new inlineState would be done inside the Editable

@ellatrix
Copy link
Member Author

@youknowriad Changed some things. I'll try to get rid of requestFormat but can't see immediately how to.

@youknowriad
Copy link
Contributor

I'll try to get rid of requestFormat but can't see immediately how to.

Can we do this by passing this.state.formats instead of this.state.requestFormat as a prop? And maybe using this.editor.executeCommand once the value of a format changes?

@ellatrix
Copy link
Member Author

Yeah, we can cache the path and compare it, so we're not executing the commands on selection changes.

@youknowriad
Copy link
Contributor

Yeah, we can cache the path and compare it, so we're not executing the commands on selection changes.

I was thinking of comparing this.props.formats with nextProps.formats in componentWillReceiveProps.

@ellatrix
Copy link
Member Author

Like this? Any other thoughts?


forEach( nextProps.formats, ( state, format ) => {
if ( state !== this.props.formats[ format ] ) {
this.editor.formatter.toggle( formatMap[ format ] );
Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks for the changes @iseulde. This starts to look good for me. But I think we should not use toggle here. I'm seeing some bugs while the format is changed when I click in different places of the Editable. I suspect this is caused by the toggle because we're not taking into account the value of this.props.formats[ format ] whether it's true or false. I don't know maybe something like this.editor.execCommand( formatMap[ format ], true, this.props.formats[ format ] ) would fix it?

Copy link
Contributor

Choose a reason for hiding this comment

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

Or if toggle is the right way to do it, we should probably check whether the current path is already equivalent to the this.props.formats[ format ] value before calling toggle

Copy link
Member Author

Choose a reason for hiding this comment

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

Does the new state check make sense?

Copy link
Member Author

Choose a reason for hiding this comment

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

(And explicitly calling apply or remove.)

const formatMap = {
strong: 'bold',
em: 'italic'
};
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if we should do the mapping before calling the onFormatChange instead

Copy link
Member Author

Choose a reason for hiding this comment

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

Yep, we should.

@ellatrix ellatrix self-assigned this Apr 20, 2017
@jasmussen
Copy link
Contributor

Works great!

Can we get link and strikethrough in also?

@ellatrix
Copy link
Member Author

Sure strikethrough is the same. Link will need more UI. Maybe best a separate PR.

@ellatrix
Copy link
Member Author

We can also probably get rid for the format map by checking TinyMCE settings for that...

@@ -127,6 +146,22 @@ export default class Editable extends wp.element.Component {
}
}

componentWillReceiveProps( nextProps ) {
forEach( nextProps.formats, ( state, format ) => {
const currentState = this.formats[ format ] || false;
Copy link
Contributor

Choose a reason for hiding this comment

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

I would prefer if we find a way to avoid the cache this.formats but it's ok for now.

Copy link
Member

Choose a reason for hiding this comment

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

Is this.formats here any different than this.props.formats? Like Riad, I also instinctively cringe at the sight of instance variables, though they're sometimes justified.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, It can be different than this.props.formats right after we trigger the onFormatChange callback.

Copy link
Member Author

Choose a reason for hiding this comment

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

Any suggestions here are welcome.

@@ -47,7 +47,25 @@ registerBlock( 'core/text', {
}
],

edit( { attributes, setAttributes, insertBlockAfter } ) {
formatting: [
Copy link
Contributor

@youknowriad youknowriad Apr 20, 2017

Choose a reason for hiding this comment

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

Just pinging @aduth maybe he has some thoughts on the block API part of this PR. The formatting attribute here and the two props onFormatChange and formats

@@ -47,6 +54,17 @@ export default class Editable extends wp.element.Component {
editor.on( 'focusout', this.onChange );
editor.on( 'NewBlock', this.onNewBlock );
editor.on( 'focusin', this.onFocus );

if ( this.props.onFormatChange ) {
editor.on( 'nodechange', ( { parents } ) => {
Copy link
Member

Choose a reason for hiding this comment

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

Can we move the callback logic to a separate named function on the class? Just in an effort to avoid a sprawling setup method.

formatMap[ node.nodeName.toLowerCase() ]
) );

this.formats = zipObject( path, path.map( () => true ) );
Copy link
Member

Choose a reason for hiding this comment

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

Suggestion for O(4n) to O(n) optimization:

const formats = parents.reduce( ( memo, node ) => {
	const tagName = node.nodeName.toLowerCase();
	if ( formatMap.hasOwnProperty( tagName ) ) {
		memo[ formatMap[ tagName ] ] = true;
	}

	return memo;
}, {} );

}
],

edit( { attributes, setAttributes, insertBlockAfter, focus, setFocus, onFormatChange, formats } ) {
Copy link
Member

Choose a reason for hiding this comment

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

Not specific to this pull request, but I'm concerned about how many props an implementer is expected to be aware of just to use an Editable component. Previously I'd suggested considering context as a transparent pass-through. I don't really know whether it's a perfect solution, but we'll need to think of something. Maybe also something in the global state instead of VisualBlock's state.

Copy link
Member Author

Choose a reason for hiding this comment

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

Same here, also since these are probably things the block implementor doesn't need to know anything about.

@@ -47,7 +47,25 @@ registerBlock( 'core/text', {
}
],

edit( { attributes, setAttributes, insertBlockAfter, focus, setFocus } ) {
formatting: [
Copy link
Member

Choose a reason for hiding this comment

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

In API docs I'd remarked that "Inline controls for Editable elements are identical for every block and cannot be modified." The thinking being that it'd be quite rare for an editable to not need these standard formatting controls, even to the point that we'd want to discourage it for the break in consistency. Do you have any feelings on this? Or could we at least have a standard set and make the intent an opt-out instead of opt-in?

Copy link
Member

Choose a reason for hiding this comment

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

Also, if we're introducing changes to the block API, we should probably document them at the same time. Specifically thinking about available format options.

Copy link
Member Author

Choose a reason for hiding this comment

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

I agree with this. I don't think a plugin should be able to change these (would love to see examples where they should). From earlier conversations I thought we wanted it like this.


if ( state !== currentState ) {
if ( state ) {
this.editor.focus();
Copy link
Member

Choose a reason for hiding this comment

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

Minor: If the focus applies first to both if and else, might as well move it above the condition (single occurrence).

}

bindBlockNode( node ) {
this.node = node;
}

onFormatChange( formats ) {
if ( ! isEqual( formats, this.state.formats ) ) {
Copy link
Member

Choose a reason for hiding this comment

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

Why would we allow onFormatChange to be invoked if a change actually hasn't occurred? Or in any case, why would we optimize for it? Thinking we can drop the isEqual check altogether.

}

bindBlockNode( node ) {
this.node = node;
}

onFormatChange( formats ) {
if ( ! isEqual( formats, this.state.formats ) ) {
this.setState( ( prevState ) => {
Copy link
Member

Choose a reason for hiding this comment

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

Curious about the choice of callback signature. Why not:

onFormatChange( formats ) {
	this.setState( { formats } };
}

}

toggleFormat( format ) {
this.setState( ( prevState ) => {
Copy link
Member

Choose a reason for hiding this comment

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

Similar to above, maybe more concise:

const { formats } = this.state;
this.setState( {
	formats: {
		...formats,
		[ format ]: ! formats[ format ]
	}
} );

controls={ settings.formatting.map( ( control ) => ( {
...control,
onClick: () => this.toggleFormat( control.format ),
isActive: () => !! this.state.formats[ control.format ]
Copy link
Member

Choose a reason for hiding this comment

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

Hmm, reconsidering whether isActive really makes sense as a function. We can save this for a separate pull request, but seems like a boolean would be a better substitute

@@ -127,6 +146,22 @@ export default class Editable extends wp.element.Component {
}
}

componentWillReceiveProps( nextProps ) {
forEach( nextProps.formats, ( state, format ) => {
const currentState = this.formats[ format ] || false;
Copy link
Member

Choose a reason for hiding this comment

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

Is this.formats here any different than this.props.formats? Like Riad, I also instinctively cringe at the sight of instance variables, though they're sometimes justified.

}

bindBlockNode( node ) {
this.node = node;
}

onFormatChange( formats ) {
if ( ! this.state.hasEditable ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This feels a bit like a hack :) but in the same time I can't find a better alternative

Copy link
Member Author

Choose a reason for hiding this comment

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

It does :)

Copy link
Member Author

Choose a reason for hiding this comment

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

An alternative would be an extra setting for it, but not sure if that's any better.

Copy link
Contributor

@youknowriad youknowriad left a comment

Choose a reason for hiding this comment

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

Really good job on this @iseulde. This is good to be merged for me 👍

@aduth
Copy link
Member

aduth commented Apr 24, 2017

I'm still trying to narrow in on what's causing it, but specific to this branch I don't have a text cursor (I-beam) when hovering the text block and can't select any text.

@youknowriad
Copy link
Contributor

youknowriad commented Apr 24, 2017

@aduth Probably caused by this #494 A rebase should fix it

Copy link
Member

@aduth aduth left a comment

Choose a reason for hiding this comment

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

Sure enough, that was it @youknowriad . Works well after a local rebase. LGTM 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants