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

Commit b7c4765

Browse files
authoredMar 28, 2019
Merge pull request #79 from ckeditor/t/ckeditor5/1463
Fix: Triple clicking inside a nested editable should not select the entire widget in Safari. Closes ckeditor/ckeditor5#1463.
2 parents 1acb598 + 7bca9e1 commit b7c4765

File tree

3 files changed

+244
-16
lines changed

3 files changed

+244
-16
lines changed
 

‎src/widget.js

+12-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
1111
import MouseObserver from '@ckeditor/ckeditor5-engine/src/view/observer/mouseobserver';
1212
import { getLabel, isWidget, WIDGET_SELECTED_CLASS_NAME } from './utils';
1313
import { getCode, keyCodes, parseKeystroke } from '@ckeditor/ckeditor5-utils/src/keyboard';
14+
import env from '@ckeditor/ckeditor5-utils/src/env';
1415

1516
import '../theme/widget.css';
1617

@@ -114,8 +115,18 @@ export default class Widget extends Plugin {
114115
const viewDocument = view.document;
115116
let element = domEventData.target;
116117

117-
// Do nothing if inside nested editable.
118+
// Do nothing for single or double click inside nested editable.
118119
if ( isInsideNestedEditable( element ) ) {
120+
// But at least triple click inside nested editable causes broken selection in Safari.
121+
// For such event, we select the entire nested editable element.
122+
// See: https://github.com/ckeditor/ckeditor5/issues/1463.
123+
if ( env.isSafari && domEventData.domEvent.detail >= 3 ) {
124+
this.editor.editing.view.change( writer => {
125+
domEventData.preventDefault();
126+
writer.setSelection( element, 'in' );
127+
} );
128+
}
129+
119130
return;
120131
}
121132

‎tests/widget-integration.js

+232
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
/**
2+
* @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
3+
* For licensing, see LICENSE.md.
4+
*/
5+
6+
/* global document */
7+
8+
import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
9+
import Typing from '@ckeditor/ckeditor5-typing/src/typing';
10+
import Widget from '../src/widget';
11+
import DomEventData from '@ckeditor/ckeditor5-engine/src/view/observer/domeventdata';
12+
13+
import { toWidget } from '../src/utils';
14+
import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
15+
import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
16+
17+
import env from '@ckeditor/ckeditor5-utils/src/env';
18+
import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
19+
20+
describe( 'Widget - integration', () => {
21+
let editor, model, view, viewDocument, editorElement;
22+
23+
testUtils.createSinonSandbox();
24+
25+
beforeEach( () => {
26+
// Most tests assume non-edge environment but we do not set `contenteditable=false` on Edge so stub `env.isEdge`.
27+
testUtils.sinon.stub( env, 'isEdge' ).get( () => false );
28+
testUtils.sinon.stub( env, 'isSafari' ).get( () => true );
29+
30+
editorElement = document.createElement( 'div' );
31+
document.body.appendChild( editorElement );
32+
33+
return ClassicEditor.create( editorElement, { plugins: [ Widget, Typing ] } )
34+
.then( newEditor => {
35+
editor = newEditor;
36+
model = editor.model;
37+
view = editor.editing.view;
38+
viewDocument = view.document;
39+
40+
model.schema.register( 'widget', {
41+
inheritAllFrom: '$block',
42+
isObject: true
43+
} );
44+
model.schema.register( 'paragraph', {
45+
inheritAllFrom: '$block',
46+
allowIn: '$root'
47+
} );
48+
model.schema.register( 'nested', {
49+
allowIn: 'widget',
50+
isLimit: true
51+
} );
52+
model.schema.extend( '$text', {
53+
allowIn: [ 'nested', 'editable', 'inline-widget' ]
54+
} );
55+
model.schema.register( 'editable', {
56+
allowIn: [ 'widget', '$root' ]
57+
} );
58+
model.schema.register( 'inline-widget', {
59+
allowWhere: '$text',
60+
isObject: true,
61+
isInline: true
62+
} );
63+
64+
editor.conversion.for( 'downcast' )
65+
.elementToElement( { model: 'paragraph', view: 'p' } )
66+
.elementToElement( { model: 'inline', view: 'figure' } )
67+
.elementToElement( { model: 'image', view: 'img' } )
68+
.elementToElement( {
69+
model: 'widget',
70+
view: ( modelItem, viewWriter ) => {
71+
const div = viewWriter.createContainerElement( 'div' );
72+
73+
return toWidget( div, viewWriter, { label: 'element label' } );
74+
}
75+
} )
76+
.elementToElement( {
77+
model: 'inline-widget',
78+
view: ( modelItem, viewWriter ) => {
79+
const span = viewWriter.createContainerElement( 'span' );
80+
81+
return toWidget( span, viewWriter );
82+
}
83+
} )
84+
.elementToElement( {
85+
model: 'nested',
86+
view: ( modelItem, viewWriter ) => viewWriter.createEditableElement( 'figcaption', { contenteditable: true } )
87+
} )
88+
.elementToElement( {
89+
model: 'editable',
90+
view: ( modelItem, viewWriter ) => viewWriter.createEditableElement( 'figcaption', { contenteditable: true } )
91+
} );
92+
} );
93+
} );
94+
95+
afterEach( () => {
96+
editorElement.remove();
97+
98+
return editor.destroy();
99+
} );
100+
101+
it( 'should do nothing if clicked inside a nested editable', () => {
102+
setModelData( model, '[]<widget><nested>foo bar</nested></widget>' );
103+
const viewDiv = viewDocument.getRoot().getChild( 0 );
104+
const viewFigcaption = viewDiv.getChild( 0 );
105+
106+
const preventDefault = sinon.spy();
107+
108+
const domEventDataMock = new DomEventData( view, {
109+
target: view.domConverter.mapViewToDom( viewFigcaption ),
110+
preventDefault,
111+
detail: 1
112+
} );
113+
114+
viewDocument.fire( 'mousedown', domEventDataMock );
115+
116+
sinon.assert.notCalled( preventDefault );
117+
118+
expect( getViewData( view ) ).to.equal(
119+
'[]<div class="ck-widget" contenteditable="false"><figcaption contenteditable="true">foo bar</figcaption></div>'
120+
);
121+
} );
122+
123+
it( 'should select the entire nested editable if triple clicked', () => {
124+
setModelData( model, '[]<widget><nested>foo bar</nested></widget>' );
125+
126+
const viewDiv = viewDocument.getRoot().getChild( 0 );
127+
const viewFigcaption = viewDiv.getChild( 0 );
128+
const preventDefault = sinon.spy();
129+
const domEventDataMock = new DomEventData( view, {
130+
target: view.domConverter.mapViewToDom( viewFigcaption ),
131+
preventDefault,
132+
detail: 3
133+
} );
134+
135+
viewDocument.fire( 'mousedown', domEventDataMock );
136+
137+
sinon.assert.called( preventDefault );
138+
139+
expect( getViewData( view ) ).to.equal(
140+
'<div class="ck-widget" contenteditable="false"><figcaption contenteditable="true">[foo bar]</figcaption></div>'
141+
);
142+
} );
143+
144+
it( 'should select proper nested editable if triple clicked', () => {
145+
setModelData( model, '[]<widget><nested>foo</nested><nested>bar</nested></widget>' );
146+
147+
const viewDiv = viewDocument.getRoot().getChild( 0 );
148+
const secondViewFigcaption = viewDiv.getChild( 1 );
149+
const preventDefault = sinon.spy();
150+
const domEventDataMock = new DomEventData( view, {
151+
target: view.domConverter.mapViewToDom( secondViewFigcaption ),
152+
preventDefault,
153+
detail: 3
154+
} );
155+
156+
viewDocument.fire( 'mousedown', domEventDataMock );
157+
158+
sinon.assert.called( preventDefault );
159+
160+
expect( getViewData( view ) ).to.equal(
161+
'<div class="ck-widget" contenteditable="false">' +
162+
'<figcaption contenteditable="true">foo</figcaption>' +
163+
'<figcaption contenteditable="true">[bar]</figcaption>' +
164+
'</div>'
165+
);
166+
} );
167+
168+
it( 'should select the entire nested editable if quadra clicked', () => {
169+
setModelData( model, '[]<widget><nested>foo bar</nested></widget>' );
170+
171+
const viewDiv = viewDocument.getRoot().getChild( 0 );
172+
const viewFigcaption = viewDiv.getChild( 0 );
173+
const preventDefault = sinon.spy();
174+
const domEventDataMock = new DomEventData( view, {
175+
target: view.domConverter.mapViewToDom( viewFigcaption ),
176+
preventDefault,
177+
detail: 4
178+
} );
179+
180+
viewDocument.fire( 'mousedown', domEventDataMock );
181+
182+
sinon.assert.called( preventDefault );
183+
184+
expect( getViewData( view ) ).to.equal(
185+
'<div class="ck-widget" contenteditable="false"><figcaption contenteditable="true">[foo bar]</figcaption></div>'
186+
);
187+
} );
188+
189+
it( 'should select the inline widget if triple clicked', () => {
190+
setModelData( model, '<paragraph>Foo<inline-widget>foo bar</inline-widget>Bar</paragraph>' );
191+
192+
const viewParagraph = viewDocument.getRoot().getChild( 0 );
193+
const viewInlineWidget = viewParagraph.getChild( 1 );
194+
const preventDefault = sinon.spy();
195+
const domEventDataMock = new DomEventData( view, {
196+
target: view.domConverter.mapViewToDom( viewInlineWidget ),
197+
preventDefault,
198+
detail: 3
199+
} );
200+
201+
viewDocument.fire( 'mousedown', domEventDataMock );
202+
203+
sinon.assert.called( preventDefault );
204+
205+
expect( getViewData( view ) ).to.equal(
206+
'<p>Foo{<span class="ck-widget ck-widget_selected" contenteditable="false">foo bar</span>}Bar</p>'
207+
);
208+
} );
209+
210+
it( 'should does nothing for non-Safari browser', () => {
211+
testUtils.sinon.stub( env, 'isSafari' ).get( () => false );
212+
213+
setModelData( model, '[]<widget><nested>foo bar</nested></widget>' );
214+
215+
const viewDiv = viewDocument.getRoot().getChild( 0 );
216+
const viewFigcaption = viewDiv.getChild( 0 );
217+
const preventDefault = sinon.spy();
218+
const domEventDataMock = new DomEventData( view, {
219+
target: view.domConverter.mapViewToDom( viewFigcaption ),
220+
preventDefault,
221+
detail: 4
222+
} );
223+
224+
viewDocument.fire( 'mousedown', domEventDataMock );
225+
226+
sinon.assert.notCalled( preventDefault );
227+
228+
expect( getViewData( view ) ).to.equal(
229+
'[]<div class="ck-widget" contenteditable="false"><figcaption contenteditable="true">foo bar</figcaption></div>'
230+
);
231+
} );
232+
} );

‎tests/widget.js

-15
Original file line numberDiff line numberDiff line change
@@ -151,21 +151,6 @@ describe( 'Widget', () => {
151151
sinon.assert.calledOnce( domEventDataMock.preventDefault );
152152
} );
153153

154-
it( 'should do nothing if clicked inside nested editable', () => {
155-
setModelData( model, '[]<widget><nested>foo bar</nested></widget>' );
156-
const viewDiv = viewDocument.getRoot().getChild( 0 );
157-
const viewFigcaption = viewDiv.getChild( 0 );
158-
159-
const domEventDataMock = {
160-
target: viewFigcaption,
161-
preventDefault: sinon.spy()
162-
};
163-
164-
viewDocument.fire( 'mousedown', domEventDataMock );
165-
166-
sinon.assert.notCalled( domEventDataMock.preventDefault );
167-
} );
168-
169154
it( 'should do nothing if clicked in non-widget element', () => {
170155
setModelData( model, '<paragraph>[]foo bar</paragraph><widget></widget>' );
171156
const viewP = viewDocument.getRoot().getChild( 0 );

0 commit comments

Comments
 (0)
This repository has been archived.