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

Added method 'getClientRects()' to CKEDITOR.dom.range #1499

Merged
merged 23 commits into from Apr 17, 2018
Merged

Added method 'getClientRects()' to CKEDITOR.dom.range #1499

merged 23 commits into from Apr 17, 2018

Conversation

engineering-this
Copy link
Contributor

@engineering-this engineering-this commented Jan 25, 2018

What is the purpose of this pull request?

New feature

Does your PR contain necessary tests?

All patches which change the editor code must include tests. You can always read more
on PR testing,
how to set the testing environment and
how to create tests
in the official CKEditor documentation.

This PR contains

  • Unit tests
  • Manual tests
    This feature will be used in my upcoming PR on balloonPanel and this usage will be covered in manual tests.

What changes did you make?

I've added getClientRects method which uses native method with same name, also implemented fallback for browsers that doesn't support it.

Closes #1498

* Returns Array of rects from dom elements contained within current ranges.
*
* @since 4.9.0
* @private
Copy link
Contributor

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 mark this method as a private. Note that private members are usually prefixed with an underscore.

/**
* Returns Array of rects from dom elements contained within current ranges.
*
* @since 4.9.0
Copy link
Contributor

Choose a reason for hiding this comment

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

There are no docs on returned type.

@@ -2818,6 +2818,69 @@ CKEDITOR.dom.range = function( root ) {
reference.remove();
},

/**
* Returns Array of rects from dom elements contained within current ranges.
Copy link
Contributor

Choose a reason for hiding this comment

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

rects -> rectangles
Array -> array (you're not using it as a type, but a regular word)
dom -> DOM

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure if this summary describes well enough what this method does.

It returns rectangles of areas covered by ranges, not by elements contained by the range (element could be contained partially, and based on this description one could think that despite that fact returned rectangles will contain the entire element, which is not true).

rects = [ start.getClientRect(), start.getClientRect() ];
start.remove();
}
rect.right = Math.max( rects[ 0].right, rects[ 1 ].right );
Copy link
Contributor

Choose a reason for hiding this comment

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

Code style issue, missing space before bracket. same in next line.

var tests = {
'test paragraphs': function() {
var bot = this.editorBot,
editor = this.editor;
Copy link
Contributor

Choose a reason for hiding this comment

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

Wrong indentation.

( function() {
'use strict';

bender.editor = {};
Copy link
Contributor

Choose a reason for hiding this comment

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

You don't really need to use editor here. Actually you can work on regular HTML elements. Just go with:

var rng = new CKEDITOR.dom.range( CKEDITOR.document );

and play with your range.

}

if ( this.document.$.getSelection !== undefined ) {
var sel = this.document.$.getSelection(),
Copy link
Contributor

Choose a reason for hiding this comment

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

You're working on a global selection here, which is not the same as your context selection.

Just imagine that you create a new range in memory in order to use it later or whatever, but you do no actually select it. E.g.

var rng = new CKEDITOR.dom.range( CKEDITOR.document )
rng.setStart(  );
rng.setEnd(  );
var rects = rng.getClientRects();

👆 this case needs to be included in unit tests.

Copy link
Contributor

Choose a reason for hiding this comment

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

I can see also a design flaw in our Range implementation which will make your task harder - AFAICT we're not keeping a reference to the native Range object.

If that's the case your only solution for this will be to temporarily create a new range, use it to probe what you need to check and then destroy it.

Copy link
Contributor

Choose a reason for hiding this comment

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

Also there is a need to handle our fake selection - however we could extract it into a separate ticket. But please familiarize your self with this aspect already (talk to other devs if you need a help with that).

rect.bottom = Math.max( rects[ 0 ].bottom, rects[ 1 ].bottom );
rect.left = Math.min( rects[ 0 ].left, rects[ 1 ].left );
rect.top = Math.min( rects[ 0 ].top, rects[ 1 ].top );
rect.width = Math.abs( rects[ 0 ].left - rects[ 1 ].left );
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 am thinking if I should use extra tabulations to improve readability. i.e:

				rect.bottom	= Math.max( rects[ 0 ].bottom,	rects[ 1 ].bottom );
 				rect.left	= Math.min( rects[ 0 ].left,	rects[ 1 ].left );
 				rect.top	= Math.min( rects[ 0 ].top,	rects[ 1 ].top );
 				rect.width	= Math.abs( rects[ 0 ].left - rects[ 1 ].left );
 				rect.height	= rect.bottom - rect.top;

Copy link
Contributor

Choose a reason for hiding this comment

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

Late answer but we are not using this kind of whitespace alignment. Reason is that it is just hard to maintain as you change one of these 5 lines, you need to modify 4 other.

It's fine the way it currently is 👍

Copy link
Contributor

@mlewand mlewand left a comment

Choose a reason for hiding this comment

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

One TC is way too little.

There are cases that are not covered, for instance, a selection where there are multiple rectangles returned, for instance selection like that:

Three lines of text, where in the first and last line only part of the line are selected, causing selection to contain three different rectangles.

Also please, rebase the branch onto latest 4.10.0 branch. Make sure to move the changelog entry.

var listToCompare = getListToCompare( range );
rectList.forEach( function( item, index ) {
for ( var key in item ) {
if ( typeof item[ key ] === 'number' ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Be more explicit about asserted properties, for instance you could simply whitelist them in some array.

rectList.forEach( function( item, index ) {
for ( var key in item ) {
if ( typeof item[ key ] === 'number' ) {
assert.isTrue( item[ key ] === listToCompare[ index ][ key ] );
Copy link
Contributor

Choose a reason for hiding this comment

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

This is not a proper way to assert primitive values. Assertion message will not be useful at all if the assertion fails.

It:

  • will not contain any useful expected, actual value (will only say that was expecting true, while false given)
  • will not contain any useful information on what number caused the fail. Was it top, left etc? This information should be included in a short assertion message.

// This assert is for IE8, but it should pass on newer browsers.
var keys = [ 'width', 'height', 'top', 'bottom', 'left', 'right' ];
CKEDITOR.tools.array.forEach( keys, function( key ) {
assert.isTrue( rectList[ 0 ][ key ] >= 0 );
Copy link
Contributor

Choose a reason for hiding this comment

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

Not really a solid assertion for IE8. Can't we write a better assertion here? Something with real data?


if ( this.document.$.getSelection !== undefined ) {
var range = document.createRange(),
rectList,
Copy link
Contributor

Choose a reason for hiding this comment

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

Uninited vars should be declared last.

var range = document.createRange(),
rectList,
rectArray = [];
range.setStart( this.startContainer.$, this.startOffset );
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be nice to put one line of space here, before this line.

return rect;
}

if ( this.document.$.getSelection !== undefined ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Rather than executing this code every time function getClientRects is called, it make sense to do it only once like it's done in other core mentods: https://github.com/ckeditor/ckeditor-dev/blob/4fa6edd/core/dom/element.js#L1073-L1116

@engineering-this engineering-this changed the base branch from major to next March 12, 2018 16:37
@mlewand mlewand closed this Mar 16, 2018
@mlewand mlewand changed the base branch from next to major March 16, 2018 10:15
@mlewand mlewand reopened this Mar 16, 2018
@mlewand
Copy link
Contributor

mlewand commented Mar 16, 2018

Sorry, I've removed the next branch before rebasing all the PRs that were based on it. This PR is now rebased onto latest major (4.10.0).

@Comandeer
Copy link
Member

Please also refer to my comments from #1627 (review).

Copy link
Contributor

@mlewand mlewand left a comment

Choose a reason for hiding this comment

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

I was concerned with the result returned in Internet Explorer 8 (using this polyfill):

image

Note that returned rectangle is marked with a red border.

Given the fact that there's no easy way to retrieve all selection markup, I think that the solution you took is the most optimal one. Return a rectangle between the boundary points. This will work great e.g. with context menu and will work just fine if we are to use it with something like balloon toolbar.

However this needs to be well documented, so in API docs provide an information what kind of result is returned for browsers that do not support this feature. So please add a proper docs on it.

I don't like the fact that tests are not precise, and have some tolerance. In addition to that the tests could be made more easy to read/understand in general.

I gave it some testing, and what we can make selections on something that has a fixed size, and then compare the returned rectangle(s). Now intuitively you'd want to use a monospace font for that purpose, however browser do not use fixed width for monospace fonts (shame). So from there the only alternative solution is some fixed size inline content, like images or simply spans with a fixed width.

Lastly during the testing I have witnessed that IE 11 (and possibly others) have a (upstream) bug that will not return rectangle for image selected:

image

This is something that we could work around with a feature in this function but that's outside of a scope here - please create a followup ticket for htat.

CHANGES.md Outdated
@@ -8,6 +8,10 @@ Fixed Issues:
* [#1458](https://github.com/ckeditor/ckeditor-dev/issues/1458): [Edge] Fixed: After blurring editor it takes 2 clicks to focus a widget.
* [#1034](https://github.com/ckeditor/ckeditor-dev/issues/1034): Fixed: JAWS leaves forms mode after pressing <kbd>Enter</kbd> key in an inline CKEditor instance.

API changes:

* [#1498](https://github.com/ckeditor/ckeditor-dev/issues/1498) : Added new method 'getClientRects()' to CKEDITOR.dom.range, which returns list of rects for each selected element.
Copy link
Contributor

Choose a reason for hiding this comment

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

Move changelog entry to the new target release (4.10.0).

@@ -2818,6 +2818,83 @@ CKEDITOR.dom.range = function( root ) {
reference.remove();
},

/**
* Returns array with rectangles of areas covered by ranges. For each DOM element within range there is one rectangle, but it represents only part of element which is within range, not whole element.
Copy link
Contributor

Choose a reason for hiding this comment

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

This line is too long. It looks that you had linter disabled, as otherwise it would prevent the commit.

Please split it into a bit narrower text.

Copy link
Contributor

Choose a reason for hiding this comment

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

Also this description is not clear, please rephrase it as simply as you can. Feel free to ask Anna for help - this method needs to be understandable.

Also it is very important to describe the fallback for browsers that do not support range.getClientRects() natively. I mean describe what area of selection is used.

It would be nice to use images, eventually ascii art to present this information as it might be hard to describe it with words only 🙂

/**
* Returns array with rectangles of areas covered by ranges. For each DOM element within range there is one rectangle, but it represents only part of element which is within range, not whole element.
*
* @since 4.9.0
Copy link
Contributor

Choose a reason for hiding this comment

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

Please update the target, it's now 4.10.0.

'test inline element': function() {
setupTest( getHtmlForTest( 'span', 'test' ), true );
},
'test multi-line inline element': function() {
Copy link
Contributor

Choose a reason for hiding this comment

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

It's multiline rather than multi-line (I believe the later is correct too, but we're not using it in our codebase).

* @returns {Array}
*/
getClientRects: ( function() {
// Extending empty object wth rect, to prevent inheriting from DOMRect, same approach as in CKEDITOR.dom.element.getClientRect().
Copy link
Contributor

Choose a reason for hiding this comment

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

"wth" -> with


if ( this.document.getSelection !== undefined ) {
return function() {
var range = document.createRange(),
Copy link
Contributor

Choose a reason for hiding this comment

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

How about a word on why are we duplicating the range? I'm missing short comment with rationale here.

rectList = range.getClientRects();
range.detach();

for ( var i = 0; i < rectList.length; i++ ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

You could simplify this significantly by using CKEDITOR.tools.array.map( range.getClientRects(), … ).

start.remove();
}

rect.right = Math.max( rects[ 0 ].right, rects[ 1 ].right );
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of assigning rect like that, how about simply returning with

return {
	top: Math.min( rects[ 0 ].top, rects[ 1 ].top ),};

That way you'll have one variable less in the scope and you'll get rid of these assignments.

@mlewand
Copy link
Contributor

mlewand commented Apr 5, 2018

Also a manual test would help a lot, I have pushed a branch showing how easier unit tests could be achieved and that contains a nice base for manual test. You can find it on branch 1498_tests.

@mlewand
Copy link
Contributor

mlewand commented Apr 6, 2018

Also since #1866 got into the major branch, please mark the returned value as an array of CKEDITOR.dom.rect.

Copy link
Contributor

@mlewand mlewand left a comment

Choose a reason for hiding this comment

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

Checked just couple of things in docs, it needs further corrections before it could be further reviewed.

*
* In the following example:
*
* <p><span>first { span</span><span> second text</span></p>
Copy link
Contributor

Choose a reason for hiding this comment

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

Use tabs for indentation instead of spaces.

Copy link
Contributor

Choose a reason for hiding this comment

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

Use triple backticks for code samples, see ckeditor/ckeditor4-docs#131

Copy link
Contributor

Choose a reason for hiding this comment

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

Also mark the code listing language for proper syntax highlighting.

* └ ┘
*
* @since 4.10.0
* @returns {Array} Returns an array of {@link CKEDITOR.dom.rect}
Copy link
Contributor

Choose a reason for hiding this comment

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

This is not an optimal way of marking returned array. It should be marked as @returns {Type[]}.

* Brackets represent the beginning and the end of the selection.
*
* Returned rectangles would be represented by areas like below:
* text [ text ] [ text text ]
Copy link
Contributor

Choose a reason for hiding this comment

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

That doesn't render correctly in our docs. Be sure to always build docs and preview it before you publish something.

*
* ```html
* <p><span>first { span</span><span>second span</span></p>
* <p><span>very long } span</span></p>
Copy link
Contributor Author

Choose a reason for hiding this comment

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

For some reason JSDuck wont create codeblock here if I don't put two spaces or two tabulations here. Couldn't fix that even with help from @msamsel.

* └ ┘
* ```
* @since 4.10.0
* @returns {CKEDITOR.dom.rect[]} Returns an array of {@link CKEDITOR.dom.rect}
Copy link
Contributor

Choose a reason for hiding this comment

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

Returns an array of {@link CKEDITOR.dom.rect} does not bring any value here, this informaiton is already expressed by @returns {CKEDITOR.dom.rect[]} so it's OK to remove it.

The same thing is repeated at the beginning of description for this method.

@@ -2818,6 +2818,114 @@ CKEDITOR.dom.range = function( root ) {
reference.remove();
},

/**
* Returns an array of {@link CKEDITOR.dom.rect} elements that are represented as rectangles which are covered by ranges.
* For each DOM element within the selection range there is only one rectangle.
Copy link
Contributor

Choose a reason for hiding this comment

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

For each DOM element within the selection range there is only one rectangle.

I don't feel like going though spec of (native browser) range.getClientRects() but I believe it's not guaranteed to return a rectangle for each DOM element. For instance Edge does not do that.

Let's be more generic about that, saying that it's a rect list describing the selection.

* Returned rectangles would be represented by areas like below:
*
* ```
* first [ span ] [ second span ]
Copy link
Contributor

Choose a reason for hiding this comment

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

You should be more strict about spacing in this example. I know that you wanted to do that for readability, but it's important to use spaces as they're used in your input HTML.

For instance there's no space between "span" and "second".

Copy link
Contributor

Choose a reason for hiding this comment

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

Again the returned result will differ accross the browsers, as some of them might unify (simplify/optimize) returned rectangles. That should be somehow marked as a note.

*
* Where each pair of brackets represents one rectangle.
*
* In Internet Explorer 8 this method will return an array containing only one rectangle which would start in the top left hand corner of the selection and end in the bottom right hand corner.
Copy link
Contributor

Choose a reason for hiding this comment

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

We should also mention why is this the case. That's because IE8 has no implementation of range.getClientRects().

* ┌ ┐
* first span second span
* very long span
* └ ┘
Copy link
Contributor

Choose a reason for hiding this comment

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

To be honest I find it very difficult to provide accurate ASCI art here. The one propsed by you implies like there's area bove "span" and below "long".

I came with something like that:

     +-----+
first| span| second span
very |long |span
     +-----+

Which has similar issue and is still far from decent in my opinion.

Instead how about linking an image (I guess that would be a first image in our API docs, I'm so excited). We could use Cloud Services for hosting the image, contact me on how to perform that.

this.playground = doc.getById( 'playground' );
},

'test simple single line selection': function() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Am I missing something or is there only one unit test? There are much more cases to be covered.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Edge is squashing rects into one per line. Now I'm not sure which is better: write extra assertions for Edge, or skip tests with more than one element selected? What is your thought on that @mlewand

Copy link
Contributor

Choose a reason for hiding this comment

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

We want to have proper testing for each environment, so we can't skip any browser. I think there are going to be three correct results:

  • resutls returned by modern browsers
  • results returned by Edge and IE
  • results returned by polyfilled getClientRects (IE8)

Copy link
Contributor

@mlewand mlewand left a comment

Choose a reason for hiding this comment

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

LGTM

@uladzimir-miadzinski
Copy link

uladzimir-miadzinski commented Feb 22, 2019

can you also fix bug with setStart and setEnd methods of CKEDITOR.dom.range?
when offset bigger than content length, I have an error

zone.js:199 Uncaught DOMException: Failed to execute 'setStart' on 'Range': The offset 1 is larger than the node's length (0).
    at push.../node_modules/ckeditor/ckeditor.js.window.CKEDITOR.window.CKEDITOR.dom.CKEDITOR.dom.range.getClientRects

i got this error in 4.11.2
and I think the fix can be like that:

            if (this.startContainer.$.length < this.startOffset) {
                this.startOffset = this.startContainer.$.length;
            }

            if (this.endContainer.$.length < this.endOffset) {
                this.endOffset = this.endContainer.$.length;
            }

@mlewand
Copy link
Contributor

mlewand commented Feb 25, 2019

@medinsky this is a request unrelated to this PR. Feel free to request a feature request like that. I'll mention though that it shows that there might be a problem somewhere underneath which should be fixed.

In other words that would be fixing a symptoms not the root cause of the problem.

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.

None yet

4 participants