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

Improving edge detection in blocks #3015

Closed
wants to merge 9 commits into from

Conversation

ephox-mogran
Copy link
Contributor

@ephox-mogran ephox-mogran commented Oct 13, 2017

Description

Added some cursor position APIs to help with edge detection

How Has This Been Tested?

I can't add any tests to test the only exported method (isDom) because it requires window.getSelection to be in the test runner.

Types of changes

Bug fix / improvement.

Checklist:

  • My code is tested.
  • My code follows the WordPress code style.
  • My code follows has proper inline documentation.

@codecov
Copy link

codecov bot commented Oct 13, 2017

Codecov Report

Merging #3015 into master will increase coverage by 0.01%.
The diff coverage is 21.42%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #3015      +/-   ##
==========================================
+ Coverage   34.54%   34.55%   +0.01%     
==========================================
  Files         261      261              
  Lines        6710     6699      -11     
  Branches     1225     1229       +4     
==========================================
- Hits         2318     2315       -3     
+ Misses       3704     3692      -12     
- Partials      688      692       +4
Impacted Files Coverage Δ
blocks/editable/index.js 11.07% <0%> (+0.83%) ⬆️
editor/utils/dom.js 11.76% <23.07%> (-3.44%) ⬇️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 38d0c74...d50f6c6. Read the comment docs.

@@ -298,7 +299,7 @@ export default class Editable extends Component {

isStartOfEditor() {
const range = this.editor.selection.getRng();
if ( range.startOffset !== 0 || ! range.collapsed ) {
if ( ! isAtCursorStart( range.startContainer, range.startOffset ) || ! range.collapsed ) {
Copy link
Member

Choose a reason for hiding this comment

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

Could we replace these methods entirely?

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 is use isEdge( editor.getBody(), true ) for isStart and isEdge( editor.getBody(), false ) for end ? I think you're right. Nice catch :)

Copy link
Contributor Author

@ephox-mogran ephox-mogran Oct 13, 2017

Choose a reason for hiding this comment

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

In terms of avoiding the need to understand that boolean, are you OK with me exporting LEFT_EDGE = true and RIGHT_EDGE = false as constants, and then using them in this file?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Or alternatively exposing isLeftEdge and isRightEdge?

Copy link
Member

Choose a reason for hiding this comment

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

Maybe isEdge( { container: Element, start: Boolean } )?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll defer to your preference :) I guess it matches the name in the range API for collapsing, which is most likely what you're basing it on. I've always found start a very strange name for a boolean, but that's just person taste.

I'm working on this now, so are you OK with the idea of exposing constants for the second argument or would you prefer passing an object like your last suggestion?

Copy link
Member

Choose a reason for hiding this comment

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

I think an object might be better for now, since it's just adding the braces. :) But that's just my opinion.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Used object notation as recommended.

* @param {Integer} offset the offset
* @return {Boolean} whether or not the offset is at the last cursor position in node
*/
export function isAtCursorEnd( node, offset ) {
Copy link
Member

Choose a reason for hiding this comment

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

Might it make sense to only introduce one method (isAtCursorEnd) for now? In more sure if it's useful to create 4 functions right now. Additionally, if editable uses the rest of the logic in this file, we might not need to export it.

Copy link
Member

Choose a reason for hiding this comment

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

You would, for the tests, but maybe we can test isEdge as a whole instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

With your above change to just use isEdge outside instead of needing to call an internal method, my preference would be to keep the methods for isAtCursorStart, but only expose isEdge. How does that sound?

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 major reason is it just increases the likelihood that lots of code won't have to change if it turns out that you have to start ignoring some offsets for start (like empty text nodes). It can get fairly gnarly with text node fragmentation. Having the cursor calculations in one spot helps in the long run. But yep, if we are able to use isEdge the other place I changed, then definitely only expose that one and just test it.

Copy link
Member

@ellatrix ellatrix Oct 13, 2017

Choose a reason for hiding this comment

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

With your above change to just use isEdge outside instead of needing to call an internal method, my preference would be to keep the methods for isAtCursorStart, but only expose isEdge. How does that sound?

That's exactly what I mean. :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Only isEdge is exposed now.

Copy link
Member

Choose a reason for hiding this comment

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

What I also meant though: maybe we can only keep only getCursorEnd if the other one is just a stub for now, and isAtCursorEnd is just an equality check? Why create so many functions?

@@ -1,3 +1,50 @@
const NODE_TEXT_TYPE = 3;
const NODE_ELEMENT_TYPE = 1;
Copy link
Member

Choose a reason for hiding this comment

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

In other modules, we've been using:

/**
 * Browser dependencies
 */
const { ELEMENT_NODE, TEXT_NODE } = window.Node

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, I was hoping it would be somewhere.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

@ellatrix
Copy link
Member

Thanks for the PR, I'll test in Classic Text a bit.

@ellatrix
Copy link
Member

Works well with an image in Classic Test. I'm seeing a regression though. In the demo content there's an image without a caption. I select it, then put the caret in the caption. Right arrow doesn't do anything. Looks like the node is the editor, but startOffset and endOffset stays 0.

@ephox-mogran
Copy link
Contributor Author

ephox-mogran commented Oct 13, 2017

What's the DOM structure? Also, whereabouts on the demo page?

@ellatrix
Copy link
Member

<figcaption contenteditable="true" class="blocks-editable__tinymce mce-content-body" aria-label="Write caption…" id="mce_36" data-is-placeholder-visible="false"><br data-mce-bogus="1"></figcaption>

@ephox-mogran
Copy link
Contributor Author

ephox-mogran commented Oct 13, 2017

And the parent of that node? (Ignore, I now see it's content-editable itself)

@ellatrix
Copy link
Member

A non editable div. Not sure why that matters? :)

@ephox-mogran
Copy link
Contributor Author

ephox-mogran commented Oct 13, 2017

Yeah, I see what's happening here. It looks like tiny is injecting a <br> tag into the figcaption, which means that its child nodes are 1. The problem is that the browser doesn't seem to like selecting past the br, and tends to make your selection before it. Therefore, it's , 0. This worked before because figcaption had no text content, so 0 was the right offset. Did it definitely work before when there was something in the caption, out of curiosity ?

@ephox-mogran
Copy link
Contributor Author

So I think what we'll have to do is strip any BR tags from the end of the child nodes when counting the length. This is where the edge-case part begins.

@ellatrix
Copy link
Member

Why not always return true when requesting if it's the edge, and the range is at the content editable container?

@ellatrix
Copy link
Member

	if ( ! range || ! range.collapsed ) {
		return false;
	}

	if ( node.isContentEditable ) {
		return true;
	}

	if ( start && ! isAtCursorStart( node, offset ) ) {
		return false;
	}

	if ( ! start && ! isAtCursorEnd( node, offset ) ) {
		return false;
	}

@ellatrix
Copy link
Member

In that case we also don't need to bother checking parentNodes.

@ephox-mogran
Copy link
Contributor Author

ephox-mogran commented Oct 13, 2017

Because the content could be like this:

<div><p>First paragraph</p><br /><br /><p>Second paragraph</p></div>

with the selection being (div, 2) (i.e. just before the 2nd br). Unfortunately, you can't make generalisations like that in complex content. The browser will report the selection being at the root any time it doesn't have a text node to reference.

@ellatrix
Copy link
Member

True. Then it should be this?

if ( node.isContentEditable && offset === 0 ) {
	return true;
}

@ephox-mogran
Copy link
Contributor Author

ephox-mogran commented Oct 13, 2017

You could have that shortcut, yes. But the problem at the moment is that it isn't detecting the right edge, because there's a <br> there.

@ephox-mogran
Copy link
Contributor Author

So I think I have a solution for that particular case (filtering out trailing br tags and checking again), but it's going to need a lot more work as you go forward. You'll identify nodes which don't create valid cursor positions for the browser but do occupy positions in the DOM. Empty text and formatting nodes and zero width cursors are particularly painful here.

@gziolo gziolo added the [Status] Stale Gives the original author opportunity to update before closing. Can be reopened as needed. label Mar 8, 2018
@gziolo
Copy link
Member

gziolo commented Jun 10, 2018

There was no activity in this PR for over 6 months. It looks like it was abandoned. Let’s close it. We can always reopen and rebase it with master branch to align with the current state of Gutenberg.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Status] Stale Gives the original author opportunity to update before closing. Can be reopened as needed.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants