Szymon Cofalik пре 10 година
родитељ
комит
4523c7c9d3

+ 129 - 0
packages/ckeditor5-engine/src/treemodel/liveposition.js

@@ -0,0 +1,129 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+CKEDITOR.define( [
+	'treemodel/position',
+	'treemodel/range',
+	'emittermixin',
+	'utils'
+], ( Position, Range, EmitterMixin, utils ) => {
+	/**
+	 * LivePosition is a position in the Tree Model that updates itself as the tree changes. It may be used as a bookmark.
+	 * **Note:** Be very careful when dealing with LivePosition. Each LivePosition instance bind events that might
+	 * have to be unbound. Use {@link #detach} whenever you don't need LivePosition anymore.
+	 *
+	 * @class treeModel.LivePosition
+	 */
+
+	class LivePosition extends Position {
+		/**
+		 * Creates a smart position.
+		 *
+		 * @see treeModel.Position
+		 * @param root
+		 * @param path
+		 * @param {Boolean} [stickToLeft] Flag representing what side the smart position is "sticking to". LivePosition
+		 * might be sticking to it's left side or right side. Whenever some nodes are inserted at the same position
+		 * as LivePosition, "stickiness" is checked to decide how LivePosition should be moved. Similar applies
+		 * when a range of nodes is moved and one of it's boundary position is same as LivePosition. Defaults to false.
+		 * @constructor
+		 */
+		constructor( root, path, stickToLeft ) {
+			super( root, path );
+
+			/**
+			 * Decides whether this position is sticking to it's left side or right side.
+			 *
+			 * @type {Boolean}
+			 */
+			this.stickToLeft = !!stickToLeft;
+
+			bindWithDocument.call( this );
+		}
+
+		/**
+		 * Creates and returns a new position which is equal to this LivePosition. Returned object may be
+		 * an instance of Position or LivePosition, depending on passed argument.
+		 *
+		 * @param {Boolean} [makeLivePosition] Flag representing whether new object should be "live". Defaults to false.
+		 * @returns {treeModel.Position|treeModel.LivePosition}
+		 */
+		clone( makeLivePosition ) {
+			if ( makeLivePosition ) {
+				return new LivePosition( this.root, this.path.slice() );
+			} else {
+				return super.clone();
+			}
+		}
+
+		/**
+		 * Unbinds all events previously binded by LivePosition. Use it whenever you don't need LivePosition instance
+		 * anymore (i.e. when leaving scope in which it was declared or before re-assigning variable that was
+		 * referring to it).
+		 */
+		detach() {
+			this.stopListening();
+		}
+	}
+
+	/**
+	 * Binds this LivePosition to the {@link treeModel.Document} that owns this position {@link treeModel.RootElement root}.
+	 *
+	 * @private
+	 * @method bindWithDocument
+	 * @private
+	 */
+	function bindWithDocument() {
+		/*jshint validthis: true */
+
+		this.listenTo( this.root.document, 'change', ( e, t, r, p ) => transform.call( this, t, r, p ), this );
+	}
+
+	/**
+	 * Updates this position accordingly to the updates applied to the Tree Model. Bases on change events.
+	 *
+	 * @private
+	 * @method transform
+	 * @param {String} type Type of changes applied to the Tree Model.
+	 * @param {treeModel.Range} range Range containing the result of applied change.
+	 * @param {treeModel.Position} [position] Additional position parameter provided by some change events.
+	 */
+	function transform( type, range, position ) {
+		/*jshint validthis: true */
+
+		let howMany = range.end.offset - range.start.offset;
+		let transformed;
+
+		switch ( type ) {
+			case 'insert':
+				transformed = this.getTransformedByInsertion( range.start, howMany, !this.stickToLeft );
+				break;
+
+			case 'move':
+			case 'remove':
+			case 'reinsert':
+				let originalRange = Range.createFromPositionAndShift( position, howMany );
+
+				if ( originalRange.containsPosition( this ) ) {
+					// We can't use .getTransformedByMove() because it would not combine
+					// path if LivePosition is at the same tree-level as range.
+					// Here, we always want to combine.
+					transformed = this._getCombined( position, range.start );
+				} else {
+					transformed = this.getTransformedByMove( position, range.start, howMany, !this.stickToLeft );
+				}
+				break;
+		}
+
+		this.path = transformed.path;
+		this.root = transformed.root;
+	}
+
+	utils.extend( LivePosition.prototype, EmitterMixin );
+
+	return LivePosition;
+} );

+ 334 - 0
packages/ckeditor5-engine/tests/treemodel/liveposition.js

@@ -0,0 +1,334 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: treemodel */
+
+'use strict';
+
+const modules = bender.amd.require(
+	'treemodel/document',
+	'treemodel/element',
+	'treemodel/position',
+	'treemodel/liveposition',
+	'treemodel/range',
+	'emittermixin'
+);
+
+describe( 'LivePosition', () => {
+	let Document, Element, Position, LivePosition, Range, EmitterMixin;
+	let doc, root, ul, p, li1, li2;
+
+	before( () => {
+		Document = modules[ 'treemodel/document' ];
+		Element = modules[ 'treemodel/element' ];
+		Position = modules[ 'treemodel/position' ];
+		LivePosition = modules[ 'treemodel/liveposition' ];
+		Range = modules[ 'treemodel/range' ];
+		EmitterMixin = modules.emittermixin;
+
+		doc = new Document();
+		root = doc.createRoot( 'root' );
+
+		li1 = new Element( 'li', [], 'abcdef' );
+		li2 = new Element( 'li', [], 'foobar' );
+		ul = new Element( 'ul', [], [ li1, li2 ] );
+		p = new Element( 'p', [], 'qwerty' );
+
+		root.insertChildren( 0, [ p, ul ] );
+	} );
+
+	it( 'should be an instance of Position', () => {
+		let live = new LivePosition( root, [ 0 ] );
+		live.detach();
+
+		expect( live ).to.be.instanceof( Position );
+	} );
+
+	it( 'should return instance of Position when cloned', () => {
+		let live = new LivePosition( root, [ 0 ] );
+		let clone = live.clone();
+
+		expect( clone ).to.be.instanceof( Position );
+
+		live.detach();
+	} );
+
+	it( 'should return instance of LivePosition when cloned with flag set to true', () => {
+		let live = new LivePosition( root, [ 0 ] );
+		let clone = live.clone( true );
+
+		expect( clone ).to.be.instanceof( LivePosition );
+
+		live.detach();
+		clone.detach();
+	} );
+
+	it( 'should listen to a change event of the document that owns this position root', () => {
+		sinon.spy( LivePosition.prototype, 'listenTo' );
+
+		let live = new LivePosition( root, [ 0 ] );
+		live.detach();
+
+		expect( live.listenTo.calledWith( doc, 'change' ) ).to.be.true;
+
+		LivePosition.prototype.listenTo.restore();
+	} );
+
+	it( 'should stop listening when detached', () => {
+		sinon.spy( LivePosition.prototype, 'stopListening' );
+
+		let live = new LivePosition( root, [ 0 ] );
+		live.detach();
+
+		expect( live.stopListening.called ).to.be.true;
+
+		LivePosition.prototype.stopListening.restore();
+	} );
+
+	it( 'createFromParentAndOffset should return LivePosition', () => {
+		let position = LivePosition.createFromParentAndOffset( ul, 0 );
+		expect( position ).to.be.instanceof( LivePosition );
+		position.detach();
+	} );
+
+	it( 'createBefore should return LivePosition', () => {
+		let position = LivePosition.createBefore( ul );
+		expect( position ).to.be.instanceof( LivePosition );
+		position.detach();
+	} );
+
+	it( 'createAfter should return LivePosition', () => {
+		let position = LivePosition.createAfter( ul );
+		expect( position ).to.be.instanceof( LivePosition );
+		position.detach();
+	} );
+
+	describe( 'should get transformed if', () => {
+		let live;
+
+		beforeEach( () => {
+			live = new LivePosition( root, [ 1, 4, 6 ] );
+		} );
+
+		afterEach( () => {
+			live.detach();
+		} );
+
+		describe( 'insertion', () => {
+			it( 'is in the same parent and closer offset', () => {
+				let insertRange = new Range( new Position( root, [ 1, 4, 0 ] ), new Position( root, [ 1, 4, 3 ] ) );
+
+				doc.fire( 'change', 'insert', insertRange, null );
+
+				expect( live.path ).to.deep.equal( [ 1, 4, 9 ] );
+			} );
+
+			it( 'is at the same position and live position is sticking to right side', () => {
+				let insertRange = new Range( new Position( root, [ 1, 4, 6 ] ), new Position( root, [ 1, 4, 9 ] ) );
+
+				doc.fire( 'change', 'insert', insertRange, null );
+
+				expect( live.path ).to.deep.equal( [ 1, 4, 9 ] );
+			} );
+
+			it( 'is before a node from the live position path', () => {
+				let insertRange = new Range( new Position( root, [ 1, 0 ] ), new Position( root, [ 1, 2 ] ) );
+
+				doc.fire( 'change', 'insert', insertRange, null );
+
+				expect( live.path ).to.deep.equal( [ 1, 6, 6 ] );
+			} );
+		} );
+
+		describe( 'range move', () => {
+			it( 'is at the same parent and closer offset', () => {
+				let moveSource = new Position( root, [ 2 ] );
+				let moveRange = new Range( new Position( root, [ 1, 4, 0 ] ), new Position( root, [ 1, 4, 3 ] ) );
+
+				doc.fire( 'change', 'move', moveRange, moveSource, null );
+
+				expect( live.path ).to.deep.equal( [ 1, 4, 9 ] );
+			} );
+
+			it( 'is at the same position and live position is sticking to right side', () => {
+				let moveSource = new Position( root, [ 2 ] );
+				let moveRange = new Range( new Position( root, [ 1, 4, 6 ] ), new Position( root, [ 1, 4, 9 ] ) );
+
+				doc.fire( 'change', 'move', moveRange, moveSource, null );
+
+				expect( live.path ).to.deep.equal( [ 1, 4, 9 ] );
+			} );
+
+			it( 'is at a position before a node from the live position path', () => {
+				let moveSource = new Position( root, [ 2 ] );
+				let moveRange = new Range( new Position( root, [ 1, 0 ] ), new Position( root, [ 1, 2 ] ) );
+
+				doc.fire( 'change', 'move', moveRange, moveSource, null );
+
+				expect( live.path ).to.deep.equal( [ 1, 6, 6 ] );
+			} );
+
+			it( 'is from the same parent and closer offset', () => {
+				let moveSource = new Position( root, [ 1, 4, 0 ] );
+				let moveRange = new Range( new Position( root, [ 2, 0 ] ), new Position( root, [ 2, 4 ] ) );
+
+				doc.fire( 'change', 'move', moveRange, moveSource, null );
+
+				expect( live.path ).to.deep.equal( [ 1, 4, 2 ] );
+			} );
+
+			it( 'is from a position before a node from the live position path', () => {
+				let moveSource = new Position( root, [ 1, 0 ] );
+				let moveRange = new Range( new Position( root, [ 2, 0 ] ), new Position( root, [ 2, 4 ] ) );
+
+				doc.fire( 'change', 'move', moveRange, moveSource, null );
+
+				expect( live.path ).to.deep.equal( [ 1, 0, 6 ] );
+			} );
+
+			it( 'contains live position (same level)', () => {
+				let moveSource = new Position( root, [ 1, 4, 4 ] );
+				let moveRange = new Range( new Position( root, [ 2, 0 ] ), new Position( root, [ 2, 4 ] ) );
+
+				doc.fire( 'change', 'move', moveRange, moveSource, null );
+
+				expect( live.path ).to.deep.equal( [ 2, 2 ] );
+			} );
+
+			it( 'contains live position (deep)', () => {
+				let moveSource = new Position( root, [ 1, 3 ] );
+				let moveRange = new Range( new Position( root, [ 2, 0 ] ), new Position( root, [ 2, 4 ] ) );
+
+				doc.fire( 'change', 'move', moveRange, moveSource, null );
+
+				expect( live.path ).to.deep.equal( [ 2, 1, 6 ] );
+			} );
+		} );
+	} );
+
+	describe( 'should not get transformed if', () => {
+		let path, otherRoot;
+
+		before( () => {
+			path = [ 1, 4, 6 ];
+			otherRoot = doc.createRoot( 'otherRoot' );
+		} );
+
+		let live;
+
+		beforeEach( () => {
+			live = new LivePosition( root, path );
+		} );
+
+		afterEach( () => {
+			live.detach();
+		} );
+
+		describe( 'insertion', () => {
+			it( 'is in the same parent and further offset', () => {
+				let insertRange = new Range( new Position( root, [ 1, 4, 7 ] ), new Position( root, [ 1, 4, 9 ] ) );
+
+				doc.fire( 'change', 'insert', insertRange, null );
+
+				expect( live.path ).to.deep.equal( path );
+			} );
+
+			it( 'is at the same position and live position is sticking to left side', () => {
+				let live = new LivePosition( root, path, true );
+				let insertRange = new Range( new Position( root, [ 1, 4, 6 ] ), new Position( root, [ 1, 4, 9 ] ) );
+
+				doc.fire( 'change', 'insert', insertRange, null );
+
+				expect( live.path ).to.deep.equal( path );
+
+				live.detach();
+			} );
+
+			it( 'is after a node from the position path', () => {
+				let insertRange = new Range( new Position( root, [ 1, 5 ] ), new Position( root, [ 1, 7 ] ) );
+
+				doc.fire( 'change', 'insert', insertRange, null );
+
+				expect( live.path ).to.deep.equal( path );
+			} );
+
+			it( 'is in different root', () => {
+				let insertRange = new Range( new Position( otherRoot, [ 1, 4, 0 ] ), new Position( otherRoot, [ 1, 4, 4 ] ) );
+
+				doc.fire( 'change', 'insert', insertRange, null );
+
+				expect( live.path ).to.deep.equal( path );
+			} );
+		} );
+
+		describe( 'range move', () => {
+			it( 'is at the same parent and further offset', () => {
+				let moveSource = new Position( root, [ 2 ] );
+				let moveRange = new Range( new Position( root, [ 1, 4, 7 ] ), new Position( root, [ 1, 4, 9 ] ) );
+
+				doc.fire( 'change', 'move', moveRange, moveSource, null );
+
+				expect( live.path ).to.deep.equal( path );
+			} );
+
+			it( 'is at the same position and live position is sticking to left side', () => {
+				let live = new LivePosition( root, path, true );
+				let moveSource = new Position( root, [ 2 ] );
+				let moveRange = new Range( new Position( root, [ 1, 4, 6 ] ), new Position( root, [ 1, 4, 9 ] ) );
+
+				doc.fire( 'change', 'move', moveRange, moveSource, null );
+
+				expect( live.path ).to.deep.equal( path );
+
+				live.detach();
+			} );
+
+			it( 'is at a position after a node from the live position path', () => {
+				let moveSource = new Position( root, [ 2 ] );
+				let moveRange = new Range( new Position( root, [ 1, 5 ] ), new Position( root, [ 1, 7 ] ) );
+
+				doc.fire( 'change', 'move', moveRange, moveSource, null );
+
+				expect( live.path ).to.deep.equal( path );
+			} );
+
+			it( 'is from the same parent and further offset', () => {
+				let moveSource = new Position( root, [ 1, 4, 7 ] );
+				let moveRange = new Range( new Position( root, [ 2, 0 ] ), new Position( root, [ 2, 4 ] ) );
+
+				doc.fire( 'change', 'move', moveRange, moveSource, null );
+
+				expect( live.path ).to.deep.equal( path );
+			} );
+
+			it( 'is from a position after a node from the live position path', () => {
+				let moveSource = new Position( root, [ 1, 5 ] );
+				let moveRange = new Range( new Position( root, [ 2, 0 ] ), new Position( root, [ 2, 4 ] ) );
+
+				doc.fire( 'change', 'move', moveRange, moveSource, null );
+
+				expect( live.path ).to.deep.equal( path );
+			} );
+
+			it( 'is to different root', () => {
+				let moveSource = new Position( root, [ 2, 0 ] );
+				let moveRange = new Range( new Position( otherRoot, [ 1, 0 ] ), new Position( otherRoot, [ 1, 4 ] ) );
+
+				doc.fire( 'change', 'move', moveRange, moveSource, null );
+
+				expect( live.path ).to.deep.equal( path );
+			} );
+
+			it( 'is from different root', () => {
+				let moveSource = new Position( otherRoot, [ 1, 0 ] );
+				let moveRange = new Range( new Position( root, [ 2, 0 ] ), new Position( root, [ 2, 4 ] ) );
+
+				doc.fire( 'change', 'move', moveRange, moveSource, null );
+
+				expect( live.path ).to.deep.equal( path );
+			} );
+		} );
+	} );
+} );