Răsfoiți Sursa

Created the RestrictedEditingNavigationCommand.

Aleksander Nowodzinski 6 ani în urmă
părinte
comite
fa44417996

+ 112 - 0
packages/ckeditor5-restricted-editing/src/restrictededitingnavigationcommand.js

@@ -0,0 +1,112 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module restricted-editing/restrictededitingnavigationcommand
+ */
+
+import Command from '@ckeditor/ckeditor5-core/src/command';
+
+/**
+ * The command that allows navigation across the exceptions in the editor content.
+ *
+ * @extends module:core/command~Command
+ */
+export default class RestrictedEditingNavigationCommand extends Command {
+	/**
+	 * Creates an instance of the command.
+	 *
+	 * @param {module:core/editor/editor~Editor} editor Editor instance.
+	 * @param {String} direction Direction the command works. Can be either `'forward'` or `'backward'`.
+	 */
+	constructor( editor, direction ) {
+		super( editor );
+
+		/**
+		 * A direction of the command. Can be `'forward'` or `'backward'`.
+		 *
+		 * @readonly
+		 * @private
+		 * @member {String}
+		 */
+		this._direction = direction;
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	refresh() {
+		this.isEnabled = this._checkEnabled();
+	}
+
+	/**
+	 * Executes the command.
+	 *
+	 * @fires execute
+	 */
+	execute() {
+		const position = getNearestExceptionPosition( this.editor.model, this._direction );
+
+		this.editor.model.change( writer => {
+			writer.setSelection( position );
+		} );
+	}
+
+	/**
+	 * Checks whether the command can be enabled in the current context.
+	 *
+	 * @private
+	 * @returns {Boolean} Whether the command should be enabled.
+	 */
+	_checkEnabled() {
+		return !!getNearestExceptionPosition( this.editor.model, this._direction );
+	}
+}
+
+// Returns the start position of the exception marker closest to the last position of the
+// model selection.
+//
+// @param {module:engine/model/model~Model} model
+// @param {String} direction Either "forward" or "backward".
+// @returns {module:engine/model/position~Position|null}
+function getNearestExceptionPosition( model, direction ) {
+	const selection = model.document.selection;
+	const selectionPosition = selection.getFirstPosition();
+	const markerStartPositions = [];
+
+	// Get all exception marker positions that start after/before the selection position.
+	for ( const marker of model.markers.getMarkersGroup( 'restricted-editing-exception' ) ) {
+		const markerRange = marker.getRange();
+		const markerRangeStart = markerRange.start;
+		const isMarkerRangeTouching = selectionPosition.isTouching( markerRange.start ) || selectionPosition.isTouching( markerRange.end );
+
+		// <paragraph>foo <marker≥b[]ar</marker> baz</paragraph>
+		// <paragraph>foo <marker≥b[ar</marker> ba]z</paragraph>
+		// <paragraph>foo <marker≥bar</marker>[] baz</paragraph>
+		// <paragraph>foo []<marker≥bar</marker> baz</paragraph>
+		if ( markerRange.containsPosition( selectionPosition ) || isMarkerRangeTouching ) {
+			continue;
+		}
+
+		if ( direction === 'forward' && markerRangeStart.isAfter( selectionPosition ) ) {
+			markerStartPositions.push( markerRangeStart );
+		} else if ( direction === 'backward' && markerRangeStart.isBefore( selectionPosition ) ) {
+			markerStartPositions.push( markerRangeStart );
+		}
+	}
+
+	if ( !markerStartPositions.length ) {
+		return null;
+	}
+
+	// Get the marker closest to the selection position among many.
+	return markerStartPositions.sort( ( posA, posB ) => {
+		if ( direction === 'forward' ) {
+			return posA.isAfter( posB ) ? 1 : -1;
+		} else {
+			return posA.isBefore( posB ) ? 1 : -1;
+		}
+	} ).shift();
+}

+ 476 - 0
packages/ckeditor5-restricted-editing/tests/restrictededitingnavigationcommand.js

@@ -0,0 +1,476 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
+import { setData as setModelData, getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+
+import RestrictedEditingNavigationCommand from '../src/restrictededitingnavigationcommand';
+
+describe( 'RestrictedEditingNavigationCommand', () => {
+	let editor, forwardCommand, backwardCommand, model;
+
+	beforeEach( () => {
+		return ModelTestEditor
+			.create()
+			.then( newEditor => {
+				editor = newEditor;
+				model = editor.model;
+
+				forwardCommand = new RestrictedEditingNavigationCommand( editor, 'forward' );
+				backwardCommand = new RestrictedEditingNavigationCommand( editor, 'backward' );
+
+				model.schema.register( 'paragraph', { inheritAllFrom: '$block' } );
+				editor.model.schema.extend( '$text', { allowAttributes: [ 'restrictedEditingException' ] } );
+			} );
+	} );
+
+	afterEach( () => {
+		forwardCommand.destroy();
+		backwardCommand.destroy();
+
+		return editor.destroy();
+	} );
+
+	describe( 'forward command', () => {
+		describe( 'isEnabled', () => {
+			describe( 'collapsed selection', () => {
+				it( 'should be true when there is a marker after the selection position', () => {
+					setModelData( model, '<paragraph>[]foo bar baz</paragraph>' );
+
+					const paragraph = model.document.getRoot().getChild( 0 );
+
+					// <paragraph>[]foo <marker≥bar</marker> baz</paragraph>
+					model.change( writer => {
+						writer.addMarker( 'restricted-editing-exception:1', {
+							range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
+							usingOperation: true,
+							affectsData: true
+						} );
+					} );
+
+					expect( forwardCommand.isEnabled ).to.be.true;
+				} );
+
+				it( 'should be false when there is no marker after the selection position', () => {
+					setModelData( model, '<paragraph>foo bar baz[]</paragraph>' );
+
+					const paragraph = model.document.getRoot().getChild( 0 );
+
+					// <paragraph>foo <marker≥bar</marker> baz[]</paragraph>
+					model.change( writer => {
+						writer.addMarker( 'restricted-editing-exception:1', {
+							range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
+							usingOperation: true,
+							affectsData: true
+						} );
+					} );
+
+					expect( forwardCommand.isEnabled ).to.be.false;
+				} );
+
+				it( 'should be false when the selection position is at a marker start and there is no more markers', () => {
+					setModelData( model, '<paragraph>foo []bar baz</paragraph>' );
+
+					const paragraph = model.document.getRoot().getChild( 0 );
+
+					// <paragraph>foo []<marker≥bar</marker> baz</paragraph>
+					model.change( writer => {
+						writer.addMarker( 'restricted-editing-exception:1', {
+							range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
+							usingOperation: true,
+							affectsData: true
+						} );
+					} );
+
+					expect( forwardCommand.isEnabled ).to.be.false;
+				} );
+
+				it( 'should be false when the selection position is in a marker and there is no more markers', () => {
+					setModelData( model, '<paragraph>foo b[]ar baz</paragraph>' );
+
+					const paragraph = model.document.getRoot().getChild( 0 );
+
+					// <paragraph>foo <marker≥b[]ar</marker> baz</paragraph>
+					model.change( writer => {
+						writer.addMarker( 'restricted-editing-exception:1', {
+							range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
+							usingOperation: true,
+							affectsData: true
+						} );
+					} );
+
+					expect( forwardCommand.isEnabled ).to.be.false;
+				} );
+
+				it( 'should be false when the selection position is at a marker end and there is no more markers', () => {
+					setModelData( model, '<paragraph>foo bar[] baz</paragraph>' );
+
+					const paragraph = model.document.getRoot().getChild( 0 );
+
+					// <paragraph>foo <marker≥bar</marker>[] baz</paragraph>
+					model.change( writer => {
+						writer.addMarker( 'restricted-editing-exception:1', {
+							range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
+							usingOperation: true,
+							affectsData: true
+						} );
+					} );
+
+					expect( forwardCommand.isEnabled ).to.be.false;
+				} );
+			} );
+
+			describe( 'expanded selection', () => {
+				it( 'should be true when there is a marker after the first selection position', () => {
+					setModelData( model, '<paragraph>[fo]o bar baz</paragraph>' );
+
+					const paragraph = model.document.getRoot().getChild( 0 );
+
+					// <paragraph>[fo]o <marker≥bar</marker> baz</paragraph>
+					model.change( writer => {
+						writer.addMarker( 'restricted-editing-exception:1', {
+							range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
+							usingOperation: true,
+							affectsData: true
+						} );
+					} );
+
+					expect( forwardCommand.isEnabled ).to.be.true;
+				} );
+
+				it( 'should be true when the selection overlaps the marker but the start position is before it', () => {
+					setModelData( model, '<paragraph>[foo ba]r baz</paragraph>' );
+
+					const paragraph = model.document.getRoot().getChild( 0 );
+
+					// <paragraph>[foo <marker≥ba]r</marker> baz</paragraph>
+					model.change( writer => {
+						writer.addMarker( 'restricted-editing-exception:1', {
+							range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
+							usingOperation: true,
+							affectsData: true
+						} );
+					} );
+
+					expect( forwardCommand.isEnabled ).to.be.true;
+				} );
+
+				it( 'should be false when the selection overlaps the marker but the start position is after it', () => {
+					setModelData( model, '<paragraph>foo ba[r baz]</paragraph>' );
+
+					const paragraph = model.document.getRoot().getChild( 0 );
+
+					// <paragraph>foo <marker≥ba[r</marker> baz]</paragraph>
+					model.change( writer => {
+						writer.addMarker( 'restricted-editing-exception:1', {
+							range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
+							usingOperation: true,
+							affectsData: true
+						} );
+					} );
+
+					expect( forwardCommand.isEnabled ).to.be.false;
+				} );
+			} );
+		} );
+
+		describe( 'execute()', () => {
+			describe( 'collapsed selection', () => {
+				it( 'should move to the next marker', () => {
+					setModelData( model, '<paragraph>[]foo bar baz</paragraph>' );
+
+					const paragraph = model.document.getRoot().getChild( 0 );
+
+					// <paragraph>[]foo <marker≥bar</marker> baz</paragraph>
+					model.change( writer => {
+						writer.addMarker( 'restricted-editing-exception:1', {
+							range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
+							usingOperation: true,
+							affectsData: true
+						} );
+					} );
+
+					// <paragraph>[]foo <marker≥bar</marker> <marker≥baz</marker≥</paragraph>
+					model.change( writer => {
+						writer.addMarker( 'restricted-editing-exception:2', {
+							range: writer.createRange( writer.createPositionAt( paragraph, 8 ), writer.createPositionAt( paragraph, 11 ) ),
+							usingOperation: true,
+							affectsData: true
+						} );
+					} );
+
+					forwardCommand.execute();
+					expect( getModelData( model ) ).to.equal( '<paragraph>foo []bar baz</paragraph>' );
+
+					forwardCommand.execute();
+					expect( getModelData( model ) ).to.equal( '<paragraph>foo bar []baz</paragraph>' );
+				} );
+			} );
+
+			describe( 'expanded selection', () => {
+				it( 'should move to the next marker when the selection end overlaps the marker', () => {
+					setModelData( model, '<paragraph>[foo b]ar baz</paragraph>' );
+
+					const paragraph = model.document.getRoot().getChild( 0 );
+
+					// <paragraph>[foo <marker≥b]ar</marker> baz</paragraph>
+					model.change( writer => {
+						writer.addMarker( 'restricted-editing-exception:1', {
+							range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
+							usingOperation: true,
+							affectsData: true
+						} );
+					} );
+
+					// <paragraph>[foo <marker≥b]ar</marker> <marker≥baz</marker≥</paragraph>
+					model.change( writer => {
+						writer.addMarker( 'restricted-editing-exception:2', {
+							range: writer.createRange( writer.createPositionAt( paragraph, 8 ), writer.createPositionAt( paragraph, 11 ) ),
+							usingOperation: true,
+							affectsData: true
+						} );
+					} );
+
+					forwardCommand.execute();
+					expect( getModelData( model ) ).to.equal( '<paragraph>foo []bar baz</paragraph>' );
+
+					forwardCommand.execute();
+					expect( getModelData( model ) ).to.equal( '<paragraph>foo bar []baz</paragraph>' );
+				} );
+
+				it( 'should move to the next marker when the selection start overlaps the marker', () => {
+					setModelData( model, '<paragraph>foo b[ar b]az</paragraph>' );
+
+					const paragraph = model.document.getRoot().getChild( 0 );
+
+					// <paragraph>foo <marker≥b[ar</marker> b]az</paragraph>
+					model.change( writer => {
+						writer.addMarker( 'restricted-editing-exception:1', {
+							range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
+							usingOperation: true,
+							affectsData: true
+						} );
+					} );
+
+					// <paragraph>foo <marker≥b[ar</marker> <marker≥b]az</marker≥</paragraph>
+					model.change( writer => {
+						writer.addMarker( 'restricted-editing-exception:2', {
+							range: writer.createRange( writer.createPositionAt( paragraph, 8 ), writer.createPositionAt( paragraph, 11 ) ),
+							usingOperation: true,
+							affectsData: true
+						} );
+					} );
+
+					forwardCommand.execute();
+					expect( getModelData( model ) ).to.equal( '<paragraph>foo bar []baz</paragraph>' );
+				} );
+			} );
+		} );
+	} );
+
+	describe( 'backward command', () => {
+		describe( 'isEnabled', () => {
+			describe( 'collapsed selection', () => {
+				it( 'should be true when there is a marker before the selection position', () => {
+					setModelData( model, '<paragraph>foo bar baz[]</paragraph>' );
+
+					const paragraph = model.document.getRoot().getChild( 0 );
+
+					// <paragraph>foo <marker≥bar</marker> baz</paragraph>
+					model.change( writer => {
+						writer.addMarker( 'restricted-editing-exception:1', {
+							range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
+							usingOperation: true,
+							affectsData: true
+						} );
+					} );
+
+					expect( backwardCommand.isEnabled ).to.be.true;
+				} );
+
+				it( 'should be false when there is no marker before the selection position', () => {
+					setModelData( model, '<paragraph>[]foo bar baz</paragraph>' );
+
+					const paragraph = model.document.getRoot().getChild( 0 );
+
+					// <paragraph>[]foo <marker≥bar</marker> baz</paragraph>
+					model.change( writer => {
+						writer.addMarker( 'restricted-editing-exception:1', {
+							range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
+							usingOperation: true,
+							affectsData: true
+						} );
+					} );
+
+					expect( backwardCommand.isEnabled ).to.be.false;
+				} );
+
+				it( 'should be false when the selection position is at a marker end and there is no more markers', () => {
+					setModelData( model, '<paragraph>foo bar[] baz</paragraph>' );
+
+					const paragraph = model.document.getRoot().getChild( 0 );
+
+					// <paragraph>foo <marker≥bar</marker>[] baz</paragraph>
+					model.change( writer => {
+						writer.addMarker( 'restricted-editing-exception:1', {
+							range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
+							usingOperation: true,
+							affectsData: true
+						} );
+					} );
+
+					expect( backwardCommand.isEnabled ).to.be.false;
+				} );
+
+				it( 'should be false when the selection position is in a marker and there is no more markers', () => {
+					setModelData( model, '<paragraph>foo b[]ar baz</paragraph>' );
+
+					const paragraph = model.document.getRoot().getChild( 0 );
+
+					// <paragraph>foo <marker≥b[]ar</marker> baz</paragraph>
+					model.change( writer => {
+						writer.addMarker( 'restricted-editing-exception:1', {
+							range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
+							usingOperation: true,
+							affectsData: true
+						} );
+					} );
+
+					expect( backwardCommand.isEnabled ).to.be.false;
+				} );
+
+				it( 'should be false when the selection position is at a marker start and there is no more markers', () => {
+					setModelData( model, '<paragraph>foo []bar baz</paragraph>' );
+
+					const paragraph = model.document.getRoot().getChild( 0 );
+
+					// <paragraph>foo []<marker≥bar</marker> baz</paragraph>
+					model.change( writer => {
+						writer.addMarker( 'restricted-editing-exception:1', {
+							range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
+							usingOperation: true,
+							affectsData: true
+						} );
+					} );
+
+					expect( backwardCommand.isEnabled ).to.be.false;
+				} );
+			} );
+
+			describe( 'expanded selection', () => {
+				it( 'should be true when there is a marker before the first selection position', () => {
+					setModelData( model, '<paragraph>foo bar b[az]</paragraph>' );
+
+					const paragraph = model.document.getRoot().getChild( 0 );
+
+					// <paragraph>[fo]o <marker≥bar</marker> b[az]</paragraph>
+					model.change( writer => {
+						writer.addMarker( 'restricted-editing-exception:1', {
+							range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
+							usingOperation: true,
+							affectsData: true
+						} );
+					} );
+
+					expect( backwardCommand.isEnabled ).to.be.true;
+				} );
+
+				it( 'should be false when the selection overlaps the marker but the start position is after it', () => {
+					setModelData( model, '<paragraph>foo b[ar baz]</paragraph>' );
+
+					const paragraph = model.document.getRoot().getChild( 0 );
+
+					// <paragraph>foo <marker≥b[ar</marker> baz]</paragraph>
+					model.change( writer => {
+						writer.addMarker( 'restricted-editing-exception:1', {
+							range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
+							usingOperation: true,
+							affectsData: true
+						} );
+					} );
+
+					expect( backwardCommand.isEnabled ).to.be.false;
+				} );
+
+				it( 'should be false when the selection overlaps the marker but the after position is after it', () => {
+					setModelData( model, '<paragraph>[foo b]ar baz</paragraph>' );
+
+					const paragraph = model.document.getRoot().getChild( 0 );
+
+					// <paragraph>[foo <marker≥b]ar</marker> baz</paragraph>
+					model.change( writer => {
+						writer.addMarker( 'restricted-editing-exception:1', {
+							range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
+							usingOperation: true,
+							affectsData: true
+						} );
+					} );
+
+					expect( backwardCommand.isEnabled ).to.be.false;
+				} );
+			} );
+		} );
+
+		describe( 'execute()', () => {
+			describe( 'collapsed selection', () => {
+				it( 'should move to the previous marker', () => {
+					setModelData( model, '<paragraph>foo bar baz[]</paragraph>' );
+
+					const paragraph = model.document.getRoot().getChild( 0 );
+
+					// <paragraph>foo <marker≥bar</marker> baz[]</paragraph>
+					model.change( writer => {
+						writer.addMarker( 'restricted-editing-exception:1', {
+							range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
+							usingOperation: true,
+							affectsData: true
+						} );
+					} );
+
+					// <paragraph>foo <marker≥bar</marker> <marker≥baz</marker≥[]</paragraph>
+					model.change( writer => {
+						writer.addMarker( 'restricted-editing-exception:2', {
+							range: writer.createRange( writer.createPositionAt( paragraph, 8 ), writer.createPositionAt( paragraph, 11 ) ),
+							usingOperation: true,
+							affectsData: true
+						} );
+					} );
+
+					backwardCommand.execute();
+					expect( getModelData( model ) ).to.equal( '<paragraph>foo []bar baz</paragraph>' );
+				} );
+			} );
+
+			describe( 'expanded selection', () => {
+				it( 'should move to the previous marker when the selection end overlaps the marker', () => {
+					setModelData( model, '<paragraph>foo bar b[az]</paragraph>' );
+
+					const paragraph = model.document.getRoot().getChild( 0 );
+
+					// <paragraph>foo <marker≥bar</marker> b[az]</paragraph>
+					model.change( writer => {
+						writer.addMarker( 'restricted-editing-exception:1', {
+							range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
+							usingOperation: true,
+							affectsData: true
+						} );
+					} );
+
+					// <paragraph>foo <marker≥bar</marker> <marker≥b[az</marker≥]</paragraph>
+					model.change( writer => {
+						writer.addMarker( 'restricted-editing-exception:2', {
+							range: writer.createRange( writer.createPositionAt( paragraph, 8 ), writer.createPositionAt( paragraph, 11 ) ),
+							usingOperation: true,
+							affectsData: true
+						} );
+					} );
+
+					backwardCommand.execute();
+					expect( getModelData( model ) ).to.equal( '<paragraph>foo []bar baz</paragraph>' );
+				} );
+			} );
+		} );
+	} );
+} );