浏览代码

Added fake selection rendering to DOM with tests.

Szymon Kupś 9 年之前
父节点
当前提交
3793d6aafb
共有 2 个文件被更改,包括 124 次插入6 次删除
  1. 58 6
      packages/ckeditor5-engine/src/view/renderer.js
  2. 66 0
      packages/ckeditor5-engine/tests/view/renderer.js

+ 58 - 6
packages/ckeditor5-engine/src/view/renderer.js

@@ -101,6 +101,14 @@ export default class Renderer {
 		 * @member {Boolean} engine.view.Renderer#isFocused
 		 */
 		this.isFocused = false;
+
+		/**
+		 * DOM element containing fake selection.
+		 *
+		 * @private
+		 * @type {null|HTMLElement}
+		 */
+		this._fakeSelectionContainer = null;
 	}
 
 	/**
@@ -418,24 +426,59 @@ export default class Renderer {
 	 * @private
 	 */
 	_updateSelection() {
-		// If there is no selection - remove it from DOM elements that belongs to the editor.
+		// If there is no selection - remove DOM and fake selections.
 		if ( this.selection.rangeCount === 0 ) {
 			this._removeDomSelection();
+			this._removeFakeSelection();
 
 			return;
 		}
 
-		if ( !this.isFocused ) {
+		const domRoot = this.domConverter.getCorrespondingDomElement( this.selection.editableElement );
+
+		// Do nothing if there is no focus, or there is no DOM element corresponding to selection's editable element.
+		if ( !this.isFocused || !domRoot ) {
 			return;
 		}
 
-		const selectedEditable = this.selection.editableElement;
-		const domRoot = this.domConverter.getCorrespondingDomElement( selectedEditable );
+		// Render selection.
+		if ( this.selection.isFake ) {
+			this._updateFakeSelection( domRoot );
+		} else {
+			this._removeFakeSelection();
+			this._updateDomSelection( domRoot );
+		}
+	}
 
-		if ( !domRoot ) {
-			return;
+	_updateFakeSelection( domRoot ) {
+		const domDocument = domRoot.ownerDocument;
+
+		// Create fake selection container if one does not exist.
+		if ( this._fakeSelectionContainer === null ) {
+			this._fakeSelectionContainer = domDocument.createElement( 'div' );
 		}
 
+		// Add fake container if not already added.
+		if (  this._fakeSelectionContainer.parentElement === null ) {
+			domRoot.appendChild( this._fakeSelectionContainer );
+		}
+
+		// Update contents.
+		const content = this.selection.fakeSelectionLabel || ' ';
+
+		if ( content !== this._fakeSelectionContainer.textContent ) {
+			this._fakeSelectionContainer.textContent = content;
+		}
+
+		// Update selection.
+		const domSelection = domDocument.getSelection();
+		domSelection.removeAllRanges();
+		const domRange = new Range();
+		domRange.selectNodeContents( this._fakeSelectionContainer );
+		domSelection.addRange( domRange );
+	}
+
+	_updateDomSelection( domRoot ) {
 		const domSelection = domRoot.ownerDocument.defaultView.getSelection();
 		const oldViewSelection = domSelection && this.domConverter.domSelectionToView( domSelection );
 
@@ -470,6 +513,15 @@ export default class Renderer {
 		}
 	}
 
+	_removeFakeSelection() {
+		const container = this._fakeSelectionContainer;
+
+		if ( container !== null ) {
+			container.remove();
+			container.textContent = '';
+		}
+	}
+
 	/**
 	 * Checks if focus needs to be updated and possibly updates it.
 	 *

+ 66 - 0
packages/ckeditor5-engine/tests/view/renderer.js

@@ -117,6 +117,7 @@ describe( 'Renderer', () => {
 			renderer.markedChildren.clear();
 
 			selection.removeAllRanges();
+			selection.setFake( false );
 
 			selectionEditable = viewRoot;
 
@@ -988,6 +989,71 @@ describe( 'Renderer', () => {
 
 			expect( domFocusSpy.called ).to.be.false;
 		} );
+
+		describe( 'fake selection', () => {
+			beforeEach( () => {
+				const { view: viewP, selection: newSelection } = parse(
+					'<container:p>[foo bar]</container:p>'
+				);
+				viewRoot.appendChildren( viewP );
+				selection.setTo( newSelection );
+				renderer.markToSync( 'children', viewRoot );
+				renderer.render();
+			} );
+
+			it( 'should render fake selection', () => {
+				const label = 'fake selection label';
+				selection.setFake( true, { label } );
+				renderer.render();
+
+				expect( domRoot.childNodes.length ).to.equal( 2 );
+				const container = domRoot.childNodes[ 1 ];
+				expect( domConverter.getCorrespondingViewElement( container ) ).to.be.undefined;
+				expect( container.textContent ).to.equal( label );
+				const domSelection = domRoot.ownerDocument.getSelection();
+				expect( domSelection.rangeCount ).to.equal( 1 );
+				const domRange = domSelection.getRangeAt( 0 );
+				expect( domRange.startContainer ).to.equal( container );
+				expect( domRange.startOffset ).to.equal( 0 );
+				expect( domRange.endContainer ).to.equal( container );
+				expect( domRange.endOffset ).to.equal( 1 );
+			} );
+
+			it( 'should render &nbsp; if no selection label is provided', () => {
+				selection.setFake( true );
+				renderer.render();
+
+				expect( domRoot.childNodes.length ).to.equal( 2 );
+				const container = domRoot.childNodes[ 1 ];
+				expect( container.textContent ).to.equal( '&nbsp;' );
+				const domSelection = domRoot.ownerDocument.getSelection();
+				expect( domSelection.rangeCount ).to.equal( 1 );
+				const domRange = domSelection.getRangeAt( 0 );
+				expect( domRange.startContainer ).to.equal( container );
+				expect( domRange.startOffset ).to.equal( 0 );
+				expect( domRange.endContainer ).to.equal( container );
+				expect( domRange.endOffset ).to.equal( 1 );
+			} );
+
+			it( 'should remove fake selection container when selection is no longer fake', () => {
+				selection.setFake( true );
+				renderer.render();
+
+				selection.setFake( false );
+				renderer.render();
+
+				expect( domRoot.childNodes.length ).to.equal( 1 );
+				const domParagraph = domRoot.childNodes[ 0 ];
+				expect( domParagraph.tagName.toLowerCase() ).to.equal( 'p' );
+				const domSelection = domRoot.ownerDocument.getSelection();
+				expect( domSelection.rangeCount ).to.equal( 1 );
+				const domRange = domSelection.getRangeAt( 0 );
+				expect( domRange.startContainer ).to.equal( domParagraph );
+				expect( domRange.startOffset ).to.equal( 0 );
+				expect( domRange.endContainer ).to.equal( domParagraph );
+				expect( domRange.endOffset ).to.equal( 1 );
+			} );
+		} );
 	} );
 } );