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

Merge pull request #13 from ckeditor/i/5802

Feature: Allow pasting content into exception areas in the restricted editing mode. Closes ckeditor/ckeditor5#5802.
Piotrek Koszuliński 6 лет назад
Родитель
Сommit
15ff7ed3c2

+ 2 - 0
packages/ckeditor5-restricted-editing/package.json

@@ -14,9 +14,11 @@
   },
   },
   "devDependencies": {
   "devDependencies": {
     "@ckeditor/ckeditor5-basic-styles": "^16.0.0",
     "@ckeditor/ckeditor5-basic-styles": "^16.0.0",
+    "@ckeditor/ckeditor5-block-quote": "^16.0.0",
     "@ckeditor/ckeditor5-clipboard": "^16.0.0",
     "@ckeditor/ckeditor5-clipboard": "^16.0.0",
     "@ckeditor/ckeditor5-editor-classic": "^16.0.0",
     "@ckeditor/ckeditor5-editor-classic": "^16.0.0",
     "@ckeditor/ckeditor5-engine": "^16.0.0",
     "@ckeditor/ckeditor5-engine": "^16.0.0",
+    "@ckeditor/ckeditor5-link": "^16.0.0",
     "@ckeditor/ckeditor5-paragraph": "^16.0.0",
     "@ckeditor/ckeditor5-paragraph": "^16.0.0",
     "@ckeditor/ckeditor5-table": "^16.0.0",
     "@ckeditor/ckeditor5-table": "^16.0.0",
     "@ckeditor/ckeditor5-typing": "^16.0.0",
     "@ckeditor/ckeditor5-typing": "^16.0.0",

+ 14 - 1
packages/ckeditor5-restricted-editing/src/restrictededitingmode.js

@@ -56,7 +56,8 @@ export default class RestrictedEditingMode extends Plugin {
  *		ClassicEditor
  *		ClassicEditor
  *			.create( {
  *			.create( {
  * 				restrictedEditing: {
  * 				restrictedEditing: {
- * 					allowedCommands: [ 'bold', 'italic' ]
+ * 					allowedCommands: [ 'bold', 'link', 'unlink' ],
+ * 					allowedAttributes: [ 'bold', 'linkHref' ]
  * 				}
  * 				}
  *			} )
  *			} )
  *			.then( ... )
  *			.then( ... )
@@ -83,3 +84,15 @@ export default class RestrictedEditingMode extends Plugin {
  *
  *
  * @member {Array.<String>} module:restricted-editing/restrictededitingmode~RestrictedEditingModeConfig#allowedCommands
  * @member {Array.<String>} module:restricted-editing/restrictededitingmode~RestrictedEditingModeConfig#allowedCommands
  */
  */
+
+/**
+ * The text attribute names allowed when pasting content ot non-restricted areas.
+ *
+ * The default value is:
+ *
+ *		const restrictedEditingConfig = {
+ *			allowedAttributes: [ 'bold', 'italic', 'linkHref' ]
+ *		};
+ *
+ * @member {Array.<String>} module:restricted-editing/restrictededitingmode~RestrictedEditingModeConfig#allowedAttributes
+ */

+ 48 - 17
packages/ckeditor5-restricted-editing/src/restrictededitingmodeediting.js

@@ -41,7 +41,8 @@ export default class RestrictedEditingModeEditing extends Plugin {
 		super( editor );
 		super( editor );
 
 
 		editor.config.define( 'restrictedEditing', {
 		editor.config.define( 'restrictedEditing', {
-			allowedCommands: [ 'bold', 'italic', 'link', 'unlink' ]
+			allowedCommands: [ 'bold', 'italic', 'link', 'unlink' ],
+			allowedAttributes: [ 'bold', 'italic', 'linkHref' ]
 		} );
 		} );
 
 
 		/**
 		/**
@@ -69,7 +70,7 @@ export default class RestrictedEditingModeEditing extends Plugin {
 	 */
 	 */
 	init() {
 	init() {
 		const editor = this.editor;
 		const editor = this.editor;
-
+		const editingView = editor.editing.view;
 		const allowedCommands = editor.config.get( 'restrictedEditing.allowedCommands' );
 		const allowedCommands = editor.config.get( 'restrictedEditing.allowedCommands' );
 
 
 		allowedCommands.forEach( commandName => this._allowedInException.add( commandName ) );
 		allowedCommands.forEach( commandName => this._allowedInException.add( commandName ) );
@@ -84,18 +85,8 @@ export default class RestrictedEditingModeEditing extends Plugin {
 		editor.keystrokes.set( 'Tab', getCommandExecuter( editor, 'goToNextRestrictedEditingException' ) );
 		editor.keystrokes.set( 'Tab', getCommandExecuter( editor, 'goToNextRestrictedEditingException' ) );
 		editor.keystrokes.set( 'Shift+Tab', getCommandExecuter( editor, 'goToPreviousRestrictedEditingException' ) );
 		editor.keystrokes.set( 'Shift+Tab', getCommandExecuter( editor, 'goToPreviousRestrictedEditingException' ) );
 
 
-		// Block clipboard completely in restricted mode.
-		this.listenTo( this.editor.editing.view.document, 'clipboardInput', evt => {
-			evt.stop();
-		}, { priority: 'highest' } );
-		this.listenTo( this.editor.editing.view.document, 'clipboardOutput', ( evt, data ) => {
-			if ( data.method == 'cut' ) {
-				evt.stop();
-			}
-		}, { priority: 'highest' } );
-
-		editor.editing.view.change( writer => {
-			for ( const root of editor.editing.view.document.roots ) {
+		editingView.change( writer => {
+			for ( const root of editingView.document.roots ) {
 				writer.addClass( 'ck-restricted-editing_mode_restricted', root );
 				writer.addClass( 'ck-restricted-editing_mode_restricted', root );
 			}
 			}
 		} );
 		} );
@@ -174,22 +165,48 @@ export default class RestrictedEditingModeEditing extends Plugin {
 	}
 	}
 
 
 	/**
 	/**
-	 * Setups additional editing restrictions beyond command toggling.
+	 * Setups additional editing restrictions beyond command toggling:
+	 *
+	 * * delete content range trimming
+	 * * disabling input command outside exception marker
+	 * * restricting clipboard holder to text only
+	 * * restricting text attributes in content
 	 *
 	 *
 	 * @private
 	 * @private
 	 */
 	 */
 	_setupRestrictions() {
 	_setupRestrictions() {
 		const editor = this.editor;
 		const editor = this.editor;
+		const model = editor.model;
+		const selection = model.document.selection;
+		const viewDoc = editor.editing.view.document;
 
 
-		this.listenTo( editor.model, 'deleteContent', restrictDeleteContent( editor ), { priority: 'high' } );
+		this.listenTo( model, 'deleteContent', restrictDeleteContent( editor ), { priority: 'high' } );
 
 
-		const inputCommand = this.editor.commands.get( 'input' );
+		const inputCommand = editor.commands.get( 'input' );
 
 
 		// The restricted editing might be configured without input support - ie allow only bolding or removing text.
 		// The restricted editing might be configured without input support - ie allow only bolding or removing text.
 		// This check is bit synthetic since only tests are used this way.
 		// This check is bit synthetic since only tests are used this way.
 		if ( inputCommand ) {
 		if ( inputCommand ) {
 			this.listenTo( inputCommand, 'execute', disallowInputExecForWrongRange( editor ), { priority: 'high' } );
 			this.listenTo( inputCommand, 'execute', disallowInputExecForWrongRange( editor ), { priority: 'high' } );
 		}
 		}
+
+		// Block clipboard outside exception marker on paste.
+		this.listenTo( viewDoc, 'clipboardInput', function( evt ) {
+			if ( !isRangeInsideSingleMarker( editor, selection.getFirstRange() ) ) {
+				evt.stop();
+			}
+		}, { priority: 'high' } );
+
+		// Block clipboard outside exception marker on cut.
+		this.listenTo( viewDoc, 'clipboardOutput', ( evt, data ) => {
+			if ( data.method == 'cut' && !isRangeInsideSingleMarker( editor, selection.getFirstRange() ) ) {
+				evt.stop();
+			}
+		}, { priority: 'high' } );
+
+		const allowedAttributes = editor.config.get( 'restrictedEditing.allowedAttributes' );
+		model.schema.addAttributeCheck( onlyAllowAttributesFromList( allowedAttributes ) );
+		model.schema.addChildCheck( allowTextOnlyInClipboardHolder );
 	}
 	}
 
 
 	/**
 	/**
@@ -406,3 +423,17 @@ function ensureNewMarkerIsFlat( editor ) {
 		}
 		}
 	};
 	};
 }
 }
+
+function onlyAllowAttributesFromList( allowedAttributes ) {
+	return ( context, attributeName ) => {
+		if ( context.startsWith( '$clipboardHolder' ) ) {
+			return allowedAttributes.includes( attributeName );
+		}
+	};
+}
+
+function allowTextOnlyInClipboardHolder( context, childDefinition ) {
+	if ( context.startsWith( '$clipboardHolder' ) ) {
+		return childDefinition.name === '$text';
+	}
+}

+ 229 - 48
packages/ckeditor5-restricted-editing/tests/restrictededitingmodeediting.js

@@ -9,14 +9,18 @@ import { getData as getModelData, setData as setModelData } from '@ckeditor/cked
 import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
 import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
 import { getCode } from '@ckeditor/ckeditor5-utils/src/keyboard';
 import { getCode } from '@ckeditor/ckeditor5-utils/src/keyboard';
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
+import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import BoldEditing from '@ckeditor/ckeditor5-basic-styles/src/bold/boldediting';
 import BoldEditing from '@ckeditor/ckeditor5-basic-styles/src/bold/boldediting';
-import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
+import StrikethroughEditing from '@ckeditor/ckeditor5-basic-styles/src/strikethrough/strikethroughediting';
+import LinkEditing from '@ckeditor/ckeditor5-link/src/linkediting';
 import Typing from '@ckeditor/ckeditor5-typing/src/typing';
 import Typing from '@ckeditor/ckeditor5-typing/src/typing';
 import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard';
 import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard';
 
 
 import RestrictedEditingModeEditing from './../src/restrictededitingmodeediting';
 import RestrictedEditingModeEditing from './../src/restrictededitingmodeediting';
 import RestrictedEditingModeNavigationCommand from '../src/restrictededitingmodenavigationcommand';
 import RestrictedEditingModeNavigationCommand from '../src/restrictededitingmodenavigationcommand';
+import ItalicEditing from '@ckeditor/ckeditor5-basic-styles/src/italic/italicediting';
+import BlockQuoteEditing from '@ckeditor/ckeditor5-block-quote/src/blockquoteediting';
 import TableEditing from '@ckeditor/ckeditor5-table/src/tableediting';
 import TableEditing from '@ckeditor/ckeditor5-table/src/tableediting';
 
 
 describe( 'RestrictedEditingModeEditing', () => {
 describe( 'RestrictedEditingModeEditing', () => {
@@ -75,10 +79,7 @@ describe( 'RestrictedEditingModeEditing', () => {
 
 
 				expect( model.markers.has( 'restrictedEditingException:1' ) ).to.be.true;
 				expect( model.markers.has( 'restrictedEditingException:1' ) ).to.be.true;
 
 
-				const marker = model.markers.get( 'restrictedEditingException:1' );
-
-				expect( marker.getStart().path ).to.deep.equal( [ 0, 4 ] );
-				expect( marker.getEnd().path ).to.deep.equal( [ 0, 7 ] );
+				assertMarkerRangePaths( [ 0, 4 ], [ 0, 7 ] );
 			} );
 			} );
 
 
 			it( 'should convert multiple <span class="restricted-editing-exception">', () => {
 			it( 'should convert multiple <span class="restricted-editing-exception">', () => {
@@ -91,10 +92,7 @@ describe( 'RestrictedEditingModeEditing', () => {
 				expect( model.markers.has( 'restrictedEditingException:2' ) ).to.be.true;
 				expect( model.markers.has( 'restrictedEditingException:2' ) ).to.be.true;
 
 
 				// Data for the first marker is the same as in previous tests so no need to test it again.
 				// Data for the first marker is the same as in previous tests so no need to test it again.
-				const secondMarker = model.markers.get( 'restrictedEditingException:2' );
-
-				expect( secondMarker.getStart().path ).to.deep.equal( [ 1, 6 ] );
-				expect( secondMarker.getEnd().path ).to.deep.equal( [ 1, 11 ] );
+				assertMarkerRangePaths( [ 1, 6 ], [ 1, 11 ], 2 );
 			} );
 			} );
 
 
 			it( 'should convert <span class="restricted-editing-exception"> inside table to marker', () => {
 			it( 'should convert <span class="restricted-editing-exception"> inside table to marker', () => {
@@ -687,16 +685,20 @@ describe( 'RestrictedEditingModeEditing', () => {
 	} );
 	} );
 
 
 	describe( 'clipboard', () => {
 	describe( 'clipboard', () => {
-		let model, viewDoc;
+		let viewDoc;
 
 
 		beforeEach( async () => {
 		beforeEach( async () => {
-			editor = await VirtualTestEditor.create( { plugins: [ Paragraph, Typing, Clipboard, RestrictedEditingModeEditing ] } );
+			editor = await VirtualTestEditor.create( {
+				plugins: [ Paragraph, BoldEditing, ItalicEditing, StrikethroughEditing, BlockQuoteEditing, LinkEditing, Typing, Clipboard,
+					RestrictedEditingModeEditing
+				]
+			} );
 			model = editor.model;
 			model = editor.model;
 			viewDoc = editor.editing.view.document;
 			viewDoc = editor.editing.view.document;
 		} );
 		} );
 
 
-		afterEach( () => {
-			return editor.destroy();
+		afterEach( async () => {
+			await editor.destroy();
 		} );
 		} );
 
 
 		describe( 'cut', () => {
 		describe( 'cut', () => {
@@ -716,27 +718,41 @@ describe( 'RestrictedEditingModeEditing', () => {
 				assertEqualMarkup( getModelData( model ), '<paragraph>foo []bar baz</paragraph>' );
 				assertEqualMarkup( getModelData( model ), '<paragraph>foo []bar baz</paragraph>' );
 			} );
 			} );
 
 
-			it( 'should be blocked inside exception marker', () => {
-				setModelData( model, '<paragraph>[]foo bar baz</paragraph>' );
+			it( 'should cut selected content inside exception marker (selection inside marker)', () => {
+				setModelData( model, '<paragraph>foo b[a]r baz</paragraph>' );
 				const firstParagraph = model.document.getRoot().getChild( 0 );
 				const firstParagraph = model.document.getRoot().getChild( 0 );
-				const spy = sinon.spy();
-				viewDoc.on( 'clipboardOutput', spy, { priority: 'high' } );
+				addExceptionMarker( 4, 7, firstParagraph );
 
 
-				model.change( writer => {
-					writer.addMarker( 'restrictedEditingException:1', {
-						range: writer.createRange(
-							writer.createPositionAt( firstParagraph, 4 ),
-							writer.createPositionAt( firstParagraph, 7 )
-						),
-						usingOperation: true,
-						affectsData: true
-					} );
+				viewDoc.fire( 'clipboardOutput', {
+					content: {
+						isEmpty: true
+					},
+					method: 'cut'
 				} );
 				} );
 
 
-				model.change( writer => {
-					writer.setSelection( firstParagraph, 5 );
+				assertEqualMarkup( getModelData( model ), '<paragraph>foo b[]r baz</paragraph>' );
+			} );
+
+			it( 'should cut selected content inside exception marker (selection touching marker start)', () => {
+				setModelData( model, '<paragraph>foo [ba]r baz</paragraph>' );
+				const firstParagraph = model.document.getRoot().getChild( 0 );
+				addExceptionMarker( 4, 7, firstParagraph );
+
+				viewDoc.fire( 'clipboardOutput', {
+					content: {
+						isEmpty: true
+					},
+					method: 'cut'
 				} );
 				} );
 
 
+				assertEqualMarkup( getModelData( model ), '<paragraph>foo []r baz</paragraph>' );
+			} );
+
+			it( 'should cut selected content inside exception marker (selection touching marker end)', () => {
+				setModelData( model, '<paragraph>foo b[ar] baz</paragraph>' );
+				const firstParagraph = model.document.getRoot().getChild( 0 );
+				addExceptionMarker( 4, 7, firstParagraph );
+
 				viewDoc.fire( 'clipboardOutput', {
 				viewDoc.fire( 'clipboardOutput', {
 					content: {
 					content: {
 						isEmpty: true
 						isEmpty: true
@@ -744,8 +760,7 @@ describe( 'RestrictedEditingModeEditing', () => {
 					method: 'cut'
 					method: 'cut'
 				} );
 				} );
 
 
-				sinon.assert.notCalled( spy );
-				assertEqualMarkup( getModelData( model ), '<paragraph>foo b[]ar baz</paragraph>' );
+				assertEqualMarkup( getModelData( model ), '<paragraph>foo b[] baz</paragraph>' );
 			} );
 			} );
 		} );
 		} );
 
 
@@ -800,7 +815,12 @@ describe( 'RestrictedEditingModeEditing', () => {
 		} );
 		} );
 
 
 		describe( 'paste', () => {
 		describe( 'paste', () => {
-			it( 'should be blocked outside exception markers', () => {
+			beforeEach( () => {
+				// Required when testing without DOM using VirtualTestEditor - Clipboard feature scrolls after paste event.
+				sinon.stub( editor.editing.view, 'scrollToTheSelection' );
+			} );
+
+			it( 'should be blocked outside exception markers (collapsed selection)', () => {
 				setModelData( model, '<paragraph>foo []bar baz</paragraph>' );
 				setModelData( model, '<paragraph>foo []bar baz</paragraph>' );
 				const spy = sinon.spy();
 				const spy = sinon.spy();
 				viewDoc.on( 'clipboardInput', spy, { priority: 'high' } );
 				viewDoc.on( 'clipboardInput', spy, { priority: 'high' } );
@@ -815,27 +835,41 @@ describe( 'RestrictedEditingModeEditing', () => {
 				assertEqualMarkup( getModelData( model ), '<paragraph>foo []bar baz</paragraph>' );
 				assertEqualMarkup( getModelData( model ), '<paragraph>foo []bar baz</paragraph>' );
 			} );
 			} );
 
 
-			it( 'should be blocked inside exception marker', () => {
-				setModelData( model, '<paragraph>[]foo bar baz</paragraph>' );
-				const firstParagraph = model.document.getRoot().getChild( 0 );
+			it( 'should be blocked outside exception markers (non-collapsed selection)', () => {
+				setModelData( model, '<paragraph>[foo bar baz]</paragraph>' );
 				const spy = sinon.spy();
 				const spy = sinon.spy();
 				viewDoc.on( 'clipboardInput', spy, { priority: 'high' } );
 				viewDoc.on( 'clipboardInput', spy, { priority: 'high' } );
 
 
-				model.change( writer => {
-					writer.addMarker( 'restrictedEditingException:1', {
-						range: writer.createRange(
-							writer.createPositionAt( firstParagraph, 4 ),
-							writer.createPositionAt( firstParagraph, 7 )
-						),
-						usingOperation: true,
-						affectsData: true
-					} );
+				viewDoc.fire( 'clipboardInput', {
+					dataTransfer: {
+						getData: sinon.spy()
+					}
 				} );
 				} );
 
 
-				model.change( writer => {
-					writer.setSelection( firstParagraph, 5 );
+				sinon.assert.notCalled( spy );
+				assertEqualMarkup( getModelData( model ), '<paragraph>[foo bar baz]</paragraph>' );
+			} );
+
+			it( 'should be blocked outside exception markers (non-collapsed selection, starts inside exception marker)', () => {
+				setModelData( model, '<paragraph>foo b[ar baz]</paragraph>' );
+				const spy = sinon.spy();
+				viewDoc.on( 'clipboardInput', spy, { priority: 'high' } );
+
+				viewDoc.fire( 'clipboardInput', {
+					dataTransfer: {
+						getData: sinon.spy()
+					}
 				} );
 				} );
 
 
+				sinon.assert.notCalled( spy );
+				assertEqualMarkup( getModelData( model ), '<paragraph>foo b[ar baz]</paragraph>' );
+			} );
+
+			it( 'should be blocked outside exception markers (non-collapsed selection, ends inside exception marker)', () => {
+				setModelData( model, '<paragraph>[foo ba]r baz</paragraph>' );
+				const spy = sinon.spy();
+				viewDoc.on( 'clipboardInput', spy, { priority: 'high' } );
+
 				viewDoc.fire( 'clipboardInput', {
 				viewDoc.fire( 'clipboardInput', {
 					dataTransfer: {
 					dataTransfer: {
 						getData: sinon.spy()
 						getData: sinon.spy()
@@ -843,13 +877,145 @@ describe( 'RestrictedEditingModeEditing', () => {
 				} );
 				} );
 
 
 				sinon.assert.notCalled( spy );
 				sinon.assert.notCalled( spy );
-				assertEqualMarkup( getModelData( model ), '<paragraph>foo b[]ar baz</paragraph>' );
+				assertEqualMarkup( getModelData( model ), '<paragraph>[foo ba]r baz</paragraph>' );
+			} );
+
+			describe( 'collapsed selection', () => {
+				it( 'should paste text inside exception marker', () => {
+					setModelData( model, '<paragraph>foo b[]ar baz</paragraph>' );
+					const firstParagraph = model.document.getRoot().getChild( 0 );
+					addExceptionMarker( 4, 7, firstParagraph );
+
+					viewDoc.fire( 'clipboardInput', {
+						dataTransfer: createDataTransfer( { 'text/html': '<p>XXX</p>', 'text/plain': 'XXX' } )
+					} );
+
+					assertEqualMarkup( getModelData( model ), '<paragraph>foo bXXX[]ar baz</paragraph>' );
+					assertMarkerRangePaths( [ 0, 4 ], [ 0, 10 ] );
+				} );
+
+				it( 'should paste allowed text attributes inside exception marker', () => {
+					setModelData( model, '<paragraph>foo b[]ar baz</paragraph>' );
+					const firstParagraph = model.document.getRoot().getChild( 0 );
+					addExceptionMarker( 4, 7, firstParagraph );
+
+					viewDoc.fire( 'clipboardInput', {
+						dataTransfer: createDataTransfer( {
+							'text/html': '<p><a href="foo"><b><i>XXX</i></b></a></p>',
+							'text/plain': 'XXX'
+						} )
+					} );
+
+					assertEqualMarkup( getModelData( model ),
+						'<paragraph>foo b<$text bold="true" italic="true" linkHref="foo">XXX[]</$text>ar baz</paragraph>'
+					);
+					assertMarkerRangePaths( [ 0, 4 ], [ 0, 10 ] );
+				} );
+
+				it( 'should not allow to paste disallowed text attributes inside exception marker', () => {
+					setModelData( model, '<paragraph>foo b[]ar baz</paragraph>' );
+					const firstParagraph = model.document.getRoot().getChild( 0 );
+					addExceptionMarker( 4, 7, firstParagraph );
+
+					viewDoc.fire( 'clipboardInput', {
+						dataTransfer: createDataTransfer( { 'text/html': '<p><s>XXX</s></p>', 'text/plain': 'XXX' } )
+					} );
+
+					assertEqualMarkup( getModelData( model ), '<paragraph>foo bXXX[]ar baz</paragraph>' );
+					assertMarkerRangePaths( [ 0, 4 ], [ 0, 10 ] );
+				} );
+
+				it( 'should filter out disallowed attributes from other text attributes when pasting inside exception marker', () => {
+					setModelData( model, '<paragraph>foo b[]ar baz</paragraph>' );
+					const firstParagraph = model.document.getRoot().getChild( 0 );
+					addExceptionMarker( 4, 7, firstParagraph );
+
+					viewDoc.fire( 'clipboardInput', {
+						dataTransfer: createDataTransfer( { 'text/html': '<p><b><s><i>XXX</i></s></b></p>', 'text/plain': 'XXX' } )
+					} );
+
+					assertEqualMarkup(
+						getModelData( model ),
+						'<paragraph>foo b<$text bold="true" italic="true">XXX[]</$text>ar baz</paragraph>'
+					);
+					assertMarkerRangePaths( [ 0, 4 ], [ 0, 10 ] );
+				} );
+
+				it( 'should not allow pasting block elements other then paragraph', () => {
+					setModelData( model, '<paragraph>foo b[]ar baz</paragraph>' );
+					const firstParagraph = model.document.getRoot().getChild( 0 );
+					addExceptionMarker( 4, 7, firstParagraph );
+
+					viewDoc.fire( 'clipboardInput', {
+						dataTransfer: createDataTransfer( { 'text/html': '<blockquote><p>XXX</p></blockquote>', 'text/plain': 'XXX' } )
+					} );
+
+					assertEqualMarkup( getModelData( model ), '<paragraph>foo bXXX[]ar baz</paragraph>' );
+					assertMarkerRangePaths( [ 0, 4 ], [ 0, 10 ] );
+				} );
+			} );
+
+			describe( 'non-collapsed selection', () => {
+				it( 'should paste text inside exception marker', () => {
+					setModelData( model, '<paragraph>foo b[a]r baz</paragraph>' );
+					const firstParagraph = model.document.getRoot().getChild( 0 );
+					addExceptionMarker( 4, 7, firstParagraph );
+
+					viewDoc.fire( 'clipboardInput', {
+						dataTransfer: createDataTransfer( { 'text/html': '<p>XXX</p>', 'text/plain': 'XXX' } )
+					} );
+
+					assertEqualMarkup( getModelData( model ), '<paragraph>foo bXXX[]r baz</paragraph>' );
+					assertMarkerRangePaths( [ 0, 4 ], [ 0, 9 ] );
+				} );
+
+				it( 'should paste allowed text attributes inside exception marker', () => {
+					setModelData( model, '<paragraph>foo b[a]r baz</paragraph>' );
+					const firstParagraph = model.document.getRoot().getChild( 0 );
+					addExceptionMarker( 4, 7, firstParagraph );
+
+					viewDoc.fire( 'clipboardInput', {
+						dataTransfer: createDataTransfer( { 'text/html': '<p><b>XXX</b></p>', 'text/plain': 'XXX' } )
+					} );
+
+					assertEqualMarkup( getModelData( model ), '<paragraph>foo b<$text bold="true">XXX[]</$text>r baz</paragraph>' );
+					assertMarkerRangePaths( [ 0, 4 ], [ 0, 9 ] );
+				} );
+
+				it( 'should not allow to paste disallowed text attributes inside exception marker', () => {
+					setModelData( model, '<paragraph>foo b[a]r baz</paragraph>' );
+					const firstParagraph = model.document.getRoot().getChild( 0 );
+					addExceptionMarker( 4, 7, firstParagraph );
+
+					viewDoc.fire( 'clipboardInput', {
+						dataTransfer: createDataTransfer( { 'text/html': '<p><s>XXX</s></p>', 'text/plain': 'XXX' } )
+					} );
+
+					assertEqualMarkup( getModelData( model ), '<paragraph>foo bXXX[]r baz</paragraph>' );
+					assertMarkerRangePaths( [ 0, 4 ], [ 0, 9 ] );
+				} );
+
+				it( 'should filter out disallowed attributes from other text attributes when pasting inside exception marker', () => {
+					setModelData( model, '<paragraph>foo b[a]r baz</paragraph>' );
+					const firstParagraph = model.document.getRoot().getChild( 0 );
+					addExceptionMarker( 4, 7, firstParagraph );
+
+					viewDoc.fire( 'clipboardInput', {
+						dataTransfer: createDataTransfer( { 'text/html': '<p><b><s><i>XXX</i></s></b></p>', 'text/plain': 'XXX' } )
+					} );
+
+					assertEqualMarkup(
+						getModelData( model ),
+						'<paragraph>foo b<$text bold="true" italic="true">XXX[]</$text>r baz</paragraph>'
+					);
+					assertMarkerRangePaths( [ 0, 4 ], [ 0, 9 ] );
+				} );
 			} );
 			} );
 		} );
 		} );
 	} );
 	} );
 
 
 	describe( 'exception highlighting', () => {
 	describe( 'exception highlighting', () => {
-		let model, view;
+		let view;
 
 
 		beforeEach( async () => {
 		beforeEach( async () => {
 			editor = await VirtualTestEditor.create( {
 			editor = await VirtualTestEditor.create( {
@@ -1082,7 +1248,7 @@ describe( 'RestrictedEditingModeEditing', () => {
 	} );
 	} );
 
 
 	describe( 'exception cycling with the keyboard', () => {
 	describe( 'exception cycling with the keyboard', () => {
-		let model, view, domEvtDataStub;
+		let view, domEvtDataStub;
 
 
 		beforeEach( async () => {
 		beforeEach( async () => {
 			editor = await VirtualTestEditor.create( {
 			editor = await VirtualTestEditor.create( {
@@ -1226,4 +1392,19 @@ describe( 'RestrictedEditingModeEditing', () => {
 			} );
 			} );
 		} );
 		} );
 	}
 	}
+
+	function createDataTransfer( data ) {
+		return {
+			getData( type ) {
+				return data[ type ];
+			}
+		};
+	}
+
+	function assertMarkerRangePaths( startPath, endPath, markerId = 1 ) {
+		const marker = model.markers.get( `restrictedEditingException:${ markerId }` );
+
+		expect( marker.getStart().path ).to.deep.equal( startPath );
+		expect( marker.getEnd().path ).to.deep.equal( endPath );
+	}
 } );
 } );