Skip to content
This repository was archived by the owner on Jun 26, 2020. It is now read-only.

Commit 80392ad

Browse files
authored
Merge pull request #1582 from ckeditor/t/1439
Fix: Firefox should visually move the caret to the new line after a soft break. Closes #1439.
2 parents 5d26bc3 + 0342eba commit 80392ad

File tree

5 files changed

+147
-0
lines changed

5 files changed

+147
-0
lines changed

src/view/renderer.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
* For licensing, see LICENSE.md.
44
*/
55

6+
/* globals Node */
7+
68
/**
79
* @module engine/view/renderer
810
*/
@@ -20,6 +22,7 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
2022
import isText from '@ckeditor/ckeditor5-utils/src/dom/istext';
2123
import isNode from '@ckeditor/ckeditor5-utils/src/dom/isnode';
2224
import fastDiff from '@ckeditor/ckeditor5-utils/src/fastdiff';
25+
import env from '@ckeditor/ckeditor5-utils/src/env';
2326

2427
/**
2528
* Renderer is responsible for updating the DOM structure and the DOM selection based on
@@ -752,6 +755,11 @@ export default class Renderer {
752755

753756
domSelection.collapse( anchor.parent, anchor.offset );
754757
domSelection.extend( focus.parent, focus.offset );
758+
759+
// Firefox–specific hack (https://github.com/ckeditor/ckeditor5-engine/issues/1439).
760+
if ( env.isGecko ) {
761+
fixGeckoSelectionAfterBr( focus, domSelection );
762+
}
755763
}
756764

757765
/**
@@ -923,3 +931,28 @@ function sameNodes( blockFiller, actualDomChild, expectedDomChild ) {
923931
// Not matching types.
924932
return false;
925933
}
934+
935+
// The following is a Firefox–specific hack (https://github.com/ckeditor/ckeditor5-engine/issues/1439).
936+
// When the native DOM selection is at the end of the block and preceded by <br /> e.g.
937+
//
938+
// <p>foo<br/>[]</p>
939+
//
940+
// which happens a lot when using the soft line break, the browser fails to (visually) move the
941+
// caret to the new line. A quick fix is as simple as force–refreshing the selection with the same range.
942+
function fixGeckoSelectionAfterBr( focus, domSelection ) {
943+
const parent = focus.parent;
944+
945+
// This fix works only when the focus point is at the very end of an element.
946+
// There is no point in running it in cases unrelated to the browser bug.
947+
if ( parent.nodeType != Node.ELEMENT_NODE || focus.offset != parent.childNodes.length - 1 ) {
948+
return;
949+
}
950+
951+
const childAtOffset = parent.childNodes[ focus.offset ];
952+
953+
// To stay on the safe side, the fix being as specific as possible, it targets only the
954+
// selection which is at the very end of the element and preceded by <br />.
955+
if ( childAtOffset && childAtOffset.tagName == 'BR' ) {
956+
domSelection.addRange( domSelection.getRangeAt( 0 ) );
957+
}
958+
}

tests/manual/tickets/1439/1.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<div id="editor"></div>

tests/manual/tickets/1439/1.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/**
2+
* @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
3+
* For licensing, see LICENSE.md.
4+
*/
5+
6+
/* globals console, window, document */
7+
8+
import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
9+
import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset';
10+
11+
ClassicEditor
12+
.create( document.querySelector( '#editor' ), {
13+
plugins: [ ArticlePluginSet ],
14+
toolbar: {
15+
items: [
16+
'heading',
17+
'bold',
18+
'italic',
19+
'link',
20+
'bulletedList',
21+
'numberedList',
22+
'blockQuote',
23+
'undo',
24+
'redo'
25+
]
26+
}
27+
} )
28+
.then( editor => {
29+
window.editor = editor;
30+
} )
31+
.catch( err => {
32+
console.error( err.stack );
33+
} );

tests/manual/tickets/1439/1.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
## Firefox should visually move the caret to the new line after a soft break [#1439](https://github.com/ckeditor/ckeditor5-engine/issues/1439)
2+
3+
4+
1. Open Firefox,
5+
2. In an empty editor type "foo",
6+
3. Press <kbd>Shift</kbd>+<kbd>Enter</kbd>.
7+
8+
**Expected**
9+
10+
1. The soft break should be created,
11+
2. The caret should be **in the new line and blinking**.

tests/view/renderer.js

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
2525
import createViewRoot from './_utils/createroot';
2626
import createElement from '@ckeditor/ckeditor5-utils/src/dom/createelement';
2727
import normalizeHtml from '@ckeditor/ckeditor5-utils/tests/_utils/normalizehtml';
28+
import env from '@ckeditor/ckeditor5-utils/src/env';
2829

2930
describe( 'Renderer', () => {
3031
let selection, domConverter, renderer;
@@ -1714,6 +1715,74 @@ describe( 'Renderer', () => {
17141715
expect( domRoot.innerHTML ).to.equal( '<ul><li>Foo<ul><li><b>Bar</b><i>Baz</i></li></ul></li></ul>' );
17151716
} );
17161717

1718+
// #1439
1719+
it( 'should not force–refresh the selection in non–Gecko browsers after a soft break', () => {
1720+
const domSelection = domRoot.ownerDocument.defaultView.getSelection();
1721+
1722+
testUtils.sinon.stub( env, 'isGecko' ).get( () => false );
1723+
const spy = testUtils.sinon.stub( domSelection, 'addRange' );
1724+
1725+
// <p>foo<br/>[]</p>
1726+
const { view: viewP, selection: newSelection } = parse(
1727+
'<container:p>' +
1728+
'foo[]' +
1729+
'<empty:br></empty:br>[]' +
1730+
'</container:p>' );
1731+
1732+
viewRoot._appendChild( viewP );
1733+
selection._setTo( newSelection );
1734+
renderer.markToSync( 'children', viewRoot );
1735+
renderer.render();
1736+
1737+
sinon.assert.notCalled( spy );
1738+
} );
1739+
1740+
// #1439
1741+
it( 'should force–refresh the selection in Gecko browsers after a soft break to nudge the caret', () => {
1742+
const domSelection = domRoot.ownerDocument.defaultView.getSelection();
1743+
1744+
testUtils.sinon.stub( env, 'isGecko' ).get( () => true );
1745+
const spy = testUtils.sinon.stub( domSelection, 'addRange' );
1746+
1747+
// <p>foo[]<b>bar</b></p>
1748+
let { view: viewP, selection: newSelection } = parse(
1749+
'<container:p>' +
1750+
'foo[]' +
1751+
'<attribute:b>bar</attribute:b>' +
1752+
'</container:p>' );
1753+
1754+
viewRoot._appendChild( viewP );
1755+
selection._setTo( newSelection );
1756+
renderer.markToSync( 'children', viewRoot );
1757+
renderer.render();
1758+
1759+
sinon.assert.notCalled( spy );
1760+
1761+
// <p>foo<b>bar</b></p><p>foo[]<br/></p>
1762+
( { view: viewP, selection: newSelection } = parse(
1763+
'<container:p>' +
1764+
'foo[]' +
1765+
'<empty:br></empty:br>' +
1766+
'</container:p>' ) );
1767+
1768+
viewRoot._appendChild( viewP );
1769+
selection._setTo( newSelection );
1770+
renderer.markToSync( 'children', viewRoot );
1771+
renderer.render();
1772+
1773+
sinon.assert.notCalled( spy );
1774+
1775+
// <p>foo<b>bar</b></p><p>foo<br/>[]</p>
1776+
selection._setTo( [
1777+
new ViewRange( new ViewPosition( viewP, 2 ), new ViewPosition( viewP, 2 ) )
1778+
] );
1779+
1780+
renderer.markToSync( 'children', viewRoot );
1781+
renderer.render();
1782+
1783+
sinon.assert.calledOnce( spy );
1784+
} );
1785+
17171786
describe( 'fake selection', () => {
17181787
beforeEach( () => {
17191788
const { view: viewP, selection: newSelection } = parse(

0 commit comments

Comments
 (0)