Browse Source

Merge pull request #4 from ckeditor/i/5762

Feature: Enforcing restricted editing mode. Closes ckeditor/ckeditor5#5762.
Piotrek Koszuliński 6 years ago
parent
commit
f89fc48a39
28 changed files with 2210 additions and 474 deletions
  1. 31 0
      packages/ckeditor5-restricted-editing/docs/_snippets/features/restricted-editing.html
  2. 72 0
      packages/ckeditor5-restricted-editing/docs/_snippets/features/restricted-editing.js
  3. 34 0
      packages/ckeditor5-restricted-editing/docs/api/restricted-editing.md
  4. 76 0
      packages/ckeditor5-restricted-editing/docs/features/restricted-editing.md
  5. 3 0
      packages/ckeditor5-restricted-editing/package.json
  6. 0 30
      packages/ckeditor5-restricted-editing/src/restrictedediting.js
  7. 0 171
      packages/ckeditor5-restricted-editing/src/restrictededitingediting.js
  8. 0 31
      packages/ckeditor5-restricted-editing/src/restrictededitingexception.js
  9. 85 0
      packages/ckeditor5-restricted-editing/src/restrictededitingmode.js
  10. 190 0
      packages/ckeditor5-restricted-editing/src/restrictededitingmode/converters.js
  11. 70 0
      packages/ckeditor5-restricted-editing/src/restrictededitingmode/utils.js
  12. 286 0
      packages/ckeditor5-restricted-editing/src/restrictededitingmodeediting.js
  13. 3 3
      packages/ckeditor5-restricted-editing/src/restrictededitingnavigationcommand.js
  14. 9 8
      packages/ckeditor5-restricted-editing/src/restrictededitingui.js
  15. 38 0
      packages/ckeditor5-restricted-editing/src/standardeditingmode.js
  16. 9 3
      packages/ckeditor5-restricted-editing/src/restrictededitingexceptionediting.js
  17. 6 2
      packages/ckeditor5-restricted-editing/src/restrictededitingexceptionui.js
  18. 15 11
      packages/ckeditor5-restricted-editing/tests/manual/restrictedediting.js
  19. 0 44
      packages/ckeditor5-restricted-editing/tests/restrictededitingexception.js
  20. 44 0
      packages/ckeditor5-restricted-editing/tests/restrictededitingmode.js
  21. 787 0
      packages/ckeditor5-restricted-editing/tests/restrictededitingmodeediting-commands.js
  22. 375 56
      packages/ckeditor5-restricted-editing/tests/restrictededitingediting.js
  23. 40 40
      packages/ckeditor5-restricted-editing/tests/restrictededitingnavigationcommand.js
  24. 15 19
      packages/ckeditor5-restricted-editing/tests/restrictededitingui.js
  25. 10 10
      packages/ckeditor5-restricted-editing/tests/restrictedediting.js
  26. 5 5
      packages/ckeditor5-restricted-editing/tests/restrictededitingexceptionediting.js
  27. 4 4
      packages/ckeditor5-restricted-editing/tests/restrictededitingexceptionui.js
  28. 3 37
      packages/ckeditor5-restricted-editing/theme/restrictedediting.css

+ 31 - 0
packages/ckeditor5-restricted-editing/docs/_snippets/features/restricted-editing.html

@@ -0,0 +1,31 @@
+<p>
+	<b>Mode</b>:
+	<input type="radio" id="mode-standard" name="editor-restriction-mode" value="standard" checked><label for="mode-standard">Standard</label>
+	<input type="radio" id="mode-restricted" name="editor-restriction-mode" value="restricted"><label for="mode-restricted">Restricted</label>
+</p>
+
+<div id="restricted-editing-editor">
+	<h2>Heading 1</h2>
+	<p>Paragraph <span class="ck-restricted-editing-exception">it is editable</span></p>
+	<p><strong>Bold</strong> <i>Italic</i> <a href="https://ckeditor.com">Link</a></p>
+	<ul>
+		<li>UL List item 1</li>
+		<li>UL List item 2</li>
+	</ul>
+	<ol>
+		<li><span class="ck-restricted-editing-exception">OL List item 1</span></li>
+		<li>OL List item 2</li>
+	</ol>
+	<figure class="image image-style-side">
+		<img alt="bar" src="%BASE_PATH%/assets/img/fields.jpg">
+		<figcaption>Caption</figcaption>
+	</figure>
+	<blockquote>
+		<p>Quote</p>
+		<ul>
+			<li>Quoted UL List item 1</li>
+			<li>Quoted UL List item <span class="ck-restricted-editing-exception">2</span></li>
+		</ul>
+		<p>Quote</p>
+	</blockquote>
+</div>

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

@@ -0,0 +1,72 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/* globals window, document */
+
+import ClassicEditor from '@ckeditor/ckeditor5-build-classic/src/ckeditor';
+import RestrictedEditingMode from '@ckeditor/ckeditor5-restricted-editing/src/restrictededitingmode';
+import StandardEditingMode from '@ckeditor/ckeditor5-restricted-editing/src/standardeditingmode';
+
+import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset';
+import Table from '@ckeditor/ckeditor5-table/src/table';
+
+ClassicEditor.builtinPlugins.push(
+	RestrictedEditingMode, StandardEditingMode, ArticlePluginSet, Table );
+
+const restrictedModeButton = document.getElementById( 'mode-restricted' );
+const standardModeButton = document.getElementById( 'mode-standard' );
+
+restrictedModeButton.addEventListener( 'change', handleModeChange );
+standardModeButton.addEventListener( 'change', handleModeChange );
+
+startMode( document.querySelector( 'input[name="editor-restriction-mode"]:checked' ).value );
+
+async function handleModeChange( evt ) {
+	await startMode( evt.target.value );
+}
+
+async function startMode( selectedMode ) {
+	if ( selectedMode === 'standard' ) {
+		await startStandardEditingMode();
+	} else {
+		await startRestrictedEditingMode();
+	}
+}
+
+async function startStandardEditingMode() {
+	await reloadEditor( {
+		removePlugins: [ 'RestrictedEditingMode' ],
+		toolbar: [
+			'heading', '|', 'bold', 'italic', 'link', '|',
+			'bulletedList', 'numberedList', 'blockQuote', 'insertTable', '|',
+			'restrictedEditingException', '|', 'undo', 'redo'
+		],
+		image: {
+			toolbar: [ 'imageStyle:full', 'imageStyle:side', '|', 'imageTextAlternative' ]
+		},
+		table: {
+			contentToolbar: [
+				'tableColumn',
+				'tableRow',
+				'mergeTableCells'
+			]
+		}
+	} );
+}
+
+async function startRestrictedEditingMode() {
+	await reloadEditor( {
+		removePlugins: [ 'StandardEditingMode' ],
+		toolbar: [ 'bold', 'italic', 'link', '|', 'restrictedEditing', '|', 'undo', 'redo' ]
+	} );
+}
+
+async function reloadEditor( config ) {
+	if ( window.editor ) {
+		await window.editor.destroy();
+	}
+
+	window.editor = await ClassicEditor.create( document.querySelector( '#restricted-editing-editor' ), config );
+}

+ 34 - 0
packages/ckeditor5-restricted-editing/docs/api/restricted-editing.md

@@ -0,0 +1,34 @@
+---
+category: api-reference
+---
+
+# CKEditor 5 restricted editing feature
+
+[![npm version](https://badge.fury.io/js/%40ckeditor%2Fckeditor5-restricted-editing.svg)](https://www.npmjs.com/package/@ckeditor/ckeditor5-restricted-editing)
+
+This package implements the restricted editing feature for CKEditor 5.
+
+## Demo
+
+Check out the {@link features/restricted-editing#demo demo in the restricted editing feature} guide.
+
+## Documentation
+
+See the {@link features/restricted-editing restricted editing feature} guide and the {@link module:restricted-editing/restrictededitingmode~RestrictedEditingMode} and {@link module:restricted-editing/standardeditingmode~StandardEditingMode} plugins documentation.
+
+## Installation
+
+```bash
+npm install --save @ckeditor/ckeditor5-restricted-editing
+```
+
+## Contribute
+
+The source code of this package is available on GitHub in https://github.com/ckeditor/ckeditor5-restricted-editing.
+
+## External links
+
+* [`@ckeditor/ckeditor5-restricted-editing` on npm](https://www.npmjs.com/package/@ckeditor/ckeditor5-restricted-editing)
+* [`ckeditor/ckeditor5-restricted-editing` on GitHub](https://github.com/ckeditor/ckeditor5-restricted-editing)
+* [Issue tracker](https://github.com/ckeditor/ckeditor5/issues)
+* [Changelog](https://github.com/ckeditor/ckeditor5-restricted-editing/blob/master/CHANGELOG.md)

+ 76 - 0
packages/ckeditor5-restricted-editing/docs/features/restricted-editing.md

@@ -0,0 +1,76 @@
+---
+title: Restricted editing
+menu-title: Restricted editing
+category: features
+---
+
+The restricted editing feature allows to define editable areas in a documents that have restricted editing options. This feature defines
+two modes for the editor:
+
+1. Standard editing mode
+2. Restricted editing mode
+
+The standard editing mode is used as normal editor instance to create content. It also allows to mark regions of the document as non-restricted areas.
+
+The restricted editing mode allows other set of users to fill those non-restricted areas with text. You can additionally define which of editor commands are allowed inside non-restricted areas. The restricted editing mode expects that allowed commands are a text-formatting commands, like `'bold'`, `'italic'` or `'fontColor'`.
+
+## Demo
+
+The demo below works in two modes: "Standard Editing Mode" and "Restricted Editing Mode". Using the radio buttons below you can switch between modes.
+
+{@snippet features/restricted-editing}
+
+## Installation
+
+To add this feature to your rich-text editor, install the [`@ckeditor/ckeditor5-restricted-editing`](https://www.npmjs.com/package/@ckeditor/ckeditor5-restricted-editing) package:
+
+```bash
+npm install --save @ckeditor/ckeditor5-restricted-editing
+```
+
+### Running the Standard Editing Mode
+
+And add it to your plugin list and the toolbar configuration:
+
+```js
+import StandardEditingMode from '@ckeditor/ckeditor5-restricted-editing/src/standardeditingmode';
+
+ClassicEditor
+	.create( document.querySelector( '#editor' ), {
+		plugins: [ StandardEditingMode, ... ],
+		toolbar: [ 'restrictedEditingException', ... ]
+	} )
+	.then( ... )
+	.catch( ... );
+```
+
+### Running the Restricted Editing Mode
+
+And add it to your plugin list and the toolbar configuration:
+
+```js
+import RestrictedEditingMode from '@ckeditor/ckeditor5-restricted-editing/src/restrictededitingmode';
+
+ClassicEditor
+	.create( document.querySelector( '#editor' ), {
+		plugins: [ RestrictedEditingMode, ... ],
+		toolbar: [ 'restrictedEditing', ... ]
+	} )
+	.then( ... )
+	.catch( ... );
+```
+
+
+<info-box info>
+	Read more about {@link builds/guides/integration/installing-plugins installing plugins}.
+</info-box>
+
+## Common API
+
+<info-box>
+	We recommend using the official {@link framework/guides/development-tools#ckeditor-5-inspector CKEditor 5 inspector} for development and debugging. It will give you tons of useful information about the state of the editor such as internal data structures, selection, commands, and many more.
+</info-box>
+
+## Contribute
+
+The source code of the feature is available on GitHub in https://github.com/ckeditor/ckeditor5-restricted-editing.

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

@@ -14,11 +14,14 @@
   },
   "devDependencies": {
     "@ckeditor/ckeditor5-basic-styles": "^15.0.0",
+	"@ckeditor/ckeditor5-clipboard": "^15.0.0",
     "@ckeditor/ckeditor5-editor-classic": "^15.0.0",
     "@ckeditor/ckeditor5-engine": "^15.0.0",
     "@ckeditor/ckeditor5-paragraph": "^15.0.0",
     "@ckeditor/ckeditor5-table": "^15.0.0",
+    "@ckeditor/ckeditor5-typing": "^15.0.0",
     "@ckeditor/ckeditor5-utils": "^15.0.0",
+    "@ckeditor/ckeditor5-undo": "^15.0.0",
     "eslint": "^5.5.0",
     "eslint-config-ckeditor5": "^2.0.0",
     "husky": "^2.4.1",

+ 0 - 30
packages/ckeditor5-restricted-editing/src/restrictedediting.js

@@ -1,30 +0,0 @@
-/**
- * @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/restrictedediting
- */
-
-import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
-import RestrictedEditingEditing from './restrictededitingediting';
-import RestrictedEditingUI from './restrictededitingui';
-
-import '../theme/restrictedediting.css';
-
-/**
- * @extends module:core/plugin~Plugin
- */
-export default class RestrictedEditing extends Plugin {
-	/**
-	 * @inheritDoc
-	 */
-	static get pluginName() {
-		return 'RestrictedEditing';
-	}
-
-	static get requires() {
-		return [ RestrictedEditingEditing, RestrictedEditingUI ];
-	}
-}

+ 0 - 171
packages/ckeditor5-restricted-editing/src/restrictededitingediting.js

@@ -1,171 +0,0 @@
-/**
- * @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/restrictedediting
- */
-
-import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
-import Matcher from '@ckeditor/ckeditor5-engine/src/view/matcher';
-import RestrictedEditingNavigationCommand from './restrictededitingnavigationcommand';
-
-const HIGHLIGHT_CLASS = 'ck-restricted-editing-exception_selected';
-
-/**
- * The Restricted Editing editing feature.
- *
- * * It introduces the exception marker group that renders to `<spans>` with the `ck-restricted-editing-exception` CSS class.
- * * It registers the `'goToPreviousRestrictedEditingRegion'` and `'goToNextRestrictedEditingRegion'` commands.
- * * Also enables highlighting exception markers that are selected.
- *
- * @extends module:core/plugin~Plugin
- */
-export default class RestrictedEditingEditing extends Plugin {
-	/**
-	 * @inheritDoc
-	 */
-	static get pluginName() {
-		return 'RestrictedEditingEditing';
-	}
-
-	/**
-	 * @inheritDoc
-	 */
-	init() {
-		const editor = this.editor;
-
-		// Commands that allow navigation in the content.
-		editor.commands.add( 'goToPreviousRestrictedEditingRegion', new RestrictedEditingNavigationCommand( editor, 'backward' ) );
-		editor.commands.add( 'goToNextRestrictedEditingRegion', new RestrictedEditingNavigationCommand( editor, 'forward' ) );
-
-		let createdMarkers = 0;
-
-		editor.conversion.for( 'upcast' ).add( upcastHighlightToMarker( {
-			view: {
-				name: 'span',
-				classes: 'ck-restricted-editing-exception'
-			},
-			model: () => {
-				createdMarkers++;
-
-				return `restricted-editing-exception:${ createdMarkers }`;
-			}
-		} ) );
-
-		editor.conversion.for( 'downcast' ).markerToHighlight( {
-			model: 'restricted-editing-exception',
-			// Use callback to return new object every time new marker instance is created - otherwise it will be seen as the same marker.
-			view: () => ( {
-				name: 'span',
-				classes: 'ck-restricted-editing-exception',
-				priority: -10
-			} )
-		} );
-
-		const getCommandExecuter = commandName => {
-			return ( data, cancel ) => {
-				const command = this.editor.commands.get( commandName );
-
-				if ( command.isEnabled ) {
-					this.editor.execute( commandName );
-				}
-
-				cancel();
-			};
-		};
-
-		editor.keystrokes.set( 'Tab', getCommandExecuter( 'goToNextRestrictedEditingRegion' ) );
-		editor.keystrokes.set( 'Shift+Tab', getCommandExecuter( 'goToPreviousRestrictedEditingRegion' ) );
-
-		this._setupExceptionHighlighting();
-	}
-
-	/**
-	 * Adds a visual highlight style to a restricted editing exception the selection is anchored to.
-	 *
-	 * Highlight is turned on by adding the `.ck-restricted-editing-exception_selected` class to the
-	 * exception in the view:
-	 *
-	 * * The class is removed before the conversion has started, as callbacks added with the `'highest'` priority
-	 * to {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher} events.
-	 * * The class is added in the view post fixer, after other changes in the model tree were converted to the view.
-	 *
-	 * This way, adding and removing the highlight does not interfere with conversion.
-	 *
-	 * @private
-	 */
-	_setupExceptionHighlighting() {
-		const editor = this.editor;
-		const view = editor.editing.view;
-		const model = editor.model;
-		const highlightedMarkers = new Set();
-
-		// Adding the class.
-		view.document.registerPostFixer( writer => {
-			const modelSelection = model.document.selection;
-
-			for ( const marker of model.markers.getMarkersAtPosition( modelSelection.anchor ) ) {
-				for ( const viewElement of editor.editing.mapper.markerNameToElements( marker.name ) ) {
-					writer.addClass( HIGHLIGHT_CLASS, viewElement );
-					highlightedMarkers.add( viewElement );
-				}
-			}
-		} );
-
-		// Removing the class.
-		editor.conversion.for( 'editingDowncast' ).add( dispatcher => {
-			// Make sure the highlight is removed on every possible event, before conversion is started.
-			dispatcher.on( 'insert', removeHighlight, { priority: 'highest' } );
-			dispatcher.on( 'remove', removeHighlight, { priority: 'highest' } );
-			dispatcher.on( 'attribute', removeHighlight, { priority: 'highest' } );
-			dispatcher.on( 'selection', removeHighlight, { priority: 'highest' } );
-
-			function removeHighlight() {
-				view.change( writer => {
-					for ( const item of highlightedMarkers.values() ) {
-						writer.removeClass( HIGHLIGHT_CLASS, item );
-						highlightedMarkers.delete( item );
-					}
-				} );
-			}
-		} );
-	}
-}
-
-function upcastHighlightToMarker( config ) {
-	return dispatcher => dispatcher.on( 'element:span', ( evt, data, conversionApi ) => {
-		const { writer } = conversionApi;
-
-		const matcher = new Matcher( config.view );
-		const matcherResult = matcher.match( data.viewItem );
-
-		// If there is no match, this callback should not do anything.
-		if ( !matcherResult ) {
-			return;
-		}
-
-		const match = matcherResult.match;
-
-		// Force consuming element's name (taken from upcast helpers elementToElement converter).
-		match.name = true;
-
-		const { modelRange: convertedChildrenRange } = conversionApi.convertChildren( data.viewItem, data.modelCursor );
-		conversionApi.consumable.consume( data.viewItem, match );
-
-		const markerName = config.model( data.viewItem );
-		const fakeMarkerStart = writer.createElement( '$marker', { 'data-name': markerName } );
-		const fakeMarkerEnd = writer.createElement( '$marker', { 'data-name': markerName } );
-
-		// Insert in reverse order to use converter content positions directly (without recalculating).
-		writer.insert( fakeMarkerEnd, convertedChildrenRange.end );
-		writer.insert( fakeMarkerStart, convertedChildrenRange.start );
-
-		data.modelRange = writer.createRange(
-			writer.createPositionBefore( fakeMarkerStart ),
-			writer.createPositionAfter( fakeMarkerEnd )
-		);
-		data.modelCursor = data.modelRange.end;
-	} );
-}

+ 0 - 31
packages/ckeditor5-restricted-editing/src/restrictededitingexception.js

@@ -1,31 +0,0 @@
-/**
- * @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/restrictededitingexception
- */
-
-import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
-
-import RestrictedEditingExceptionEditing from './restrictededitingexceptionediting';
-import RestrictedEditingExceptionUI from './restrictededitingexceptionui';
-
-import '../theme/restrictedediting.css';
-
-/**
- * @extends module:core/plugin~Plugin
- */
-export default class RestrictedEditingException extends Plugin {
-	/**
-	 * @inheritDoc
-	 */
-	static get pluginName() {
-		return 'RestrictedEditingException';
-	}
-
-	static get requires() {
-		return [ RestrictedEditingExceptionEditing, RestrictedEditingExceptionUI ];
-	}
-}

+ 85 - 0
packages/ckeditor5-restricted-editing/src/restrictededitingmode.js

@@ -0,0 +1,85 @@
+/**
+ * @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/restrictededitingmode
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import RestrictedEditingModeEditing from './restrictededitingmodeediting';
+import RestrictedEditingModeUI from './restrictededitingmodeui';
+
+import '../theme/restrictedediting.css';
+
+/**
+ * The Restricted Editing Mode plugin.
+ *
+ * This is a "glue" plugin which loads the following plugins:
+ *
+ * * The {@link module:restricted-editing/restrictededitingmodeediting~RestrictedEditingModeEditing restricted mode editing feature} and
+ * * The {@link module:restricted-editing/restrictededitingmodeui~RestrictedEditingModeUI restricted mode ui feature}.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class RestrictedEditingMode extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get pluginName() {
+		return 'RestrictedEditingMode';
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static get requires() {
+		return [ RestrictedEditingModeEditing, RestrictedEditingModeUI ];
+	}
+}
+
+/**
+ * The configuration of the restricted editing mode feature. Introduced by the
+ * {@link module:restricted-editing/restrictededitingmode~RestrictedEditingMode} feature.
+ *
+ * Read more in {@link module:restricted-editing/restrictededitingmode~RestrictedEditingModeConfig}.
+ *
+ * @member {module:restricted-editing/restrictededitingmode~RestrictedEditingModeConfig}
+ * module:core/editor/editorconfig~EditorConfig#restrictedEditing
+ */
+
+/**
+ * The configuration of the restricted editing mode feature.
+ * The option is used by the {@link module:restricted-editing/restrictededitingmode~RestrictedEditingMode} feature.
+ *
+ *		ClassicEditor
+ *			.create( {
+ * 				restrictedEditing: {
+ * 					allowedCommands: [ 'bold', 'italic' ]
+ * 				}
+ *			} )
+ *			.then( ... )
+ *			.catch( ... );
+ *
+ * See {@link module:core/editor/editorconfig~EditorConfig all editor options}.
+ *
+ * @interface module:restricted-editing/restrictededitingmode~RestrictedEditingModeConfig
+ */
+
+/**
+ * The commands names allowed in non-restricted areas of the content.
+ *
+ * Define which feature commands should be enabled in restricted editing mode. The commands used for typing and deleting text
+ * (`'input'`, `'delete'` and `'forwardDelete'`) are allowed by the feature inside non-restricted regions and does not have to be defined.
+ *
+ * **Note**: The restricted editing mode always allows to use restricted mode navigation commands as well as `'undo'` and `'redo'` commands.
+ *
+ * The default value is:
+ *
+ *		const restrictedEditingConfig = {
+ *			allowedCommands: [ 'bold', 'italic', 'link', 'unlink' ]
+ *		};
+ *
+ * @member {Array.<String>} module:restricted-editing/restrictededitingmode~RestrictedEditingModeConfig#allowedCommands
+ */

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

@@ -0,0 +1,190 @@
+/**
+ * @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/restrictededitingmode/converters
+ */
+
+import Matcher from '@ckeditor/ckeditor5-engine/src/view/matcher';
+import { getMarkerAtPosition } from './utils';
+
+const HIGHLIGHT_CLASS = 'ck-restricted-editing-exception_selected';
+
+/**
+ * Adds a visual highlight style to a restricted editing exception the selection is anchored to.
+ *
+ * Highlight is turned on by adding the `.ck-restricted-editing-exception_selected` class to the
+ * exception in the view:
+ *
+ * * The class is removed before the conversion has started, as callbacks added with the `'highest'` priority
+ * to {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher} events.
+ * * The class is added in the view post fixer, after other changes in the model tree were converted to the view.
+ *
+ * This way, adding and removing the highlight does not interfere with conversion.
+ *
+ * @param {module:core/editor/editor~Editor} editor
+ */
+export function setupExceptionHighlighting( editor ) {
+	const view = editor.editing.view;
+	const model = editor.model;
+	const highlightedMarkers = new Set();
+
+	// Adding the class.
+	view.document.registerPostFixer( writer => {
+		const modelSelection = model.document.selection;
+
+		const marker = getMarkerAtPosition( editor, modelSelection.anchor );
+
+		if ( !marker ) {
+			return;
+		}
+
+		for ( const viewElement of editor.editing.mapper.markerNameToElements( marker.name ) ) {
+			writer.addClass( HIGHLIGHT_CLASS, viewElement );
+			highlightedMarkers.add( viewElement );
+		}
+	} );
+
+	// Removing the class.
+	editor.conversion.for( 'editingDowncast' ).add( dispatcher => {
+		// Make sure the highlight is removed on every possible event, before conversion is started.
+		dispatcher.on( 'insert', removeHighlight, { priority: 'highest' } );
+		dispatcher.on( 'remove', removeHighlight, { priority: 'highest' } );
+		dispatcher.on( 'attribute', removeHighlight, { priority: 'highest' } );
+		dispatcher.on( 'selection', removeHighlight, { priority: 'highest' } );
+
+		function removeHighlight() {
+			view.change( writer => {
+				for ( const item of highlightedMarkers.values() ) {
+					writer.removeClass( HIGHLIGHT_CLASS, item );
+					highlightedMarkers.delete( item );
+				}
+			} );
+		}
+	} );
+}
+
+/**
+ * A post-fixer that prevents removing collapsed marker from the document.
+ *
+ * @param {module:core/editor/editor~Editor} editor
+ * @returns {Function}
+ */
+export function resurrectCollapsedMarkerPostFixer( editor ) {
+	// This post-fixer shouldn't be necessary after https://github.com/ckeditor/ckeditor5/issues/5778.
+	return writer => {
+		let changeApplied = false;
+
+		for ( const [ name, data ] of editor.model.document.differ._changedMarkers ) {
+			if ( name.startsWith( 'restrictedEditingException' ) && data.newRange.root.rootName == '$graveyard' ) {
+				writer.updateMarker( name, {
+					range: writer.createRange( writer.createPositionAt( editor.model.document.selection.focus ) )
+				} );
+
+				changeApplied = true;
+			}
+		}
+
+		return changeApplied;
+	};
+}
+
+/**
+ * A post-fixer that extends a marker when user types on it boundaries.
+ *
+ * @param {module:core/editor/editor~Editor} editor
+ * @returns {Function}
+ */
+export function extendMarkerOnTypingPostFixer( editor ) {
+	// This post-fixer shouldn't be necessary after https://github.com/ckeditor/ckeditor5/issues/5778.
+	return writer => {
+		let changeApplied = false;
+
+		for ( const change of editor.model.document.differ.getChanges() ) {
+			if ( change.type == 'insert' && change.name == '$text' && change.length === 1 ) {
+				changeApplied = _tryExtendMarkerStart( editor, change.position, writer ) || changeApplied;
+				changeApplied = _tryExtendMarkedEnd( editor, change.position, writer ) || changeApplied;
+			}
+		}
+
+		return false;
+	};
+}
+
+/**
+ * A view highlight to marker conversion helper.
+ *
+ * @param {Object} config Conversion configuration.
+ * @param {module:engine/view/matcher~MatcherPattern} [config.view] Pattern matching all view elements which should be converted. If not
+ * set, the converter will fire for every view element.
+ * @param {String|module:engine/model/element~Element|Function} config.model Name of the model element, a model element
+ * instance or a function that takes a view element and returns a model element. The model element will be inserted in the model.
+ * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
+ */
+export function upcastHighlightToMarker( config ) {
+	return dispatcher => dispatcher.on( 'element:span', ( evt, data, conversionApi ) => {
+		const { writer } = conversionApi;
+
+		const matcher = new Matcher( config.view );
+		const matcherResult = matcher.match( data.viewItem );
+
+		// If there is no match, this callback should not do anything.
+		if ( !matcherResult ) {
+			return;
+		}
+
+		const match = matcherResult.match;
+
+		// Force consuming element's name (taken from upcast helpers elementToElement converter).
+		match.name = true;
+
+		const { modelRange: convertedChildrenRange } = conversionApi.convertChildren( data.viewItem, data.modelCursor );
+		conversionApi.consumable.consume( data.viewItem, match );
+
+		const markerName = config.model( data.viewItem );
+		const fakeMarkerStart = writer.createElement( '$marker', { 'data-name': markerName } );
+		const fakeMarkerEnd = writer.createElement( '$marker', { 'data-name': markerName } );
+
+		// Insert in reverse order to use converter content positions directly (without recalculating).
+		writer.insert( fakeMarkerEnd, convertedChildrenRange.end );
+		writer.insert( fakeMarkerStart, convertedChildrenRange.start );
+
+		data.modelRange = writer.createRange(
+			writer.createPositionBefore( fakeMarkerStart ),
+			writer.createPositionAfter( fakeMarkerEnd )
+		);
+		data.modelCursor = data.modelRange.end;
+	} );
+}
+
+// Extend marker if typing detected on marker's start position.
+function _tryExtendMarkerStart( editor, position, writer ) {
+	const markerAtStart = getMarkerAtPosition( editor, position.getShiftedBy( 1 ) );
+
+	if ( markerAtStart && markerAtStart.getStart().isEqual( position.getShiftedBy( 1 ) ) ) {
+		writer.updateMarker( markerAtStart, {
+			range: writer.createRange( markerAtStart.getStart().getShiftedBy( -1 ), markerAtStart.getEnd() )
+		} );
+
+		return true;
+	}
+
+	return false;
+}
+
+// Extend marker if typing detected on marker's end position.
+function _tryExtendMarkedEnd( editor, position, writer ) {
+	const markerAtEnd = getMarkerAtPosition( editor, position );
+
+	if ( markerAtEnd && markerAtEnd.getEnd().isEqual( position ) ) {
+		writer.updateMarker( markerAtEnd, {
+			range: writer.createRange( markerAtEnd.getStart(), markerAtEnd.getEnd().getShiftedBy( 1 ) )
+		} );
+
+		return true;
+	}
+
+	return false;
+}

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

@@ -0,0 +1,70 @@
+/**
+ * @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/restrictededitingmode/utils
+ */
+
+/**
+ * Returns a single "restricted-editing-exception" marker at a given position. Contrary to
+ * {@link module:engine/model/markercollection~MarkerCollection#getMarkersAtPosition} it return a marker also when the postion is equal
+ * to one of markers start or end positions.
+ *
+ * @param {module:core/editor/editor~Editor} editor
+ * @param {module:engine/model/position~Position} position
+ * @returns {module:engine/model/markercollection~Marker|undefined} marker
+ */
+export function getMarkerAtPosition( editor, position ) {
+	for ( const marker of editor.model.markers ) {
+		const markerRange = marker.getRange();
+
+		if ( isPositionInRangeBoundaries( markerRange, position ) ) {
+			if ( marker.name.startsWith( 'restrictedEditingException:' ) ) {
+				return marker;
+			}
+		}
+	}
+}
+
+/**
+ * Checks if the position is fully contained in range. Positions equal to range start or end are considered "in".
+ *
+ * @param {module:engine/model/range~Range} range
+ * @param {module:engine/model/position~Position} position
+ * @returns {Boolean}
+ */
+export function isPositionInRangeBoundaries( range, position ) {
+	return (
+		range.containsPosition( position ) ||
+		range.end.isEqual( position ) ||
+		range.start.isEqual( position )
+	);
+}
+
+/**
+ * Checks if the selection is fully contained in marker. Positions on marker boundaries are considered "in".
+ *
+ *		<marker>[]foo</marker> -> true
+ *		<marker>f[oo]</marker> -> true
+ *		<marker>f[oo</marker> ba]r -> false
+ *		<marker>foo</marker> []bar -> false
+ *
+ * @param {module:engine/model/selection~Selection} selection
+ * @param {module:engine/model/markercollection~Marker} marker
+ * @returns {Boolean}
+ */
+export function isSelectionInMarker( selection, marker ) {
+	if ( !marker ) {
+		return false;
+	}
+
+	const markerRange = marker.getRange();
+
+	if ( selection.isCollapsed ) {
+		return isPositionInRangeBoundaries( markerRange, selection.focus );
+	}
+
+	return markerRange.containsRange( selection.getFirstRange(), true );
+}

+ 286 - 0
packages/ckeditor5-restricted-editing/src/restrictededitingmodeediting.js

@@ -0,0 +1,286 @@
+/**
+ * @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/restrictededitingmodeediting
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import RestrictedEditingNavigationCommand from './restrictededitingmodenavigationcommand';
+import {
+	extendMarkerOnTypingPostFixer,
+	resurrectCollapsedMarkerPostFixer,
+	setupExceptionHighlighting,
+	upcastHighlightToMarker
+} from './restrictededitingmode/converters';
+import { getMarkerAtPosition, isSelectionInMarker } from './restrictededitingmode/utils';
+
+/**
+ * The Restricted Editing Mode editing feature.
+ *
+ * * It introduces the exception marker group that renders to `<spans>` with the `ck-restricted-editing-exception` CSS class.
+ * * It registers the `'goToPreviousRestrictedEditingException'` and `'goToNextRestrictedEditingException'` commands.
+ * * Also enables highlighting exception markers that are selected.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class RestrictedEditingModeEditing extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get pluginName() {
+		return 'RestrictedEditingModeEditing';
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	constructor( editor ) {
+		super( editor );
+
+		editor.config.define( 'restrictedEditing', {
+			allowedCommands: [ 'bold', 'italic', 'link', 'unlink' ]
+		} );
+
+		/**
+		 * Command names that are enabled outside non-restricted regions.
+		 *
+		 * @type {Set.<String>}
+		 * @private
+		 */
+		this._alwaysEnabled = new Set( [ 'undo', 'redo', 'goToPreviousRestrictedEditingException', 'goToNextRestrictedEditingException' ] );
+
+		/**
+		 * Commands allowed in non-restricted areas.
+		 *
+		 * Commands always enabled combines typing feature commands: `'typing'`, `'delete'` and `'forwardDelete'` with commands defined
+		 * in the feature configuration.
+		 *
+		 * @type {Set<string>}
+		 * @private
+		 */
+		this._allowedInException = new Set( [ 'input', 'delete', 'forwardDelete' ] );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		const editor = this.editor;
+
+		const allowedCommands = editor.config.get( 'restrictedEditing.allowedCommands' );
+
+		allowedCommands.forEach( commandName => this._allowedInException.add( commandName ) );
+
+		this._setupConversion();
+		this._setupCommandsToggling();
+
+		// Commands & keystrokes that allow navigation in the content.
+		editor.commands.add( 'goToPreviousRestrictedEditingException', new RestrictedEditingNavigationCommand( editor, 'backward' ) );
+		editor.commands.add( 'goToNextRestrictedEditingException', new RestrictedEditingNavigationCommand( editor, 'forward' ) );
+		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' } );
+	}
+
+	/**
+	 * Setups restricted mode editing conversion:
+	 *
+	 * * ucpast & downcast converters
+	 * * marker highlighting in the edting area
+	 * * marker post-fixers
+	 *
+	 * @private
+	 */
+	_setupConversion() {
+		const editor = this.editor;
+		const model = editor.model;
+		const doc = model.document;
+
+		// The restricted editing does not attach additional data to the zones so there's no need for smarter markers management.
+		// Also, the markers will only be created when  when loading the data.
+		let markerNumber = 0;
+
+		editor.conversion.for( 'upcast' ).add( upcastHighlightToMarker( {
+			view: {
+				name: 'span',
+				classes: 'ck-restricted-editing-exception'
+			},
+			model: () => {
+				markerNumber++; // Starting from restrictedEditingException:1 marker.
+
+				return `restrictedEditingException:${ markerNumber }`;
+			}
+		} ) );
+
+		// Currently the marker helpers are tied to other use-cases and do not render collapsed marker as highlight.
+		// That's why there are 2 downcast converters for them:
+		// 1. The default marker-to-highlight will wrap selected text with `<span>`.
+		editor.conversion.for( 'downcast' ).markerToHighlight( {
+			model: 'restrictedEditingException',
+			// Use callback to return new object every time new marker instance is created - otherwise it will be seen as the same marker.
+			view: () => {
+				return {
+					name: 'span',
+					classes: 'ck-restricted-editing-exception',
+					priority: -10
+				};
+			}
+		} );
+
+		// 2. But for collapsed marker we need to render it as an element.
+		// Additionally the editing pipeline should always display a collapsed markers.
+		editor.conversion.for( 'editingDowncast' ).markerToElement( {
+			model: 'restrictedEditingException',
+			view: ( markerData, viewWriter ) => {
+				return viewWriter.createUIElement( 'span', {
+					class: 'ck-restricted-editing-exception ck-restricted-editing-exception_collapsed'
+				} );
+			}
+		} );
+
+		editor.conversion.for( 'dataDowncast' ).markerToElement( {
+			model: 'restrictedEditingException',
+			view: ( markerData, viewWriter ) => {
+				return viewWriter.createEmptyElement( 'span', {
+					class: 'ck-restricted-editing-exception'
+				} );
+			}
+		} );
+
+		doc.registerPostFixer( extendMarkerOnTypingPostFixer( editor ) );
+		doc.registerPostFixer( resurrectCollapsedMarkerPostFixer( editor ) );
+
+		setupExceptionHighlighting( editor );
+	}
+
+	/**
+	 * Setups the commands toggling - enables or disables commands based on user selection.
+	 *
+	 * @private
+	 */
+	_setupCommandsToggling() {
+		const editor = this.editor;
+		const model = editor.model;
+		const doc = model.document;
+
+		this._disableCommands( editor );
+
+		this.listenTo( doc.selection, 'change', this._checkCommands.bind( this ) );
+		this.listenTo( doc, 'change:data', this._checkCommands.bind( this ) );
+	}
+
+	/**
+	 * Checks if commands should be enabled or disabled based on current selection.
+	 *
+	 * @private
+	 */
+	_checkCommands() {
+		const editor = this.editor;
+		const selection = editor.model.document.selection;
+
+		if ( selection.rangeCount > 1 ) {
+			this._disableCommands( editor );
+
+			return;
+		}
+
+		const marker = getMarkerAtPosition( editor, selection.focus );
+
+		this._disableCommands();
+
+		if ( isSelectionInMarker( selection, marker ) ) {
+			this._enableCommands( marker );
+		}
+	}
+
+	/**
+	 * Enables commands in non-restricted regions.
+	 *
+	 * @returns {module:engine/model/markercollection~Marker} marker
+	 * @private
+	 */
+	_enableCommands( marker ) {
+		const editor = this.editor;
+
+		const commands = this._getCommandNamesToToggle( editor, this._allowedInException )
+			.filter( name => this._allowedInException.has( name ) )
+			.filter( filterDeleteCommandsOnMarkerBoundaries( editor.model.document.selection, marker.getRange() ) )
+			.map( name => editor.commands.get( name ) );
+
+		for ( const command of commands ) {
+			command.clearForceDisabled( 'RestrictedEditingMode' );
+		}
+	}
+
+	/**
+	 * Disables commands outside non-restricted regions.
+	 *
+	 * @private
+	 */
+	_disableCommands() {
+		const editor = this.editor;
+		const commands = this._getCommandNamesToToggle( editor )
+			.map( name => editor.commands.get( name ) );
+
+		for ( const command of commands ) {
+			command.forceDisabled( 'RestrictedEditingMode' );
+		}
+	}
+
+	/**
+	 * Returns command names that should be toggleable.
+	 *
+	 * @param {module:core/editor/editor~Editor} editor
+	 * @returns {Array.<String>}
+	 * @private
+	 */
+	_getCommandNamesToToggle( editor ) {
+		return Array.from( editor.commands.names() )
+			.filter( name => !this._alwaysEnabled.has( name ) );
+	}
+}
+
+// Helper method for executing enabled commands only.
+function getCommandExecuter( editor, commandName ) {
+	return ( data, cancel ) => {
+		const command = editor.commands.get( commandName );
+
+		if ( command.isEnabled ) {
+			editor.execute( commandName );
+		}
+
+		cancel();
+	};
+}
+
+// Additional filtering rule for enabling "delete" and "forwardDelete" commands if selection is on range boundaries:
+//
+// Does not allow to enable command when selection focus is:
+// - is on marker start - "delete" - to prevent removing content before marker
+// - is on marker end - "forwardDelete" - to prevent removing content after marker
+function filterDeleteCommandsOnMarkerBoundaries( selection, markerRange ) {
+	return name => {
+		if ( name == 'delete' && markerRange.start.isEqual( selection.focus ) ) {
+			return false;
+		}
+
+		// Only for collapsed selection - non-collapsed seleciton that extends over a marker is handled elsewhere.
+		if ( name == 'forwardDelete' && selection.isCollapsed && markerRange.end.isEqual( selection.focus ) ) {
+			return false;
+		}
+
+		return true;
+	};
+}

+ 3 - 3
packages/ckeditor5-restricted-editing/src/restrictededitingnavigationcommand.js

@@ -4,7 +4,7 @@
  */
 
 /**
- * @module restricted-editing/restrictededitingnavigationcommand
+ * @module restricted-editing/restrictededitingmodenavigationcommand
  */
 
 import Command from '@ckeditor/ckeditor5-core/src/command';
@@ -14,7 +14,7 @@ import Command from '@ckeditor/ckeditor5-core/src/command';
  *
  * @extends module:core/command~Command
  */
-export default class RestrictedEditingNavigationCommand extends Command {
+export default class RestrictedEditingModeNavigationCommand extends Command {
 	/**
 	 * Creates an instance of the command.
 	 *
@@ -77,7 +77,7 @@ function getNearestExceptionRange( model, direction ) {
 	const markerRanges = [];
 
 	// Get all exception marker positions that start after/before the selection position.
-	for ( const marker of model.markers.getMarkersGroup( 'restricted-editing-exception' ) ) {
+	for ( const marker of model.markers.getMarkersGroup( 'restrictedEditingException' ) ) {
 		const markerRange = marker.getRange();
 
 		// Checking parent because there two positions <paragraph>foo^</paragraph><paragraph>^bar</paragraph>

+ 9 - 8
packages/ckeditor5-restricted-editing/src/restrictededitingui.js

@@ -4,7 +4,7 @@
  */
 
 /**
- * @module restricted-editing/restrictededitingui
+ * @module restricted-editing/restrictededitingmodeui
  */
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
@@ -14,19 +14,19 @@ import lockIcon from '../theme/icons/contentlock.svg';
 import Collection from '@ckeditor/ckeditor5-utils/src/collection';
 
 /**
- * The Restricted Editing UI feature.
+ * The Restricted Editing Mode UI feature.
  *
  * It introduces the `'restrictedEditing'` dropdown that offers tools to navigate exceptions across
  * the document.
  *
  * @extends module:core/plugin~Plugin
  */
-export default class RestrictedEditingUI extends Plugin {
+export default class RestrictedEditingModeUI extends Plugin {
 	/**
 	 * @inheritDoc
 	 */
 	static get pluginName() {
-		return 'RestrictedEditingUI';
+		return 'RestrictedEditingModeUI';
 	}
 
 	/**
@@ -41,12 +41,12 @@ export default class RestrictedEditingUI extends Plugin {
 			const listItems = new Collection();
 
 			listItems.add( this._getButtonDefinition(
-				'goToPreviousRestrictedEditingRegion',
+				'goToPreviousRestrictedEditingException',
 				t( 'Previous editable region' ),
 				'Shift+Tab'
 			) );
 			listItems.add( this._getButtonDefinition(
-				'goToNextRestrictedEditingRegion',
+				'goToNextRestrictedEditingException',
 				t( 'Next editable region' ),
 				'Tab'
 			) );
@@ -76,9 +76,10 @@ export default class RestrictedEditingUI extends Plugin {
 	 * @private
 	 * @param {String} commandName Name of the command the button represents.
 	 * @param {String} label Translated label of the button.
+	 * @param {String} keystroke Keystroke of the button.
 	 * @returns {module:ui/dropdown/utils~ListDropdownItemDefinition}
 	 */
-	_getButtonDefinition( commandName, label, kestroke ) {
+	_getButtonDefinition( commandName, label, keystroke ) {
 		const editor = this.editor;
 		const command = editor.commands.get( commandName );
 		const definition = {
@@ -86,7 +87,7 @@ export default class RestrictedEditingUI extends Plugin {
 			model: new Model( {
 				label,
 				withText: true,
-				keystroke: kestroke,
+				keystroke,
 				withKeystroke: true,
 				_commandName: commandName
 			} )

+ 38 - 0
packages/ckeditor5-restricted-editing/src/standardeditingmode.js

@@ -0,0 +1,38 @@
+/**
+ * @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/standardeditingmode
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+
+import StandardEditingModeEditing from './standardeditingmodeediting';
+import StandardEditingModeUI from './standardeditingmodeui';
+
+import '../theme/restrictedediting.css';
+
+/**
+ * The Standard Editing Mode plugin.
+ *
+ * This is a "glue" plugin which loads the following plugins:
+ *
+ * * The {@link module:restricted-editing/standardeditingmodeediting~StandardEditingModeEditing standard mode editing feature} and
+ * * The {@link module:restricted-editing/standardeditingmodeui~StandardEditingModeUI standard mode ui feature}.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class StandardEditingMode extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get pluginName() {
+		return 'StandardEditingMode';
+	}
+
+	static get requires() {
+		return [ StandardEditingModeEditing, StandardEditingModeUI ];
+	}
+}

+ 9 - 3
packages/ckeditor5-restricted-editing/src/restrictededitingexceptionediting.js

@@ -4,21 +4,27 @@
  */
 
 /**
- * @module restricted-editing/restrictededitingexceptionediting
+ * @module restricted-editing/standardeditingmodeediting
  */
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import RestrictedEditingExceptionCommand from './restrictededitingexceptioncommand';
 
 /**
+ * The Standard Editing Mode editing feature.
+ *
+ * * It introduces the `restrictedEditingException` text attributes that is rendered as
+ * `<spans>` with the `ck-restricted-editing-exception` CSS class.
+ * * It registers the `'restrictedEditingException'` command.
+ *
  * @extends module:core/plugin~Plugin
  */
-export default class RestrictedEditingExceptionEditing extends Plugin {
+export default class StandardEditingModeEditing extends Plugin {
 	/**
 	 * @inheritDoc
 	 */
 	static get pluginName() {
-		return 'RestrictedEditingExceptionEditing';
+		return 'StandardEditingModeEditing';
 	}
 
 	/**

+ 6 - 2
packages/ckeditor5-restricted-editing/src/restrictededitingexceptionui.js

@@ -4,7 +4,7 @@
  */
 
 /**
- * @module restricted-editing/restrictededitingexceptionui
+ * @module restricted-editing/standardeditingmodeui
  */
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
@@ -13,9 +13,13 @@ import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
 import unlockIcon from '../theme/icons/contentunlock.svg';
 
 /**
+ * The Standard Editing Mode UI feature.
+ *
+ * It introduces the `'restrictedEditingException'` button that marks text as unrestricted for editing.
+ *
  * @extends module:core/plugin~Plugin
  */
-export default class RestrictedEditingExceptionUI extends Plugin {
+export default class StandardEditingModeUI extends Plugin {
 	/**
 	 * @inheritDoc
 	 */

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

@@ -9,8 +9,8 @@ import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'
 import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset';
 import Table from '@ckeditor/ckeditor5-table/src/table';
 
-import RestrictedEditingException from '../../src/restrictededitingexception';
-import RestrictedEditing from '../../src/restrictedediting';
+import StandardEditingMode from '../../src/standardeditingmode';
+import RestrictedEditingMode from '../../src/restrictededitingmode';
 
 const restrictedModeButton = document.getElementById( 'mode-restricted' );
 const standardModeButton = document.getElementById( 'mode-standard' );
@@ -18,19 +18,23 @@ const standardModeButton = document.getElementById( 'mode-standard' );
 restrictedModeButton.addEventListener( 'change', handleModeChange );
 standardModeButton.addEventListener( 'change', handleModeChange );
 
-startStandardMode();
+startMode( document.querySelector( 'input[name="mode"]:checked' ).value );
 
-function handleModeChange( evt ) {
-	if ( evt.target.value === 'standard' ) {
-		startStandardMode();
+async function handleModeChange( evt ) {
+	await startMode( evt.target.value );
+}
+
+async function startMode( selectedMode ) {
+	if ( selectedMode === 'standard' ) {
+		await startStandardEditingMode();
 	} else {
-		startRestrictedMode();
+		await startRestrictedEditingMode();
 	}
 }
 
-async function startStandardMode() {
+async function startStandardEditingMode() {
 	await reloadEditor( {
-		plugins: [ ArticlePluginSet, Table, RestrictedEditingException ],
+		plugins: [ ArticlePluginSet, Table, StandardEditingMode ],
 		toolbar: [
 			'heading', '|', 'bold', 'italic', 'link', '|',
 			'bulletedList', 'numberedList', 'blockQuote', 'insertTable', '|',
@@ -49,9 +53,9 @@ async function startStandardMode() {
 	} );
 }
 
-async function startRestrictedMode() {
+async function startRestrictedEditingMode() {
 	await reloadEditor( {
-		plugins: [ ArticlePluginSet, Table, RestrictedEditing ],
+		plugins: [ ArticlePluginSet, Table, RestrictedEditingMode ],
 		toolbar: [ 'bold', 'italic', 'link', '|', 'restrictedEditing', '|', 'undo', 'redo' ]
 	} );
 }

+ 0 - 44
packages/ckeditor5-restricted-editing/tests/restrictededitingexception.js

@@ -1,44 +0,0 @@
-/**
- * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
- */
-
-/* global document */
-
-import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
-import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
-
-import RestrictedEditingException from './../src/restrictededitingexception';
-import RestrictedEditingExceptionUI from './../src/restrictededitingexceptionui';
-import RestrictedEditingExceptionEditing from './../src/restrictededitingexceptionediting';
-
-describe( 'RestrictedEditingException', () => {
-	let editor, element;
-
-	testUtils.createSinonSandbox();
-
-	beforeEach( async () => {
-		element = document.createElement( 'div' );
-		document.body.appendChild( element );
-
-		editor = await ClassicTestEditor.create( element, { plugins: [ RestrictedEditingException ] } );
-	} );
-
-	afterEach( () => {
-		element.remove();
-
-		return editor.destroy();
-	} );
-
-	it( 'should be named', () => {
-		expect( RestrictedEditingException.pluginName ).to.equal( 'RestrictedEditingException' );
-	} );
-
-	it( 'should load the RestrictedEditingExceptionEditing plugin', () => {
-		expect( editor.plugins.get( RestrictedEditingExceptionEditing ) ).to.be.instanceOf( RestrictedEditingExceptionEditing );
-	} );
-
-	it( 'should load the RestrictedEditingExceptionUI plugin', () => {
-		expect( editor.plugins.get( RestrictedEditingExceptionUI ) ).to.be.instanceOf( RestrictedEditingExceptionUI );
-	} );
-} );

+ 44 - 0
packages/ckeditor5-restricted-editing/tests/restrictededitingmode.js

@@ -0,0 +1,44 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/* global document */
+
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
+
+import RestrictedEditingMode from './../src/restrictededitingmode';
+import RestrictedEditingModeUI from './../src/restrictededitingmodeui';
+import RestrictedEditingModeEditing from './../src/restrictededitingmodeediting';
+
+describe( 'RestrictedEditingMode', () => {
+	let editor, element;
+
+	testUtils.createSinonSandbox();
+
+	beforeEach( async () => {
+		element = document.createElement( 'div' );
+		document.body.appendChild( element );
+
+		editor = await ClassicTestEditor.create( element, { plugins: [ RestrictedEditingMode ] } );
+	} );
+
+	afterEach( () => {
+		element.remove();
+
+		return editor.destroy();
+	} );
+
+	it( 'should be named', () => {
+		expect( RestrictedEditingMode.pluginName ).to.equal( 'RestrictedEditingMode' );
+	} );
+
+	it( 'should load the RestrictedEditingModeEditing plugin', () => {
+		expect( editor.plugins.get( RestrictedEditingModeEditing ) ).to.be.instanceOf( RestrictedEditingModeEditing );
+	} );
+
+	it( 'should load the RestrictedEditingModeUI plugin', () => {
+		expect( editor.plugins.get( RestrictedEditingModeUI ) ).to.be.instanceOf( RestrictedEditingModeUI );
+	} );
+} );

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

@@ -0,0 +1,787 @@
+/**
+ * @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 testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import Typing from '@ckeditor/ckeditor5-typing/src/typing';
+import Command from '@ckeditor/ckeditor5-core/src/command';
+import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+
+import RestrictedEditingModeEditing from './../src/restrictededitingmodeediting';
+import UndoEditing from '@ckeditor/ckeditor5-undo/src/undoediting';
+
+describe( 'RestrictedEditingEditing - commands', () => {
+	let editor;
+
+	testUtils.createSinonSandbox();
+
+	describe( 'commands toggling', () => {
+		let model, firstParagraph;
+
+		beforeEach( async () => {
+			editor = await VirtualTestEditor.create( { plugins: [ Paragraph, Typing, RestrictedEditingModeEditing ] } );
+			model = editor.model;
+
+			setModelData( model, '<paragraph>[]foo bar baz</paragraph>' );
+			firstParagraph = model.document.getRoot().getChild( 0 );
+
+			model.change( writer => {
+				writer.addMarker( 'restrictedEditingException:1', {
+					range: writer.createRange(
+						writer.createPositionAt( firstParagraph, 4 ),
+						writer.createPositionAt( firstParagraph, 7 ) ),
+					usingOperation: true,
+					affectsData: true
+				} );
+			} );
+		} );
+
+		afterEach( async () => {
+			await editor.destroy();
+		} );
+
+		describe( 'always enabled commands', () => {
+			describe( 'undo', () => {
+				beforeEach( () => {
+					editor.commands.add( 'undo', buildFakeCommand( editor ) );
+				} );
+
+				it( 'should be enabled outside exception marker', () => {
+					model.change( writer => {
+						writer.setSelection( firstParagraph, 1 );
+					} );
+
+					expect( editor.commands.get( 'undo' ).isEnabled ).to.be.true;
+				} );
+
+				it( 'should be enabled inside exception marker', () => {
+					model.change( writer => {
+						writer.setSelection( firstParagraph, 5 );
+					} );
+
+					expect( editor.commands.get( 'undo' ).isEnabled ).to.be.true;
+				} );
+			} );
+
+			describe( 'redo', () => {
+				beforeEach( () => {
+					editor.commands.add( 'redo', buildFakeCommand( editor ) );
+				} );
+
+				it( 'should be enabled outside exception marker', () => {
+					model.change( writer => {
+						writer.setSelection( firstParagraph, 1 );
+					} );
+
+					expect( editor.commands.get( 'redo' ).isEnabled ).to.be.true;
+				} );
+
+				it( 'should be enabled inside exception marker', () => {
+					model.change( writer => {
+						writer.setSelection( firstParagraph, 5 );
+					} );
+
+					expect( editor.commands.get( 'redo' ).isEnabled ).to.be.true;
+				} );
+			} );
+		} );
+
+		describe( 'commands enabled in exception marker', () => {
+			describe( 'input', () => {
+				beforeEach( () => {
+					editor.commands.add( 'input', buildFakeCommand( editor ) );
+
+					model.change( writer => {
+						writer.setSelection( firstParagraph, 'end' );
+					} );
+				} );
+
+				it( 'should be disabled when caret is outside exception marker', () => {
+					model.change( writer => {
+						writer.setSelection( firstParagraph, 1 );
+					} );
+
+					expect( editor.commands.get( 'input' ).isEnabled ).to.be.false;
+				} );
+
+				it( 'should be enabled when caret is inside exception marker (not touching boundaries)', () => {
+					model.change( writer => {
+						writer.setSelection( firstParagraph, 5 );
+					} );
+
+					expect( editor.commands.get( 'input' ).isEnabled ).to.be.true;
+				} );
+
+				it( 'should be disabled when caret is inside other marker', () => {
+					model.change( writer => {
+						writer.addMarker( 'foo-bar:1', {
+							range: writer.createRange(
+								writer.createPositionAt( firstParagraph, 0 ),
+								writer.createPositionAt( firstParagraph, 3 ) ),
+							usingOperation: true,
+							affectsData: true
+						} );
+
+						writer.setSelection( firstParagraph, 1 );
+					} );
+
+					expect( editor.commands.get( 'input' ).isEnabled ).to.be.false;
+				} );
+
+				it( 'should be enabled when caret is inside exception marker (start boundary)', () => {
+					model.change( writer => {
+						writer.setSelection( firstParagraph, 4 );
+					} );
+
+					expect( editor.commands.get( 'input' ).isEnabled ).to.be.true;
+				} );
+
+				it( 'should be enabled when caret is inside exception marker (end boundary)', () => {
+					model.change( writer => {
+						writer.setSelection( firstParagraph, 7 );
+					} );
+
+					expect( editor.commands.get( 'input' ).isEnabled ).to.be.true;
+				} );
+
+				it( 'should be disabled for multi-range selection (collapsed ranges)', () => {
+					model.change( writer => {
+						writer.setSelection( [
+							writer.createRange(
+								writer.createPositionAt( firstParagraph, 5 )
+							),
+							writer.createRange(
+								writer.createPositionAt( firstParagraph, 9 )
+							)
+						] );
+					} );
+
+					expect( editor.commands.get( 'input' ).isEnabled ).to.be.false;
+				} );
+
+				it( 'should be disabled for non-collapsed selection that expands over exception marker', () => {
+					model.change( writer => {
+						writer.setSelection( writer.createRange(
+							writer.createPositionAt( firstParagraph, 0 ),
+							writer.createPositionAt( firstParagraph, 5 )
+						) );
+					} );
+
+					expect( editor.commands.get( 'input' ).isEnabled ).to.be.false;
+				} );
+
+				it( 'should be enabled for non-collapsed selection that is fully contained inside exception marker', () => {
+					model.change( writer => {
+						writer.setSelection( writer.createRange(
+							writer.createPositionAt( firstParagraph, 5 ),
+							writer.createPositionAt( firstParagraph, 6 )
+						) );
+					} );
+
+					expect( editor.commands.get( 'input' ).isEnabled ).to.be.true;
+				} );
+
+				it( 'should be enabled for non-collapsed selection inside exception marker (start position on marker boundary)', () => {
+					model.change( writer => {
+						writer.setSelection( writer.createRange(
+							writer.createPositionAt( firstParagraph, 4 ),
+							writer.createPositionAt( firstParagraph, 6 )
+						) );
+					} );
+
+					expect( editor.commands.get( 'input' ).isEnabled ).to.be.true;
+				} );
+
+				it( 'should be enabled for non-collapsed selection inside exception marker (end position on marker boundary)', () => {
+					model.change( writer => {
+						writer.setSelection( writer.createRange(
+							writer.createPositionAt( firstParagraph, 5 ),
+							writer.createPositionAt( firstParagraph, 7 )
+						) );
+					} );
+
+					expect( editor.commands.get( 'input' ).isEnabled ).to.be.true;
+				} );
+
+				it( 'should be enabled for non-collapsed selection is equal to exception marker', () => {
+					model.change( writer => {
+						writer.setSelection( writer.createRange(
+							writer.createPositionAt( firstParagraph, 4 ),
+							writer.createPositionAt( firstParagraph, 7 )
+						) );
+					} );
+
+					expect( editor.commands.get( 'input' ).isEnabled ).to.be.true;
+				} );
+
+				it( 'should be disabled for non-collapsed selection with more then one range', () => {
+					model.change( writer => {
+						writer.setSelection( [
+							writer.createRange(
+								writer.createPositionAt( firstParagraph, 5 ),
+								writer.createPositionAt( firstParagraph, 6 )
+							),
+							writer.createRange(
+								writer.createPositionAt( firstParagraph, 8 ),
+								writer.createPositionAt( firstParagraph, 9 )
+							)
+						] );
+					} );
+
+					expect( editor.commands.get( 'input' ).isEnabled ).to.be.false;
+				} );
+			} );
+
+			describe( 'delete', () => {
+				beforeEach( () => {
+					editor.commands.add( 'delete', buildFakeCommand( editor ) );
+
+					model.change( writer => {
+						writer.setSelection( firstParagraph, 'end' );
+					} );
+				} );
+
+				it( 'should be disabled when caret is outside exception marker', () => {
+					model.change( writer => {
+						writer.setSelection( firstParagraph, 1 );
+					} );
+
+					expect( editor.commands.get( 'delete' ).isEnabled ).to.be.false;
+				} );
+
+				it( 'should be enabled when caret is inside exception marker (not touching boundaries)', () => {
+					model.change( writer => {
+						writer.setSelection( firstParagraph, 5 );
+					} );
+
+					expect( editor.commands.get( 'delete' ).isEnabled ).to.be.true;
+				} );
+
+				it( 'should be disabled when caret is inside exception marker (start boundary)', () => {
+					model.change( writer => {
+						writer.setSelection( firstParagraph, 4 );
+					} );
+
+					expect( editor.commands.get( 'delete' ).isEnabled ).to.be.false;
+				} );
+
+				it( 'should be disabled when caret moves to start boundary and it was enabled previously', () => {
+					model.change( writer => {
+						writer.setSelection( firstParagraph, 5 );
+					} );
+
+					expect( editor.commands.get( 'delete' ).isEnabled ).to.be.true;
+
+					model.change( writer => {
+						writer.setSelection( firstParagraph, 4 );
+					} );
+
+					expect( editor.commands.get( 'delete' ).isEnabled ).to.be.false;
+				} );
+
+				it( 'should be enabled when caret is inside exception marker (end boundary)', () => {
+					model.change( writer => {
+						writer.setSelection( firstParagraph, 7 );
+					} );
+
+					expect( editor.commands.get( 'delete' ).isEnabled ).to.be.true;
+				} );
+
+				it( 'should be disabled for non-collapsed selection that expands over exception marker', () => {
+					model.change( writer => {
+						writer.setSelection( writer.createRange(
+							writer.createPositionAt( firstParagraph, 0 ),
+							writer.createPositionAt( firstParagraph, 5 )
+						) );
+					} );
+
+					expect( editor.commands.get( 'delete' ).isEnabled ).to.be.false;
+				} );
+
+				it( 'should be enabled for non-collapsed selection that is fully contained inside exception marker', () => {
+					model.change( writer => {
+						writer.setSelection( writer.createRange(
+							writer.createPositionAt( firstParagraph, 5 ),
+							writer.createPositionAt( firstParagraph, 6 )
+						) );
+					} );
+
+					expect( editor.commands.get( 'delete' ).isEnabled ).to.be.true;
+				} );
+
+				it( 'should be enabled for non-collapsed selection inside exception marker (start position on marker boundary)', () => {
+					model.change( writer => {
+						writer.setSelection( writer.createRange(
+							writer.createPositionAt( firstParagraph, 4 ),
+							writer.createPositionAt( firstParagraph, 6 )
+						) );
+					} );
+
+					expect( editor.commands.get( 'delete' ).isEnabled ).to.be.true;
+				} );
+
+				it( 'should be enabled for non-collapsed selection inside exception marker (end position on marker boundary)', () => {
+					model.change( writer => {
+						writer.setSelection( writer.createRange(
+							writer.createPositionAt( firstParagraph, 5 ),
+							writer.createPositionAt( firstParagraph, 7 )
+						) );
+					} );
+
+					expect( editor.commands.get( 'delete' ).isEnabled ).to.be.true;
+				} );
+
+				it( 'should be enabled for non-collapsed selection is equal to exception marker', () => {
+					model.change( writer => {
+						writer.setSelection( writer.createRange(
+							writer.createPositionAt( firstParagraph, 4 ),
+							writer.createPositionAt( firstParagraph, 7 )
+						) );
+					} );
+
+					expect( editor.commands.get( 'delete' ).isEnabled ).to.be.true;
+				} );
+			} );
+
+			describe( 'forwardDelete', () => {
+				beforeEach( () => {
+					editor.commands.add( 'forwardDelete', buildFakeCommand( editor ) );
+
+					model.change( writer => {
+						writer.setSelection( firstParagraph, 'end' );
+					} );
+				} );
+
+				it( 'should be disabled when caret is outside exception marker', () => {
+					model.change( writer => {
+						writer.setSelection( firstParagraph, 1 );
+					} );
+
+					expect( editor.commands.get( 'forwardDelete' ).isEnabled ).to.be.false;
+				} );
+
+				it( 'should be enabled when caret is inside exception marker (not touching boundaries)', () => {
+					model.change( writer => {
+						writer.setSelection( firstParagraph, 5 );
+					} );
+
+					expect( editor.commands.get( 'forwardDelete' ).isEnabled ).to.be.true;
+				} );
+
+				it( 'should be enabled when caret is inside exception marker (start boundary)', () => {
+					model.change( writer => {
+						writer.setSelection( firstParagraph, 4 );
+					} );
+
+					expect( editor.commands.get( 'forwardDelete' ).isEnabled ).to.be.true;
+				} );
+
+				it( 'should be disabled when caret is inside exception marker (end boundary)', () => {
+					model.change( writer => {
+						writer.setSelection( firstParagraph, 7 );
+					} );
+
+					expect( editor.commands.get( 'forwardDelete' ).isEnabled ).to.be.false;
+				} );
+
+				it( 'should be disabled when caret moves to end boundary and it was enabled previously', () => {
+					model.change( writer => {
+						writer.setSelection( firstParagraph, 5 );
+					} );
+
+					expect( editor.commands.get( 'forwardDelete' ).isEnabled ).to.be.true;
+
+					model.change( writer => {
+						writer.setSelection( firstParagraph, 7 );
+					} );
+
+					expect( editor.commands.get( 'forwardDelete' ).isEnabled ).to.be.false;
+				} );
+
+				it( 'should be disabled for non-collapsed selection that expands over exception marker', () => {
+					model.change( writer => {
+						writer.setSelection( writer.createRange(
+							writer.createPositionAt( firstParagraph, 0 ),
+							writer.createPositionAt( firstParagraph, 5 )
+						) );
+					} );
+
+					expect( editor.commands.get( 'forwardDelete' ).isEnabled ).to.be.false;
+				} );
+
+				it( 'should be enabled for non-collapsed selection that is fully contained inside exception marker', () => {
+					model.change( writer => {
+						writer.setSelection( writer.createRange(
+							writer.createPositionAt( firstParagraph, 5 ),
+							writer.createPositionAt( firstParagraph, 6 )
+						) );
+					} );
+
+					expect( editor.commands.get( 'forwardDelete' ).isEnabled ).to.be.true;
+				} );
+
+				it( 'should be enabled for non-collapsed selection inside exception marker (start position on marker boundary)', () => {
+					model.change( writer => {
+						writer.setSelection( writer.createRange(
+							writer.createPositionAt( firstParagraph, 4 ),
+							writer.createPositionAt( firstParagraph, 6 )
+						) );
+					} );
+
+					expect( editor.commands.get( 'forwardDelete' ).isEnabled ).to.be.true;
+				} );
+
+				it( 'should be enabled for non-collapsed selection inside exception marker (end position on marker boundary)', () => {
+					model.change( writer => {
+						writer.setSelection( writer.createRange(
+							writer.createPositionAt( firstParagraph, 5 ),
+							writer.createPositionAt( firstParagraph, 7 )
+						) );
+					} );
+
+					expect( editor.commands.get( 'forwardDelete' ).isEnabled ).to.be.true;
+				} );
+
+				it( 'should be enabled for non-collapsed selection is equal to exception marker', () => {
+					model.change( writer => {
+						writer.setSelection( writer.createRange(
+							writer.createPositionAt( firstParagraph, 4 ),
+							writer.createPositionAt( firstParagraph, 7 )
+						) );
+					} );
+
+					expect( editor.commands.get( 'forwardDelete' ).isEnabled ).to.be.true;
+				} );
+			} );
+		} );
+
+		describe( 'non-enabled commands', () => {
+			beforeEach( () => {
+				editor.commands.add( 'other', buildFakeCommand( editor ) );
+
+				model.change( writer => {
+					writer.setSelection( firstParagraph, 'end' );
+				} );
+			} );
+
+			it( 'should be disabled outside exception marker', () => {
+				model.change( writer => {
+					writer.setSelection( firstParagraph, 1 );
+				} );
+
+				expect( editor.commands.get( 'other' ).isEnabled ).to.be.false;
+			} );
+
+			it( 'should be disabled inside exception marker', () => {
+				model.change( writer => {
+					writer.setSelection( firstParagraph, 1 );
+				} );
+				model.change( writer => {
+					writer.setSelection( firstParagraph, 5 );
+				} );
+
+				expect( editor.commands.get( 'other' ).isEnabled ).to.be.false;
+			} );
+
+			it( 'should be disabled when caret is inside exception marker (not touching boundaries)', () => {
+				model.change( writer => {
+					writer.setSelection( firstParagraph, 5 );
+				} );
+
+				expect( editor.commands.get( 'other' ).isEnabled ).to.be.false;
+			} );
+
+			it( 'should be disabled when caret is inside exception marker (start boundary)', () => {
+				model.change( writer => {
+					writer.setSelection( firstParagraph, 4 );
+				} );
+
+				expect( editor.commands.get( 'other' ).isEnabled ).to.be.false;
+			} );
+
+			it( 'should be disabled when caret is inside exception marker (end boundary)', () => {
+				model.change( writer => {
+					writer.setSelection( firstParagraph, 7 );
+				} );
+
+				expect( editor.commands.get( 'other' ).isEnabled ).to.be.false;
+			} );
+
+			it( 'should be disabled for non-collapsed selection that expands over exception marker', () => {
+				model.change( writer => {
+					writer.setSelection( writer.createRange(
+						writer.createPositionAt( firstParagraph, 0 ),
+						writer.createPositionAt( firstParagraph, 5 )
+					) );
+				} );
+
+				expect( editor.commands.get( 'other' ).isEnabled ).to.be.false;
+			} );
+
+			it( 'should be disabled for non-collapsed selection that is fully contained inside exception marker', () => {
+				model.change( writer => {
+					writer.setSelection( writer.createRange(
+						writer.createPositionAt( firstParagraph, 5 ),
+						writer.createPositionAt( firstParagraph, 6 )
+					) );
+				} );
+
+				expect( editor.commands.get( 'other' ).isEnabled ).to.be.false;
+			} );
+
+			it( 'should be disabled for non-collapsed selection inside exception marker (start position on marker boundary)', () => {
+				model.change( writer => {
+					writer.setSelection( writer.createRange(
+						writer.createPositionAt( firstParagraph, 4 ),
+						writer.createPositionAt( firstParagraph, 6 )
+					) );
+				} );
+
+				expect( editor.commands.get( 'other' ).isEnabled ).to.be.false;
+			} );
+
+			it( 'should be disabled for non-collapsed selection inside exception marker (end position on marker boundary)', () => {
+				model.change( writer => {
+					writer.setSelection( writer.createRange(
+						writer.createPositionAt( firstParagraph, 5 ),
+						writer.createPositionAt( firstParagraph, 7 )
+					) );
+				} );
+
+				expect( editor.commands.get( 'other' ).isEnabled ).to.be.false;
+			} );
+
+			it( 'should be disabled for non-collapsed selection is equal to exception marker', () => {
+				model.change( writer => {
+					writer.setSelection( writer.createRange(
+						writer.createPositionAt( firstParagraph, 4 ),
+						writer.createPositionAt( firstParagraph, 7 )
+					) );
+				} );
+
+				expect( editor.commands.get( 'other' ).isEnabled ).to.be.false;
+			} );
+		} );
+	} );
+
+	describe( 'commands enabled in exception marker by configuration', () => {
+		let model, firstParagraph;
+
+		beforeEach( async () => {
+			editor = await VirtualTestEditor.create( {
+				plugins: [ Paragraph, Typing, RestrictedEditingModeEditing ],
+				restrictedEditing: {
+					allowedCommands: [ 'allowed' ]
+				}
+			} );
+			model = editor.model;
+			editor.commands.add( 'allowed', buildFakeCommand( editor ) );
+
+			setModelData( model, '<paragraph>[]foo bar baz</paragraph>' );
+			firstParagraph = model.document.getRoot().getChild( 0 );
+
+			model.change( writer => {
+				writer.addMarker( 'restrictedEditingException:1', {
+					range: writer.createRange(
+						writer.createPositionAt( firstParagraph, 4 ),
+						writer.createPositionAt( firstParagraph, 7 ) ),
+					usingOperation: true,
+					affectsData: true
+				} );
+
+				writer.setSelection( firstParagraph, 'end' );
+			} );
+		} );
+
+		afterEach( async () => {
+			await editor.destroy();
+		} );
+
+		it( 'should be disabled when caret is outside exception marker', () => {
+			model.change( writer => {
+				writer.setSelection( firstParagraph, 1 );
+			} );
+
+			expect( editor.commands.get( 'allowed' ).isEnabled ).to.be.false;
+		} );
+
+		it( 'should be enabled when caret is inside exception marker (not touching boundaries)', () => {
+			model.change( writer => {
+				writer.setSelection( firstParagraph, 5 );
+			} );
+
+			expect( editor.commands.get( 'allowed' ).isEnabled ).to.be.true;
+		} );
+
+		it( 'should be disabled when caret is inside other marker', () => {
+			model.change( writer => {
+				writer.addMarker( 'foo-bar:1', {
+					range: writer.createRange(
+						writer.createPositionAt( firstParagraph, 0 ),
+						writer.createPositionAt( firstParagraph, 3 ) ),
+					usingOperation: true,
+					affectsData: true
+				} );
+
+				writer.setSelection( firstParagraph, 1 );
+			} );
+
+			expect( editor.commands.get( 'allowed' ).isEnabled ).to.be.false;
+		} );
+
+		it( 'should be enabled when caret is inside exception marker (start boundary)', () => {
+			model.change( writer => {
+				writer.setSelection( firstParagraph, 4 );
+			} );
+
+			expect( editor.commands.get( 'allowed' ).isEnabled ).to.be.true;
+		} );
+
+		it( 'should be enabled when caret is inside exception marker (end boundary)', () => {
+			model.change( writer => {
+				writer.setSelection( firstParagraph, 7 );
+			} );
+
+			expect( editor.commands.get( 'allowed' ).isEnabled ).to.be.true;
+		} );
+
+		it( 'should be disabled for multi-range selection (collapsed ranges)', () => {
+			model.change( writer => {
+				writer.setSelection( [
+					writer.createRange(
+						writer.createPositionAt( firstParagraph, 5 )
+					),
+					writer.createRange(
+						writer.createPositionAt( firstParagraph, 9 )
+					)
+				] );
+			} );
+
+			expect( editor.commands.get( 'allowed' ).isEnabled ).to.be.false;
+		} );
+
+		it( 'should be disabled for non-collapsed selection that expands over exception marker', () => {
+			model.change( writer => {
+				writer.setSelection( writer.createRange(
+					writer.createPositionAt( firstParagraph, 0 ),
+					writer.createPositionAt( firstParagraph, 5 )
+				) );
+			} );
+
+			expect( editor.commands.get( 'allowed' ).isEnabled ).to.be.false;
+		} );
+
+		it( 'should be enabled for non-collapsed selection that is fully contained inside exception marker', () => {
+			model.change( writer => {
+				writer.setSelection( writer.createRange(
+					writer.createPositionAt( firstParagraph, 5 ),
+					writer.createPositionAt( firstParagraph, 6 )
+				) );
+			} );
+
+			expect( editor.commands.get( 'allowed' ).isEnabled ).to.be.true;
+		} );
+
+		it( 'should be enabled for non-collapsed selection inside exception marker (start position on marker boundary)', () => {
+			model.change( writer => {
+				writer.setSelection( writer.createRange(
+					writer.createPositionAt( firstParagraph, 4 ),
+					writer.createPositionAt( firstParagraph, 6 )
+				) );
+			} );
+
+			expect( editor.commands.get( 'allowed' ).isEnabled ).to.be.true;
+		} );
+
+		it( 'should be enabled for non-collapsed selection inside exception marker (end position on marker boundary)', () => {
+			model.change( writer => {
+				writer.setSelection( writer.createRange(
+					writer.createPositionAt( firstParagraph, 5 ),
+					writer.createPositionAt( firstParagraph, 7 )
+				) );
+			} );
+
+			expect( editor.commands.get( 'allowed' ).isEnabled ).to.be.true;
+		} );
+
+		it( 'should be enabled for non-collapsed selection is equal to exception marker', () => {
+			model.change( writer => {
+				writer.setSelection( writer.createRange(
+					writer.createPositionAt( firstParagraph, 4 ),
+					writer.createPositionAt( firstParagraph, 7 )
+				) );
+			} );
+
+			expect( editor.commands.get( 'allowed' ).isEnabled ).to.be.true;
+		} );
+
+		it( 'should be disabled for non-collapsed selection with more then one range', () => {
+			model.change( writer => {
+				writer.setSelection( [
+					writer.createRange(
+						writer.createPositionAt( firstParagraph, 5 ),
+						writer.createPositionAt( firstParagraph, 6 )
+					),
+					writer.createRange(
+						writer.createPositionAt( firstParagraph, 8 ),
+						writer.createPositionAt( firstParagraph, 9 )
+					)
+				] );
+			} );
+
+			expect( editor.commands.get( 'allowed' ).isEnabled ).to.be.false;
+		} );
+	} );
+
+	describe( 'integration', () => {
+		let model, firstParagraph;
+
+		beforeEach( async () => {
+			editor = await VirtualTestEditor.create( { plugins: [ Paragraph, Typing, UndoEditing, RestrictedEditingModeEditing ] } );
+			model = editor.model;
+
+			setModelData( model, '<paragraph>[]foo bar baz</paragraph>' );
+			firstParagraph = model.document.getRoot().getChild( 0 );
+
+			model.change( writer => {
+				writer.addMarker( 'restrictedEditingException:1', {
+					range: writer.createRange(
+						writer.createPositionAt( firstParagraph, 4 ),
+						writer.createPositionAt( firstParagraph, 7 ) ),
+					usingOperation: true,
+					affectsData: true
+				} );
+			} );
+		} );
+
+		afterEach( async () => {
+			await editor.destroy();
+		} );
+
+		describe( 'delete + undo', () => {
+			it( 'should be enabled after data change (no selection change event on undo)', () => {
+				model.change( writer => {
+					writer.setSelection( firstParagraph, 7 );
+				} );
+
+				editor.execute( 'delete' );
+				editor.execute( 'undo' );
+
+				expect( editor.commands.get( 'delete' ).isEnabled ).to.be.true;
+			} );
+		} );
+	} );
+
+	class FakeCommand extends Command {
+		refresh() {
+			this.isEnabled = true;
+		}
+	}
+
+	function buildFakeCommand( editor ) {
+		return new FakeCommand( editor );
+	}
+} );

+ 375 - 56
packages/ckeditor5-restricted-editing/tests/restrictededitingediting.js

@@ -3,53 +3,52 @@
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
-/* global document */
-
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
-import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
 
-import RestrictedEditingEditing from './../src/restrictededitingediting';
-import RestrictedEditingNavigationCommand from '../src/restrictededitingnavigationcommand';
-import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 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 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 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';
 
-describe( 'RestrictedEditingEditing', () => {
-	let editor, element;
+describe( 'RestrictedEditingModeEditing', () => {
+	let editor;
 
 	testUtils.createSinonSandbox();
 
 	describe( 'plugin', () => {
 		beforeEach( async () => {
-			element = document.createElement( 'div' );
-			document.body.appendChild( element );
-
-			editor = await ClassicTestEditor.create( element, { plugins: [ RestrictedEditingEditing ] } );
+			editor = await VirtualTestEditor.create( { plugins: [ RestrictedEditingModeEditing ] } );
 		} );
 
-		afterEach( () => {
-			element.remove();
-
-			return editor.destroy();
+		afterEach( async () => {
+			await editor.destroy();
 		} );
 
 		it( 'should be named', () => {
-			expect( RestrictedEditingEditing.pluginName ).to.equal( 'RestrictedEditingEditing' );
+			expect( RestrictedEditingModeEditing.pluginName ).to.equal( 'RestrictedEditingModeEditing' );
 		} );
 
 		it( 'should be loaded', () => {
-			expect( editor.plugins.get( RestrictedEditingEditing ) ).to.be.instanceOf( RestrictedEditingEditing );
+			expect( editor.plugins.get( RestrictedEditingModeEditing ) ).to.be.instanceOf( RestrictedEditingModeEditing );
 		} );
 
-		it( 'adds a "goToPreviousRestrictedEditingRegion" command', () => {
-			expect( editor.commands.get( 'goToPreviousRestrictedEditingRegion' ) ).to.be.instanceOf( RestrictedEditingNavigationCommand );
+		it( 'adds a "goToPreviousRestrictedEditingException" command', () => {
+			expect( editor.commands.get( 'goToPreviousRestrictedEditingException' ) )
+				.to.be.instanceOf( RestrictedEditingModeNavigationCommand );
 		} );
 
-		it( 'adds a "goToNextRestrictedEditingRegion" command', () => {
-			expect( editor.commands.get( 'goToNextRestrictedEditingRegion' ) ).to.be.instanceOf( RestrictedEditingNavigationCommand );
+		it( 'adds a "goToNextRestrictedEditingException" command', () => {
+			expect(
+				editor.commands.get( 'goToNextRestrictedEditingException' )
+			).to.be.instanceOf( RestrictedEditingModeNavigationCommand );
 		} );
 	} );
 
@@ -57,7 +56,7 @@ describe( 'RestrictedEditingEditing', () => {
 		let model;
 
 		beforeEach( async () => {
-			editor = await VirtualTestEditor.create( { plugins: [ Paragraph, RestrictedEditingEditing ] } );
+			editor = await VirtualTestEditor.create( { plugins: [ Paragraph, RestrictedEditingModeEditing ] } );
 			model = editor.model;
 		} );
 
@@ -69,9 +68,9 @@ describe( 'RestrictedEditingEditing', () => {
 			it( 'should convert <span class="ck-restricted-editing-exception"> to marker', () => {
 				editor.setData( '<p>foo <span class="ck-restricted-editing-exception">bar</span> baz</p>' );
 
-				expect( model.markers.has( 'restricted-editing-exception:1' ) ).to.be.true;
+				expect( model.markers.has( 'restrictedEditingException:1' ) ).to.be.true;
 
-				const marker = model.markers.get( 'restricted-editing-exception:1' );
+				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 ] );
@@ -83,11 +82,11 @@ describe( 'RestrictedEditingEditing', () => {
 					'<p>ABCDEF<span class="ck-restricted-editing-exception">GHIJK</span>LMNOPQRST</p>'
 				);
 
-				expect( model.markers.has( 'restricted-editing-exception:1' ) ).to.be.true;
-				expect( model.markers.has( 'restricted-editing-exception:2' ) ).to.be.true;
+				expect( model.markers.has( 'restrictedEditingException:1' ) ).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.
-				const secondMarker = model.markers.get( 'restricted-editing-exception:2' );
+				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 ] );
@@ -96,7 +95,7 @@ describe( 'RestrictedEditingEditing', () => {
 			it( 'should not convert other <span> elements', () => {
 				editor.setData( '<p>foo <span class="foo bar">bar</span> baz</p>' );
 
-				expect( model.markers.has( 'restricted-editing-exception:1' ) ).to.be.false;
+				expect( model.markers.has( 'restrictedEditingException:1' ) ).to.be.false;
 			} );
 		} );
 
@@ -107,7 +106,7 @@ describe( 'RestrictedEditingEditing', () => {
 				const paragraph = model.document.getRoot().getChild( 0 );
 
 				model.change( writer => {
-					writer.addMarker( 'restricted-editing-exception:1', {
+					writer.addMarker( 'restrictedEditingException:1', {
 						range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
 						usingOperation: true,
 						affectsData: true
@@ -119,6 +118,25 @@ describe( 'RestrictedEditingEditing', () => {
 				expect( getViewData( editor.editing.view, { withoutSelection: true } ) ).to.equal( expectedView );
 			} );
 
+			it( 'should convert collapsed model marker to <span>', () => {
+				setModelData( model, '<paragraph>foo bar baz</paragraph>' );
+
+				const paragraph = model.document.getRoot().getChild( 0 );
+
+				model.change( writer => {
+					writer.addMarker( 'restrictedEditingException:1', {
+						range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 4 ) ),
+						usingOperation: true,
+						affectsData: true
+					} );
+				} );
+
+				expect( editor.getData() ).to.equal( '<p>foo <span class="ck-restricted-editing-exception"></span>bar baz</p>' );
+				expect( getViewData( editor.editing.view, { withoutSelection: true } ) ).to.equal(
+					'<p>foo <span class="ck-restricted-editing-exception ck-restricted-editing-exception_collapsed"></span>bar baz</p>'
+				);
+			} );
+
 			it( 'converted <span> should be the outermost attribute element', () => {
 				editor.conversion.for( 'downcast' ).attributeToElement( { model: 'bold', view: 'b' } );
 				setModelData( model, '<paragraph><$text bold="true">foo bar baz</$text></paragraph>' );
@@ -126,16 +144,21 @@ describe( 'RestrictedEditingEditing', () => {
 				const paragraph = model.document.getRoot().getChild( 0 );
 
 				model.change( writer => {
-					writer.addMarker( 'restricted-editing-exception:1', {
+					writer.addMarker( 'restrictedEditingException:1', {
 						range: writer.createRange( writer.createPositionAt( paragraph, 0 ), writer.createPositionAt( paragraph, 'end' ) ),
 						usingOperation: true,
 						affectsData: true
 					} );
 				} );
 
-				const expectedView = '<p><span class="ck-restricted-editing-exception"><b>foo bar baz</b></span></p>';
-				expect( editor.getData() ).to.equal( expectedView );
-				expect( getViewData( editor.editing.view, { withoutSelection: true } ) ).to.equal( expectedView );
+				expect( editor.getData() ).to.equal(
+					'<p><span class="ck-restricted-editing-exception"><b>foo bar baz</b></span></p>'
+				);
+				expect( getViewData( editor.editing.view, { withoutSelection: true } ) ).to.equal(
+					'<p>' +
+						'<span class="ck-restricted-editing-exception ck-restricted-editing-exception_selected"><b>foo bar baz</b></span>' +
+					'</p>'
+				);
 			} );
 		} );
 	} );
@@ -144,7 +167,7 @@ describe( 'RestrictedEditingEditing', () => {
 		let model;
 
 		beforeEach( async () => {
-			editor = await VirtualTestEditor.create( { plugins: [ Paragraph, RestrictedEditingEditing ] } );
+			editor = await VirtualTestEditor.create( { plugins: [ Paragraph, Typing, RestrictedEditingModeEditing ] } );
 			model = editor.model;
 		} );
 
@@ -162,12 +185,12 @@ describe( 'RestrictedEditingEditing', () => {
 			const secondParagraph = model.document.getRoot().getChild( 1 );
 
 			model.change( writer => {
-				writer.addMarker( 'restricted-editing-exception:1', {
+				writer.addMarker( 'restrictedEditingException:1', {
 					range: writer.createRange( writer.createPositionAt( firstParagraph, 4 ), writer.createPositionAt( firstParagraph, 7 ) ),
 					usingOperation: true,
 					affectsData: true
 				} );
-				writer.addMarker( 'restricted-editing-exception:2', {
+				writer.addMarker( 'restrictedEditingException:2', {
 					range: writer.createRange(
 						writer.createPositionAt( secondParagraph, 4 ),
 						writer.createPositionAt( secondParagraph, 7 )
@@ -189,6 +212,302 @@ describe( 'RestrictedEditingEditing', () => {
 				'<p>foo <span class="ck-restricted-editing-exception">bar</span> baz</p>' +
 				'<p>xxx <span class="ck-restricted-editing-exception ck-restricted-editing-exception_selected">yRyy</span> zzz</p>' );
 		} );
+
+		it( 'should block user typing outside exception markers', () => {
+			setModelData( model, '<paragraph>foo []bar baz</paragraph>' );
+
+			editor.execute( 'input', { text: 'X' } );
+
+			assertEqualMarkup( getModelData( model ), '<paragraph>foo []bar baz</paragraph>' );
+		} );
+
+		it( 'should not block user typing inside exception marker', () => {
+			setModelData( model, '<paragraph>[]foo bar baz</paragraph>' );
+			const firstParagraph = model.document.getRoot().getChild( 0 );
+
+			model.change( writer => {
+				writer.addMarker( 'restrictedEditingException:1', {
+					range: writer.createRange( writer.createPositionAt( firstParagraph, 4 ), writer.createPositionAt( firstParagraph, 7 ) ),
+					usingOperation: true,
+					affectsData: true
+				} );
+			} );
+
+			model.change( writer => {
+				writer.setSelection( firstParagraph, 5 );
+			} );
+			editor.execute( 'input', { text: 'X' } );
+
+			assertEqualMarkup( getModelData( model ), '<paragraph>foo bX[]ar baz</paragraph>' );
+		} );
+
+		it( 'should extend maker when typing on the marker boundary (end)', () => {
+			setModelData( model, '<paragraph>foo bar[] baz</paragraph>' );
+			const firstParagraph = model.document.getRoot().getChild( 0 );
+
+			model.change( writer => {
+				writer.addMarker( 'restrictedEditingException:1', {
+					range: writer.createRange( writer.createPositionAt( firstParagraph, 4 ), writer.createPositionAt( firstParagraph, 7 ) ),
+					usingOperation: true,
+					affectsData: true
+				} );
+			} );
+
+			editor.execute( 'input', { text: 'X' } );
+
+			assertEqualMarkup( getModelData( model ), '<paragraph>foo barX[] baz</paragraph>' );
+			const markerRange = editor.model.markers.get( 'restrictedEditingException:1' ).getRange();
+			const expectedRange = model.createRange(
+				model.createPositionAt( firstParagraph, 4 ),
+				model.createPositionAt( firstParagraph, 8 )
+			);
+
+			expect( markerRange.isEqual( expectedRange ) ).to.be.true;
+		} );
+
+		it( 'should extend marker when typing on the marker boundary (start)', () => {
+			setModelData( model, '<paragraph>foo []bar baz</paragraph>' );
+			const firstParagraph = model.document.getRoot().getChild( 0 );
+
+			model.change( writer => {
+				writer.addMarker( 'restrictedEditingException:1', {
+					range: writer.createRange( writer.createPositionAt( firstParagraph, 4 ), writer.createPositionAt( firstParagraph, 7 ) ),
+					usingOperation: true,
+					affectsData: true
+				} );
+			} );
+
+			editor.execute( 'input', { text: 'X' } );
+
+			assertEqualMarkup( getModelData( model ), '<paragraph>foo X[]bar baz</paragraph>' );
+			const markerRange = editor.model.markers.get( 'restrictedEditingException:1' ).getRange();
+
+			const expectedRange = model.createRange(
+				model.createPositionAt( firstParagraph, 4 ),
+				model.createPositionAt( firstParagraph, 8 )
+			);
+
+			expect( markerRange.isEqual( expectedRange ) ).to.be.true;
+		} );
+
+		it( 'should extend marker when typing on the marker boundary (collapsed marker)', () => {
+			setModelData( model, '<paragraph>[]foo bar baz</paragraph>' );
+			const firstParagraph = model.document.getRoot().getChild( 0 );
+
+			model.change( writer => {
+				writer.addMarker( 'restrictedEditingException:1', {
+					range: writer.createRange( writer.createPositionAt( firstParagraph, 4 ) ),
+					usingOperation: true,
+					affectsData: true
+				} );
+			} );
+
+			model.change( writer => {
+				writer.setSelection( writer.createPositionAt( firstParagraph, 4 ) );
+			} );
+
+			editor.execute( 'input', { text: 'X' } );
+
+			assertEqualMarkup( getModelData( model ), '<paragraph>foo X[]bar baz</paragraph>' );
+			const markerRange = editor.model.markers.get( 'restrictedEditingException:1' ).getRange();
+
+			const expectedRange = model.createRange(
+				model.createPositionAt( firstParagraph, 4 ),
+				model.createPositionAt( firstParagraph, 5 )
+			);
+
+			expect( markerRange.isEqual( expectedRange ) ).to.be.true;
+		} );
+
+		it( 'should not move collapsed marker to $graveyard', () => {
+			setModelData( model, '<paragraph>foo b[]ar baz</paragraph>' );
+			const firstParagraph = model.document.getRoot().getChild( 0 );
+
+			model.change( writer => {
+				writer.addMarker( 'restrictedEditingException:1', {
+					range: writer.createRange(
+						writer.createPositionAt( firstParagraph, 4 ),
+						writer.createPositionAt( firstParagraph, 5 )
+					),
+					usingOperation: true,
+					affectsData: true
+				} );
+			} );
+
+			editor.execute( 'delete' );
+
+			assertEqualMarkup( getModelData( model ), '<paragraph>foo []ar baz</paragraph>' );
+			const markerRange = editor.model.markers.get( 'restrictedEditingException:1' ).getRange();
+
+			const expectedRange = model.createRange(
+				model.createPositionAt( firstParagraph, 4 ),
+				model.createPositionAt( firstParagraph, 4 )
+			);
+
+			expect( markerRange.isEqual( expectedRange ) ).to.be.true;
+		} );
+	} );
+
+	describe( 'clipboard', () => {
+		let model, viewDoc;
+
+		beforeEach( async () => {
+			editor = await VirtualTestEditor.create( { plugins: [ Paragraph, Typing, Clipboard, RestrictedEditingModeEditing ] } );
+			model = editor.model;
+			viewDoc = editor.editing.view.document;
+		} );
+
+		afterEach( () => {
+			return editor.destroy();
+		} );
+
+		describe( 'cut', () => {
+			it( 'should be blocked outside exception markers', () => {
+				setModelData( model, '<paragraph>foo []bar baz</paragraph>' );
+				const spy = sinon.spy();
+				viewDoc.on( 'clipboardOutput', spy, { priority: 'high' } );
+
+				viewDoc.fire( 'clipboardOutput', {
+					content: {
+						isEmpty: true
+					},
+					method: 'cut'
+				} );
+
+				sinon.assert.notCalled( spy );
+				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 );
+				const spy = sinon.spy();
+				viewDoc.on( 'clipboardOutput', 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
+					} );
+				} );
+
+				model.change( writer => {
+					writer.setSelection( firstParagraph, 5 );
+				} );
+
+				viewDoc.fire( 'clipboardOutput', {
+					content: {
+						isEmpty: true
+					},
+					method: 'cut'
+				} );
+
+				sinon.assert.notCalled( spy );
+				assertEqualMarkup( getModelData( model ), '<paragraph>foo b[]ar baz</paragraph>' );
+			} );
+		} );
+
+		describe( 'copy', () => {
+			it( 'should not be blocked outside exception markers', () => {
+				setModelData( model, '<paragraph>foo []bar baz</paragraph>' );
+				const spy = sinon.spy();
+				viewDoc.on( 'clipboardOutput', spy, { priority: 'high' } );
+
+				viewDoc.fire( 'clipboardOutput', {
+					content: {
+						isEmpty: true
+					},
+					method: 'copy'
+				} );
+
+				sinon.assert.calledOnce( spy );
+				assertEqualMarkup( getModelData( model ), '<paragraph>foo []bar baz</paragraph>' );
+			} );
+
+			it( 'should not be blocked inside exception marker', () => {
+				setModelData( model, '<paragraph>[]foo bar baz</paragraph>' );
+				const firstParagraph = model.document.getRoot().getChild( 0 );
+				const spy = sinon.spy();
+				viewDoc.on( 'clipboardOutput', 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
+					} );
+				} );
+
+				model.change( writer => {
+					writer.setSelection( firstParagraph, 5 );
+				} );
+
+				viewDoc.fire( 'clipboardOutput', {
+					content: {
+						isEmpty: true
+					},
+					method: 'copy'
+				} );
+
+				sinon.assert.calledOnce( spy );
+				assertEqualMarkup( getModelData( model ), '<paragraph>foo b[]ar baz</paragraph>' );
+			} );
+		} );
+
+		describe( 'paste', () => {
+			it( 'should be blocked outside exception markers', () => {
+				setModelData( model, '<paragraph>foo []bar 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 []bar baz</paragraph>' );
+			} );
+
+			it( 'should be blocked inside exception marker', () => {
+				setModelData( model, '<paragraph>[]foo bar baz</paragraph>' );
+				const firstParagraph = model.document.getRoot().getChild( 0 );
+				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
+					} );
+				} );
+
+				model.change( writer => {
+					writer.setSelection( firstParagraph, 5 );
+				} );
+
+				viewDoc.fire( 'clipboardInput', {
+					dataTransfer: {
+						getData: sinon.spy()
+					}
+				} );
+
+				sinon.assert.notCalled( spy );
+				assertEqualMarkup( getModelData( model ), '<paragraph>foo b[]ar baz</paragraph>' );
+			} );
+		} );
 	} );
 
 	describe( 'exception highlighting', () => {
@@ -196,7 +515,7 @@ describe( 'RestrictedEditingEditing', () => {
 
 		beforeEach( async () => {
 			editor = await VirtualTestEditor.create( {
-				plugins: [ Paragraph, RestrictedEditingEditing, BoldEditing ]
+				plugins: [ Paragraph, RestrictedEditingModeEditing, BoldEditing ]
 			} );
 			model = editor.model;
 			view = editor.editing.view;
@@ -213,7 +532,7 @@ describe( 'RestrictedEditingEditing', () => {
 
 			// <paragraph>foo <$marker>b[a]r</$marker> baz</paragraph>
 			model.change( writer => {
-				writer.addMarker( 'restricted-editing-exception:1', {
+				writer.addMarker( 'restrictedEditingException:1', {
 					range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
 					usingOperation: true,
 					affectsData: true
@@ -232,7 +551,7 @@ describe( 'RestrictedEditingEditing', () => {
 
 			// <paragraph>foo <$marker>b[a]r</$marker> baz</paragraph>
 			model.change( writer => {
-				writer.addMarker( 'restricted-editing-exception:1', {
+				writer.addMarker( 'restrictedEditingException:1', {
 					range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
 					usingOperation: true,
 					affectsData: true
@@ -257,7 +576,7 @@ describe( 'RestrictedEditingEditing', () => {
 
 			// <paragraph>[]foo <$marker>bar</$marker> baz</paragraph>
 			model.change( writer => {
-				writer.addMarker( 'restricted-editing-exception:1', {
+				writer.addMarker( 'restrictedEditingException:1', {
 					range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
 					usingOperation: true,
 					affectsData: true
@@ -283,7 +602,7 @@ describe( 'RestrictedEditingEditing', () => {
 
 				// <paragraph>foo <$marker>b[a]r</$marker> baz</paragraph>
 				model.change( writer => {
-					writer.addMarker( 'restricted-editing-exception:1', {
+					writer.addMarker( 'restrictedEditingException:1', {
 						range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
 						usingOperation: true,
 						affectsData: true
@@ -306,7 +625,7 @@ describe( 'RestrictedEditingEditing', () => {
 
 				// <paragraph>foo <$marker>b[a]r</$marker> baz</paragraph>
 				model.change( writer => {
-					writer.addMarker( 'restricted-editing-exception:1', {
+					writer.addMarker( 'restrictedEditingException:1', {
 						range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
 						usingOperation: true,
 						affectsData: true
@@ -332,7 +651,7 @@ describe( 'RestrictedEditingEditing', () => {
 
 				// <paragraph>foo <$marker>b[a]r</$marker> baz</paragraph>
 				model.change( writer => {
-					writer.addMarker( 'restricted-editing-exception:1', {
+					writer.addMarker( 'restrictedEditingException:1', {
 						range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
 						usingOperation: true,
 						affectsData: true
@@ -362,7 +681,7 @@ describe( 'RestrictedEditingEditing', () => {
 
 				// <paragraph>foo <$marker>b[a]r</$marker> baz</paragraph>
 				model.change( writer => {
-					writer.addMarker( 'restricted-editing-exception:1', {
+					writer.addMarker( 'restrictedEditingException:1', {
 						range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
 						usingOperation: true,
 						affectsData: true
@@ -377,7 +696,7 @@ describe( 'RestrictedEditingEditing', () => {
 				} );
 
 				expect( getViewData( view ) ).to.equal(
-					'<p>foo {<span class="ck-restricted-editing-exception">ba}r</span> baz</p>'
+					'<p>foo {<span class="ck-restricted-editing-exception ck-restricted-editing-exception_selected">ba}r</span> baz</p>'
 				);
 			} );
 
@@ -390,7 +709,7 @@ describe( 'RestrictedEditingEditing', () => {
 
 				// <paragraph>foo <$marker>b[a]r</$marker> baz</paragraph>
 				model.change( writer => {
-					writer.addMarker( 'restricted-editing-exception:1', {
+					writer.addMarker( 'restrictedEditingException:1', {
 						range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
 						usingOperation: true,
 						affectsData: true
@@ -429,7 +748,7 @@ describe( 'RestrictedEditingEditing', () => {
 
 		beforeEach( async () => {
 			editor = await VirtualTestEditor.create( {
-				plugins: [ Paragraph, RestrictedEditingEditing, BoldEditing ]
+				plugins: [ Paragraph, RestrictedEditingModeEditing, BoldEditing ]
 			} );
 
 			model = editor.model;
@@ -455,7 +774,7 @@ describe( 'RestrictedEditingEditing', () => {
 
 			// <paragraph>[]foo <marker>bar</marker> baz qux</paragraph>
 			model.change( writer => {
-				writer.addMarker( 'restricted-editing-exception:1', {
+				writer.addMarker( 'restrictedEditingException:1', {
 					range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
 					usingOperation: true,
 					affectsData: true
@@ -464,7 +783,7 @@ describe( 'RestrictedEditingEditing', () => {
 
 			// <paragraph>[]foo <marker>bar</marker> <marker>baz</marker≥ qux</paragraph>
 			model.change( writer => {
-				writer.addMarker( 'restricted-editing-exception:2', {
+				writer.addMarker( 'restrictedEditingException:2', {
 					range: writer.createRange( writer.createPositionAt( paragraph, 8 ), writer.createPositionAt( paragraph, 11 ) ),
 					usingOperation: true,
 					affectsData: true
@@ -474,7 +793,7 @@ describe( 'RestrictedEditingEditing', () => {
 			view.document.fire( 'keydown', domEvtDataStub );
 
 			sinon.assert.calledOnce( editor.execute );
-			sinon.assert.calledWithExactly( editor.execute, 'goToNextRestrictedEditingRegion' );
+			sinon.assert.calledWithExactly( editor.execute, 'goToNextRestrictedEditingException' );
 			sinon.assert.calledOnce( domEvtDataStub.preventDefault );
 			sinon.assert.calledOnce( domEvtDataStub.stopPropagation );
 		} );
@@ -486,7 +805,7 @@ describe( 'RestrictedEditingEditing', () => {
 
 			// <paragraph><marker>foo</marker> qux[]</paragraph>
 			model.change( writer => {
-				writer.addMarker( 'restricted-editing-exception:1', {
+				writer.addMarker( 'restrictedEditingException:1', {
 					range: writer.createRange( writer.createPositionAt( paragraph, 0 ), writer.createPositionAt( paragraph, 3 ) ),
 					usingOperation: true,
 					affectsData: true
@@ -507,7 +826,7 @@ describe( 'RestrictedEditingEditing', () => {
 
 			// <paragraph>foo <marker>bar</marker> baz qux[]</paragraph>
 			model.change( writer => {
-				writer.addMarker( 'restricted-editing-exception:1', {
+				writer.addMarker( 'restrictedEditingException:1', {
 					range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
 					usingOperation: true,
 					affectsData: true
@@ -516,7 +835,7 @@ describe( 'RestrictedEditingEditing', () => {
 
 			// <paragraph>foo <marker>bar</marker> <marker>baz</marker≥ qux[]</paragraph>
 			model.change( writer => {
-				writer.addMarker( 'restricted-editing-exception:2', {
+				writer.addMarker( 'restrictedEditingException:2', {
 					range: writer.createRange( writer.createPositionAt( paragraph, 8 ), writer.createPositionAt( paragraph, 11 ) ),
 					usingOperation: true,
 					affectsData: true
@@ -527,7 +846,7 @@ describe( 'RestrictedEditingEditing', () => {
 			view.document.fire( 'keydown', domEvtDataStub );
 
 			sinon.assert.calledOnce( editor.execute );
-			sinon.assert.calledWithExactly( editor.execute, 'goToPreviousRestrictedEditingRegion' );
+			sinon.assert.calledWithExactly( editor.execute, 'goToPreviousRestrictedEditingException' );
 			sinon.assert.calledOnce( domEvtDataStub.preventDefault );
 			sinon.assert.calledOnce( domEvtDataStub.stopPropagation );
 		} );
@@ -539,7 +858,7 @@ describe( 'RestrictedEditingEditing', () => {
 
 			// <paragraph>[]foo <marker>qux</marker></paragraph>
 			model.change( writer => {
-				writer.addMarker( 'restricted-editing-exception:1', {
+				writer.addMarker( 'restrictedEditingException:1', {
 					range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
 					usingOperation: true,
 					affectsData: true

+ 40 - 40
packages/ckeditor5-restricted-editing/tests/restrictededitingnavigationcommand.js

@@ -6,9 +6,9 @@
 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';
+import RestrictedEditingModeNavigationCommand from '../src/restrictededitingmodenavigationcommand';
 
-describe( 'RestrictedEditingNavigationCommand', () => {
+describe( 'RestrictedEditingModeNavigationCommand', () => {
 	let editor, forwardCommand, backwardCommand, model;
 
 	beforeEach( () => {
@@ -18,8 +18,8 @@ describe( 'RestrictedEditingNavigationCommand', () => {
 				editor = newEditor;
 				model = editor.model;
 
-				forwardCommand = new RestrictedEditingNavigationCommand( editor, 'forward' );
-				backwardCommand = new RestrictedEditingNavigationCommand( editor, 'backward' );
+				forwardCommand = new RestrictedEditingModeNavigationCommand( editor, 'forward' );
+				backwardCommand = new RestrictedEditingModeNavigationCommand( editor, 'backward' );
 
 				model.schema.register( 'paragraph', { inheritAllFrom: '$block' } );
 				editor.model.schema.extend( '$text', { allowAttributes: [ 'restrictedEditingException' ] } );
@@ -43,7 +43,7 @@ describe( 'RestrictedEditingNavigationCommand', () => {
 
 					// <paragraph>[]foo <marker>bar</marker> baz</paragraph>
 					model.change( writer => {
-						writer.addMarker( 'restricted-editing-exception:1', {
+						writer.addMarker( 'restrictedEditingException:1', {
 							range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
 							usingOperation: true,
 							affectsData: true
@@ -60,7 +60,7 @@ describe( 'RestrictedEditingNavigationCommand', () => {
 
 					// <paragraph>foo <marker>bar</marker> baz[]</paragraph>
 					model.change( writer => {
-						writer.addMarker( 'restricted-editing-exception:1', {
+						writer.addMarker( 'restrictedEditingException:1', {
 							range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
 							usingOperation: true,
 							affectsData: true
@@ -77,7 +77,7 @@ describe( 'RestrictedEditingNavigationCommand', () => {
 
 					// <paragraph>foo []<marker>bar</marker> baz</paragraph>
 					model.change( writer => {
-						writer.addMarker( 'restricted-editing-exception:1', {
+						writer.addMarker( 'restrictedEditingException:1', {
 							range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
 							usingOperation: true,
 							affectsData: true
@@ -94,7 +94,7 @@ describe( 'RestrictedEditingNavigationCommand', () => {
 
 					// <paragraph>foo <marker>b[]ar</marker> baz</paragraph>
 					model.change( writer => {
-						writer.addMarker( 'restricted-editing-exception:1', {
+						writer.addMarker( 'restrictedEditingException:1', {
 							range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
 							usingOperation: true,
 							affectsData: true
@@ -111,7 +111,7 @@ describe( 'RestrictedEditingNavigationCommand', () => {
 
 					// <paragraph>foo <marker>bar</marker>[] baz</paragraph>
 					model.change( writer => {
-						writer.addMarker( 'restricted-editing-exception:1', {
+						writer.addMarker( 'restrictedEditingException:1', {
 							range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
 							usingOperation: true,
 							affectsData: true
@@ -130,7 +130,7 @@ describe( 'RestrictedEditingNavigationCommand', () => {
 
 					// <paragraph>[fo]o <marker>bar</marker> baz</paragraph>
 					model.change( writer => {
-						writer.addMarker( 'restricted-editing-exception:1', {
+						writer.addMarker( 'restrictedEditingException:1', {
 							range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
 							usingOperation: true,
 							affectsData: true
@@ -147,7 +147,7 @@ describe( 'RestrictedEditingNavigationCommand', () => {
 
 					// <paragraph>[foo <marker>ba]r</marker> baz</paragraph>
 					model.change( writer => {
-						writer.addMarker( 'restricted-editing-exception:1', {
+						writer.addMarker( 'restrictedEditingException:1', {
 							range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
 							usingOperation: true,
 							affectsData: true
@@ -164,7 +164,7 @@ describe( 'RestrictedEditingNavigationCommand', () => {
 
 					// <paragraph>foo <marker>ba[r</marker> baz]</paragraph>
 					model.change( writer => {
-						writer.addMarker( 'restricted-editing-exception:1', {
+						writer.addMarker( 'restrictedEditingException:1', {
 							range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
 							usingOperation: true,
 							affectsData: true
@@ -185,7 +185,7 @@ describe( 'RestrictedEditingNavigationCommand', () => {
 
 					// <paragraph>[]foo <marker>bar</marker> baz</paragraph>
 					model.change( writer => {
-						writer.addMarker( 'restricted-editing-exception:1', {
+						writer.addMarker( 'restrictedEditingException:1', {
 							range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
 							usingOperation: true,
 							affectsData: true
@@ -194,7 +194,7 @@ describe( 'RestrictedEditingNavigationCommand', () => {
 
 					// <paragraph>[]foo <marker>bar</marker> <marker>baz</marker></paragraph>
 					model.change( writer => {
-						writer.addMarker( 'restricted-editing-exception:2', {
+						writer.addMarker( 'restrictedEditingException:2', {
 							range: writer.createRange( writer.createPositionAt( paragraph, 8 ), writer.createPositionAt( paragraph, 11 ) ),
 							usingOperation: true,
 							affectsData: true
@@ -216,7 +216,7 @@ describe( 'RestrictedEditingNavigationCommand', () => {
 
 					// <paragraph><marker>foo</marker>[]</paragraph><paragraph>bar</paragraph>
 					model.change( writer => {
-						writer.addMarker( 'restricted-editing-exception:1', {
+						writer.addMarker( 'restrictedEditingException:1', {
 							range: writer.createRangeIn( fiirstParagraph ),
 							usingOperation: true,
 							affectsData: true
@@ -225,7 +225,7 @@ describe( 'RestrictedEditingNavigationCommand', () => {
 
 					// <paragraph><marker>foo</marker>[]</paragraph><paragraph><marker>bar</marker></paragraph>
 					model.change( writer => {
-						writer.addMarker( 'restricted-editing-exception:2', {
+						writer.addMarker( 'restrictedEditingException:2', {
 							range: writer.createRangeIn( secondParagraph ),
 							usingOperation: true,
 							affectsData: true
@@ -243,7 +243,7 @@ describe( 'RestrictedEditingNavigationCommand', () => {
 
 					// <paragraph>[]foo bar <marker>baz</marker></paragraph>
 					model.change( writer => {
-						writer.addMarker( 'restricted-editing-exception:2', {
+						writer.addMarker( 'restrictedEditingException:2', {
 							range: writer.createRange( writer.createPositionAt( paragraph, 8 ), writer.createPositionAt( paragraph, 11 ) ),
 							usingOperation: true,
 							affectsData: true
@@ -252,7 +252,7 @@ describe( 'RestrictedEditingNavigationCommand', () => {
 
 					// <paragraph>[]foo <marker>bar</marker> <marker>baz</marker></paragraph>
 					model.change( writer => {
-						writer.addMarker( 'restricted-editing-exception:1', {
+						writer.addMarker( 'restrictedEditingException:1', {
 							range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
 							usingOperation: true,
 							affectsData: true
@@ -275,7 +275,7 @@ describe( 'RestrictedEditingNavigationCommand', () => {
 
 					// <paragraph>[foo <marker>b]ar</marker> baz</paragraph>
 					model.change( writer => {
-						writer.addMarker( 'restricted-editing-exception:1', {
+						writer.addMarker( 'restrictedEditingException:1', {
 							range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
 							usingOperation: true,
 							affectsData: true
@@ -284,7 +284,7 @@ describe( 'RestrictedEditingNavigationCommand', () => {
 
 					// <paragraph>[foo <marker>b]ar</marker> <marker>baz</marker></paragraph>
 					model.change( writer => {
-						writer.addMarker( 'restricted-editing-exception:2', {
+						writer.addMarker( 'restrictedEditingException:2', {
 							range: writer.createRange( writer.createPositionAt( paragraph, 8 ), writer.createPositionAt( paragraph, 11 ) ),
 							usingOperation: true,
 							affectsData: true
@@ -305,7 +305,7 @@ describe( 'RestrictedEditingNavigationCommand', () => {
 
 					// <paragraph>foo <marker>b[ar</marker> b]az</paragraph>
 					model.change( writer => {
-						writer.addMarker( 'restricted-editing-exception:1', {
+						writer.addMarker( 'restrictedEditingException:1', {
 							range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
 							usingOperation: true,
 							affectsData: true
@@ -314,7 +314,7 @@ describe( 'RestrictedEditingNavigationCommand', () => {
 
 					// <paragraph>foo <marker>b[ar</marker> <marker>b]az</marker></paragraph>
 					model.change( writer => {
-						writer.addMarker( 'restricted-editing-exception:2', {
+						writer.addMarker( 'restrictedEditingException:2', {
 							range: writer.createRange( writer.createPositionAt( paragraph, 8 ), writer.createPositionAt( paragraph, 11 ) ),
 							usingOperation: true,
 							affectsData: true
@@ -338,7 +338,7 @@ describe( 'RestrictedEditingNavigationCommand', () => {
 
 					// <paragraph>foo <marker>bar</marker> baz</paragraph>
 					model.change( writer => {
-						writer.addMarker( 'restricted-editing-exception:1', {
+						writer.addMarker( 'restrictedEditingException:1', {
 							range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
 							usingOperation: true,
 							affectsData: true
@@ -355,7 +355,7 @@ describe( 'RestrictedEditingNavigationCommand', () => {
 
 					// <paragraph>[]foo <marker>bar</marker> baz</paragraph>
 					model.change( writer => {
-						writer.addMarker( 'restricted-editing-exception:1', {
+						writer.addMarker( 'restrictedEditingException:1', {
 							range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
 							usingOperation: true,
 							affectsData: true
@@ -372,7 +372,7 @@ describe( 'RestrictedEditingNavigationCommand', () => {
 
 					// <paragraph>foo <marker>bar</marker>[] baz</paragraph>
 					model.change( writer => {
-						writer.addMarker( 'restricted-editing-exception:1', {
+						writer.addMarker( 'restrictedEditingException:1', {
 							range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
 							usingOperation: true,
 							affectsData: true
@@ -389,7 +389,7 @@ describe( 'RestrictedEditingNavigationCommand', () => {
 
 					// <paragraph>foo <marker>b[]ar</marker> baz</paragraph>
 					model.change( writer => {
-						writer.addMarker( 'restricted-editing-exception:1', {
+						writer.addMarker( 'restrictedEditingException:1', {
 							range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
 							usingOperation: true,
 							affectsData: true
@@ -406,7 +406,7 @@ describe( 'RestrictedEditingNavigationCommand', () => {
 
 					// <paragraph>foo []<marker>bar</marker> baz</paragraph>
 					model.change( writer => {
-						writer.addMarker( 'restricted-editing-exception:1', {
+						writer.addMarker( 'restrictedEditingException:1', {
 							range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
 							usingOperation: true,
 							affectsData: true
@@ -425,7 +425,7 @@ describe( 'RestrictedEditingNavigationCommand', () => {
 
 					// <paragraph>[fo]o <marker>bar</marker> b[az]</paragraph>
 					model.change( writer => {
-						writer.addMarker( 'restricted-editing-exception:1', {
+						writer.addMarker( 'restrictedEditingException:1', {
 							range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
 							usingOperation: true,
 							affectsData: true
@@ -442,7 +442,7 @@ describe( 'RestrictedEditingNavigationCommand', () => {
 
 					// <paragraph>foo <marker>b[ar</marker> baz]</paragraph>
 					model.change( writer => {
-						writer.addMarker( 'restricted-editing-exception:1', {
+						writer.addMarker( 'restrictedEditingException:1', {
 							range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
 							usingOperation: true,
 							affectsData: true
@@ -459,7 +459,7 @@ describe( 'RestrictedEditingNavigationCommand', () => {
 
 					// <paragraph>[foo <marker>b]ar</marker> baz</paragraph>
 					model.change( writer => {
-						writer.addMarker( 'restricted-editing-exception:1', {
+						writer.addMarker( 'restrictedEditingException:1', {
 							range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
 							usingOperation: true,
 							affectsData: true
@@ -480,7 +480,7 @@ describe( 'RestrictedEditingNavigationCommand', () => {
 
 					// <paragraph>foo <marker>bar</marker> baz[]</paragraph>
 					model.change( writer => {
-						writer.addMarker( 'restricted-editing-exception:1', {
+						writer.addMarker( 'restrictedEditingException:1', {
 							range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
 							usingOperation: true,
 							affectsData: true
@@ -489,7 +489,7 @@ describe( 'RestrictedEditingNavigationCommand', () => {
 
 					// <paragraph>foo <marker>bar</marker> <marker>baz</marker>[]</paragraph>
 					model.change( writer => {
-						writer.addMarker( 'restricted-editing-exception:2', {
+						writer.addMarker( 'restrictedEditingException:2', {
 							range: writer.createRange( writer.createPositionAt( paragraph, 8 ), writer.createPositionAt( paragraph, 11 ) ),
 							usingOperation: true,
 							affectsData: true
@@ -508,7 +508,7 @@ describe( 'RestrictedEditingNavigationCommand', () => {
 
 					// <paragraph><marker>foo</marker></paragraph><paragraph>[]bar</paragraph>
 					model.change( writer => {
-						writer.addMarker( 'restricted-editing-exception:1', {
+						writer.addMarker( 'restrictedEditingException:1', {
 							range: writer.createRangeIn( fiirstParagraph ),
 							usingOperation: true,
 							affectsData: true
@@ -517,7 +517,7 @@ describe( 'RestrictedEditingNavigationCommand', () => {
 
 					// <paragraph><marker>foo</marker></paragraph><paragraph><marker>[]bar</marker></paragraph>
 					model.change( writer => {
-						writer.addMarker( 'restricted-editing-exception:2', {
+						writer.addMarker( 'restrictedEditingException:2', {
 							range: writer.createRangeIn( secondParagraph ),
 							usingOperation: true,
 							affectsData: true
@@ -535,7 +535,7 @@ describe( 'RestrictedEditingNavigationCommand', () => {
 
 					// <paragraph>foo <marker>bar</marker> baz qux[]</paragraph>
 					model.change( writer => {
-						writer.addMarker( 'restricted-editing-exception:1', {
+						writer.addMarker( 'restrictedEditingException:1', {
 							range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
 							usingOperation: true,
 							affectsData: true
@@ -544,7 +544,7 @@ describe( 'RestrictedEditingNavigationCommand', () => {
 
 					// <paragraph>foo <marker>bar</marker> <marker>baz</marker> qux[]</paragraph>
 					model.change( writer => {
-						writer.addMarker( 'restricted-editing-exception:2', {
+						writer.addMarker( 'restrictedEditingException:2', {
 							range: writer.createRange( writer.createPositionAt( paragraph, 8 ), writer.createPositionAt( paragraph, 11 ) ),
 							usingOperation: true,
 							affectsData: true
@@ -565,7 +565,7 @@ describe( 'RestrictedEditingNavigationCommand', () => {
 
 					// <paragraph>foo bar <marker>baz</marker> qux[]</paragraph>
 					model.change( writer => {
-						writer.addMarker( 'restricted-editing-exception:2', {
+						writer.addMarker( 'restrictedEditingException:2', {
 							range: writer.createRange( writer.createPositionAt( paragraph, 8 ), writer.createPositionAt( paragraph, 11 ) ),
 							usingOperation: true,
 							affectsData: true
@@ -574,7 +574,7 @@ describe( 'RestrictedEditingNavigationCommand', () => {
 
 					// <paragraph>foo <marker>bar</marker> <marker>baz</marker> qux[]</paragraph>
 					model.change( writer => {
-						writer.addMarker( 'restricted-editing-exception:1', {
+						writer.addMarker( 'restrictedEditingException:1', {
 							range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
 							usingOperation: true,
 							affectsData: true
@@ -597,7 +597,7 @@ describe( 'RestrictedEditingNavigationCommand', () => {
 
 					// <paragraph>foo <marker>bar</marker> b[az]</paragraph>
 					model.change( writer => {
-						writer.addMarker( 'restricted-editing-exception:1', {
+						writer.addMarker( 'restrictedEditingException:1', {
 							range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
 							usingOperation: true,
 							affectsData: true
@@ -606,7 +606,7 @@ describe( 'RestrictedEditingNavigationCommand', () => {
 
 					// <paragraph>foo <marker>bar</marker> <marker>b[az</marker>]</paragraph>
 					model.change( writer => {
-						writer.addMarker( 'restricted-editing-exception:2', {
+						writer.addMarker( 'restrictedEditingException:2', {
 							range: writer.createRange( writer.createPositionAt( paragraph, 8 ), writer.createPositionAt( paragraph, 11 ) ),
 							usingOperation: true,
 							affectsData: true

+ 15 - 19
packages/ckeditor5-restricted-editing/tests/restrictededitingui.js

@@ -8,44 +8,40 @@
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
 
-import RestrictedEditingEditing from './../src/restrictededitingediting';
-import RestrictedEditingUI from './../src/restrictededitingui';
+import RestrictedEditingModeEditing from './../src/restrictededitingmodeediting';
+import RestrictedEditingModeUI from './../src/restrictededitingmodeui';
 import lockIcon from '../theme/icons/contentlock.svg';
 
-describe( 'RestrictedEditingUI', () => {
+describe( 'RestrictedEditingModeUI', () => {
 	let editor, element, goToPreviousCommand, goToNextCommand;
 
 	testUtils.createSinonSandbox();
 
-	beforeEach( () => {
+	beforeEach( async () => {
 		element = document.createElement( 'div' );
 		document.body.appendChild( element );
 
-		return ClassicTestEditor
-			.create( element, {
-				plugins: [ RestrictedEditingEditing, RestrictedEditingUI ]
-			} )
-			.then( newEditor => {
-				editor = newEditor;
+		editor = await ClassicTestEditor.create( element, {
+			plugins: [ RestrictedEditingModeEditing, RestrictedEditingModeUI ]
+		} );
 
-				goToPreviousCommand = editor.commands.get( 'goToPreviousRestrictedEditingRegion' );
-				goToNextCommand = editor.commands.get( 'goToNextRestrictedEditingRegion' );
-			} );
+		goToPreviousCommand = editor.commands.get( 'goToPreviousRestrictedEditingException' );
+		goToNextCommand = editor.commands.get( 'goToNextRestrictedEditingException' );
 	} );
 
-	afterEach( () => {
+	afterEach( async () => {
 		element.remove();
 
-		return editor.destroy();
+		await editor.destroy();
 	} );
 
 	describe( 'plugin', () => {
 		it( 'should be named', () => {
-			expect( RestrictedEditingUI.pluginName ).to.equal( 'RestrictedEditingUI' );
+			expect( RestrictedEditingModeUI.pluginName ).to.equal( 'RestrictedEditingModeUI' );
 		} );
 
 		it( 'should be loaded', () => {
-			expect( editor.plugins.get( RestrictedEditingUI ) ).to.be.instanceOf( RestrictedEditingUI );
+			expect( editor.plugins.get( RestrictedEditingModeUI ) ).to.be.instanceOf( RestrictedEditingModeUI );
 		} );
 	} );
 
@@ -124,10 +120,10 @@ describe( 'RestrictedEditingUI', () => {
 				const spy = sinon.spy( editor, 'execute' );
 
 				goToPreviousButton.fire( 'execute' );
-				sinon.assert.calledWith( spy.firstCall, 'goToPreviousRestrictedEditingRegion' );
+				sinon.assert.calledWith( spy.firstCall, 'goToPreviousRestrictedEditingException' );
 
 				goToNextButton.fire( 'execute' );
-				sinon.assert.calledWith( spy.secondCall, 'goToNextRestrictedEditingRegion' );
+				sinon.assert.calledWith( spy.secondCall, 'goToNextRestrictedEditingException' );
 			} );
 		} );
 	} );

+ 10 - 10
packages/ckeditor5-restricted-editing/tests/restrictedediting.js

@@ -8,11 +8,11 @@
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
 
-import RestrictedEditing from './../src/restrictedediting';
-import RestrictedEditingUI from './../src/restrictededitingui';
-import RestrictedEditingEditing from './../src/restrictededitingediting';
+import StandardEditingMode from './../src/standardeditingmode';
+import StandardEditingModeUI from './../src/standardeditingmodeui';
+import StandardEditingModeEditing from './../src/standardeditingmodeediting';
 
-describe( 'RestrictedEditing', () => {
+describe( 'StandardEditingMode', () => {
 	let editor, element;
 
 	testUtils.createSinonSandbox();
@@ -21,7 +21,7 @@ describe( 'RestrictedEditing', () => {
 		element = document.createElement( 'div' );
 		document.body.appendChild( element );
 
-		editor = await ClassicTestEditor.create( element, { plugins: [ RestrictedEditing ] } );
+		editor = await ClassicTestEditor.create( element, { plugins: [ StandardEditingMode ] } );
 	} );
 
 	afterEach( () => {
@@ -31,14 +31,14 @@ describe( 'RestrictedEditing', () => {
 	} );
 
 	it( 'should be named', () => {
-		expect( RestrictedEditing.pluginName ).to.equal( 'RestrictedEditing' );
+		expect( StandardEditingMode.pluginName ).to.equal( 'StandardEditingMode' );
 	} );
 
-	it( 'should load the RestrictedEditingEditing plugin', () => {
-		expect( editor.plugins.get( RestrictedEditingEditing ) ).to.be.instanceOf( RestrictedEditingEditing );
+	it( 'should load the StandardEditingModeEditing plugin', () => {
+		expect( editor.plugins.get( StandardEditingModeEditing ) ).to.be.instanceOf( StandardEditingModeEditing );
 	} );
 
-	it( 'should load the RestrictedEditingUI plugin', () => {
-		expect( editor.plugins.get( RestrictedEditingUI ) ).to.be.instanceOf( RestrictedEditingUI );
+	it( 'should load the StandardEditingModeUI plugin', () => {
+		expect( editor.plugins.get( StandardEditingModeUI ) ).to.be.instanceOf( StandardEditingModeUI );
 	} );
 } );

+ 5 - 5
packages/ckeditor5-restricted-editing/tests/restrictededitingexceptionediting.js

@@ -9,17 +9,17 @@ import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils
 import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 
-import RestrictedEditingExceptionEditing from '../src/restrictededitingexceptionediting';
+import StandardEditingModeEditing from '../src/standardeditingmodeediting';
 import RestrictedEditingExceptionCommand from '../src/restrictededitingexceptioncommand';
 import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
-describe( 'RestrictedEditingExceptionEditing', () => {
+describe( 'StandardEditingModeEditing', () => {
 	let editor, model;
 
 	testUtils.createSinonSandbox();
 
 	beforeEach( async () => {
-		editor = await VirtualTestEditor.create( { plugins: [ Paragraph, RestrictedEditingExceptionEditing ] } );
+		editor = await VirtualTestEditor.create( { plugins: [ Paragraph, StandardEditingModeEditing ] } );
 		model = editor.model;
 	} );
 
@@ -28,11 +28,11 @@ describe( 'RestrictedEditingExceptionEditing', () => {
 	} );
 
 	it( 'should be named', () => {
-		expect( RestrictedEditingExceptionEditing.pluginName ).to.equal( 'RestrictedEditingExceptionEditing' );
+		expect( StandardEditingModeEditing.pluginName ).to.equal( 'StandardEditingModeEditing' );
 	} );
 
 	it( 'should be loaded', () => {
-		expect( editor.plugins.get( 'RestrictedEditingExceptionEditing' ) ).to.be.instanceOf( RestrictedEditingExceptionEditing );
+		expect( editor.plugins.get( 'StandardEditingModeEditing' ) ).to.be.instanceOf( StandardEditingModeEditing );
 	} );
 
 	it( 'should set proper schema rules', () => {

+ 4 - 4
packages/ckeditor5-restricted-editing/tests/restrictededitingexceptionui.js

@@ -10,10 +10,10 @@ import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictest
 import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 
-import RestrictedEditingExceptionUI from '../src/restrictededitingexceptionui';
-import RestrictedEditingException from '../src/restrictededitingexception';
+import StandardEditingModeUI from '../src/standardeditingmodeui';
+import StandardEditingModeEditing from '../src/standardeditingmodeediting';
 
-describe( 'RestrictedEditingExceptionUI', () => {
+describe( 'StandardEditingModeUI', () => {
 	let editor, buttonView;
 
 	testUtils.createSinonSandbox();
@@ -23,7 +23,7 @@ describe( 'RestrictedEditingExceptionUI', () => {
 		document.body.appendChild( editorElement );
 
 		editor = await ClassicTestEditor.create( editorElement, {
-			plugins: [ Paragraph, RestrictedEditingException, RestrictedEditingExceptionUI ]
+			plugins: [ Paragraph, StandardEditingModeEditing, StandardEditingModeUI ]
 		} );
 
 		buttonView = editor.ui.componentFactory.create( 'restrictedEditingException' );

+ 3 - 37
packages/ckeditor5-restricted-editing/theme/restrictedediting.css

@@ -4,41 +4,7 @@
  */
 
 /*
- *                                     !! IMPORTANT & TODO !!
- *
- * This file contains all kinds of style and some of them should land in theme-lark before going to prod.
- *                  For now, let's keep them here to avoid additional branch and PR.
+ * Note: This file should contain the wireframe styles only. But since there are no such styles,
+ * it acts as a message to the builder telling that it should look for the corresponding styles
+ * **in the theme** when compiling the editor.
  */
-
-:root {
-	--ck-restricted-editing-color-exception-background: hsla(31, 100%, 65%, .2);
-	--ck-restricted-editing-color-exception-brackets: hsla(31, 100%, 40%, .4);
-	--ck-restricted-editing-color-selected-exception-background: hsla(31, 100%, 65%, .5);
-	--ck-restricted-editing-color-selected-exception-brackets: hsla(31, 100%, 40%, .6);
-}
-
-.ck-editor__editable .ck-restricted-editing-exception {
-	transition: .2s ease-in-out background;
-	background-color: var(--ck-restricted-editing-color-exception-background);
-	border: 1px solid;
-	border-image: linear-gradient(
-		to right,
-		var(--ck-restricted-editing-color-exception-brackets) 0%,
-		var(--ck-restricted-editing-color-exception-brackets) 5px,
-		hsla(0, 0%, 0%, 0) 6px,
-		hsla(0, 0%, 0%, 0) calc(100% - 6px),
-		var(--ck-restricted-editing-color-exception-brackets) calc(100% - 5px),
-		var(--ck-restricted-editing-color-exception-brackets) 100%
-	) 1;
-
-	&.ck-restricted-editing-exception_selected {
-		background-color: var(--ck-restricted-editing-color-selected-exception-background);
-		border-image: linear-gradient(
-			to right,
-			var(--ck-restricted-editing-color-selected-exception-brackets) 0%,
-			var(--ck-restricted-editing-color-selected-exception-brackets) 5px,
-			var(--ck-restricted-editing-color-selected-exception-brackets) calc(100% - 5px),
-			var(--ck-restricted-editing-color-selected-exception-brackets) 100%
-		) 1;
-	}
-}