-
Notifications
You must be signed in to change notification settings - Fork 40
T/ckeditor5 typing/101 Multiple spaces fix #1083
Changes from 3 commits
46084a2
91dbb6a
99d5f24
e29624b
4ca5095
dbb7b6f
decc702
4aaaf7b
9515717
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,6 +10,7 @@ | |
import ViewText from './text'; | ||
import ViewPosition from './position'; | ||
import { INLINE_FILLER, INLINE_FILLER_LENGTH, startsWithFiller, isInlineFiller, isBlockFiller } from './filler'; | ||
import { getTouchingTextNode } from './utils'; | ||
|
||
import mix from '@ckeditor/ckeditor5-utils/src/mix'; | ||
import diff from '@ckeditor/ckeditor5-utils/src/diff'; | ||
|
@@ -198,9 +199,7 @@ export default class Renderer { | |
} | ||
|
||
for ( const node of this.markedTexts ) { | ||
if ( !this.markedChildren.has( node.parent ) && this.domConverter.mapViewToDom( node.parent ) ) { | ||
this._updateText( node, { inlineFillerPosition } ); | ||
} | ||
this._updateText( node, { inlineFillerPosition } ); | ||
} | ||
|
||
for ( const element of this.markedAttributes ) { | ||
|
@@ -407,6 +406,12 @@ export default class Renderer { | |
*/ | ||
_updateText( viewText, options ) { | ||
const domText = this.domConverter.findCorrespondingDomText( viewText ); | ||
|
||
// If this is a new text node and it is not in DOM, it will be created and handled in `_updateChildren`. | ||
if ( !domText ) { | ||
return; | ||
} | ||
|
||
const newDomText = this.domConverter.viewToDom( viewText, domText.ownerDocument ); | ||
|
||
const actualText = domText.data; | ||
|
@@ -420,6 +425,12 @@ export default class Renderer { | |
|
||
if ( actualText != expectedText ) { | ||
domText.data = expectedText; | ||
|
||
const nextTouchingTextNode = getTouchingTextNode( viewText, true ); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing comment why we need to do that. |
||
|
||
if ( nextTouchingTextNode ) { | ||
this.markedTexts.add( nextTouchingTextNode ); | ||
} | ||
} | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
/** | ||
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | ||
* For licensing, see LICENSE.md. | ||
*/ | ||
|
||
import ViewPosition from './position'; | ||
import ViewTreeWalker from './treewalker'; | ||
|
||
/** | ||
* Contains utility functions for working on view. | ||
* | ||
* @module engine/view/utils | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's rename this to |
||
*/ | ||
|
||
/** | ||
* For given {@link module:engine/view/text~Text view text node}, it finds previous or next sibling that is contained | ||
* in the same container element. If there is no such sibling, `null` is returned. | ||
* | ||
* @param {module:engine/view/text~Text} node Reference node. | ||
* @param {Boolean} getNext If `true` next touching sibling will be returned. If `false` previous touching sibling will be returned. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It'd be good to find it a better name... but I can't :D. Also, this is an optional param. Should be marked with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As for this flag being optional – it's always passed in the tests: https://github.com/ckeditor/ckeditor5-engine/pull/1083/files#diff-41de3c28cf4c9630dfaec4e35850f0faR38. This should be changed also there. |
||
* @returns {module:engine/view/text~Text|null} Touching text node or `null` if there is no next or previous touching text node. | ||
*/ | ||
export function getTouchingTextNode( node, getNext ) { | ||
const treeWalker = new ViewTreeWalker( { | ||
startPosition: getNext ? ViewPosition.createAfter( node ) : ViewPosition.createBefore( node ), | ||
direction: getNext ? 'forward' : 'backward' | ||
} ); | ||
|
||
for ( const value of treeWalker ) { | ||
if ( value.item.is( 'containerElement' ) ) { | ||
// ViewContainerElement is found on a way to next ViewText node, so given `node` was first/last | ||
// text node in it's container element. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. its |
||
return null; | ||
} else if ( value.item.is( 'text' ) ) { | ||
// Found a text node in the same container element. | ||
return value.item; | ||
} | ||
} | ||
|
||
return null; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -269,7 +269,7 @@ describe( 'DomConverter', () => { | |
// At the end. | ||
test( 'x ', 'x_' ); | ||
test( 'x ', 'x _' ); | ||
test( 'x ', 'x_ _' ); | ||
test( 'x ', 'x __' ); | ||
test( 'x ', 'x _ _' ); | ||
|
||
// In the middle. | ||
|
@@ -282,11 +282,19 @@ describe( 'DomConverter', () => { | |
test( ' x ', '_x_' ); | ||
test( ' x x ', '_ x _x _' ); | ||
test( ' x x ', '_ _x x _' ); | ||
test( ' x x ', '_ _x x_ _' ); | ||
test( ' x x ', '_ _x x __' ); | ||
test( ' x x ', '_ _x _ _x_' ); | ||
|
||
// Only spaces. | ||
test( ' ', '_' ); | ||
test( ' ', '__' ); | ||
test( ' ', '_ _' ); | ||
test( ' ', '_ __' ); | ||
test( ' ', '_ _ _' ); | ||
test( ' ', '_ _ __' ); | ||
|
||
// With hard | ||
// It should be treated like a normal sign. | ||
test( '_x', '_x' ); | ||
test( ' _x', '__x' ); | ||
test( ' _x', '_ _x' ); | ||
|
@@ -323,33 +331,80 @@ describe( 'DomConverter', () => { | |
|
||
test( [ 'x', ' y' ], 'x y' ); | ||
test( [ 'x ', ' y' ], 'x _y' ); | ||
test( [ 'x ', ' y' ], 'x_ _y' ); | ||
test( [ 'x ', ' y' ], 'x _ y' ); | ||
test( [ 'x ', ' y' ], 'x _ _y' ); | ||
test( [ 'x ', ' y' ], 'x_ _ _y' ); | ||
test( [ 'x ', ' y' ], 'x _ _ y' ); | ||
|
||
test( [ 'x', '_y' ], 'x_y' ); | ||
test( [ 'x ', '_y' ], 'x _y' ); | ||
test( [ 'x ', '_y' ], 'x_ _y' ); | ||
test( [ 'x ', '_y' ], 'x __y' ); | ||
test( [ 'x ', '_y' ], 'x _ _y' ); | ||
test( [ 'x ', '_y' ], 'x_ _ _y' ); | ||
test( [ 'x ', '_y' ], 'x _ __y' ); | ||
|
||
test( [ 'x', ' y' ], 'x _y' ); | ||
test( [ 'x ', ' y' ], 'x _ y' ); | ||
test( [ 'x ', ' y' ], 'x_ _ y' ); | ||
test( [ 'x ', ' y' ], 'x _ _y' ); | ||
test( [ 'x ', ' y' ], 'x _ _ y' ); | ||
test( [ 'x ', ' y' ], 'x_ _ _ y' ); | ||
test( [ 'x ', ' y' ], 'x _ _ _y' ); | ||
|
||
test( [ 'x', ' y' ], 'x _ y' ); | ||
test( [ 'x ', ' y' ], 'x _ _y' ); | ||
test( [ 'x ', ' y' ], 'x_ _ _y' ); | ||
test( [ 'x ', ' y' ], 'x _ _ y' ); | ||
test( [ 'x ', ' y' ], 'x _ _ _y' ); | ||
test( [ 'x ', ' y' ], 'x_ _ _ _y' ); | ||
test( [ 'x ', ' y' ], 'x _ _ _ y' ); | ||
|
||
// "Non-empty" + "empty" text nodes. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about mirrored cases with "empty" + "non-empty"? |
||
test( [ 'x', ' ' ], 'x_' ); | ||
test( [ 'x', ' ' ], 'x _' ); | ||
test( [ 'x', ' ' ], 'x __' ); | ||
test( [ 'x ', ' ' ], 'x _' ); | ||
test( [ 'x ', ' ' ], 'x __' ); | ||
test( [ 'x ', ' ' ], 'x _ _' ); | ||
test( [ 'x ', ' ' ], 'x __' ); | ||
test( [ 'x ', ' ' ], 'x _ _' ); | ||
test( [ 'x ', ' ' ], 'x _ __' ); | ||
test( [ 'x ', ' ' ], 'x _ _' ); | ||
test( [ 'x ', ' ' ], 'x _ __' ); | ||
test( [ 'x ', ' ' ], 'x _ _ _' ); | ||
|
||
test( [ 'x', ' ', 'x' ], 'x x' ); | ||
test( [ 'x', ' ', ' x' ], 'x _x' ); | ||
test( [ 'x', ' ', ' x' ], 'x _ x' ); | ||
test( [ 'x', ' ', ' x' ], 'x _ _ x' ); | ||
test( [ 'x ', ' ', ' x' ], 'x _ x' ); | ||
test( [ 'x ', ' ', ' x' ], 'x _ _x' ); | ||
test( [ 'x ', ' ', ' x' ], 'x _ _ _x' ); | ||
test( [ 'x ', ' ', ' x' ], 'x _ _x' ); | ||
test( [ 'x ', ' ', ' x' ], 'x _ _ x' ); | ||
test( [ 'x ', ' ', ' x' ], 'x _ _ _ x' ); | ||
test( [ 'x ', ' ', ' x' ], 'x _ _ x' ); | ||
test( [ 'x ', ' ', ' x' ], 'x _ _ _x' ); | ||
test( [ 'x ', ' ', ' x' ], 'x _ _ _ _x' ); | ||
|
||
// "Empty" + "empty" text nodes. | ||
test( [ ' ', ' ' ], '__' ); | ||
test( [ ' ', ' ' ], '_ _' ); | ||
test( [ ' ', ' ' ], '_ __' ); | ||
test( [ ' ', ' ' ], '_ _' ); | ||
test( [ ' ', ' ' ], '_ __' ); | ||
test( [ ' ', ' ' ], '_ __' ); | ||
test( [ ' ', ' ' ], '_ _ _' ); | ||
test( [ ' ', ' ' ], '_ _ _' ); | ||
test( [ ' ', ' ' ], '_ _ __' ); | ||
|
||
it( 'not in preformatted blocks', () => { | ||
const viewDiv = new ViewContainerElement( 'pre', null, new ViewText( ' foo ' ) ); | ||
const viewDiv = new ViewContainerElement( 'pre', null, [ new ViewText( ' foo ' ), new ViewText( ' bar ' ) ] ); | ||
const domDiv = converter.viewToDom( viewDiv, document ); | ||
|
||
expect( domDiv.innerHTML ).to.equal( ' foo bar ' ); | ||
} ); | ||
|
||
it( 'text node before in a preformatted node', () => { | ||
const viewCode = new ViewAttributeElement( 'code', null, new ViewText( 'foo ' ) ); | ||
const viewDiv = new ViewContainerElement( 'div', null, [ viewCode, new ViewText( ' bar' ) ] ); | ||
const domDiv = converter.viewToDom( viewDiv, document ); | ||
|
||
expect( domDiv.innerHTML ).to.equal( ' foo ' ); | ||
expect( domDiv.innerHTML ).to.equal( '<code>foo </code> bar' ); | ||
} ); | ||
} ); | ||
} ); | ||
|
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.
This looks very suspicious. How did this function work without this condition so far? Why is it needed here now?
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.
It worked because there was a condition in
view.Renderer#render
method.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.
So why doesn't that condition save us from adding one 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.
I don't understand. Before there was a condition in
render
method in the loop where_updateText
is called. I removed this condition because I wanted_updateText
to be fired more often. But this caused a situation where_upadateText
was fired on text nodes that weren't yet converted to DOM. That's why I needed to add the statement in_updateText
.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.
Aaand it turned out we can't make this change ;> We'll also need to make a step back and remove the fix for subsequent text nodes updating. It seems that we may not be able to finalise it now because of how the text nodes are mapped.
We can neither update all text nodes before children are updated because changes in children might've changed the indexes of text nodes (so updating text nodes would fail). Changing the order to 1) update children, 2) update text nodes doesn't help too because in the following case, the whole paragraph is updated first and then text nodes are not:
<p>x<strong> b</strong></p>
"x "
text node is up to date when it comes to updating text nodes, so we don't mark" b"
to be updated.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.
@scofalik will you report a ticket for this case and the one you mentioned in #1083 (comment)? And of course, please revert changes which we can't do now.
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.
https://github.com/ckeditor/ckeditor5-engine/issues/1093