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

Commit e3bc4d1

Browse files
authored
Merge pull request #958 from ckeditor/t/957
Fix: None of the editable's ancestors should scroll when the DomConverter focuses an editable. Closes #957.
2 parents 71984a3 + a61e847 commit e3bc4d1

File tree

2 files changed

+77
-13
lines changed

2 files changed

+77
-13
lines changed

src/view/domconverter.js

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -736,14 +736,33 @@ export default class DomConverter {
736736
const domEditable = this.getCorrespondingDomElement( viewEditable );
737737

738738
if ( domEditable && domEditable.ownerDocument.activeElement !== domEditable ) {
739+
// Save the scrollX and scrollY positions before the focus.
739740
const { scrollX, scrollY } = global.window;
740-
const { scrollLeft, scrollTop } = domEditable;
741+
const scrollPositions = [];
742+
743+
// Save all scrollLeft and scrollTop values starting from domEditable up to
744+
// document#documentElement.
745+
forEachDomNodeAncestor( domEditable, node => {
746+
const { scrollLeft, scrollTop } = node;
747+
748+
scrollPositions.push( [ scrollLeft, scrollTop ] );
749+
} );
741750

742751
domEditable.focus();
743752

753+
// Restore scrollLeft and scrollTop values starting from domEditable up to
754+
// document#documentElement.
755+
// https://github.com/ckeditor/ckeditor5-engine/issues/951
756+
// https://github.com/ckeditor/ckeditor5-engine/issues/957
757+
forEachDomNodeAncestor( domEditable, node => {
758+
const [ scrollLeft, scrollTop ] = scrollPositions.shift();
759+
760+
node.scrollLeft = scrollLeft;
761+
node.scrollTop = scrollTop;
762+
} );
763+
764+
// Restore the scrollX and scrollY positions after the focus.
744765
// https://github.com/ckeditor/ckeditor5-engine/issues/951
745-
domEditable.scrollLeft = scrollLeft;
746-
domEditable.scrollTop = scrollTop;
747766
global.window.scrollTo( scrollX, scrollY );
748767
}
749768
}
@@ -1058,3 +1077,15 @@ function _hasDomParentOfType( node, types, boundaryParent ) {
10581077

10591078
return parents.some( parent => parent.tagName && types.includes( parent.tagName.toLowerCase() ) );
10601079
}
1080+
1081+
// A helper that executes given callback for each DOM node's ancestor, starting from the given node
1082+
// and ending in document#documentElement.
1083+
//
1084+
// @param {Node} node
1085+
// @param {Function} callback A callback to be executed for each ancestor.
1086+
function forEachDomNodeAncestor( node, callback ) {
1087+
while ( node && node != global.document ) {
1088+
callback( node );
1089+
node = node.parentNode;
1090+
}
1091+
}

tests/view/domconverter/domconverter.js

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,21 +33,23 @@ describe( 'DomConverter', () => {
3333
} );
3434

3535
describe( 'focus()', () => {
36-
let viewEditable, domEditable, viewDocument;
36+
let viewEditable, domEditable, domEditableParent, viewDocument;
3737

3838
beforeEach( () => {
3939
viewDocument = new ViewDocument();
4040
viewEditable = new ViewEditable( 'div' );
4141
viewEditable.document = viewDocument;
4242

4343
domEditable = document.createElement( 'div' );
44+
domEditableParent = document.createElement( 'div' );
4445
converter.bindElements( domEditable, viewEditable );
4546
domEditable.setAttribute( 'contenteditable', 'true' );
46-
document.body.appendChild( domEditable );
47+
domEditableParent.appendChild( domEditable );
48+
document.body.appendChild( domEditableParent );
4749
} );
4850

4951
afterEach( () => {
50-
document.body.removeChild( domEditable );
52+
document.body.removeChild( domEditableParent );
5153
viewDocument.destroy();
5254
} );
5355

@@ -69,19 +71,46 @@ describe( 'DomConverter', () => {
6971
} );
7072

7173
// https://github.com/ckeditor/ckeditor5-engine/issues/951
72-
it( 'should actively prevent window scroll in WebKit', () => {
74+
// https://github.com/ckeditor/ckeditor5-engine/issues/957
75+
it( 'should actively prevent scrolling', () => {
7376
const scrollToSpy = testUtils.sinon.stub( global.window, 'scrollTo' );
74-
const scrollLeftSpy = sinon.spy();
75-
const scrollTopSpy = sinon.spy();
77+
const editableScrollLeftSpy = sinon.spy();
78+
const editableScrollTopSpy = sinon.spy();
79+
const parentScrollLeftSpy = sinon.spy();
80+
const parentScrollTopSpy = sinon.spy();
81+
const documentElementScrollLeftSpy = sinon.spy();
82+
const documentElementScrollTopSpy = sinon.spy();
7683

7784
Object.defineProperties( domEditable, {
7885
scrollLeft: {
7986
get: () => 20,
80-
set: scrollLeftSpy
87+
set: editableScrollLeftSpy
8188
},
8289
scrollTop: {
8390
get: () => 200,
84-
set: scrollTopSpy
91+
set: editableScrollTopSpy
92+
}
93+
} );
94+
95+
Object.defineProperties( domEditableParent, {
96+
scrollLeft: {
97+
get: () => 40,
98+
set: parentScrollLeftSpy
99+
},
100+
scrollTop: {
101+
get: () => 400,
102+
set: parentScrollTopSpy
103+
}
104+
} );
105+
106+
Object.defineProperties( global.document.documentElement, {
107+
scrollLeft: {
108+
get: () => 60,
109+
set: documentElementScrollLeftSpy
110+
},
111+
scrollTop: {
112+
get: () => 600,
113+
set: documentElementScrollTopSpy
85114
}
86115
} );
87116

@@ -90,8 +119,12 @@ describe( 'DomConverter', () => {
90119

91120
converter.focus( viewEditable );
92121
sinon.assert.calledWithExactly( scrollToSpy, 10, 100 );
93-
sinon.assert.calledWithExactly( scrollLeftSpy, 20 );
94-
sinon.assert.calledWithExactly( scrollTopSpy, 200 );
122+
sinon.assert.calledWithExactly( editableScrollLeftSpy, 20 );
123+
sinon.assert.calledWithExactly( editableScrollTopSpy, 200 );
124+
sinon.assert.calledWithExactly( parentScrollLeftSpy, 40 );
125+
sinon.assert.calledWithExactly( parentScrollTopSpy, 400 );
126+
sinon.assert.calledWithExactly( documentElementScrollLeftSpy, 60 );
127+
sinon.assert.calledWithExactly( documentElementScrollTopSpy, 600 );
95128
} );
96129
} );
97130

0 commit comments

Comments
 (0)