8
0
Просмотр исходного кода

Merge branch 'master' into t/1267

Szymon Kupś 8 лет назад
Родитель
Сommit
298b7ab527

+ 11 - 2
packages/ckeditor5-engine/src/model/writer.js

@@ -144,6 +144,9 @@ export default class Writer {
 	 *
 	 * Note that if the item already has parent it will be removed from the previous parent.
 	 *
+	 * Note that you cannot re-insert a node from a document to a different document or document fragment. In this case,
+	 * `model-writer-insert-forbidden-move` is thrown.
+	 *
 	 * If you want to move {@link module:engine/model/range~Range range} instead of an
 	 * {@link module:engine/model/item~Item item} use {@link module:engine/model/writer~Writer#move move}.
 	 *
@@ -172,8 +175,14 @@ export default class Writer {
 			}
 			// If it isn't the same root.
 			else {
-				// We need to remove this item from old position first.
-				this.remove( item );
+				if ( item.root.document ) {
+					// It is forbidden to move a node that was already in a document outside of it.
+					throw new Error( 'model-writer-insert-forbidden-move: Cannot move a node from a document to a different tree.' );
+				} else {
+					// Move between two different document fragments or from document fragment to a document is possible.
+					// In that case, remove the item from it's original parent.
+					this.remove( item );
+				}
 			}
 		}
 

+ 12 - 0
packages/ckeditor5-engine/src/view/document.js

@@ -66,6 +66,18 @@ export default class Document {
 		 */
 		this.set( 'isFocused', false );
 
+		/**
+		 * True if composition is in progress inside the document.
+		 *
+		 * This property is updated by the {@link module:engine/view/observer/compositionobserver~CompositionObserver}.
+		 * If the {@link module:engine/view/observer/compositionobserver~CompositionObserver} is disabled this property will not change.
+		 *
+		 * @readonly
+		 * @observable
+		 * @member {Boolean} module:engine/view/document~Document#isComposing
+		 */
+		this.set( 'isComposing', false );
+
 		/**
 		 * Post-fixer callbacks registered to the view document.
 		 *

+ 79 - 0
packages/ckeditor5-engine/src/view/observer/compositionobserver.js

@@ -0,0 +1,79 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module engine/view/observer/compositionobserver
+ */
+
+import DomEventObserver from './domeventobserver';
+
+/**
+ * {@link module:engine/view/document~Document#event:compositionstart Compositionstart},
+ * {@link module:engine/view/document~Document#event:compositionupdate compositionupdate} and
+ * {@link module:engine/view/document~Document#event:compositionend compositionend} events observer.
+ *
+ * Note that this observer is attached by the {@link module:engine/view/view~View} and is available by default.
+ *
+ * @extends module:engine/view/observer/domeventobserver~DomEventObserver
+ */
+export default class CompositionObserver extends DomEventObserver {
+	constructor( view ) {
+		super( view );
+
+		this.domEventType = [ 'compositionstart', 'compositionupdate', 'compositionend' ];
+		const document = this.document;
+
+		document.on( 'compositionstart', () => {
+			document.isComposing = true;
+		} );
+
+		document.on( 'compositionend', () => {
+			document.isComposing = false;
+		} );
+	}
+
+	onDomEvent( domEvent ) {
+		this.fire( domEvent.type, domEvent );
+	}
+}
+
+/**
+ * Fired when composition starts inside one of the editables.
+ *
+ * Introduced by {@link module:engine/view/observer/compositionobserver~CompositionObserver}.
+ *
+ * Note that because {@link module:engine/view/observer/compositionobserver~CompositionObserver} is attached by the
+ * {@link module:engine/view/view~View} this event is available by default.
+ *
+ * @see module:engine/view/observer/compositionobserver~CompositionObserver
+ * @event module:engine/view/document~Document#event:compositionstart
+ * @param {module:engine/view/observer/domeventdata~DomEventData} data Event data.
+ */
+
+/**
+ * Fired when composition is updated inside one of the editables.
+ *
+ * Introduced by {@link module:engine/view/observer/compositionobserver~CompositionObserver}.
+ *
+ * Note that because {@link module:engine/view/observer/compositionobserver~CompositionObserver} is attached by the
+ * {@link module:engine/view/view~View} this event is available by default.
+ *
+ * @see module:engine/view/observer/compositionobserver~CompositionObserver
+ * @event module:engine/view/document~Document#event:compositionupdate
+ * @param {module:engine/view/observer/domeventdata~DomEventData} data Event data.
+ */
+
+/**
+ * Fired when composition ends inside one of the editables.
+ *
+ * Introduced by {@link module:engine/view/observer/compositionobserver~CompositionObserver}.
+ *
+ * Note that because {@link module:engine/view/observer/compositionobserver~CompositionObserver} is attached by the
+ * {@link module:engine/view/view~View} this event is available by default.
+ *
+ * @see module:engine/view/observer/compositionobserver~CompositionObserver
+ * @event module:engine/view/document~Document#event:compositionend
+ * @param {module:engine/view/observer/domeventdata~DomEventData} data Event data.
+ */

+ 3 - 0
packages/ckeditor5-engine/src/view/view.js

@@ -17,6 +17,7 @@ import KeyObserver from './observer/keyobserver';
 import FakeSelectionObserver from './observer/fakeselectionobserver';
 import SelectionObserver from './observer/selectionobserver';
 import FocusObserver from './observer/focusobserver';
+import CompositionObserver from './observer/compositionobserver';
 
 import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
 import log from '@ckeditor/ckeditor5-utils/src/log';
@@ -47,6 +48,7 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  * * {@link module:engine/view/observer/focusobserver~FocusObserver},
  * * {@link module:engine/view/observer/keyobserver~KeyObserver},
  * * {@link module:engine/view/observer/fakeselectionobserver~FakeSelectionObserver}.
+ * * {@link module:engine/view/observer/compositionobserver~CompositionObserver}.
  *
  * This class also {@link module:engine/view/view~View#attachDomRoot bind DOM and View elements}.
  *
@@ -138,6 +140,7 @@ export default class View {
 		this.addObserver( FocusObserver );
 		this.addObserver( KeyObserver );
 		this.addObserver( FakeSelectionObserver );
+		this.addObserver( CompositionObserver );
 
 		// Inject quirks handlers.
 		injectQuirksHandling( this );

+ 28 - 50
packages/ckeditor5-engine/tests/model/writer.js

@@ -258,29 +258,6 @@ describe( 'Writer', () => {
 			expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch );
 		} );
 
-		it( 'should move element from one parent to the other within different document (document -> docFrag)', () => {
-			const root = doc.createRoot();
-			const docFrag = createDocumentFragment();
-			const node = createText( 'foo' );
-
-			insert( node, root );
-
-			const spy = sinon.spy( model, 'applyOperation' );
-
-			insert( node, docFrag );
-
-			// Verify result.
-			expect( Array.from( root.getChildren() ) ).to.deep.equal( [] );
-			expect( Array.from( docFrag.getChildren() ) ).to.deep.equal( [ node ] );
-
-			// Verify deltas and operations.
-			sinon.assert.calledTwice( spy );
-			expect( spy.firstCall.args[ 0 ].type ).to.equal( 'remove' );
-			expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch );
-			expect( spy.secondCall.args[ 0 ].type ).to.equal( 'insert' );
-			expect( spy.secondCall.args[ 0 ].delta.batch ).to.equal( batch );
-		} );
-
 		it( 'should move element from one parent to the other within different document (docFragA -> docFragB)', () => {
 			const docFragA = createDocumentFragment();
 			const docFragB = createDocumentFragment();
@@ -304,6 +281,18 @@ describe( 'Writer', () => {
 			expect( spy.secondCall.args[ 0 ].delta.batch ).to.equal( batch );
 		} );
 
+		it( 'should throw when moving element from document to document fragment', () => {
+			const root = doc.createRoot();
+			const docFrag = createDocumentFragment();
+			const node = createText( 'foo' );
+
+			insert( node, root );
+
+			expect( () => {
+				insert( node, docFrag );
+			} ).to.throw();
+		} );
+
 		it( 'should transfer markers from given DocumentFragment', () => {
 			const root = doc.createRoot();
 
@@ -608,7 +597,7 @@ describe( 'Writer', () => {
 			expect( spy.lastCall.args[ 0 ].delta.batch ).to.equal( batch );
 		} );
 
-		it( 'should move element from one parent to the other within the same document (documentA -> documentA)', () => {
+		it( 'should move element from one parent to the other within the same root (rootA -> rootA)', () => {
 			const rootA = doc.createRoot();
 
 			const parent1 = createElement( 'parent' );
@@ -633,7 +622,7 @@ describe( 'Writer', () => {
 			expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch );
 		} );
 
-		it( 'should move element from one parent to the other within the same document (documentA -> documentB)', () => {
+		it( 'should move element from one parent to the other within the same document (rootA -> rootB)', () => {
 			const rootA = doc.createRoot( '$root', 'A' );
 			const rootB = doc.createRoot( '$root', 'B' );
 			const node = createText( 'foo' );
@@ -654,7 +643,7 @@ describe( 'Writer', () => {
 			expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch );
 		} );
 
-		it( 'should move element from one parent to the other within the same document (docFragA -> docFragA)', () => {
+		it( 'should move element from one parent to the other within the same document fragment (docFragA -> docFragA)', () => {
 			const docFragA = createDocumentFragment();
 			const parent1 = createElement( 'parent' );
 			const parent2 = createElement( 'parent' );
@@ -678,30 +667,7 @@ describe( 'Writer', () => {
 			expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch );
 		} );
 
-		it( 'should move element from one parent to the other within different document (document -> docFrag)', () => {
-			const root = doc.createRoot();
-			const docFrag = createDocumentFragment();
-			const node = createText( 'foo' );
-
-			insert( node, root );
-
-			const spy = sinon.spy( model, 'applyOperation' );
-
-			append( node, docFrag );
-
-			// Verify result.
-			expect( Array.from( root.getChildren() ) ).to.deep.equal( [] );
-			expect( Array.from( docFrag.getChildren() ) ).to.deep.equal( [ node ] );
-
-			// Verify deltas and operations.
-			sinon.assert.calledTwice( spy );
-			expect( spy.firstCall.args[ 0 ].type ).to.equal( 'remove' );
-			expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch );
-			expect( spy.secondCall.args[ 0 ].type ).to.equal( 'insert' );
-			expect( spy.secondCall.args[ 0 ].delta.batch ).to.equal( batch );
-		} );
-
-		it( 'should move element from one parent to the other within different document (docFragA -> docFragB)', () => {
+		it( 'should move element from one parent to the other within different document fragments (docFragA -> docFragB)', () => {
 			const docFragA = createDocumentFragment();
 			const docFragB = createDocumentFragment();
 			const node = createText( 'foo' );
@@ -723,6 +689,18 @@ describe( 'Writer', () => {
 			expect( spy.secondCall.args[ 0 ].type ).to.equal( 'insert' );
 			expect( spy.secondCall.args[ 0 ].delta.batch ).to.equal( batch );
 		} );
+
+		it( 'should throw when moving element from document to document fragment', () => {
+			const root = doc.createRoot();
+			const docFrag = createDocumentFragment();
+			const node = createText( 'foo' );
+
+			insert( node, root );
+
+			expect( () => {
+				append( node, docFrag );
+			} ).to.throw();
+		} );
 	} );
 
 	describe( 'appendText()', () => {

+ 3 - 0
packages/ckeditor5-engine/tests/view/manual/compositionobserver.html

@@ -0,0 +1,3 @@
+<div id="editor">
+	<p>foo bar</p>
+</div>

+ 30 - 0
packages/ckeditor5-engine/tests/view/manual/compositionobserver.js

@@ -0,0 +1,30 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals console, window, document */
+
+import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
+import Typing from '@ckeditor/ckeditor5-typing/src/typing';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+
+ClassicEditor
+	.create( document.querySelector( '#editor' ), {
+		plugins: [ Typing, Paragraph ]
+	} )
+	.then( editor => {
+		window.editor = editor;
+
+		const view = editor.editing.view;
+		const viewDocument = view.document;
+
+		viewDocument.on( 'compositionstart', ( evt, data ) => console.log( 'compositionstart', data ) );
+		viewDocument.on( 'compositionupdate', ( evt, data ) => console.log( 'compositionupdate', data ) );
+		viewDocument.on( 'compositionend', ( evt, data ) => console.log( 'compositionend', data ) );
+
+		view.focus();
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );

+ 10 - 0
packages/ckeditor5-engine/tests/view/manual/compositionobserver.md

@@ -0,0 +1,10 @@
+* Expected initialization: `{}foo bar`.
+* Check whether composition events are logged to the console with proper data:
+  * `compositionstart`,
+  * `compositionupdate`,
+  * `compositionend`
+
+**Composition events are fired while typing:**
+* Hiragana,
+* Spanish-ISO: accent `' + a`,
+* MacOS: long `a` press (accent balloon)

+ 109 - 0
packages/ckeditor5-engine/tests/view/observer/compositionobserver.js

@@ -0,0 +1,109 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals document */
+import CompositionObserver from '../../../src/view/observer/compositionobserver';
+import View from '../../../src/view/view';
+
+describe( 'CompositionObserver', () => {
+	let view, viewDocument, observer;
+
+	beforeEach( () => {
+		view = new View();
+		viewDocument = view.document;
+		observer = view.getObserver( CompositionObserver );
+	} );
+
+	afterEach( () => {
+		view.destroy();
+	} );
+
+	it( 'should define domEventType', () => {
+		expect( observer.domEventType ).to.deep.equal( [ 'compositionstart', 'compositionupdate', 'compositionend' ] );
+	} );
+
+	describe( 'onDomEvent', () => {
+		it( 'should fire compositionstart with the right event data', () => {
+			const spy = sinon.spy();
+
+			viewDocument.on( 'compositionstart', spy );
+
+			observer.onDomEvent( { type: 'compositionstart', target: document.body } );
+
+			expect( spy.calledOnce ).to.be.true;
+
+			const data = spy.args[ 0 ][ 1 ];
+			expect( data.domTarget ).to.equal( document.body );
+		} );
+
+		it( 'should fire compositionupdate with the right event data', () => {
+			const spy = sinon.spy();
+
+			viewDocument.on( 'compositionupdate', spy );
+
+			observer.onDomEvent( { type: 'compositionupdate', target: document.body } );
+
+			expect( spy.calledOnce ).to.be.true;
+
+			const data = spy.args[ 0 ][ 1 ];
+			expect( data.domTarget ).to.equal( document.body );
+		} );
+
+		it( 'should fire compositionend with the right event data', () => {
+			const spy = sinon.spy();
+
+			viewDocument.on( 'compositionend', spy );
+
+			observer.onDomEvent( { type: 'compositionend', target: document.body } );
+
+			expect( spy.calledOnce ).to.be.true;
+
+			const data = spy.args[ 0 ][ 1 ];
+			expect( data.domTarget ).to.equal( document.body );
+		} );
+	} );
+
+	describe( 'handle isComposing property of the document', () => {
+		let domMain;
+
+		beforeEach( () => {
+			domMain = document.createElement( 'div' );
+		} );
+
+		it( 'should set isComposing to true on compositionstart', () => {
+			observer.onDomEvent( { type: 'compositionstart', target: domMain } );
+
+			expect( viewDocument.isComposing ).to.equal( true );
+		} );
+
+		it( 'should set isComposing to false on compositionend', () => {
+			observer.onDomEvent( { type: 'compositionstart', target: domMain } );
+
+			expect( viewDocument.isComposing ).to.equal( true );
+
+			observer.onDomEvent( { type: 'compositionend', target: domMain } );
+
+			expect( viewDocument.isComposing ).to.equal( false );
+		} );
+
+		it( 'should not change isComposing on compositionupdate during composition', () => {
+			observer.onDomEvent( { type: 'compositionstart', target: domMain } );
+
+			expect( viewDocument.isComposing ).to.equal( true );
+
+			observer.onDomEvent( { type: 'compositionupdate', target: domMain } );
+
+			expect( viewDocument.isComposing ).to.equal( true );
+		} );
+
+		it( 'should not change isComposing on compositionupdate outside composition', () => {
+			expect( viewDocument.isComposing ).to.equal( false );
+
+			observer.onDomEvent( { type: 'compositionupdate', target: domMain } );
+
+			expect( viewDocument.isComposing ).to.equal( false );
+		} );
+	} );
+} );

+ 3 - 1
packages/ckeditor5-engine/tests/view/view/view.js

@@ -7,6 +7,7 @@ import KeyObserver from '../../../src/view/observer/keyobserver';
 import FakeSelectionObserver from '../../../src/view/observer/fakeselectionobserver';
 import SelectionObserver from '../../../src/view/observer/selectionobserver';
 import FocusObserver from '../../../src/view/observer/focusobserver';
+import CompositionObserver from '../../../src/view/observer/compositionobserver';
 import createViewRoot from '../_utils/createroot';
 import Observer from '../../../src/view/observer/observer';
 import log from '@ckeditor/ckeditor5-utils/src/log';
@@ -21,7 +22,7 @@ import createElement from '@ckeditor/ckeditor5-utils/src/dom/createelement';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 describe( 'view', () => {
-	const DEFAULT_OBSERVERS_COUNT = 5;
+	const DEFAULT_OBSERVERS_COUNT = 6;
 	let domRoot, view, viewDocument, ObserverMock, instantiated, enabled, ObserverMockGlobalCount;
 
 	testUtils.createSinonSandbox();
@@ -76,6 +77,7 @@ describe( 'view', () => {
 		expect( view.getObserver( FocusObserver ) ).to.be.instanceof( FocusObserver );
 		expect( view.getObserver( KeyObserver ) ).to.be.instanceof( KeyObserver );
 		expect( view.getObserver( FakeSelectionObserver ) ).to.be.instanceof( FakeSelectionObserver );
+		expect( view.getObserver( CompositionObserver ) ).to.be.instanceof( CompositionObserver );
 	} );
 
 	describe( 'attachDomRoot()', () => {