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

Merge pull request #1 from ckeditor/i/5683

Feature: Implemented the "standard" mode of the restricted editing that allows creating editable regions in the content. Closes ckeditor/ckeditor5#5754.
Aleksander Nowodzinski 6 лет назад
Родитель
Сommit
539eb98387

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

@@ -8,7 +8,15 @@
     "ckeditor 5",
     "ckeditor5-lib"
   ],
+  "dependencies": {
+    "@ckeditor/ckeditor5-core": "^15.0.0",
+    "@ckeditor/ckeditor5-ui": "^15.0.0"
+  },
   "devDependencies": {
+    "@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",
     "eslint": "^5.5.0",
     "eslint-config-ckeditor5": "^2.0.0",
     "husky": "^2.4.1",

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

@@ -0,0 +1,29 @@
+/**
+ * @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';
+
+/**
+ * @extends module:core/plugin~Plugin
+ */
+export default class RestrictedEditingException extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get pluginName() {
+		return 'RestrictedEditingException';
+	}
+
+	static get requires() {
+		return [ RestrictedEditingExceptionEditing, RestrictedEditingExceptionUI ];
+	}
+}

+ 57 - 0
packages/ckeditor5-restricted-editing/src/restrictededitingexceptioncommand.js

@@ -0,0 +1,57 @@
+/**
+ * @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/restrictededitingexceptioncommand
+ */
+
+import Command from '@ckeditor/ckeditor5-core/src/command';
+
+/**
+ * @extends module:core/command~Command
+ */
+export default class RestrictedEditingExceptionCommand extends Command {
+	/**
+	 * @inheritDoc
+	 */
+	refresh() {
+		const model = this.editor.model;
+		const doc = model.document;
+
+		this.value = !!doc.selection.getAttribute( 'restrictedEditingException' );
+
+		this.isEnabled = model.schema.checkAttributeInSelection( doc.selection, 'restrictedEditingException' );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	execute( options = {} ) {
+		const model = this.editor.model;
+		const document = model.document;
+		const selection = document.selection;
+		const valueToSet = ( options.forceValue === undefined ) ? !this.value : options.forceValue;
+
+		model.change( writer => {
+			const ranges = model.schema.getValidRanges( selection.getRanges(), 'restrictedEditingException' );
+
+			if ( selection.isCollapsed ) {
+				if ( valueToSet ) {
+					writer.setSelectionAttribute( 'restrictedEditingException', true );
+				} else {
+					writer.removeSelectionAttribute( 'restrictedEditingException' );
+				}
+			} else {
+				for ( const range of ranges ) {
+					if ( valueToSet ) {
+						writer.setAttribute( 'restrictedEditingException', valueToSet, range );
+					} else {
+						writer.removeAttribute( 'restrictedEditingException', range );
+					}
+				}
+			}
+		} );
+	}
+}

+ 42 - 0
packages/ckeditor5-restricted-editing/src/restrictededitingexceptionediting.js

@@ -0,0 +1,42 @@
+/**
+ * @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/restrictededitingexceptionediting
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import RestrictedEditingExceptionCommand from './restrictededitingexceptioncommand';
+
+/**
+ * @extends module:core/plugin~Plugin
+ */
+export default class RestrictedEditingExceptionEditing extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get pluginName() {
+		return 'RestrictedEditingExceptionEditing';
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		const editor = this.editor;
+
+		editor.model.schema.extend( '$text', { allowAttributes: [ 'restrictedEditingException' ] } );
+
+		editor.conversion.attributeToElement( {
+			model: 'restrictedEditingException',
+			view: {
+				name: 'span',
+				classes: 'ck-restricted-editing-exception'
+			}
+		} );
+
+		editor.commands.add( 'restrictedEditingException', new RestrictedEditingExceptionCommand( editor ) );
+	}
+}

+ 44 - 0
packages/ckeditor5-restricted-editing/src/restrictededitingexceptionui.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
+ */
+
+/**
+ * @module restricted-editing/restrictededitingexceptionui
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
+
+import restrictedDocumentIcon from '../theme/icons/contentlock.svg';
+
+/**
+ * @extends module:core/plugin~Plugin
+ */
+export default class RestrictedEditingExceptionUI extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		const editor = this.editor;
+		const t = editor.t;
+
+		editor.ui.componentFactory.add( 'restrictedEditingException', locale => {
+			const command = editor.commands.get( 'restrictedEditingException' );
+			const view = new ButtonView( locale );
+
+			view.set( {
+				label: t( 'Enable editing' ),
+				icon: restrictedDocumentIcon,
+				tooltip: true,
+				isToggleable: true
+			} );
+
+			view.bind( 'isOn', 'isEnabled' ).to( command, 'value', 'isEnabled' );
+
+			this.listenTo( view, 'execute', () => editor.execute( 'restrictedEditingException' ) );
+
+			return view;
+		} );
+	}
+}

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

@@ -0,0 +1,31 @@
+<div id="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="foo">Link</a></p>
+	<ul>
+		<li>UL List item 1</li>
+		<li>UL List item 2</li>
+	</ul>
+	<ol>
+		<li>OL List item 1</li>
+		<li>OL List item 2</li>
+	</ol>
+	<figure class="image image-style-side">
+		<img alt="bar" src="sample.jpg">
+		<figcaption>Caption</figcaption>
+	</figure>
+	<blockquote>
+		<p>Quote</p>
+		<ul>
+			<li>Quoted UL List item 1</li>
+			<li>Quoted UL List item 2</li>
+		</ul>
+		<p>Quote</p>
+	</blockquote>
+</div>
+
+<style>
+	.ck-restricted-editing-exception {
+		background-color: #ffcd96;
+	}
+</style>

+ 38 - 0
packages/ckeditor5-restricted-editing/tests/manual/restrictedediting.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
+ */
+
+/* globals console, window, document */
+
+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';
+
+ClassicEditor
+	.create( document.querySelector( '#editor' ), {
+		plugins: [ ArticlePluginSet, Table, RestrictedEditingException ],
+		toolbar: [ 'heading', '|',
+			'bold', 'italic', 'link', '|',
+			'bulletedList', 'numberedList', 'blockQuote', 'insertTable', '|',
+			'restrictedEditingException', '|', 'undo', 'redo'
+		],
+		image: {
+			toolbar: [ 'imageStyle:full', 'imageStyle:side', '|', 'imageTextAlternative' ]
+		},
+		table: {
+			contentToolbar: [
+				'tableColumn',
+				'tableRow',
+				'mergeTableCells'
+			]
+		}
+	} )
+	.then( editor => {
+		window.editor = editor;
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );

+ 4 - 0
packages/ckeditor5-restricted-editing/tests/manual/restrictedediting.md

@@ -0,0 +1,4 @@
+## Restricted editing - Standard mode
+
+1. The editor data should be loaded with "it is editable" fragment marked as an exception in the first paragraph (orange-ish background).
+2. Test the "Restricted editing" button to mark parts of text as non-restricted.

BIN
packages/ckeditor5-restricted-editing/tests/manual/sample.jpg


+ 44 - 0
packages/ckeditor5-restricted-editing/tests/restrictededitingexception.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 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 );
+	} );
+} );

+ 195 - 0
packages/ckeditor5-restricted-editing/tests/restrictededitingexceptioncommand.js

@@ -0,0 +1,195 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
+import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+
+import RestrictedEditingExceptionCommand from '../src/restrictededitingexceptioncommand';
+
+describe( 'RestrictedEditingExceptionCommand', () => {
+	let editor, command, model;
+
+	beforeEach( () => {
+		return ModelTestEditor
+			.create()
+			.then( newEditor => {
+				editor = newEditor;
+				model = editor.model;
+
+				command = new RestrictedEditingExceptionCommand( editor );
+
+				model.schema.register( 'p', { inheritAllFrom: '$block' } );
+				model.schema.register( 'h1', { inheritAllFrom: '$block' } );
+				model.schema.register( 'img', {
+					allowWhere: [ '$block', '$text' ],
+					isObject: true
+				} );
+
+				editor.model.schema.extend( '$text', { allowAttributes: [ 'restrictedEditingException' ] } );
+			} );
+	} );
+
+	afterEach( () => {
+		command.destroy();
+
+		return editor.destroy();
+	} );
+
+	describe( 'value', () => {
+		it( 'is true when collapsed selection has the attribute', () => {
+			model.change( writer => {
+				writer.setSelectionAttribute( 'restrictedEditingException', true );
+			} );
+
+			expect( command.value ).to.be.true;
+		} );
+
+		it( 'is false when collapsed selection does not have the attribute', () => {
+			model.change( writer => {
+				writer.setSelectionAttribute( 'restrictedEditingException', true );
+			} );
+
+			model.change( writer => {
+				writer.removeSelectionAttribute( 'restrictedEditingException' );
+			} );
+
+			expect( command.value ).to.be.false;
+		} );
+
+		it( 'is true when the selection is inside a text with the attribute', () => {
+			setData( model, '<p><$text restrictedEditingException="true">fo[]o</$text></p><h1>bar</h1>' );
+
+			expect( command.value ).to.be.true;
+		} );
+
+		it( 'is true when the selection is on a text with the attribute', () => {
+			setData( model, '<p>foo[<$text restrictedEditingException="true">bar</$text>]baz</p>' );
+
+			expect( command.value ).to.be.true;
+		} );
+	} );
+
+	describe( 'isEnabled', () => {
+		beforeEach( () => {
+			model.schema.register( 'x', { inheritAllFrom: '$block' } );
+
+			model.schema.addAttributeCheck( ( ctx, attributeName ) => {
+				if ( ctx.endsWith( 'x $text' ) && attributeName == 'restrictedEditingException' ) {
+					return false;
+				}
+			} );
+		} );
+
+		describe( 'when the selection is collapsed', () => {
+			it( 'should return true if the attribute is allowed at the caret position', () => {
+				setData( model, '<p>f[]oo</p>' );
+				expect( command.isEnabled ).to.be.true;
+			} );
+
+			it( 'should return true if the attribute is not allowed at the caret position', () => {
+				setData( model, '<x>fo[]o</x>' );
+				expect( command.isEnabled ).to.be.false;
+			} );
+		} );
+
+		describe( 'when the selection is not collapsed', () => {
+			it( 'should return true if there is at least one node in the selection that can have the attribute', () => {
+				setData( model, '<p>[foo]</p>' );
+				expect( command.isEnabled ).to.be.true;
+			} );
+
+			it( 'should return false if there are no nodes in the selection that can have the attribute', () => {
+				setData( model, '<x>[foo]</x>' );
+				expect( command.isEnabled ).to.be.false;
+			} );
+		} );
+	} );
+
+	describe( 'execute()', () => {
+		describe( 'collapsed selection', () => {
+			it( 'should set the selection attribute if there is a text without the attribute', () => {
+				setData( model, '<p>abcfoo[]barbaz</p>' );
+
+				command.execute();
+
+				expect( model.document.selection.hasAttribute( 'restrictedEditingException' ) ).to.be.true;
+			} );
+
+			it( 'should remove the selection attribute if a text has the non-restricted attribute', () => {
+				setData( model, '<p>abc<$text restrictedEditingException="true">foo[]bar</$text>baz</p>' );
+
+				command.execute();
+
+				expect( model.document.selection.hasAttribute( 'restrictedEditingException' ) ).to.be.false;
+			} );
+		} );
+
+		describe( 'non-collapsed selection', () => {
+			it( 'should do nothing if the command is disabled', () => {
+				setData( model, '<p>fo[ob]ar</p>' );
+
+				command.isEnabled = false;
+
+				command.execute();
+
+				expect( getData( model ) ).to.equal( '<p>fo[ob]ar</p>' );
+			} );
+
+			it( 'should add the attribute on a text without the attribute', () => {
+				setData( model, '<p>foo[bar]baz</p>' );
+
+				command.execute();
+
+				expect( getData( model ) ).to.equal( '<p>foo[<$text restrictedEditingException="true">bar</$text>]baz</p>' );
+			} );
+
+			it( 'should add the attribute on a selected text if a selected part already has the attribute', () => {
+				setData( model, '<p>[foo<$text restrictedEditingException="true">bar</$text>]baz</p>' );
+
+				command.execute();
+
+				expect( getData( model ) ).to.equal( '<p>[<$text restrictedEditingException="true">foobar</$text>]baz</p>' );
+			} );
+
+			it( 'should remove the attribute only from the selected part of a non-restricted text', () => {
+				setData( model, '<p><$text restrictedEditingException="true">foo[bar]baz</$text></p>' );
+
+				command.execute();
+
+				expect( getData( model ) ).to.equal(
+					'<p><$text restrictedEditingException="true">foo</$text>[bar]<$text restrictedEditingException="true">baz</$text></p>'
+				);
+			} );
+
+			it( 'should remove the attribute from the selected text if all text contains the attribute', () => {
+				setData( model, '<p>abc[<$text restrictedEditingException="true">foo]bar</$text>baz</p>' );
+
+				command.execute();
+
+				expect( getData( model ) ).to.equal( '<p>abc[foo]<$text restrictedEditingException="true">bar</$text>baz</p>' );
+			} );
+
+			it( 'should add the attribute on a selected text if the "forceValue" parameter was true', () => {
+				setData( model, '<p>abc<$text restrictedEditingException="true">foob[ar</$text>x]yz</p>' );
+
+				expect( command.value ).to.be.true;
+
+				command.execute( { forceValue: true } );
+
+				expect( command.value ).to.be.true;
+				expect( getData( model ) ).to.equal( '<p>abc<$text restrictedEditingException="true">foob[arx</$text>]yz</p>' );
+			} );
+
+			it( 'should remove the attribute on selected nodes if the "forceValue" parameter was set false', () => {
+				setData( model, '<p>a[bc<$text restrictedEditingException="true">fo]obar</$text>xyz</p>' );
+
+				command.execute( { forceValue: false } );
+
+				expect( command.value ).to.be.false;
+				expect( getData( model ) ).to.equal( '<p>a[bcfo]<$text restrictedEditingException="true">obar</$text>xyz</p>' );
+			} );
+		} );
+	} );
+} );

+ 75 - 0
packages/ckeditor5-restricted-editing/tests/restrictededitingexceptionediting.js

@@ -0,0 +1,75 @@
+/**
+ * @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 { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
+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 RestrictedEditingExceptionCommand from '../src/restrictededitingexceptioncommand';
+
+describe( 'RestrictedEditingExceptionEditing', () => {
+	let editor, model;
+
+	testUtils.createSinonSandbox();
+
+	beforeEach( async () => {
+		editor = await VirtualTestEditor.create( { plugins: [ Paragraph, RestrictedEditingExceptionEditing ] } );
+		model = editor.model;
+	} );
+
+	afterEach( () => {
+		return editor.destroy();
+	} );
+
+	it( 'should be named', () => {
+		expect( RestrictedEditingExceptionEditing.pluginName ).to.equal( 'RestrictedEditingExceptionEditing' );
+	} );
+
+	it( 'should be loaded', () => {
+		expect( editor.plugins.get( 'RestrictedEditingExceptionEditing' ) ).to.be.instanceOf( RestrictedEditingExceptionEditing );
+	} );
+
+	it( 'should set proper schema rules', () => {
+		expect( model.schema.checkAttribute( [ '$root', '$text' ], 'restrictedEditingException' ) ).to.be.true;
+
+		expect( model.schema.checkAttribute( [ '$block', '$text' ], 'restrictedEditingException' ) ).to.be.true;
+		expect( model.schema.checkAttribute( [ '$clipboardHolder', '$text' ], 'restrictedEditingException' ) ).to.be.true;
+
+		expect( model.schema.checkAttribute( [ '$block' ], 'restrictedEditingException' ) ).to.be.false;
+	} );
+
+	it( 'should register the command', () => {
+		const command = editor.commands.get( 'restrictedEditingException' );
+
+		expect( command ).to.be.instanceof( RestrictedEditingExceptionCommand );
+	} );
+
+	describe( 'conversion', () => {
+		describe( 'upcast', () => {
+			it( 'should convert <span class="ck-restricted-editing-exception"> to the model attribute', () => {
+				editor.setData( '<p>foo <span class="ck-restricted-editing-exception">bar</span> baz</p>' );
+
+				expect( getModelData( model, { withoutSelection: true } ) )
+					.to.equal( '<paragraph>foo <$text restrictedEditingException="true">bar</$text> baz</paragraph>' );
+			} );
+		} );
+
+		describe( 'downcast', () => {
+			it( 'should convert the model attribute to a <span>', () => {
+				const expectedView = '<p>foo <span class="ck-restricted-editing-exception">bar</span> baz</p>';
+
+				setModelData( editor.model,
+					'<paragraph>foo <$text restrictedEditingException="true">bar</$text> baz</paragraph>'
+				);
+
+				expect( editor.getData() ).to.equal( expectedView );
+				expect( getViewData( editor.editing.view, { withoutSelection: true } ) ).to.equal( expectedView );
+			} );
+		} );
+	} );
+} );

+ 64 - 0
packages/ckeditor5-restricted-editing/tests/restrictededitingexceptionui.js

@@ -0,0 +1,64 @@
+/**
+ * @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 document */
+
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
+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';
+
+describe( 'RestrictedEditingExceptionUI', () => {
+	let editor, buttonView;
+
+	testUtils.createSinonSandbox();
+
+	beforeEach( async () => {
+		const editorElement = document.createElement( 'div' );
+		document.body.appendChild( editorElement );
+
+		editor = await ClassicTestEditor.create( editorElement, {
+			plugins: [ Paragraph, RestrictedEditingException, RestrictedEditingExceptionUI ]
+		} );
+
+		buttonView = editor.ui.componentFactory.create( 'restrictedEditingException' );
+	} );
+
+	afterEach( () => {
+		return editor.destroy();
+	} );
+
+	it( 'should register a button', () => {
+		expect( buttonView ).to.be.instanceOf( ButtonView );
+		expect( buttonView.isOn ).to.be.false;
+		expect( buttonView.label ).to.equal( 'Enable editing' );
+		expect( buttonView.icon ).to.match( /<svg / );
+		expect( buttonView.isToggleable ).to.be.true;
+	} );
+
+	it( 'should execute a command on the button "execute" event', () => {
+		const executeSpy = testUtils.sinon.spy( editor, 'execute' );
+
+		buttonView.fire( 'execute' );
+
+		sinon.assert.calledOnce( executeSpy );
+	} );
+
+	it( 'should bind a button to the command', () => {
+		const command = editor.commands.get( 'restrictedEditingException' );
+
+		expect( buttonView.isOn ).to.be.false;
+		expect( buttonView.isEnabled ).to.be.true;
+
+		command.value = true;
+		expect( buttonView.isOn ).to.be.true;
+
+		command.isEnabled = false;
+		expect( buttonView.isEnabled ).to.be.false;
+	} );
+} );

+ 1 - 0
packages/ckeditor5-restricted-editing/theme/icons/contentlock.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M2.15 3.93c0 .4.33.75.75.75h11.78a.75.75 0 100-1.5H2.9a.75.75 0 00-.75.75zm.75 8.75h3a.75.75 0 000-1.5h-3a.75.75 0 100 1.5zm-.75 3.25c0 .4.33.75.75.75h2.94a.75.75 0 000-1.5H2.9a.75.75 0 00-.75.75zm.75-7.25h4.96a.75.75 0 000-1.5H2.9a.75.75 0 100 1.5zm11.25-2.12a3.01 3.01 0 013 3l.01 1.53h.3c.58 0 1.04.46 1.04 1.03v4.54c0 .57-.46 1.03-1.03 1.03h-6.44c-.57 0-1.03-.46-1.03-1.03v-4.54c0-.57.46-1.03 1.03-1.03h.11V9.57a3.01 3.01 0 013-3.01zm.1 6.64a.5.5 0 00-.5.5v1.5a.5.5 0 101 0v-1.5a.5.5 0 00-.5-.5zm-.1-5.54a1.9 1.9 0 00-1.9 1.9v1.53h3.8V9.56a1.9 1.9 0 00-1.9-1.9z"/></svg>