浏览代码

Merge branch 'master' into i/5834

Aleksander Nowodzinski 6 年之前
父节点
当前提交
5bfcca1cb7
共有 28 个文件被更改,包括 462 次插入95 次删除
  1. 4 0
      packages/ckeditor5-restricted-editing/.editorconfig
  2. 1 1
      packages/ckeditor5-restricted-editing/.eslintrc.js
  3. 1 1
      packages/ckeditor5-restricted-editing/LICENSE.md
  4. 1 1
      packages/ckeditor5-restricted-editing/docs/_snippets/features/restricted-editing.js
  5. 1 1
      packages/ckeditor5-restricted-editing/lang/translations/en.po
  6. 2 0
      packages/ckeditor5-restricted-editing/package.json
  7. 1 1
      packages/ckeditor5-restricted-editing/src/restrictededitingexceptioncommand.js
  8. 15 2
      packages/ckeditor5-restricted-editing/src/restrictededitingmode.js
  9. 1 1
      packages/ckeditor5-restricted-editing/src/restrictededitingmode/converters.js
  10. 1 1
      packages/ckeditor5-restricted-editing/src/restrictededitingmode/utils.js
  11. 78 18
      packages/ckeditor5-restricted-editing/src/restrictededitingmodeediting.js
  12. 1 1
      packages/ckeditor5-restricted-editing/src/restrictededitingmodenavigationcommand.js
  13. 1 1
      packages/ckeditor5-restricted-editing/src/restrictededitingmodeui.js
  14. 1 1
      packages/ckeditor5-restricted-editing/src/standardeditingmode.js
  15. 1 1
      packages/ckeditor5-restricted-editing/src/standardeditingmodeediting.js
  16. 1 1
      packages/ckeditor5-restricted-editing/src/standardeditingmodeui.js
  17. 9 0
      packages/ckeditor5-restricted-editing/tests/manual/restrictedediting.html
  18. 1 1
      packages/ckeditor5-restricted-editing/tests/manual/restrictedediting.js
  19. 1 1
      packages/ckeditor5-restricted-editing/tests/restrictededitingexceptioncommand.js
  20. 1 1
      packages/ckeditor5-restricted-editing/tests/restrictededitingmode.js
  21. 1 1
      packages/ckeditor5-restricted-editing/tests/restrictededitingmodeediting-commands.js
  22. 329 51
      packages/ckeditor5-restricted-editing/tests/restrictededitingmodeediting.js
  23. 1 1
      packages/ckeditor5-restricted-editing/tests/restrictededitingmodenavigationcommand.js
  24. 1 1
      packages/ckeditor5-restricted-editing/tests/restrictededitingmodeui.js
  25. 1 1
      packages/ckeditor5-restricted-editing/tests/standardeditingmode.js
  26. 1 1
      packages/ckeditor5-restricted-editing/tests/standardeditingmodeediting.js
  27. 4 3
      packages/ckeditor5-restricted-editing/tests/standardeditingmodeui.js
  28. 1 1
      packages/ckeditor5-restricted-editing/theme/restrictedediting.css

+ 4 - 0
packages/ckeditor5-restricted-editing/.editorconfig

@@ -10,3 +10,7 @@ charset = utf-8
 end_of_line = lf
 trim_trailing_whitespace = true
 insert_final_newline = true
+
+[package.json]
+indent_style = space
+tab_width = 2

+ 1 - 1
packages/ckeditor5-restricted-editing/.eslintrc.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 

+ 1 - 1
packages/ckeditor5-restricted-editing/LICENSE.md

@@ -2,7 +2,7 @@ Software License Agreement
 ==========================
 
 **CKEditor 5 restricted editing feature** – https://github.com/ckeditor/ckeditor5-restricted-editing <br>
-Copyright (c) 2003-2019, [CKSource](http://cksource.com) Frederico Knabben. All rights reserved.
+Copyright (c) 2003-2020, [CKSource](http://cksource.com) Frederico Knabben. All rights reserved.
 
 Licensed under the terms of [GNU General Public License Version 2 or later](http://www.gnu.org/licenses/gpl.html).
 

+ 1 - 1
packages/ckeditor5-restricted-editing/docs/_snippets/features/restricted-editing.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 

+ 1 - 1
packages/ckeditor5-restricted-editing/lang/translations/en.po

@@ -1,4 +1,4 @@
-# Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
 #
 #                                     !!! IMPORTANT !!!
 #

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

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

+ 1 - 1
packages/ckeditor5-restricted-editing/src/restrictededitingexceptioncommand.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 

+ 15 - 2
packages/ckeditor5-restricted-editing/src/restrictededitingmode.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
@@ -56,7 +56,8 @@ export default class RestrictedEditingMode extends Plugin {
  *		ClassicEditor
  *			.create( {
  * 				restrictedEditing: {
- * 					allowedCommands: [ 'bold', 'italic' ]
+ * 					allowedCommands: [ 'bold', 'link', 'unlink' ],
+ * 					allowedAttributes: [ 'bold', 'linkHref' ]
  * 				}
  *			} )
  *			.then( ... )
@@ -83,3 +84,15 @@ export default class RestrictedEditingMode extends Plugin {
  *
  * @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
+ */

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

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 

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

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 

+ 78 - 18
packages/ckeditor5-restricted-editing/src/restrictededitingmodeediting.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
@@ -41,7 +41,8 @@ export default class RestrictedEditingModeEditing extends Plugin {
 		super( editor );
 
 		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() {
 		const editor = this.editor;
-
+		const editingView = editor.editing.view;
 		const allowedCommands = editor.config.get( 'restrictedEditing.allowedCommands' );
 
 		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( '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 );
 			}
 		} );
@@ -168,27 +159,54 @@ export default class RestrictedEditingModeEditing extends Plugin {
 
 		doc.registerPostFixer( extendMarkerOnTypingPostFixer( editor ) );
 		doc.registerPostFixer( resurrectCollapsedMarkerPostFixer( editor ) );
+		model.markers.on( 'update:restrictedEditingException', ensureNewMarkerIsFlat( editor ) );
 
 		setupExceptionHighlighting( editor );
 	}
 
 	/**
-	 * 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
 	 */
 	_setupRestrictions() {
 		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.
 		// This check is bit synthetic since only tests are used this way.
 		if ( inputCommand ) {
 			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 );
 	}
 
 	/**
@@ -376,3 +394,45 @@ function isRangeInsideSingleMarker( editor, range ) {
 
 	return markerAtStart && markerAtEnd && markerAtEnd === markerAtStart;
 }
+
+// Checks if new marker range is flat. Non-flat ranges might appear during upcast conversion in nested structures, ie tables.
+//
+// Note: This marker fixer only consider case which is possible to create using StandardEditing mode plugin.
+// Markers created by developer in the data might break in many other ways.
+//
+// See #6003.
+function ensureNewMarkerIsFlat( editor ) {
+	const model = editor.model;
+
+	return ( evt, marker, oldRange, newRange ) => {
+		if ( !oldRange && !newRange.isFlat ) {
+			model.change( writer => {
+				const start = newRange.start;
+				const end = newRange.end;
+
+				const startIsHigherInTree = start.path.length > end.path.length;
+
+				const fixedStart = startIsHigherInTree ? newRange.start : writer.createPositionAt( end.parent, 0 );
+				const fixedEnd = startIsHigherInTree ? writer.createPositionAt( start.parent, 'end' ) : newRange.end;
+
+				writer.updateMarker( marker, {
+					range: writer.createRange( fixedStart, fixedEnd )
+				} );
+			} );
+		}
+	};
+}
+
+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';
+	}
+}

+ 1 - 1
packages/ckeditor5-restricted-editing/src/restrictededitingmodenavigationcommand.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 

+ 1 - 1
packages/ckeditor5-restricted-editing/src/restrictededitingmodeui.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 

+ 1 - 1
packages/ckeditor5-restricted-editing/src/standardeditingmode.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 

+ 1 - 1
packages/ckeditor5-restricted-editing/src/standardeditingmodeediting.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 

+ 1 - 1
packages/ckeditor5-restricted-editing/src/standardeditingmodeui.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 

+ 9 - 0
packages/ckeditor5-restricted-editing/tests/manual/restrictedediting.html

@@ -9,6 +9,15 @@
 	<p>Paragraph <span class="restricted-editing-exception">it is editable</span></p>
 	<p>Exception on part of a word: <a href="ckeditor.com">coompi</a><span class="restricted-editing-exception"><a href="ckeditor.com">ter</a> (fix spelling in Firefox).</span></p>
 	<p><strong>Bold</strong> <i>Italic</i> <a href="foo">Link</a></p>
+	<figure class="table">
+		<table>
+			<tbody>
+				<tr>
+					<td><span class="restricted-editing-exception">foo bar</span></td>
+				</tr>
+			</tbody>
+		</table>
+	</figure>
 	<ul>
 		<li>UL List item 1</li>
 		<li>UL List item 2</li>

+ 1 - 1
packages/ckeditor5-restricted-editing/tests/manual/restrictedediting.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 

+ 1 - 1
packages/ckeditor5-restricted-editing/tests/restrictededitingexceptioncommand.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 

+ 1 - 1
packages/ckeditor5-restricted-editing/tests/restrictededitingmode.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 

+ 1 - 1
packages/ckeditor5-restricted-editing/tests/restrictededitingmodeediting-commands.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 

+ 329 - 51
packages/ckeditor5-restricted-editing/tests/restrictededitingmodeediting.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
@@ -9,14 +9,19 @@ import { getData as getModelData, setData as setModelData } from '@ckeditor/cked
 import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
 import { getCode } from '@ckeditor/ckeditor5-utils/src/keyboard';
 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 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 Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard';
 
 import RestrictedEditingModeEditing from './../src/restrictededitingmodeediting';
 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';
 
 describe( 'RestrictedEditingModeEditing', () => {
 	let editor, model;
@@ -60,12 +65,12 @@ describe( 'RestrictedEditingModeEditing', () => {
 
 	describe( 'conversion', () => {
 		beforeEach( async () => {
-			editor = await VirtualTestEditor.create( { plugins: [ Paragraph, RestrictedEditingModeEditing ] } );
+			editor = await VirtualTestEditor.create( { plugins: [ Paragraph, TableEditing, RestrictedEditingModeEditing ] } );
 			model = editor.model;
 		} );
 
-		afterEach( () => {
-			return editor.destroy();
+		afterEach( async () => {
+			await editor.destroy();
 		} );
 
 		describe( 'upcast', () => {
@@ -74,10 +79,7 @@ describe( 'RestrictedEditingModeEditing', () => {
 
 				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">', () => {
@@ -90,10 +92,22 @@ describe( 'RestrictedEditingModeEditing', () => {
 				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.
-				const secondMarker = model.markers.get( 'restrictedEditingException:2' );
+				assertMarkerRangePaths( [ 1, 6 ], [ 1, 11 ], 2 );
+			} );
+
+			it( 'should convert <span class="restricted-editing-exception"> inside table to marker', () => {
+				editor.setData(
+					'<figure class="table">' +
+						'<table><tbody><tr><td><span class="restricted-editing-exception">bar</span></td></tr></tbody></table>' +
+					'</figure>'
+				);
 
-				expect( secondMarker.getStart().path ).to.deep.equal( [ 1, 6 ] );
-				expect( secondMarker.getEnd().path ).to.deep.equal( [ 1, 11 ] );
+				expect( model.markers.has( 'restrictedEditingException:1' ) ).to.be.true;
+
+				const marker = model.markers.get( 'restrictedEditingException:1' );
+
+				expect( marker.getStart().path ).to.deep.equal( [ 0, 0, 0, 0, 0 ] );
+				expect( marker.getEnd().path ).to.deep.equal( [ 0, 0, 0, 0, 3 ] );
 			} );
 
 			it( 'should not convert other <span> elements', () => {
@@ -164,6 +178,87 @@ describe( 'RestrictedEditingModeEditing', () => {
 					'</p>'
 				);
 			} );
+
+			it( 'converted <span> should be the outermost attribute element', () => {
+				editor.conversion.for( 'downcast' ).attributeToElement( { model: 'bold', view: 'b' } );
+				setModelData( model,
+					'<table><tableRow><tableCell>' +
+					'<paragraph><$text bold="true">foo bar baz</$text></paragraph>' +
+					'</tableCell></tableRow></table>'
+				);
+
+				const paragraph = model.document.getRoot().getChild( 0 ).getChild( 0 ).getChild( 0 ).getChild( 0 );
+
+				model.change( writer => {
+					writer.addMarker( 'restrictedEditingException:1', {
+						range: writer.createRange( writer.createPositionAt( paragraph, 0 ), writer.createPositionAt( paragraph, 'end' ) ),
+						usingOperation: true,
+						affectsData: true
+					} );
+				} );
+
+				assertEqualMarkup( editor.getData(),
+					'<figure class="table"><table><tbody><tr><td>' +
+					'<span class="restricted-editing-exception"><b>foo bar baz</b></span>' +
+					'</td></tr></tbody></table></figure>'
+				);
+				assertEqualMarkup( getViewData( editor.editing.view, { withoutSelection: true } ),
+					'<figure class="ck-widget ck-widget_with-selection-handle table" contenteditable="false">' +
+					'<div class="ck ck-widget__selection-handle"></div>' +
+					'<table><tbody><tr><td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true">' +
+					'<span><span class="restricted-editing-exception"><b>foo bar baz</b></span></span>' +
+					'</td></tr></tbody></table>' +
+					'</figure>'
+				);
+			} );
+		} );
+
+		describe( 'flattening exception markers', () => {
+			it( 'should fix non-flat marker range (start is higher in tree)', () => {
+				setModelData( model, '<table><tableRow><tableCell><paragraph>foo bar baz</paragraph></tableCell></tableRow></table>' );
+				const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
+				const paragraph = model.document.getRoot().getNodeByPath( [ 0, 0, 0, 0 ] );
+
+				model.change( writer => {
+					writer.addMarker( `restrictedEditingException:${ 1 }`, {
+						range: writer.createRange(
+							writer.createPositionAt( paragraph, 0 ),
+							writer.createPositionAt( tableCell, 'end' )
+						),
+						usingOperation: true,
+						affectsData: true
+					} );
+				} );
+
+				const marker = model.markers.get( 'restrictedEditingException:1' );
+
+				expect( marker.getStart().parent ).to.equal( marker.getEnd().parent );
+				expect( marker.getStart().path ).to.deep.equal( [ 0, 0, 0, 0, 0 ] );
+				expect( marker.getEnd().path ).to.deep.equal( [ 0, 0, 0, 0, 11 ] );
+			} );
+
+			it( 'should fix non-flat marker range (end is higher in tree)', () => {
+				setModelData( model, '<table><tableRow><tableCell><paragraph>foo bar baz</paragraph></tableCell></tableRow></table>' );
+				const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
+				const paragraph = model.document.getRoot().getNodeByPath( [ 0, 0, 0, 0 ] );
+
+				model.change( writer => {
+					writer.addMarker( `restrictedEditingException:${ 1 }`, {
+						range: writer.createRange(
+							writer.createPositionAt( tableCell, 0 ),
+							writer.createPositionAt( paragraph, 'end' )
+						),
+						usingOperation: true,
+						affectsData: true
+					} );
+				} );
+
+				const marker = model.markers.get( 'restrictedEditingException:1' );
+
+				expect( marker.getStart().parent ).to.equal( marker.getEnd().parent );
+				expect( marker.getStart().path ).to.deep.equal( [ 0, 0, 0, 0, 0 ] );
+				expect( marker.getEnd().path ).to.deep.equal( [ 0, 0, 0, 0, 11 ] );
+			} );
 		} );
 	} );
 
@@ -590,16 +685,20 @@ describe( 'RestrictedEditingModeEditing', () => {
 	} );
 
 	describe( 'clipboard', () => {
-		let model, viewDoc;
+		let viewDoc;
 
 		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;
 			viewDoc = editor.editing.view.document;
 		} );
 
-		afterEach( () => {
-			return editor.destroy();
+		afterEach( async () => {
+			await editor.destroy();
 		} );
 
 		describe( 'cut', () => {
@@ -619,27 +718,41 @@ describe( 'RestrictedEditingModeEditing', () => {
 				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 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', {
 					content: {
 						isEmpty: true
@@ -647,8 +760,7 @@ describe( 'RestrictedEditingModeEditing', () => {
 					method: 'cut'
 				} );
 
-				sinon.assert.notCalled( spy );
-				assertEqualMarkup( getModelData( model ), '<paragraph>foo b[]ar baz</paragraph>' );
+				assertEqualMarkup( getModelData( model ), '<paragraph>foo b[] baz</paragraph>' );
 			} );
 		} );
 
@@ -703,7 +815,12 @@ describe( 'RestrictedEditingModeEditing', () => {
 		} );
 
 		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>' );
 				const spy = sinon.spy();
 				viewDoc.on( 'clipboardInput', spy, { priority: 'high' } );
@@ -718,27 +835,41 @@ describe( 'RestrictedEditingModeEditing', () => {
 				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();
 				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', {
 					dataTransfer: {
 						getData: sinon.spy()
@@ -746,13 +877,145 @@ describe( 'RestrictedEditingModeEditing', () => {
 				} );
 
 				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', () => {
-		let model, view;
+		let view;
 
 		beforeEach( async () => {
 			editor = await VirtualTestEditor.create( {
@@ -985,7 +1248,7 @@ describe( 'RestrictedEditingModeEditing', () => {
 	} );
 
 	describe( 'exception cycling with the keyboard', () => {
-		let model, view, domEvtDataStub;
+		let view, domEvtDataStub;
 
 		beforeEach( async () => {
 			editor = await VirtualTestEditor.create( {
@@ -1129,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 );
+	}
 } );

+ 1 - 1
packages/ckeditor5-restricted-editing/tests/restrictededitingmodenavigationcommand.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 

+ 1 - 1
packages/ckeditor5-restricted-editing/tests/restrictededitingmodeui.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 

+ 1 - 1
packages/ckeditor5-restricted-editing/tests/standardeditingmode.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 

+ 1 - 1
packages/ckeditor5-restricted-editing/tests/standardeditingmodeediting.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 

+ 4 - 3
packages/ckeditor5-restricted-editing/tests/standardeditingmodeui.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
@@ -14,12 +14,12 @@ import StandardEditingModeUI from '../src/standardeditingmodeui';
 import StandardEditingModeEditing from '../src/standardeditingmodeediting';
 
 describe( 'StandardEditingModeUI', () => {
-	let editor, buttonView;
+	let editor, buttonView, editorElement;
 
 	testUtils.createSinonSandbox();
 
 	beforeEach( async () => {
-		const editorElement = document.createElement( 'div' );
+		editorElement = document.createElement( 'div' );
 		document.body.appendChild( editorElement );
 
 		editor = await ClassicTestEditor.create( editorElement, {
@@ -30,6 +30,7 @@ describe( 'StandardEditingModeUI', () => {
 	} );
 
 	afterEach( () => {
+		editorElement.remove();
 		return editor.destroy();
 	} );
 

+ 1 - 1
packages/ckeditor5-restricted-editing/theme/restrictedediting.css

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */